mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-02-20 09:38:23 +08:00
🚧 add tag selection
This commit is contained in:
parent
ab2c73856d
commit
56677616b4
@ -32,7 +32,6 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-paginate": "^8.1.0",
|
|
||||||
"react-use-pagination": "^2.0.1",
|
"react-use-pagination": "^2.0.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"url-loader": "^4.1.1"
|
"url-loader": "^4.1.1"
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Link from "@docusaurus/Link";
|
import Link from "@docusaurus/Link";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import type { Obj } from "../../libs/store";
|
import type { Obj, Tag as TagType } from "../../libs/store";
|
||||||
|
|
||||||
function pickTextColor(bgColor, lightColor, darkColor) {
|
function pickTextColor(bgColor, lightColor, darkColor) {
|
||||||
var color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
var color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||||
@ -13,6 +14,32 @@ function pickTextColor(bgColor, lightColor, darkColor) {
|
|||||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Tag({
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: TagType & {
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"inline-flex px-3 rounded-full items-center align-middle mr-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
color: pickTextColor(color, "#fff", "#000"),
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Card({
|
export default function Card({
|
||||||
module_name,
|
module_name,
|
||||||
name,
|
name,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import React, { useCallback, useRef } from "react";
|
import React, { useCallback, useRef } from "react";
|
||||||
import ReactPaginate from "react-paginate";
|
|
||||||
import { usePagination } from "react-use-pagination";
|
import { usePagination } from "react-use-pagination";
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -9,11 +9,19 @@ import styles from "./styles.module.css";
|
|||||||
|
|
||||||
export default function Paginate({
|
export default function Paginate({
|
||||||
totalPages,
|
totalPages,
|
||||||
|
setPreviousPage,
|
||||||
|
setNextPage,
|
||||||
setPage,
|
setPage,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
previousEnabled,
|
||||||
|
nextEnabled,
|
||||||
}: ReturnType<typeof usePagination>): JSX.Element {
|
}: ReturnType<typeof usePagination>): JSX.Element {
|
||||||
const ref = useRef<HTMLElement>();
|
const ref = useRef<HTMLElement>();
|
||||||
const maxWidth = useContentWidth(ref.current?.parentElement ?? undefined);
|
const maxWidth = useContentWidth(ref.current?.parentElement ?? undefined);
|
||||||
|
const maxLength = Math.min(
|
||||||
|
(maxWidth && Math.floor(maxWidth / 50) - 2) || totalPages,
|
||||||
|
totalPages
|
||||||
|
);
|
||||||
|
|
||||||
const onPageChange = useCallback(
|
const onPageChange = useCallback(
|
||||||
(selectedItem: { selected: number }) => {
|
(selectedItem: { selected: number }) => {
|
||||||
@ -21,27 +29,74 @@ export default function Paginate({
|
|||||||
},
|
},
|
||||||
[setPage]
|
[setPage]
|
||||||
);
|
);
|
||||||
|
const range = useCallback((start: number, end: number) => {
|
||||||
|
const result = [];
|
||||||
|
start = start > 0 ? start : 1;
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
result.push(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// FIXME: responsive width
|
// FIXME: responsive width
|
||||||
|
const pages: (React.ReactNode | number)[] = [];
|
||||||
|
const ellipsis = <FontAwesomeIcon icon="ellipsis-h" />;
|
||||||
|
|
||||||
|
const even = maxLength % 2 === 0 ? 1 : 0;
|
||||||
|
const left = Math.floor(maxLength / 2);
|
||||||
|
const right = totalPages - left + even + 1;
|
||||||
|
currentPage = currentPage + 1;
|
||||||
|
|
||||||
|
if (totalPages <= maxLength) {
|
||||||
|
pages.push(...range(1, totalPages));
|
||||||
|
} else if (currentPage > left && currentPage < right) {
|
||||||
|
const firstItem = 1;
|
||||||
|
const lastItem = totalPages;
|
||||||
|
const start = currentPage - left + 2;
|
||||||
|
const end = currentPage + left - 2 - even;
|
||||||
|
const secondItem = start - 1 === firstItem + 1 ? 2 : ellipsis;
|
||||||
|
const beforeLastItem = end + 1 === lastItem - 1 ? end + 1 : ellipsis;
|
||||||
|
|
||||||
|
pages.push(1, secondItem, ...range(start, end), beforeLastItem, totalPages);
|
||||||
|
} else if (currentPage === left) {
|
||||||
|
const end = currentPage + left - 1 - even;
|
||||||
|
pages.push(...range(1, end), ellipsis, totalPages);
|
||||||
|
} else if (currentPage === right) {
|
||||||
|
const start = currentPage - left + 1;
|
||||||
|
pages.push(1, ellipsis, ...range(start, totalPages));
|
||||||
|
} else {
|
||||||
|
pages.push(...range(1, left), ellipsis, ...range(right, totalPages));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav role="navigation" aria-label="Pagination Navigation" ref={ref}>
|
<nav role="navigation" aria-label="Pagination Navigation" ref={ref}>
|
||||||
<ReactPaginate
|
<ul className={styles.container}>
|
||||||
pageCount={totalPages}
|
<li
|
||||||
forcePage={currentPage}
|
className={clsx(styles.li, { [styles.disabled]: !previousEnabled })}
|
||||||
onPageChange={onPageChange}
|
>
|
||||||
containerClassName={styles.container}
|
<button className={styles.button} onClick={setPreviousPage}>
|
||||||
pageClassName={styles.li}
|
<FontAwesomeIcon icon="chevron-left" />
|
||||||
pageLinkClassName={styles.a}
|
</button>
|
||||||
previousClassName={styles.li}
|
</li>
|
||||||
previousLinkClassName={styles.a}
|
{pages.map((page, index) => (
|
||||||
nextClassName={styles.li}
|
<li className={styles.li} key={index}>
|
||||||
nextLinkClassName={styles.a}
|
<button
|
||||||
activeLinkClassName={styles.active}
|
className={clsx(styles.button, {
|
||||||
disabledLinkClassName={styles.disabled}
|
[styles.active]: page === currentPage,
|
||||||
breakLabel={<FontAwesomeIcon icon="ellipsis-h" />}
|
"pointer-events-none": typeof page !== "number",
|
||||||
previousLabel={<FontAwesomeIcon icon="chevron-left" />}
|
})}
|
||||||
nextLabel={<FontAwesomeIcon icon="chevron-right" />}
|
onClick={() => typeof page === "number" && setPage(page - 1)}
|
||||||
></ReactPaginate>
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li className={clsx(styles.li, { [styles.disabled]: !nextEnabled })}>
|
||||||
|
<button className={styles.button} onClick={setNextPage}>
|
||||||
|
<FontAwesomeIcon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
@apply flex items-center;
|
@apply flex items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.a {
|
.button {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
@ -15,12 +15,12 @@
|
|||||||
@apply text-black bg-light-nonepress-100;
|
@apply text-black bg-light-nonepress-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .a {
|
:global(.dark) .button {
|
||||||
@apply border-dark-nonepress-200 shadow-dark-nonepress-300;
|
@apply border-dark-nonepress-200 shadow-dark-nonepress-300;
|
||||||
@apply text-white bg-dark-nonepress-100;
|
@apply text-white bg-dark-nonepress-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.a.active {
|
.button.active {
|
||||||
@apply bg-hero text-white border-hero;
|
@apply bg-hero text-white border-hero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import clsx from "clsx";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { ChromePicker } from "react-color";
|
||||||
import { usePagination } from "react-use-pagination";
|
import { usePagination } from "react-use-pagination";
|
||||||
|
|
||||||
import plugins from "../../static/plugins.json";
|
import plugins from "../../static/plugins.json";
|
||||||
import { useFilteredObjs } from "../libs/store";
|
import { Tag, useFilteredObjs } from "../libs/store";
|
||||||
import Card from "./Card";
|
import Card, { Tag as TagComponent } from "./Card";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import Paginate from "./Paginate";
|
import Paginate from "./Paginate";
|
||||||
|
|
||||||
@ -29,8 +31,45 @@ export default function Adapter(): JSX.Element {
|
|||||||
moduleName: string;
|
moduleName: string;
|
||||||
homepage: string;
|
homepage: string;
|
||||||
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
|
}>({ name: "", desc: "", projectLink: "", moduleName: "", homepage: "" });
|
||||||
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
const [label, setLabel] = useState<string>("");
|
||||||
|
const [color, setColor] = useState<string>("#ea5252");
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
console.log(form);
|
setModalOpen(false);
|
||||||
|
const title = encodeURIComponent(`Plugin: ${form.name}`).replace(
|
||||||
|
/%2B/gi,
|
||||||
|
"+"
|
||||||
|
);
|
||||||
|
const body = encodeURIComponent(
|
||||||
|
`
|
||||||
|
**插件名称:**
|
||||||
|
|
||||||
|
${form.name}
|
||||||
|
|
||||||
|
**插件功能:**
|
||||||
|
|
||||||
|
${form.desc}
|
||||||
|
|
||||||
|
**PyPI 项目名:**
|
||||||
|
|
||||||
|
${form.projectLink}
|
||||||
|
|
||||||
|
**插件 import 包名:**
|
||||||
|
|
||||||
|
${form.moduleName}
|
||||||
|
|
||||||
|
**插件项目仓库/主页链接:**
|
||||||
|
|
||||||
|
${form.homepage}
|
||||||
|
|
||||||
|
**标签:**
|
||||||
|
|
||||||
|
${JSON.stringify(tags)}
|
||||||
|
`.trim()
|
||||||
|
).replace(/%2B/gi, "+");
|
||||||
|
window.open(
|
||||||
|
`https://github.com/nonebot/nonebot2/issues/new?title=${title}&body=${body}&labels=Plugin`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const onChange = (event) => {
|
const onChange = (event) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
@ -43,6 +82,27 @@ export default function Adapter(): JSX.Element {
|
|||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
const onChangeLabel = (event) => {
|
||||||
|
setLabel(event.target.value);
|
||||||
|
};
|
||||||
|
const onChangeColor = (color) => {
|
||||||
|
setColor(color.hex);
|
||||||
|
};
|
||||||
|
const validateTag = () => {
|
||||||
|
return label.length >= 1 && label.length <= 10;
|
||||||
|
};
|
||||||
|
const newTag = () => {
|
||||||
|
if (tags.length >= 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (validateTag()) {
|
||||||
|
const tag = { label, color };
|
||||||
|
setTags([...tags, tag]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const delTag = (index: number) => {
|
||||||
|
setTags(tags.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -128,9 +188,47 @@ export default function Adapter(): JSX.Element {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<div className="px-4">
|
||||||
|
<label className="flex flex-wrap">
|
||||||
|
<span className="mr-2">标签:</span>
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<TagComponent
|
||||||
|
key={index}
|
||||||
|
{...tag}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => delTag(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pt-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="px-2 flex-grow rounded bg-light-nonepress-200 dark:bg-dark-nonepress-200"
|
||||||
|
onChange={onChangeLabel}
|
||||||
|
/>
|
||||||
|
<ChromePicker
|
||||||
|
className="mt-2"
|
||||||
|
color={color}
|
||||||
|
disableAlpha={true}
|
||||||
|
onChangeComplete={onChangeColor}
|
||||||
|
/>
|
||||||
|
<div className="flex mt-2">
|
||||||
|
<TagComponent label={label} color={color} />
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]",
|
||||||
|
{ "pointer-events-none opacity-60": !validateTag() }
|
||||||
|
)}
|
||||||
|
onClick={newTag}
|
||||||
|
>
|
||||||
|
添加标签
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-2 flex justify-end">
|
<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 className="px-2 h-9 min-w-[64px] rounded text-hero hover:bg-hero hover:bg-opacity-[.08]" onClick={() => setModalOpen(false)}>
|
||||||
关闭
|
关闭
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
Loading…
x
Reference in New Issue
Block a user