Refactor and enhance UI components across the application

- Improved formatting and consistency in BotConfigPage, HomeSidebar, and Plugin components.
- Removed unnecessary Spin component to prevent layout collapse in BotConfigPage.
- Enhanced sidebar selection logic to reflect current URL path in HomeSidebar.
- Updated layout styles for better responsiveness and visual appeal.
- Implemented mock data fetching in PluginMarketComponent for improved testing and development.
- Added pagination and search functionality in PluginMarketComponent.
- Refactored PluginInstalledComponent to streamline plugin list rendering and modal handling.
- Adjusted CSS styles for better alignment and spacing in various components.
- Removed commented-out code in HttpClient for cleaner codebase.
- Enhanced NotFound component layout for better user experience.
This commit is contained in:
Chris
2025-04-28 23:10:33 +08:00
parent 8eca2cba58
commit ea1a24fd1e
16 changed files with 631 additions and 420 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
align-self: flex-start; align-self: flex-start;
justify-self: flex-start; justify-self: flex-start;
width: calc(100% - 60px); width: calc(100% - 60px);
margin: auto; margin: auto;
display: grid; display: grid;
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr)); grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
+31 -31
View File
@@ -1,15 +1,15 @@
"use client" "use client"
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import styles from "./botConfig.module.css"; import styles from "./botConfig.module.css";
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent"; import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
import {useRouter} from "next/navigation"; import { useRouter } from "next/navigation";
import {BotCardVO} from "@/app/home/bots/components/bot-card/BotCardVO"; import { BotCardVO } from "@/app/home/bots/components/bot-card/BotCardVO";
import {Modal, notification, Spin} from "antd"; import { Modal, notification, Spin } from "antd";
import BotForm from "@/app/home/bots/components/bot-form/BotForm"; import BotForm from "@/app/home/bots/components/bot-form/BotForm";
import BotCard from "@/app/home/bots/components/bot-card/BotCard"; import BotCard from "@/app/home/bots/components/bot-card/BotCard";
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent" import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent"
import {httpClient} from "@/app/infra/http/HttpClient"; import { httpClient } from "@/app/infra/http/HttpClient";
import { Bot } from "@/app/infra/api/api-types"; import { Bot } from "@/app/infra/api/api-types";
export default function BotConfigPage() { export default function BotConfigPage() {
@@ -89,8 +89,9 @@ export default function BotConfigPage() {
} }
return ( return (
<Spin spinning={isLoading}>
<div className={styles.configPageContainer}> <div className={styles.configPageContainer}>
{/* 删除 spin,使用 spin 会导致盒子塌陷。 */}
<Modal <Modal
title={isEditForm ? "编辑机器人" : "创建机器人"} title={isEditForm ? "编辑机器人" : "创建机器人"}
centered centered
@@ -122,36 +123,35 @@ export default function BotConfigPage() {
} }
{pageShowRule === BotConfigPageShowRule.NO_BOT && {pageShowRule === BotConfigPageShowRule.NO_BOT &&
<EmptyAndCreateComponent <EmptyAndCreateComponent
title={"您还未配置机器人哦~"} title={"您还未配置机器人哦~"}
subTitle={"快去创建一个吧!"} subTitle={"快去创建一个吧!"}
buttonText={"创建机器人 +"} buttonText={"创建机器人 +"}
onButtonClick={handleCreateBotClick} onButtonClick={handleCreateBotClick}
/> />
} }
{pageShowRule === BotConfigPageShowRule.HAVE_BOT && {pageShowRule === BotConfigPageShowRule.HAVE_BOT &&
<div className={`${styles.botListContainer}`} <div className={`${styles.botListContainer}`}
> >
{botList.map(cardVO => { {botList.map(cardVO => {
return ( return (
<div <div
key={cardVO.id} key={cardVO.id}
onClick={() => {selectBot(cardVO)}} onClick={() => { selectBot(cardVO) }}
> >
<BotCard botCardVO={cardVO} /> <BotCard botCardVO={cardVO} />
</div>) </div>)
})} })}
<CreateCardComponent <CreateCardComponent
width={360} height={200}
height={200} plusSize={90}
plusSize={90} onClick={handleCreateBotClick}
onClick={handleCreateBotClick} />
/> </div>
</div>
} }
</div> </div>
</Spin>
) )
} }
@@ -1,100 +1,118 @@
"use client"; "use client"
import styles from "./HomeSidebar.module.css"; import styles from "./HomeSidebar.module.css"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import { SidebarChild, SidebarChildVO } from "@/app/home/components/home-sidebar/HomeSidebarChild";
SidebarChild, import { useRouter, usePathname, useSearchParams } from "next/navigation";
SidebarChildVO
} from "@/app/home/components/home-sidebar/HomeSidebarChild";
import { useRouter, usePathname } from "next/navigation";
import { sidebarConfigList } from "@/app/home/components/home-sidebar/sidbarConfigList"; import { sidebarConfigList } from "@/app/home/components/home-sidebar/sidbarConfigList";
// TODO 侧边导航栏要加动画 // TODO 侧边导航栏要加动画
export default function HomeSidebar({ export default function HomeSidebar({
onSelectedChangeAction onSelectedChange
}: { }: {
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void; onSelectedChange: (sidebarChild: SidebarChildVO) => void
}) { }) {
// 路由相关 // 路由相关
const router = useRouter(); const router = useRouter()
const pathname = usePathname(); const pathname = usePathname();
// 路由被动变化时处理 const searchParams = useSearchParams();
useEffect(() => { // 路由被动变化时处理
handleRouteChange(pathname); useEffect(() => {
}, [pathname]); handleRouteChange(pathname)
}, [pathname, searchParams]);
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>( const [selectedChild, setSelectedChild] = useState<SidebarChildVO>(sidebarConfigList[0])
sidebarConfigList[0]
);
useEffect(() => { useEffect(() => {
console.log("HomeSidebar挂载完成"); console.log('HomeSidebar挂载完成');
initSelect(); initSelect()
return () => console.log("HomeSidebar卸载"); return () => console.log('HomeSidebar卸载');
// eslint-disable-next-line react-hooks/exhaustive-deps }, []);
}, []);
function handleChildClick(child: SidebarChildVO) {
setSelectedChild(child);
handleRoute(child);
onSelectedChangeAction(child);
}
function initSelect() {
handleChildClick(sidebarConfigList[0]);
}
function handleRoute(child: SidebarChildVO) { function handleChildClick(child: SidebarChildVO) {
console.log(child); setSelectedChild(child)
router.push(`${child.route}`); handleRoute(child)
} onSelectedChange(child)
function handleRouteChange(pathname: string) {
// TODO 这段逻辑并不好,未来router封装好后改掉
// 判断在home下,并且路由更改的是自己的路由子组件则更新UI
const routeList = pathname.split("/");
if (
routeList[1] === "home" &&
sidebarConfigList.find((childConfig) => childConfig.route === pathname)
) {
console.log("find success");
const routeSelectChild = sidebarConfigList.find(
(childConfig) => childConfig.route === pathname
);
if (routeSelectChild) {
setSelectedChild(routeSelectChild);
}
} }
}
return ( function initSelect() {
<div className={`${styles.sidebarContainer}`}> // 根据当前URL路径选择相应的菜单项,而不是总是使用第一个菜单项
{/* LangBot、ICON区域 */} const currentPath = pathname;
<div className={`${styles.langbotIconContainer}`}> const matchedChild = sidebarConfigList.find(child => child.route === currentPath);
{/* icon */}
<div className={`${styles.langbotIcon}`}>L</div> if (matchedChild) {
<div className={`${styles.langbotText}`}>Langbot</div> // 如果找到匹配的菜单项,则选择它
</div> setSelectedChild(matchedChild);
{/* 菜单列表,后期可升级成配置驱动 */} onSelectedChange(matchedChild);
<div> } else {
{sidebarConfigList.map((config) => { // 如果没有匹配项,则回退到默认选择第一个菜单项
return ( handleChildClick(sidebarConfigList[0]);
<div }
key={config.id} }
onClick={() => {
console.log("click:", config.id); function handleRoute(child: SidebarChildVO) {
handleChildClick(config); console.log(child)
}} router.push(`${child.route}`)
> }
<SidebarChild
isSelected={selectedChild.id === config.id} function handleRouteChange(pathname: string) {
icon={config.icon} // TODO 这段逻辑并不好,未来router封装好后改掉
name={config.name} // 判断在home下,并且路由更改的是自己的路由子组件则更新UI
/> const routeList = pathname.split('/')
if (
routeList[1] === "home" &&
sidebarConfigList.find(childConfig =>
childConfig.route === pathname
)
) {
console.log("find success")
const routeSelectChild = sidebarConfigList.find(childConfig =>
childConfig.route === pathname
)
if (routeSelectChild) {
setSelectedChild(routeSelectChild)
}
}
}
return (
<div className={`${styles.sidebarContainer}`}>
{/* LangBot、ICON区域 */}
<div className={`${styles.langbotIconContainer}`}>
{/* icon */}
<div className={`${styles.langbotIcon}`}>
L
</div>
<div className={`${styles.langbotText}`}>
Langbot
</div>
</div> </div>
); {/* 菜单列表,后期可升级成配置驱动 */}
})} <div>
</div> {
</div> sidebarConfigList.map(config => {
); return (
} <div
key={config.id}
onClick={() => {
console.log('click:', config.id)
handleChildClick(config)
}}
>
<SidebarChild
isSelected={selectedChild.id === config.id}
icon={config.icon}
name={config.name}
/>
</div>
)
})
}
</div>
</div>
);
}
+6 -7
View File
@@ -1,3 +1,4 @@
/* 主布局容器 */
.homeLayoutContainer { .homeLayoutContainer {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@@ -5,14 +6,12 @@
flex-direction: row; flex-direction: row;
} }
/* 主内容区域 */
.main { .main {
background-color: #f5f5f7;
padding: 0;
overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #FAFBFB; padding: 20px;
}
.mainContent {
width: calc(100% - 40px);
height: calc(100% - 110px);
margin: 20px;
} }
+32 -14
View File
@@ -1,30 +1,48 @@
"use client"; "use client"
import "@ant-design/v5-patch-for-react-19"; import '@ant-design/v5-patch-for-react-19';
import styles from "./layout.module.css"; import styles from "./layout.module.css"
import HomeSidebar from "@/app/home/components/home-sidebar/HomeSidebar"; import HomeSidebar from "@/app/home/components/home-sidebar/HomeSidebar";
import HomeTitleBar from "@/app/home/components/home-titlebar/HomeTitleBar"; import HomeTitleBar from "@/app/home/components/home-titlebar/HomeTitleBar";
import React, { useState } from "react"; import React, { useState } from "react";
import { SidebarChildVO } from "@/app/home/components/home-sidebar/HomeSidebarChild"; import { SidebarChildVO } from "@/app/home/components/home-sidebar/HomeSidebarChild";
import { useRouter } from 'next/navigation';
import { Layout } from 'antd';
const { Sider, Content } = Layout;
export default function HomeLayout({ export default function HomeLayout({
children children
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const [title, setTitle] = useState<string>(""); const router = useRouter();
const [title, setTitle] = useState<string>("")
const onSelectedChange = (child: SidebarChildVO) => { const onSelectedChange = (child: SidebarChildVO) => {
setTitle(child.name); setTitle(child.name)
}; }
return ( return (
<div className={`${styles.homeLayoutContainer}`}> <Layout className={styles.homeLayoutContainer}>
<HomeSidebar onSelectedChangeAction={onSelectedChange} /> {/* homeLayoutContainer 是整个容器的入口,使用 flex 的左右布局 */}
<div className={`${styles.main}`}>
<Sider className="left">
<HomeSidebar
onSelectedChange={onSelectedChange}
/>
{/* HomeSidebar 为侧边栏 */}
</Sider>
<Layout className="right">
{/* right 为内容显示区域,right使用 flex 上下布局,right 使用 flex 布局吃掉剩余部分 */}
<HomeTitleBar title={title} /> <HomeTitleBar title={title} />
{/* 主页面 */}
<div className={`${styles.mainContent}`}>{children}</div> <Content className={styles.main}>
</div> {/* mainContent 为主页面 */}
</div> {children}
); </Content>
</Layout>
</Layout>
)
} }
+18 -27
View File
@@ -1,6 +1,6 @@
"use client" "use client"
import { Radio } from 'antd'; import { Radio } from 'antd';
import {useState} from "react"; import { useState } from "react";
import PluginInstalledComponent from "@/app/home/plugins/plugin-installed/PluginInstalledComponent"; import PluginInstalledComponent from "@/app/home/plugins/plugin-installed/PluginInstalledComponent";
import PluginMarketComponent from "@/app/home/plugins/plugin-market/PluginMarketComponent"; import PluginMarketComponent from "@/app/home/plugins/plugin-market/PluginMarketComponent";
import styles from './plugins.module.css' import styles from './plugins.module.css'
@@ -14,33 +14,24 @@ export default function PluginConfigPage() {
const [nowPageType, setNowPageType] = useState(PageType.INSTALLED) const [nowPageType, setNowPageType] = useState(PageType.INSTALLED)
return ( return (
<div className={`${styles.pageContainer}`}> <div className={styles.pageContainer}>
<div> <Radio.Group
<Radio.Group block
block options={[
options={[ { label: '已安装', value: PageType.INSTALLED },
{ label: '已安装', value: PageType.INSTALLED }, { label: '插件市场', value: PageType.MARKET },
{ label: '插件市场', value: PageType.MARKET }, ]}
]} defaultValue={PageType.INSTALLED}
defaultValue={PageType.INSTALLED} value={nowPageType}
value={nowPageType} optionType="button"
optionType="button" buttonStyle="solid"
buttonStyle="solid" style={{ marginBottom: '20px' }}
onChange={(e) => { onChange={(e) => {
// 这里静态类型检测有问题 setNowPageType(e.target.value as PageType)
setNowPageType(e.target.value) }}
}} />
/>
</div>
<div className={`${styles.pageContainer}`}>
{
nowPageType === PageType.INSTALLED && <PluginInstalledComponent/>
}
{
nowPageType === PageType.MARKET && <PluginMarketComponent/>
}
</div>
{nowPageType === PageType.INSTALLED ? <PluginInstalledComponent /> : <PluginMarketComponent />}
</div> </div>
); );
} }
@@ -1,4 +1,4 @@
"use client"; "use client"
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent"; import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO"; import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO";
@@ -7,54 +7,221 @@ import PluginCardComponent from "@/app/home/plugins/plugin-installed/plugin-card
import styles from "@/app/home/plugins/plugins.module.css"; import styles from "@/app/home/plugins/plugins.module.css";
import { Modal, Input } from "antd"; import { Modal, Input } from "antd";
import { GithubOutlined } from "@ant-design/icons"; import { GithubOutlined } from "@ant-design/icons";
import { httpClient } from "@/app/infra/http/HttpClient";
export default function PluginInstalledComponent() { export default function PluginInstalledComponent() {
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]); const [pluginList, setPluginList] = useState<PluginCardVO[]>([])
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false)
const [githubURL, setGithubURL] = useState(""); const [githubURL, setGithubURL] = useState("")
useEffect(() => { useEffect(() => {
initData(); initData()
// eslint-disable-next-line react-hooks/exhaustive-deps }, [])
}, []);
function initData() { function initData() {
getPluginList(); getPluginList().then((value) => {
setPluginList(value)
})
} }
function getPluginList() { async function getPluginList() {
httpClient.getPlugins().then((value) => { return [
setPluginList( new PluginCardVO({
value.plugins.map((plugin) => { description: "一般的描述",
return new PluginCardVO({ handlerCount: 0,
author: plugin.author, name: "插件AAA",
description: plugin.description.zh_CN, author: "/hana",
handlerCount: 0, version: "0.1",
name: plugin.name, isInitialized: false
version: plugin.version, }),
isInitialized: plugin.status === "initialized" new PluginCardVO({
}); description: "一般的描述",
}) handlerCount: 0,
); name: "插件AAA",
}); author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}), new PluginCardVO({
description: "一般的描述",
handlerCount: 0,
name: "插件AAA",
author: "/hana",
version: "0.1",
isInitialized: false
}),
]
} }
function handleModalConfirm() { function handleModalConfirm() {
installPlugin(githubURL); installPlugin(githubURL)
setModalOpen(false); setModalOpen(false)
} }
function installPlugin(url: string) { function installPlugin(url: string) {
httpClient // TODO 接安装Plugin的接口
.installPluginFromGithub(url) console.log("installPlugin: ", url)
.then(() => {
// 安装后重新拉取
getPluginList();
})
.catch((err) => {
console.log("error when install plugin:", err);
});
} }
return ( return (
<div className={`${styles.pluginListContainer}`}> <div className={`${styles.pluginListContainer}`}>
@@ -63,8 +230,8 @@ export default function PluginInstalledComponent() {
<div className={`${styles.modalTitle}`}> <div className={`${styles.modalTitle}`}>
<GithubOutlined <GithubOutlined
style={{ style={{
fontSize: "30px", fontSize: '30px',
marginRight: "20px" marginRight: '20px'
}} }}
type="setting" type="setting"
/> />
@@ -79,7 +246,9 @@ export default function PluginInstalledComponent() {
destroyOnClose={true} destroyOnClose={true}
> >
<div className={`${styles.modalBody}`}> <div className={`${styles.modalBody}`}>
<div> GitHub </div> <div>
GitHub
</div>
<Input <Input
placeholder="请输入插件的Github链接" placeholder="请输入插件的Github链接"
value={githubURL} value={githubURL}
@@ -87,21 +256,20 @@ export default function PluginInstalledComponent() {
/> />
</div> </div>
</Modal> </Modal>
{pluginList.map((vo, index) => { {
return ( pluginList.map((vo, index) => {
<div key={index}> return <div key={index}>
<PluginCardComponent cardVO={vo} /> <PluginCardComponent cardVO={vo} />
</div> </div>
); })
})} }
<CreateCardComponent <CreateCardComponent
width={360}
height={140} height={140}
plusSize={90} plusSize={90}
onClick={() => { onClick={() => {
setModalOpen(true); setModalOpen(true)
}} }}
/> />
</div> </div>
); )
} }
@@ -1,65 +1,47 @@
import styles from "./pluginCard.module.css"; import styles from "./pluginCard.module.css"
import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO"; import { PluginCardVO } from "@/app/home/plugins/plugin-installed/PluginCardVO";
import { GithubOutlined, LinkOutlined, ToolOutlined } from "@ant-design/icons"; import { GithubOutlined, LinkOutlined, ToolOutlined } from '@ant-design/icons';
import { Switch, Tag } from "antd"; import { Tag } from 'antd'
import { useState } from "react";
import { httpClient } from "@/app/infra/http/HttpClient";
export default function PluginCardComponent({ export default function PluginCardComponent({
cardVO cardVO
}: { }: {
cardVO: PluginCardVO; cardVO: PluginCardVO
}) { }) {
const [initialized, setInitialized] = useState(cardVO.isInitialized); return (
const [switchEnable, setSwitchEnable] = useState(true); <div className={`${styles.cardContainer}`}>
{/* header */}
function handleEnable() { <div className={`${styles.cardHeader}`}>
setSwitchEnable(false); {/* left author */}
httpClient <div className={`${styles.fontGray}`}>{cardVO.author}</div>
.togglePlugin(cardVO.author, cardVO.name, !initialized) {/* right icon & version */}
.then(() => { <div className={`${styles.iconVersionContainer}`}>
setInitialized(!initialized); <GithubOutlined
}) style={{ fontSize: '26px' }}
.catch((err) => { type="setting"
console.log("error: ", err); />
}) <Tag color="#108ee9">v{cardVO.version}</Tag>
.finally(() => { </div>
setSwitchEnable(true); </div>
}); {/* content */}
} <div className={`${styles.cardContent}`}>
return ( <div className={`${styles.boldFont}`}>{cardVO.name}</div>
<div className={`${styles.cardContainer}`}> <div className={`${styles.fontGray}`}>{cardVO.description}</div>
{/* header */} </div>
<div className={`${styles.cardHeader}`}> {/* footer */}
{/* left author */} <div className={`${styles.cardFooter}`}>
<div className={`${styles.fontGray}`}>{cardVO.author}</div> <div className={`${styles.linkSettingContainer}`}>
{/* right icon & version */} <div className={`${styles.link}`}>
<div className={`${styles.iconVersionContainer}`}> <LinkOutlined
<GithubOutlined style={{ fontSize: "26px" }} type="setting" /> style={{ fontSize: '22px' }}
<Tag color="#108ee9">v{cardVO.version}</Tag> />
<span>1</span>
</div>
<ToolOutlined
style={{ fontSize: '22px' }}
/>
</div>
</div>
</div> </div>
</div> );
{/* content */}
<div className={`${styles.cardContent}`}>
<div className={`${styles.boldFont}`}>{cardVO.name}</div>
<div className={`${styles.fontGray}`}>{cardVO.description}</div>
</div>
{/* footer */}
<div className={`${styles.cardFooter}`}>
<div className={`${styles.linkSettingContainer}`}>
<div className={`${styles.link}`}>
<LinkOutlined style={{ fontSize: "22px" }} />
<span>1</span>
</div>
<ToolOutlined style={{ fontSize: "22px" }} />
</div>
<Switch
value={initialized}
onClick={handleEnable}
disabled={!switchEnable}
/>
</div>
</div>
);
} }
@@ -1,5 +1,6 @@
.cardContainer { .cardContainer {
width: 360px; width: 100%;
/* 修改为 100% 以撑满整个网格单元 */
height: 140px; height: 140px;
box-sizing: border-box; box-sizing: border-box;
background-color: #FFF; background-color: #FFF;
@@ -40,10 +41,6 @@
.cardFooter { .cardFooter {
width: 90%; width: 90%;
height: 30px; height: 30px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
} }
@@ -73,4 +70,4 @@
align-self: center; align-self: center;
justify-content: space-between; justify-content: space-between;
} }
} }
@@ -1,87 +1,127 @@
"use client"; "use client"
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import styles from "@/app/home/plugins/plugins.module.css"; import styles from "@/app/home/plugins/plugins.module.css";
import { PluginMarketCardVO } from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO"; import { PluginMarketCardVO } from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO";
import PluginMarketCardComponent from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent"; import PluginMarketCardComponent from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent";
import { Input, Pagination } from "antd"; import { Input, Pagination } from "antd";
import { spaceClient } from "@/app/infra/http/HttpClient"; import { debounce } from "lodash"
export default function PluginMarketComponent() { export default function PluginMarketComponent() {
const [marketPluginList, setMarketPluginList] = useState< const [marketPluginList, setMarketPluginList] = useState<PluginMarketCardVO[]>([])
PluginMarketCardVO[] const [searchKeyword, setSearchKeyword] = useState("")
>([]); const [currentPage, setCurrentPage] = useState(1)
const [totalCount, setTotalCount] = useState(0); const [totalItems, setTotalItems] = useState(0)
const [nowPage, setNowPage] = useState(1); const [loading, setLoading] = useState(false)
const [searchKeyword, setSearchKeyword] = useState(""); const pageSize = 10 // 每页显示的项目数量
useEffect(() => { useEffect(() => {
initData(); fetchPlugins(searchKeyword, currentPage)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPage])
}, []);
function initData() { // 获取插件列表,整合了搜索和分页功能
getPluginList(); async function fetchPlugins(keyword: string = "", page: number = 1): Promise<void> {
} setLoading(true)
try {
// 实际应用中,这里应该调用API获取数据
const result = await mockFetchPlugins(keyword, page, pageSize)
setMarketPluginList(result.data)
setTotalItems(result.total)
} finally {
setLoading(false)
}
}
function onInputSearchKeyword(keyword: string) { // 模拟从API获取数据
// 这里记得加防抖,暂时没加 async function mockFetchPlugins(keyword: string, page: number, pageSize: number): Promise<{ data: PluginMarketCardVO[], total: number }> {
setSearchKeyword(keyword); // 模拟API延迟
setNowPage(1); await new Promise(resolve => setTimeout(resolve, 300))
getPluginList(1, keyword);
}
function getPluginList( // 创建模拟数据
page: number = nowPage, const allPlugins: PluginMarketCardVO[] = []
keyword: string = searchKeyword const totalPlugins = 50 // 模拟总数据量
) {
spaceClient.getMarketPlugins(page, 10, keyword).then((res) => {
setMarketPluginList(
res.plugins.map(
(marketPlugin) =>
new PluginMarketCardVO({
author: marketPlugin.author,
description: marketPlugin.description,
githubURL: marketPlugin.repository,
name: marketPlugin.name,
pluginId: String(marketPlugin.ID),
starCount: marketPlugin.stars
})
)
);
setTotalCount(res.total);
console.log("market plugins:", res);
});
}
return ( for (let i = 0; i < totalPlugins; i++) {
<div className={`${styles.marketComponentBody}`}> allPlugins.push(new PluginMarketCardVO({
<Input pluginId: `plugin-${i}`,
style={{ description: `这是插件 ${i} 的描述,包含一些详细信息`,
width: "300px", name: `插件 ${i}`,
marginTop: "10px" author: `/author-${i % 5}`, // 模拟5个不同的作者
}} version: `0.${i % 10}`,
value={searchKeyword} githubURL: `https://github.com/author-${i % 5}/plugin-${i}`,
placeholder="搜索插件" starCount: 10 + Math.floor(Math.random() * 100)
onChange={(e) => onInputSearchKeyword(e.target.value)} }))
/> }
<div className={`${styles.pluginListContainer}`}>
{marketPluginList.map((vo, index) => { // 根据关键词过滤
return ( const filtered = keyword
<div key={index}> ? allPlugins.filter(p =>
<PluginMarketCardComponent cardVO={vo} /> p.name.toLowerCase().includes(keyword.toLowerCase()) ||
p.description.toLowerCase().includes(keyword.toLowerCase()))
: allPlugins
// 分页处理
const start = (page - 1) * pageSize
const end = start + pageSize
const paginatedData = filtered.slice(start, end)
return {
data: paginatedData,
total: filtered.length
}
}
function onInputSearchKeyword(keyword: string) {
setSearchKeyword(keyword)
setCurrentPage(1) // 搜索时重置为第一页
debounceSearch(keyword)
}
const debounceSearch = useCallback(
debounce((keyword: string) => {
fetchPlugins(keyword, 1)
}, 500), []
)
function handlePageChange(page: number) {
setCurrentPage(page)
}
return (
<div className={`${styles.marketComponentBody}`}>
<Input
style={{
width: '300px',
marginBottom: '10px',
}}
value={searchKeyword}
placeholder="搜索插件"
onChange={(e) => onInputSearchKeyword(e.target.value)}
/>
<div className={`${styles.pluginListContainer}`}>
{loading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>...</div>
) : marketPluginList.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}></div>
) : (
marketPluginList.map((vo, index) => (
<div key={`${vo.pluginId}-${index}`}>
<PluginMarketCardComponent cardVO={vo} />
</div>
))
)}
</div> </div>
); {totalItems > 0 && (
})} <div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginTop: '20px' }}>
</div> <Pagination
<Pagination current={currentPage}
defaultCurrent={1} total={totalItems}
total={totalCount} pageSize={pageSize}
onChange={(pageNumber) => { onChange={handlePageChange}
setNowPage(pageNumber); showSizeChanger={false}
getPluginList(pageNumber); />
}} </div>
/> )}
</div> </div>
); )
} }
@@ -5,6 +5,7 @@ export interface IPluginMarketCardVO {
description: string, description: string,
starCount: number, starCount: number,
githubURL: string, githubURL: string,
version: string,
} }
export class PluginMarketCardVO implements IPluginMarketCardVO { export class PluginMarketCardVO implements IPluginMarketCardVO {
@@ -14,6 +15,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
author: string; author: string;
githubURL: string; githubURL: string;
starCount: number; starCount: number;
version: string;
constructor(prop: IPluginMarketCardVO) { constructor(prop: IPluginMarketCardVO) {
this.description = prop.description this.description = prop.description
@@ -22,5 +24,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
this.githubURL = prop.githubURL this.githubURL = prop.githubURL
this.starCount = prop.starCount this.starCount = prop.starCount
this.pluginId = prop.pluginId this.pluginId = prop.pluginId
this.version = prop.version
} }
} }
@@ -1,6 +1,4 @@
.cardContainer { .cardContainer {
width: 360px;
height: 140px;
box-sizing: border-box; box-sizing: border-box;
background-color: #FFF; background-color: #FFF;
border-radius: 9px; border-radius: 9px;
@@ -74,4 +72,4 @@
align-self: center; align-self: center;
justify-content: space-between; justify-content: space-between;
} }
} }
+4 -8
View File
@@ -1,8 +1,8 @@
.pageContainer { .pageContainer {
width: 100%; width: 100%;
height: calc(100% - 30px);
} }
.marketComponentBody { .marketComponentBody {
width: 100%; width: 100%;
height: calc(100% - 60px); height: calc(100% - 60px);
@@ -11,17 +11,13 @@
.pluginListContainer { .pluginListContainer {
align-self: flex-start; align-self: flex-start;
justify-self: flex-start; justify-self: flex-start;
width: calc(100% - 60px); margin: auto;
height: 100%;
max-height: 100%;
margin: auto;
display: grid; display: grid;
grid-template-rows: repeat(auto-fill, minmax(160px, 1fr)); grid-template-rows: repeat(auto-fill, minmax(160px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 15px; gap: 15px;
justify-items: center; /* justify-items: center; */
align-items: center; align-items: center;
overflow-y: scroll;
} }
.modalTitle { .modalTitle {
@@ -35,4 +31,4 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
} }
@@ -1,12 +1,10 @@
import styles from "./createCartComponent.module.css"; import styles from "./createCartComponent.module.css";
export default function CreateCardComponent({ export default function CreateCardComponent({
width,
height, height,
plusSize, plusSize,
onClick, onClick,
}: { }: {
width: number;
height: number; height: number;
plusSize: number; plusSize: number;
onClick: () => void onClick: () => void
@@ -15,7 +13,7 @@ export default function CreateCardComponent({
<div <div
className={`${styles.cardContainer} ${styles.createCardContainer} `} className={`${styles.cardContainer} ${styles.createCardContainer} `}
style={{ style={{
width: `${width}px`, width: `100%`,
height: `${height}px`, height: `${height}px`,
fontSize: `${plusSize}px` fontSize: `${plusSize}px`
}} }}
+3 -3
View File
@@ -129,9 +129,9 @@ class HttpClient {
const errMessage = data?.message || error.message; const errMessage = data?.message || error.message;
switch (status) { switch (status) {
case 401: // case 401:
window.location.href = "/login"; // window.location.href = "/login";
break; // break;
case 403: case 403:
console.error("Permission denied:", errMessage); console.error("Permission denied:", errMessage);
break; break;
+59 -56
View File
@@ -10,66 +10,69 @@ export default function NotFound() {
const router = useRouter(); const router = useRouter();
return ( return (
<Layout style={{ height: '100vh', display: 'flex', background: 'white', justifyContent: 'center' }}> <Row justify="center" align="middle" style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh', background: 'white' }}>
<div className="error-container" style={{ width: '100%', textAlign: 'center' }}> <Row justify="center" align="middle" style={{ minHeight: '100vh' }}>
<div className="error-card" style={{ <Col xs={22} sm={20} md={18} lg={14} xl={10}>
display: 'flex', <div className="error-container" style={{ width: '100%', padding: '20px 0', textAlign: 'center' }}>
flexDirection: 'column', <div className="error-card" style={{
alignItems: 'center', display: 'flex',
padding: '24px' flexDirection: 'column',
}}> alignItems: 'center',
{/* Ant Design 图标,可以换成 Langbot 的 Logo */} padding: '24px'
<div style={{ marginBottom: '20px', maxWidth: '100%', height: 'auto' }}> }}>
<Result {/* Ant Design 图标,可以换成 Langbot 的 Logo */}
status="404" <div style={{ marginBottom: '20px', maxWidth: '100%', height: 'auto' }}>
title={null} <Result
subTitle={null} status="404"
style={{ padding: 0 }} title={null}
/> subTitle={null}
</div> style={{ padding: 0 }}
/>
</div>
<div className="error-text" style={{ textAlign: 'center', marginBottom: '24px' }}> <div className="error-text" style={{ textAlign: 'center', marginBottom: '24px' }}>
<Title level={1} style={{ margin: '0 0 16px 0', fontSize: '72px', fontWeight: 'bold', color: '#333' }}> <Title level={1} style={{ margin: '0 0 16px 0', fontSize: '72px', fontWeight: 'bold', color: '#333' }}>
404 404
</Title> </Title>
<Title level={3} style={{ margin: '0 0 8px 0', fontWeight: 'normal', color: '#333' }}> <Title level={3} style={{ margin: '0 0 8px 0', fontWeight: 'normal', color: '#333' }}>
</Title> </Title>
<Paragraph style={{ fontSize: '16px', color: '#666', maxWidth: '450px', margin: '0 auto 32px auto' }}> <Paragraph style={{ fontSize: '16px', color: '#666', maxWidth: '450px', margin: '0 auto 32px auto' }}>
URL URL
</Paragraph> </Paragraph>
</div> </div>
<div className="error-button" style={{ marginBottom: '24px' }}> <div className="error-button" style={{ marginBottom: '24px' }}>
<Space> <Space>
<Button type="primary" style={{ <Button type="primary" style={{
backgroundColor: '#2288ee', backgroundColor: '#2288ee',
borderColor: '#2288ee', borderColor: '#2288ee',
borderRadius: '4px', borderRadius: '4px',
height: '36px', height: '36px',
padding: '0 16px' padding: '0 16px'
}} onClick={() => router.back()}> }} onClick={() => router.back()}>
</Button> </Button>
<Button style={{ <Button style={{
borderColor: '#d9d9d9', borderColor: '#d9d9d9',
borderRadius: '4px', borderRadius: '4px',
height: '36px', height: '36px',
padding: '0 16px' padding: '0 16px'
}} onClick={() => router.push('/')}> }} onClick={() => router.push('/')}>
</Button> </Button>
</Space> </Space>
</div> </div>
<div className="error-support" style={{ textAlign: 'center', marginTop: '16px' }}> <div className="error-support" style={{ textAlign: 'center', marginTop: '16px' }}>
<Paragraph style={{ fontSize: '14px', color: '#666' }}> <Paragraph style={{ fontSize: '14px', color: '#666' }}>
<a href="mailto:support@qq.com" style={{ color: '#000', textDecoration: 'none' }}>support@qq.com</a> <a href="mailto:support@qq.com" style={{ color: '#000', textDecoration: 'none' }}>support@qq.com</a>
</Paragraph> </Paragraph>
</div>
</div>
</div> </div>
</div> </Col>
</div> </Row>
</Row>
</Layout> </Layout>
); );
} }