Move forms to sidepanel

表单组件迁移到 SidePanel
原 Popup 引导打开 SidePanel
优化表单组件和 Hooks,获取 Form 组件 Ref
This commit is contained in:
奇趣保罗 2024-04-12 14:09:38 +08:00
parent 288de31580
commit 0240b13123
10 changed files with 152 additions and 54 deletions

View File

@ -17,9 +17,11 @@ h2 {
button { button {
padding: 0; padding: 0;
width: 100%;
border: none; border: none;
padding: .75em;
cursor: pointer; cursor: pointer;
background-color: transparent; border-radius: .5em;
} }
img { img {
@ -46,10 +48,6 @@ button, input, textarea {
font: inherit; font: inherit;
} }
form button {
background-color: #eee;
}
textarea { textarea {
resize: vertical; resize: vertical;
} }

View File

@ -1,14 +1,10 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import { sendToBackground } from "@plasmohq/messaging"; import { sendToBackground } from "@plasmohq/messaging";
import useForm from "~hooks/useForm"; import useForm from "~hooks/useForm";
import Tab from "~components/ui/tab"; import Tab from "~components/ui/tab";
import Form from "~components/ui/form"; import Form from "~components/ui/form";
import { add } from "~components/ui/message/utils"; import { add } from "~components/ui/message/utils";
interface ReadProps {
onBack: () => void;
}
interface FormValue { interface FormValue {
title: string; title: string;
link: string; link: string;
@ -46,10 +42,18 @@ const submitForm = (body: FormValue) => {
content: res.msg, content: res.msg,
}); });
} }
}).catch((e) => {
if (e instanceof Error) {
add({
content: e.message,
});
}
}); });
} }
function Read({ onBack }: ReadProps) { function Read() {
const formRef = useRef<HTMLFormElement>();
const { bindInput, setValues, onSubmit } = useForm<FormValue>({ const { bindInput, setValues, onSubmit } = useForm<FormValue>({
initialValues: { initialValues: {
tags: "", tags: "",
@ -68,9 +72,8 @@ function Read({ onBack }: ReadProps) {
return ( return (
<Tab> <Tab>
<Tab.Header title="在看" onBack={onBack} />
<Tab.Body> <Tab.Body>
<Form onSubmit={onSubmit(submitForm)}> <Form ref={formRef} onSubmit={onSubmit(submitForm)}>
<input {...bindInput("title")} required placeholder="标题" /> <input {...bindInput("title")} required placeholder="标题" />
<input {...bindInput("link")} required placeholder="链接" /> <input {...bindInput("link")} required placeholder="链接" />
<div> <div>
@ -97,9 +100,11 @@ function Read({ onBack }: ReadProps) {
<label htmlFor="tags"></label> <label htmlFor="tags"></label>
<input {...bindInput("tags")} placeholder="标签" /> <input {...bindInput("tags")} placeholder="标签" />
</div> </div>
<button type="submit"></button>
</Form> </Form>
</Tab.Body> </Tab.Body>
<Tab.Footer>
<button onClick={() => formRef.current?.requestSubmit()}></button>
</Tab.Footer>
</Tab> </Tab>
); );
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useState, useEffect, useRef } from "react";
import { sendToBackground } from "@plasmohq/messaging"; import { sendToBackground } from "@plasmohq/messaging";
import useForm from "~hooks/useForm"; import useForm from "~hooks/useForm";
import Tab from "~components/ui/tab"; import Tab from "~components/ui/tab";
@ -6,11 +6,7 @@ import Form from "~components/ui/form";
import { add } from "~components/ui/message/utils"; import { add } from "~components/ui/message/utils";
import Placeholder from "~components/ui/placeholder"; import Placeholder from "~components/ui/placeholder";
import styles from "./bili.module.less"; import styles from "./toy.module.less";
interface ReadProps {
onBack: () => void;
}
interface FormValue { interface FormValue {
name: string; name: string;
@ -36,7 +32,9 @@ const getInfo = async () => {
} }
} }
function Toy({ onBack }: ReadProps) { function Toy() {
const formRef = useRef<HTMLFormElement>();
const { bindInput, setValues, onSubmit } = useForm<FormValue>({ const { bindInput, setValues, onSubmit } = useForm<FormValue>({
initialValues: { initialValues: {
type: 3, type: 3,
@ -89,10 +87,9 @@ function Toy({ onBack }: ReadProps) {
}, []); }, []);
return ( return (
<Tab> <Placeholder className={styles.bili} show={imgs.length === 0} value="非 B 站会员购页面">
<Tab.Header title="添加手办" onBack={onBack} /> <Tab>
<Tab.Body> <Tab.Body>
<Placeholder className={styles.bili} show={imgs.length === 0} value="非 B 站会员购页面">
<div className={styles.image}> <div className={styles.image}>
<img src={imgs[currentImg]} alt="" /> <img src={imgs[currentImg]} alt="" />
<div className={styles.selector}> <div className={styles.selector}>
@ -134,12 +131,13 @@ function Toy({ onBack }: ReadProps) {
<label htmlFor="desc"></label> <label htmlFor="desc"></label>
<textarea {...bindInput("desc")} rows={5} placeholder="描述"></textarea> <textarea {...bindInput("desc")} rows={5} placeholder="描述"></textarea>
</div> </div>
<button type="submit"></button>
</Form> </Form>
</Placeholder> </Tab.Body>
</Tab.Body> <Tab.Footer>
</Tab> <button onClick={() => formRef.current?.requestSubmit()}></button>
</Tab.Footer>
</Tab>
</Placeholder>
); );
} }

View File

@ -1,10 +1,10 @@
.bili { .bili {
gap: 1em;
width: 600px;
display: flex;
.image { .image {
flex: 0 0 40%; margin-bottom: 2em;
img {
border-radius: .5em;
}
} }
.selector { .selector {
@ -17,6 +17,7 @@
width: 3em; width: 3em;
height: 3em; height: 3em;
cursor: pointer; cursor: pointer;
border-radius: .5em;
border: 1px solid #eee; border: 1px solid #eee;
} }
} }

View File

@ -1,4 +1,4 @@
import type { FormHTMLAttributes } from "react"; import { useImperativeHandle, type FormHTMLAttributes, useRef, forwardRef, type Ref } from "react";
import { clsn } from "~utils"; import { clsn } from "~utils";
import styles from "./form.module.less"; import styles from "./form.module.less";
@ -6,12 +6,18 @@ interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
} }
function Form({ children, className, ...props }: FormProps) { function Form({ children, className, ...props }: FormProps, ref: Ref<HTMLFormElement>) {
const formRef = useRef();
useImperativeHandle(ref, () => {
return formRef.current;
}, []);
return ( return (
<form className={clsn(styles.form, className)} {...props}> <form ref={formRef} className={clsn(styles.form, className)} {...props}>
{children} {children}
</form> </form>
); );
} }
export default Form; export default forwardRef(Form);

View File

@ -35,4 +35,12 @@ Tab.Body = function Body({ children }: PropsWithChildren) {
); );
} }
Tab.Footer = function Footer({ children }: PropsWithChildren) {
return (
<footer className={styles.footer}>
{children}
</footer>
);
}
export default Tab; export default Tab;

View File

@ -1,7 +1,14 @@
.tab { .tab {
.header, .footer {
left: 0;
right: 0;
padding: .75em 1em;
position: sticky;
background-color: #fff;
}
.header { .header {
padding: .75em; top: 0;
position: relative;
text-align: center; text-align: center;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
@ -27,4 +34,12 @@
.body { .body {
padding: 1em; padding: 1em;
} }
.footer {
bottom: 0;
border-top: 1px solid #eee;
display: flex;
align-items: flex-end;
}
} }

View File

@ -1,7 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import Menu from "./menu"; import Menu from "./menu";
import Read from "./read";
import Toy from "./bili";
import Message from "~components/ui/message"; import Message from "~components/ui/message";
import { clsn } from "~utils"; import { clsn } from "~utils";
import "assets/global.less"; import "assets/global.less";
@ -17,6 +15,7 @@ function IndexPopup() {
const onClickMenu = (value: string) => { const onClickMenu = (value: string) => {
if (value === "options") { if (value === "options") {
chrome.runtime.openOptionsPage(); chrome.runtime.openOptionsPage();
return; return;
} }
else if (value === "home") { else if (value === "home") {
@ -29,24 +28,20 @@ function IndexPopup() {
return; return;
} }
setTab(value); chrome.tabs.query({ active: true, lastFocusedWindow: true }).then((tabs) => {
} const { id, windowId } = tabs[0];
const renderBody = () => { chrome.sidePanel.setOptions({ path: `sidepanel.html?type=${value}` })
if (tab === "read") { chrome.sidePanel.open({ tabId: id, windowId });
return <Read onBack={onBack} />;
}
if (tab === "toy") {
return <Toy onBack={onBack} />;
}
return <Menu onClick={onClickMenu} />; window.close();
});
} }
return ( return (
<> <>
<div className={clsn(styles.root, tab && styles.hasTab)}> <div className={clsn(styles.root, tab && styles.hasTab)}>
{renderBody()} <Menu onClick={onClickMenu} />
</div> </div>
<Message /> <Message />
</> </>

39
sidepanel/index.tsx Normal file
View File

@ -0,0 +1,39 @@
import { useState } from "react";
import Read from "~components/biz/read";
import Toy from "~components/biz/toy";
import Message from "~components/ui/message";
import { clsn } from "~utils";
import "~assets/global.less";
import styles from "./sidepanel.module.less";
const initialType = new URLSearchParams(location.search).get("type") || "read";
const tabItems = [
{ title: "在读", value: "read" },
{ title: "手办", value: "toy" },
{ title: "语录", value: "say" },
];
function SidePanel() {
const [type, setType] = useState(initialType);
return (
<div className={styles.root}>
<nav className={styles.nav}>
{tabItems.map((item) => (
<a className={clsn(type === item.value && styles.active)}
onClick={() => setType(item.value)}
>
{item.title}
</a>
))}
</nav>
{type === "read" && <Read />}
{type === "toy" && <Toy />}
<Message />
</div>
);
}
export default SidePanel;

View File

@ -0,0 +1,33 @@
.root {
color: #333;
font-size: 14px;
.nav {
display: flex;
text-align: center;
border-bottom: 1px solid #eee;
a {
flex: 1;
cursor: pointer;
position: relative;
padding: .75em 1em;
&.active {
color: #28b9be;
&::after {
margin-top: .25em;
position: absolute;
left: calc(50% - .25em);
content: "";
width: .75em;
height: 2px;
display: block;
background-color: currentColor;
}
}
}
}
}