mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 06:56:39 +00:00 
			
		
		
		
	🚧 add modal
This commit is contained in:
		| @@ -34,6 +34,7 @@ | ||||
|     "react-dom": "^17.0.1", | ||||
|     "react-paginate": "^8.1.0", | ||||
|     "react-use-pagination": "^2.0.1", | ||||
|     "resize-observer-polyfill": "^1.5.1", | ||||
|     "url-loader": "^4.1.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|   | ||||
							
								
								
									
										39
									
								
								website/src/components/Modal/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								website/src/components/Modal/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import clsx from "clsx"; | ||||
| import React from "react"; | ||||
|  | ||||
| export default function Modal({ | ||||
|   active, | ||||
|   setActive, | ||||
|   children, | ||||
| }: { | ||||
|   active: boolean; | ||||
|   setActive: (active: boolean) => void; | ||||
|   children: React.ReactNode; | ||||
| }): JSX.Element { | ||||
|   return ( | ||||
|     <> | ||||
|       {/* overlay */} | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center transition z-[200]", | ||||
|           { | ||||
|             hidden: !active, | ||||
|             "pointer-events-auto": active, | ||||
|           } | ||||
|         )} | ||||
|         onClick={() => setActive(false)} | ||||
|       > | ||||
|         <div className="absolute top-0 bottom-0 left-0 right-0 h-full w-full bg-gray-800 opacity-[.46]"></div> | ||||
|       </div> | ||||
|       {/* modal */} | ||||
|       <div | ||||
|         className={clsx( | ||||
|           "fixed top-0 left-0 flex items-center justify-center h-full w-full transition z-[201] pointer-events-none", | ||||
|           { hidden: !active } | ||||
|         )} | ||||
|       > | ||||
|         {children} | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| import React, { useCallback } from "react"; | ||||
| import React, { useCallback, useRef } from "react"; | ||||
| import ReactPaginate from "react-paginate"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
|  | ||||
| import { useContentWidth } from "../../libs/width"; | ||||
| import styles from "./styles.module.css"; | ||||
|  | ||||
| export default function Paginate({ | ||||
| @@ -11,6 +12,9 @@ export default function Paginate({ | ||||
|   setPage, | ||||
|   currentPage, | ||||
| }: ReturnType<typeof usePagination>): JSX.Element { | ||||
|   const ref = useRef<HTMLElement>(); | ||||
|   const maxWidth = useContentWidth(ref.current?.parentElement ?? undefined); | ||||
|  | ||||
|   const onPageChange = useCallback( | ||||
|     (selectedItem: { selected: number }) => { | ||||
|       setPage(selectedItem.selected); | ||||
| @@ -18,8 +22,9 @@ export default function Paginate({ | ||||
|     [setPage] | ||||
|   ); | ||||
|  | ||||
|   // FIXME: responsive width | ||||
|   return ( | ||||
|     <nav role="navigation" aria-label="Pagination Navigation"> | ||||
|     <nav role="navigation" aria-label="Pagination Navigation" ref={ref}> | ||||
|       <ReactPaginate | ||||
|         pageCount={totalPages} | ||||
|         forcePage={currentPage} | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| import React from "react"; | ||||
| import React, { useCallback, useState } from "react"; | ||||
| import { usePagination } from "react-use-pagination"; | ||||
|  | ||||
| import plugins from "../../static/plugins.json"; | ||||
| import { useFilteredObjs } from "../libs/store"; | ||||
| import Card from "./Card"; | ||||
| import Modal from "./Modal"; | ||||
| import Paginate from "./Paginate"; | ||||
|  | ||||
| export default function Adapter(): JSX.Element { | ||||
|   const [modalOpen, setModalOpen] = useState<boolean>(false); | ||||
|   const { | ||||
|     filter, | ||||
|     setFilter, | ||||
| @@ -20,6 +22,28 @@ export default function Adapter(): JSX.Element { | ||||
|   const { startIndex, endIndex } = props; | ||||
|   const currentPlugins = filteredPlugins.slice(startIndex, endIndex + 1); | ||||
|  | ||||
|   const [form, setForm] = useState<{ | ||||
|     name: string; | ||||
|     desc: string; | ||||
|     projectLink: string; | ||||
|     moduleName: string; | ||||
|     homepage: string; | ||||
|   }>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" }); | ||||
|   const onSubmit = () => { | ||||
|     console.log(form); | ||||
|   }; | ||||
|   const onChange = (event) => { | ||||
|     const target = event.target; | ||||
|     const value = target.type === "checkbox" ? target.checked : target.value; | ||||
|     const name = target.name; | ||||
|  | ||||
|     setForm({ | ||||
|       ...form, | ||||
|       [name]: value, | ||||
|     }); | ||||
|     event.preventDefault(); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 px-4"> | ||||
| @@ -29,7 +53,10 @@ export default function Adapter(): JSX.Element { | ||||
|           placeholder="搜索插件" | ||||
|           onChange={(event) => setFilter(event.target.value)} | ||||
|         /> | ||||
|         <button className="w-full rounded-lg bg-hero text-white"> | ||||
|         <button | ||||
|           className="w-full rounded-lg bg-hero text-white" | ||||
|           onClick={() => setModalOpen(true)} | ||||
|         > | ||||
|           发布插件 | ||||
|         </button> | ||||
|       </div> | ||||
| @@ -44,6 +71,78 @@ export default function Adapter(): JSX.Element { | ||||
|       <div className="grid grid-cols-1 p-4"> | ||||
|         <Paginate {...props} /> | ||||
|       </div> | ||||
|       <Modal active={modalOpen} setActive={setModalOpen}> | ||||
|         <div className="w-full max-w-[600px] max-h-[90%] overflow-y-auto rounded shadow-lg m-6 origin-center transition z-[inherit] pointer-events-auto thin-scrollbar"> | ||||
|           <div className="bg-light-nonepress-100 dark:bg-dark-nonepress-100"> | ||||
|             <div className="px-6 pt-4 pb-2 font-medium text-xl"> | ||||
|               <span>插件信息</span> | ||||
|             </div> | ||||
|             <div className="px-6 pb-5 w-full"> | ||||
|               <form onSubmit={onSubmit}> | ||||
|                 <div className="grid grid-cols-1 gap-4 p-4"> | ||||
|                   <label className="flex flex-wrap"> | ||||
|                     <span className="mr-2">插件名称:</span> | ||||
|                     <input | ||||
|                       type="text" | ||||
|                       name="name" | ||||
|                       maxLength={20} | ||||
|                       className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                       onChange={onChange} | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label className="flex flex-wrap"> | ||||
|                     <span className="mr-2">插件介绍:</span> | ||||
|                     <input | ||||
|                       type="text" | ||||
|                       name="desc" | ||||
|                       className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                       onChange={onChange} | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label className="flex flex-wrap"> | ||||
|                     <span className="mr-2">PyPI 项目名:</span> | ||||
|                     <input | ||||
|                       type="text" | ||||
|                       name="projectLink" | ||||
|                       className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                       onChange={onChange} | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label className="flex flex-wrap"> | ||||
|                     <span className="mr-2">import 包名:</span> | ||||
|                     <input | ||||
|                       type="text" | ||||
|                       name="moduleName" | ||||
|                       className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                       onChange={onChange} | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label className="flex flex-wrap"> | ||||
|                     <span className="mr-2">仓库/主页:</span> | ||||
|                     <input | ||||
|                       type="text" | ||||
|                       name="homepage" | ||||
|                       className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200" | ||||
|                       onChange={onChange} | ||||
|                     /> | ||||
|                   </label> | ||||
|                 </div> | ||||
|               </form> | ||||
|             </div> | ||||
|             <div className="px-4 py-2 flex justify-end"> | ||||
|               <button className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]"> | ||||
|                 关闭 | ||||
|               </button> | ||||
|               <button | ||||
|                 className="ml-2 px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" | ||||
|                 onClick={onSubmit} | ||||
|               > | ||||
|                 发布 | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </Modal> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										67
									
								
								website/src/libs/resize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								website/src/libs/resize.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| import { useLayoutEffect, useRef } from "react"; | ||||
| import ResizeObserver from "resize-observer-polyfill"; | ||||
|  | ||||
| export function useResizeNotifier( | ||||
|   element: HTMLElement | undefined, | ||||
|   callback: () => void | ||||
| ) { | ||||
|   const callBackRef = useRef(callback); | ||||
|   useLayoutEffect(() => { | ||||
|     callBackRef.current = callback; | ||||
|   }, [callback]); | ||||
|  | ||||
|   useLayoutEffect(() => { | ||||
|     if (!element) return; | ||||
|  | ||||
|     const resizeObserver = new ResizeObserver( | ||||
|       withResizeLoopDetection(() => { | ||||
|         callBackRef.current!(); | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     resizeObserver.observe(element); | ||||
|  | ||||
|     return () => { | ||||
|       resizeObserver.disconnect(); | ||||
|     }; | ||||
|   }, [element]); | ||||
| } | ||||
|  | ||||
| function withResizeLoopDetection(callback: () => void) { | ||||
|   return (entries: ResizeObserverEntry[], resizeObserver: ResizeObserver) => { | ||||
|     const elements = entries.map((entry) => entry.target); | ||||
|  | ||||
|     const rectsBefore = elements.map((element) => | ||||
|       element.getBoundingClientRect() | ||||
|     ); | ||||
|  | ||||
|     callback(); | ||||
|  | ||||
|     const rectsAfter = elements.map((element) => | ||||
|       element.getBoundingClientRect() | ||||
|     ); | ||||
|  | ||||
|     const changedElements = elements.filter( | ||||
|       (_, i) => !areRectSizesEqual(rectsBefore[i], rectsAfter[i]) | ||||
|     ); | ||||
|  | ||||
|     changedElements.forEach((element) => | ||||
|       unobserveUntilNextFrame(element, resizeObserver) | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| function unobserveUntilNextFrame( | ||||
|   element: Element, | ||||
|   resizeObserver: ResizeObserver | ||||
| ) { | ||||
|   resizeObserver.unobserve(element); | ||||
|  | ||||
|   requestAnimationFrame(() => { | ||||
|     resizeObserver.observe(element); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function areRectSizesEqual(rect1: DOMRect, rect2: DOMRect) { | ||||
|   return rect1.width === rect2.width && rect1.height === rect2.height; | ||||
| } | ||||
| @@ -23,7 +23,8 @@ export function filterObjs(filter: string, objs: Obj[]): Obj[] { | ||||
|       o.project_link?.indexOf(filter) != -1 || | ||||
|       o.name.indexOf(filter) != -1 || | ||||
|       o.desc.indexOf(filter) != -1 || | ||||
|       o.author.indexOf(filter) != -1 | ||||
|       o.author.indexOf(filter) != -1 || | ||||
|       o.tags.filter((t) => t.label.indexOf(filter) != -1).length > 0 | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
|   | ||||
							
								
								
									
										64
									
								
								website/src/libs/width.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								website/src/libs/width.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import { useLayoutEffect, useState } from "react"; | ||||
|  | ||||
| import { useResizeNotifier } from "./resize"; | ||||
|  | ||||
| export function getElementWidth(element: HTMLElement) { | ||||
|   const style = getComputedStyle(element); | ||||
|  | ||||
|   return ( | ||||
|     styleMetricToInt(style.marginLeft) + | ||||
|     getWidth(element) + | ||||
|     styleMetricToInt(style.marginRight) | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function getContentWidth(element: HTMLElement) { | ||||
|   const style = getComputedStyle(element); | ||||
|  | ||||
|   return ( | ||||
|     element.getBoundingClientRect().width - | ||||
|     styleMetricToInt(style.borderLeftWidth) - | ||||
|     styleMetricToInt(style.paddingLeft) - | ||||
|     styleMetricToInt(style.paddingRight) - | ||||
|     styleMetricToInt(style.borderRightWidth) | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function getNonContentWidth(element: HTMLElement) { | ||||
|   const style = getComputedStyle(element); | ||||
|  | ||||
|   return ( | ||||
|     styleMetricToInt(style.marginLeft) + | ||||
|     styleMetricToInt(style.borderLeftWidth) + | ||||
|     styleMetricToInt(style.paddingLeft) + | ||||
|     styleMetricToInt(style.paddingRight) + | ||||
|     styleMetricToInt(style.borderRightWidth) + | ||||
|     styleMetricToInt(style.marginRight) | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function getWidth(element: HTMLElement) { | ||||
|   return element.getBoundingClientRect().width; | ||||
| } | ||||
|  | ||||
| function styleMetricToInt(styleAttribute: string | null) { | ||||
|   return styleAttribute ? parseInt(styleAttribute) : 0; | ||||
| } | ||||
|  | ||||
| export function useContentWidth(element: HTMLElement | undefined) { | ||||
|   const [width, setWidth] = useState<number>(); | ||||
|  | ||||
|   function syncWidth() { | ||||
|     const newWidth = element ? getContentWidth(element) : undefined; | ||||
|  | ||||
|     if (width !== newWidth) { | ||||
|       setWidth(newWidth); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   useResizeNotifier(element, syncWidth); | ||||
|  | ||||
|   useLayoutEffect(syncWidth); | ||||
|  | ||||
|   return width; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user