mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
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:
@@ -13,7 +13,7 @@
|
||||
align-self: flex-start;
|
||||
justify-self: flex-start;
|
||||
width: calc(100% - 60px);
|
||||
margin: auto;
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import styles from "./botConfig.module.css";
|
||||
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {BotCardVO} from "@/app/home/bots/components/bot-card/BotCardVO";
|
||||
import {Modal, notification, Spin} from "antd";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { BotCardVO } from "@/app/home/bots/components/bot-card/BotCardVO";
|
||||
import { Modal, notification, Spin } from "antd";
|
||||
import BotForm from "@/app/home/bots/components/bot-form/BotForm";
|
||||
import BotCard from "@/app/home/bots/components/bot-card/BotCard";
|
||||
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";
|
||||
|
||||
export default function BotConfigPage() {
|
||||
@@ -89,8 +89,9 @@ export default function BotConfigPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={isLoading}>
|
||||
<div className={styles.configPageContainer}>
|
||||
|
||||
{/* 删除 spin,使用 spin 会导致盒子塌陷。 */}
|
||||
<Modal
|
||||
title={isEditForm ? "编辑机器人" : "创建机器人"}
|
||||
centered
|
||||
@@ -122,36 +123,35 @@ export default function BotConfigPage() {
|
||||
}
|
||||
|
||||
{pageShowRule === BotConfigPageShowRule.NO_BOT &&
|
||||
<EmptyAndCreateComponent
|
||||
title={"您还未配置机器人哦~"}
|
||||
subTitle={"快去创建一个吧!"}
|
||||
buttonText={"创建机器人 +"}
|
||||
onButtonClick={handleCreateBotClick}
|
||||
/>
|
||||
<EmptyAndCreateComponent
|
||||
title={"您还未配置机器人哦~"}
|
||||
subTitle={"快去创建一个吧!"}
|
||||
buttonText={"创建机器人 +"}
|
||||
onButtonClick={handleCreateBotClick}
|
||||
/>
|
||||
}
|
||||
|
||||
{pageShowRule === BotConfigPageShowRule.HAVE_BOT &&
|
||||
<div className={`${styles.botListContainer}`}
|
||||
>
|
||||
{botList.map(cardVO => {
|
||||
return (
|
||||
<div
|
||||
key={cardVO.id}
|
||||
onClick={() => {selectBot(cardVO)}}
|
||||
>
|
||||
<BotCard botCardVO={cardVO} />
|
||||
</div>)
|
||||
})}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={200}
|
||||
plusSize={90}
|
||||
onClick={handleCreateBotClick}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.botListContainer}`}
|
||||
>
|
||||
{botList.map(cardVO => {
|
||||
return (
|
||||
<div
|
||||
key={cardVO.id}
|
||||
onClick={() => { selectBot(cardVO) }}
|
||||
>
|
||||
<BotCard botCardVO={cardVO} />
|
||||
</div>)
|
||||
})}
|
||||
<CreateCardComponent
|
||||
height={200}
|
||||
plusSize={90}
|
||||
onClick={handleCreateBotClick}
|
||||
/>
|
||||
</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 {
|
||||
SidebarChild,
|
||||
SidebarChildVO
|
||||
} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { SidebarChild, SidebarChildVO } from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { sidebarConfigList } from "@/app/home/components/home-sidebar/sidbarConfigList";
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
export default function HomeSidebar({
|
||||
onSelectedChangeAction
|
||||
onSelectedChange
|
||||
}: {
|
||||
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
|
||||
onSelectedChange: (sidebarChild: SidebarChildVO) => void
|
||||
}) {
|
||||
// 路由相关
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
// 路由被动变化时处理
|
||||
useEffect(() => {
|
||||
handleRouteChange(pathname);
|
||||
}, [pathname]);
|
||||
// 路由相关
|
||||
const router = useRouter()
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
// 路由被动变化时处理
|
||||
useEffect(() => {
|
||||
handleRouteChange(pathname)
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>(
|
||||
sidebarConfigList[0]
|
||||
);
|
||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>(sidebarConfigList[0])
|
||||
|
||||
useEffect(() => {
|
||||
console.log("HomeSidebar挂载完成");
|
||||
initSelect();
|
||||
return () => console.log("HomeSidebar卸载");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
console.log('HomeSidebar挂载完成');
|
||||
initSelect()
|
||||
return () => console.log('HomeSidebar卸载');
|
||||
}, []);
|
||||
|
||||
function handleChildClick(child: SidebarChildVO) {
|
||||
setSelectedChild(child);
|
||||
handleRoute(child);
|
||||
onSelectedChangeAction(child);
|
||||
}
|
||||
|
||||
function initSelect() {
|
||||
handleChildClick(sidebarConfigList[0]);
|
||||
}
|
||||
|
||||
function handleRoute(child: SidebarChildVO) {
|
||||
console.log(child);
|
||||
router.push(`${child.route}`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
function handleChildClick(child: SidebarChildVO) {
|
||||
setSelectedChild(child)
|
||||
handleRoute(child)
|
||||
onSelectedChange(child)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
{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}
|
||||
/>
|
||||
function initSelect() {
|
||||
// 根据当前URL路径选择相应的菜单项,而不是总是使用第一个菜单项
|
||||
const currentPath = pathname;
|
||||
const matchedChild = sidebarConfigList.find(child => child.route === currentPath);
|
||||
|
||||
if (matchedChild) {
|
||||
// 如果找到匹配的菜单项,则选择它
|
||||
setSelectedChild(matchedChild);
|
||||
onSelectedChange(matchedChild);
|
||||
} else {
|
||||
// 如果没有匹配项,则回退到默认选择第一个菜单项
|
||||
handleChildClick(sidebarConfigList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRoute(child: SidebarChildVO) {
|
||||
console.log(child)
|
||||
router.push(`${child.route}`)
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
{
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* 主布局容器 */
|
||||
.homeLayoutContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -5,14 +6,12 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main {
|
||||
background-color: #f5f5f7;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #FAFBFB;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 110px);
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -1,30 +1,48 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import styles from "./layout.module.css";
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
import styles from "./layout.module.css"
|
||||
import HomeSidebar from "@/app/home/components/home-sidebar/HomeSidebar";
|
||||
import HomeTitleBar from "@/app/home/components/home-titlebar/HomeTitleBar";
|
||||
import React, { useState } from "react";
|
||||
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({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState<string>("")
|
||||
const onSelectedChange = (child: SidebarChildVO) => {
|
||||
setTitle(child.name);
|
||||
};
|
||||
setTitle(child.name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.homeLayoutContainer}`}>
|
||||
<HomeSidebar onSelectedChangeAction={onSelectedChange} />
|
||||
<div className={`${styles.main}`}>
|
||||
<Layout className={styles.homeLayoutContainer}>
|
||||
{/* homeLayoutContainer 是整个容器的入口,使用 flex 的左右布局 */}
|
||||
|
||||
<Sider className="left">
|
||||
<HomeSidebar
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
{/* HomeSidebar 为侧边栏 */}
|
||||
</Sider>
|
||||
|
||||
<Layout className="right">
|
||||
{/* right 为内容显示区域,right使用 flex 上下布局,right 使用 flex 布局吃掉剩余部分 */}
|
||||
|
||||
<HomeTitleBar title={title} />
|
||||
{/* 主页面 */}
|
||||
<div className={`${styles.mainContent}`}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Content className={styles.main}>
|
||||
{/* mainContent 为主页面 */}
|
||||
{children}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import { Radio } from 'antd';
|
||||
import {useState} from "react";
|
||||
import { useState } from "react";
|
||||
import PluginInstalledComponent from "@/app/home/plugins/plugin-installed/PluginInstalledComponent";
|
||||
import PluginMarketComponent from "@/app/home/plugins/plugin-market/PluginMarketComponent";
|
||||
import styles from './plugins.module.css'
|
||||
@@ -14,33 +14,24 @@ export default function PluginConfigPage() {
|
||||
const [nowPageType, setNowPageType] = useState(PageType.INSTALLED)
|
||||
|
||||
return (
|
||||
<div className={`${styles.pageContainer}`}>
|
||||
<div>
|
||||
<Radio.Group
|
||||
block
|
||||
options={[
|
||||
{ label: '已安装', value: PageType.INSTALLED },
|
||||
{ label: '插件市场', value: PageType.MARKET },
|
||||
]}
|
||||
defaultValue={PageType.INSTALLED}
|
||||
value={nowPageType}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => {
|
||||
// 这里静态类型检测有问题
|
||||
setNowPageType(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.pageContainer}`}>
|
||||
{
|
||||
nowPageType === PageType.INSTALLED && <PluginInstalledComponent/>
|
||||
}
|
||||
{
|
||||
nowPageType === PageType.MARKET && <PluginMarketComponent/>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.pageContainer}>
|
||||
<Radio.Group
|
||||
block
|
||||
options={[
|
||||
{ label: '已安装', value: PageType.INSTALLED },
|
||||
{ label: '插件市场', value: PageType.MARKET },
|
||||
]}
|
||||
defaultValue={PageType.INSTALLED}
|
||||
value={nowPageType}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
style={{ marginBottom: '20px' }}
|
||||
onChange={(e) => {
|
||||
setNowPageType(e.target.value as PageType)
|
||||
}}
|
||||
/>
|
||||
|
||||
{nowPageType === PageType.INSTALLED ? <PluginInstalledComponent /> : <PluginMarketComponent />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||
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 { Modal, Input } from "antd";
|
||||
import { GithubOutlined } from "@ant-design/icons";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
|
||||
export default function PluginInstalledComponent() {
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [githubURL, setGithubURL] = useState("");
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([])
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [githubURL, setGithubURL] = useState("")
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
initData()
|
||||
}, [])
|
||||
|
||||
function initData() {
|
||||
getPluginList();
|
||||
getPluginList().then((value) => {
|
||||
setPluginList(value)
|
||||
})
|
||||
}
|
||||
|
||||
function getPluginList() {
|
||||
httpClient.getPlugins().then((value) => {
|
||||
setPluginList(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.author,
|
||||
description: plugin.description.zh_CN,
|
||||
handlerCount: 0,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
isInitialized: plugin.status === "initialized"
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
async function getPluginList() {
|
||||
return [
|
||||
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
|
||||
}), new PluginCardVO({
|
||||
description: "一般的描述",
|
||||
handlerCount: 0,
|
||||
name: "插件AAA",
|
||||
author: "/hana",
|
||||
version: "0.1",
|
||||
isInitialized: false
|
||||
}),
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
function handleModalConfirm() {
|
||||
installPlugin(githubURL);
|
||||
setModalOpen(false);
|
||||
installPlugin(githubURL)
|
||||
setModalOpen(false)
|
||||
}
|
||||
|
||||
function installPlugin(url: string) {
|
||||
httpClient
|
||||
.installPluginFromGithub(url)
|
||||
.then(() => {
|
||||
// 安装后重新拉取
|
||||
getPluginList();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error when install plugin:", err);
|
||||
});
|
||||
// TODO 接安装Plugin的接口
|
||||
console.log("installPlugin: ", url)
|
||||
}
|
||||
return (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
@@ -63,8 +230,8 @@ export default function PluginInstalledComponent() {
|
||||
<div className={`${styles.modalTitle}`}>
|
||||
<GithubOutlined
|
||||
style={{
|
||||
fontSize: "30px",
|
||||
marginRight: "20px"
|
||||
fontSize: '30px',
|
||||
marginRight: '20px'
|
||||
}}
|
||||
type="setting"
|
||||
/>
|
||||
@@ -79,7 +246,9 @@ export default function PluginInstalledComponent() {
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<div className={`${styles.modalBody}`}>
|
||||
<div>目前仅支持从 GitHub 安装</div>
|
||||
<div>
|
||||
目前仅支持从 GitHub 安装
|
||||
</div>
|
||||
<Input
|
||||
placeholder="请输入插件的Github链接"
|
||||
value={githubURL}
|
||||
@@ -87,21 +256,20 @@ export default function PluginInstalledComponent() {
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
{pluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{
|
||||
pluginList.map((vo, index) => {
|
||||
return <div key={index}>
|
||||
<PluginCardComponent cardVO={vo} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
}
|
||||
<CreateCardComponent
|
||||
width={360}
|
||||
height={140}
|
||||
plusSize={90}
|
||||
onClick={() => {
|
||||
setModalOpen(true);
|
||||
setModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</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 { GithubOutlined, LinkOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { Switch, Tag } from "antd";
|
||||
import { useState } from "react";
|
||||
import { httpClient } from "@/app/infra/http/HttpClient";
|
||||
import { GithubOutlined, LinkOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import { Tag } from 'antd'
|
||||
|
||||
export default function PluginCardComponent({
|
||||
cardVO
|
||||
cardVO
|
||||
}: {
|
||||
cardVO: PluginCardVO;
|
||||
cardVO: PluginCardVO
|
||||
}) {
|
||||
const [initialized, setInitialized] = useState(cardVO.isInitialized);
|
||||
const [switchEnable, setSwitchEnable] = useState(true);
|
||||
|
||||
function handleEnable() {
|
||||
setSwitchEnable(false);
|
||||
httpClient
|
||||
.togglePlugin(cardVO.author, cardVO.name, !initialized)
|
||||
.then(() => {
|
||||
setInitialized(!initialized);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("error: ", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setSwitchEnable(true);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* header */}
|
||||
<div className={`${styles.cardHeader}`}>
|
||||
{/* left author */}
|
||||
<div className={`${styles.fontGray}`}>{cardVO.author}</div>
|
||||
{/* right icon & version */}
|
||||
<div className={`${styles.iconVersionContainer}`}>
|
||||
<GithubOutlined style={{ fontSize: "26px" }} type="setting" />
|
||||
<Tag color="#108ee9">v{cardVO.version}</Tag>
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
{/* header */}
|
||||
<div className={`${styles.cardHeader}`}>
|
||||
{/* left author */}
|
||||
<div className={`${styles.fontGray}`}>{cardVO.author}</div>
|
||||
{/* right icon & version */}
|
||||
<div className={`${styles.iconVersionContainer}`}>
|
||||
<GithubOutlined
|
||||
style={{ fontSize: '26px' }}
|
||||
type="setting"
|
||||
/>
|
||||
<Tag color="#108ee9">v{cardVO.version}</Tag>
|
||||
</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>
|
||||
</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 {
|
||||
width: 360px;
|
||||
width: 100%;
|
||||
/* 修改为 100% 以撑满整个网格单元 */
|
||||
height: 140px;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFF;
|
||||
@@ -40,10 +41,6 @@
|
||||
.cardFooter {
|
||||
width: 90%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,4 +70,4 @@
|
||||
align-self: center;
|
||||
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 { PluginMarketCardVO } from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardVO";
|
||||
import PluginMarketCardComponent from "@/app/home/plugins/plugin-market/plugin-market-card/PluginMarketCardComponent";
|
||||
import { Input, Pagination } from "antd";
|
||||
import { spaceClient } from "@/app/infra/http/HttpClient";
|
||||
import { debounce } from "lodash"
|
||||
|
||||
export default function PluginMarketComponent() {
|
||||
const [marketPluginList, setMarketPluginList] = useState<
|
||||
PluginMarketCardVO[]
|
||||
>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [nowPage, setNowPage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [marketPluginList, setMarketPluginList] = useState<PluginMarketCardVO[]>([])
|
||||
const [searchKeyword, setSearchKeyword] = useState("")
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const pageSize = 10 // 每页显示的项目数量
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetchPlugins(searchKeyword, currentPage)
|
||||
}, [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) {
|
||||
// 这里记得加防抖,暂时没加
|
||||
setSearchKeyword(keyword);
|
||||
setNowPage(1);
|
||||
getPluginList(1, keyword);
|
||||
}
|
||||
// 模拟从API获取数据
|
||||
async function mockFetchPlugins(keyword: string, page: number, pageSize: number): Promise<{ data: PluginMarketCardVO[], total: number }> {
|
||||
// 模拟API延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
function getPluginList(
|
||||
page: number = nowPage,
|
||||
keyword: string = searchKeyword
|
||||
) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
// 创建模拟数据
|
||||
const allPlugins: PluginMarketCardVO[] = []
|
||||
const totalPlugins = 50 // 模拟总数据量
|
||||
|
||||
return (
|
||||
<div className={`${styles.marketComponentBody}`}>
|
||||
<Input
|
||||
style={{
|
||||
width: "300px",
|
||||
marginTop: "10px"
|
||||
}}
|
||||
value={searchKeyword}
|
||||
placeholder="搜索插件"
|
||||
onChange={(e) => onInputSearchKeyword(e.target.value)}
|
||||
/>
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{marketPluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<PluginMarketCardComponent cardVO={vo} />
|
||||
for (let i = 0; i < totalPlugins; i++) {
|
||||
allPlugins.push(new PluginMarketCardVO({
|
||||
pluginId: `plugin-${i}`,
|
||||
description: `这是插件 ${i} 的描述,包含一些详细信息`,
|
||||
name: `插件 ${i}`,
|
||||
author: `/author-${i % 5}`, // 模拟5个不同的作者
|
||||
version: `0.${i % 10}`,
|
||||
githubURL: `https://github.com/author-${i % 5}/plugin-${i}`,
|
||||
starCount: 10 + Math.floor(Math.random() * 100)
|
||||
}))
|
||||
}
|
||||
|
||||
// 根据关键词过滤
|
||||
const filtered = keyword
|
||||
? allPlugins.filter(p =>
|
||||
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>
|
||||
<Pagination
|
||||
defaultCurrent={1}
|
||||
total={totalCount}
|
||||
onChange={(pageNumber) => {
|
||||
setNowPage(pageNumber);
|
||||
getPluginList(pageNumber);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
{totalItems > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginTop: '20px' }}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
total={totalItems}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IPluginMarketCardVO {
|
||||
description: string,
|
||||
starCount: number,
|
||||
githubURL: string,
|
||||
version: string,
|
||||
}
|
||||
|
||||
export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
@@ -14,6 +15,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
author: string;
|
||||
githubURL: string;
|
||||
starCount: number;
|
||||
version: string;
|
||||
|
||||
constructor(prop: IPluginMarketCardVO) {
|
||||
this.description = prop.description
|
||||
@@ -22,5 +24,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
|
||||
this.githubURL = prop.githubURL
|
||||
this.starCount = prop.starCount
|
||||
this.pluginId = prop.pluginId
|
||||
this.version = prop.version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.cardContainer {
|
||||
width: 360px;
|
||||
height: 140px;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFF;
|
||||
border-radius: 9px;
|
||||
@@ -74,4 +72,4 @@
|
||||
align-self: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
.pageContainer {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
|
||||
.marketComponentBody {
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
@@ -11,17 +11,13 @@
|
||||
.pluginListContainer {
|
||||
align-self: flex-start;
|
||||
justify-self: flex-start;
|
||||
width: calc(100% - 60px);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
/* justify-items: center; */
|
||||
align-items: center;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
@@ -35,4 +31,4 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import styles from "./createCartComponent.module.css";
|
||||
|
||||
export default function CreateCardComponent({
|
||||
width,
|
||||
height,
|
||||
plusSize,
|
||||
onClick,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
plusSize: number;
|
||||
onClick: () => void
|
||||
@@ -15,7 +13,7 @@ export default function CreateCardComponent({
|
||||
<div
|
||||
className={`${styles.cardContainer} ${styles.createCardContainer} `}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
width: `100%`,
|
||||
height: `${height}px`,
|
||||
fontSize: `${plusSize}px`
|
||||
}}
|
||||
|
||||
@@ -129,9 +129,9 @@ class HttpClient {
|
||||
const errMessage = data?.message || error.message;
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
window.location.href = "/login";
|
||||
break;
|
||||
// case 401:
|
||||
// window.location.href = "/login";
|
||||
// break;
|
||||
case 403:
|
||||
console.error("Permission denied:", errMessage);
|
||||
break;
|
||||
|
||||
@@ -10,66 +10,69 @@ export default function NotFound() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh', display: 'flex', background: 'white', justifyContent: 'center' }}> <Row justify="center" align="middle" style={{ minHeight: '100vh' }}>
|
||||
<div className="error-container" style={{ width: '100%', textAlign: 'center' }}>
|
||||
<div className="error-card" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '24px'
|
||||
}}>
|
||||
{/* Ant Design 图标,可以换成 Langbot 的 Logo */}
|
||||
<div style={{ marginBottom: '20px', maxWidth: '100%', height: 'auto' }}>
|
||||
<Result
|
||||
status="404"
|
||||
title={null}
|
||||
subTitle={null}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<Layout style={{ minHeight: '100vh', background: 'white' }}>
|
||||
<Row justify="center" align="middle" style={{ minHeight: '100vh' }}>
|
||||
<Col xs={22} sm={20} md={18} lg={14} xl={10}>
|
||||
<div className="error-container" style={{ width: '100%', padding: '20px 0', textAlign: 'center' }}>
|
||||
<div className="error-card" style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '24px'
|
||||
}}>
|
||||
{/* Ant Design 图标,可以换成 Langbot 的 Logo */}
|
||||
<div style={{ marginBottom: '20px', maxWidth: '100%', height: 'auto' }}>
|
||||
<Result
|
||||
status="404"
|
||||
title={null}
|
||||
subTitle={null}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="error-text" style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<Title level={1} style={{ margin: '0 0 16px 0', fontSize: '72px', fontWeight: 'bold', color: '#333' }}>
|
||||
404
|
||||
</Title>
|
||||
<Title level={3} style={{ margin: '0 0 8px 0', fontWeight: 'normal', color: '#333' }}>
|
||||
页面不存在
|
||||
</Title>
|
||||
<Paragraph style={{ fontSize: '16px', color: '#666', maxWidth: '450px', margin: '0 auto 32px auto' }}>
|
||||
您要查找的页面似乎不存在。请检查您输入的 URL 是否正确,或者返回首页。
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="error-text" style={{ textAlign: 'center', marginBottom: '24px' }}>
|
||||
<Title level={1} style={{ margin: '0 0 16px 0', fontSize: '72px', fontWeight: 'bold', color: '#333' }}>
|
||||
404
|
||||
</Title>
|
||||
<Title level={3} style={{ margin: '0 0 8px 0', fontWeight: 'normal', color: '#333' }}>
|
||||
页面不存在
|
||||
</Title>
|
||||
<Paragraph style={{ fontSize: '16px', color: '#666', maxWidth: '450px', margin: '0 auto 32px auto' }}>
|
||||
您要查找的页面似乎不存在。请检查您输入的 URL 是否正确,或者返回首页。
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<div className="error-button" style={{ marginBottom: '24px' }}>
|
||||
<Space>
|
||||
<Button type="primary" style={{
|
||||
backgroundColor: '#2288ee',
|
||||
borderColor: '#2288ee',
|
||||
borderRadius: '4px',
|
||||
height: '36px',
|
||||
padding: '0 16px'
|
||||
}} onClick={() => router.back()}>
|
||||
上一级
|
||||
</Button>
|
||||
<Button style={{
|
||||
borderColor: '#d9d9d9',
|
||||
borderRadius: '4px',
|
||||
height: '36px',
|
||||
padding: '0 16px'
|
||||
}} onClick={() => router.push('/')}>
|
||||
返回主页
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="error-button" style={{ marginBottom: '24px' }}>
|
||||
<Space>
|
||||
<Button type="primary" style={{
|
||||
backgroundColor: '#2288ee',
|
||||
borderColor: '#2288ee',
|
||||
borderRadius: '4px',
|
||||
height: '36px',
|
||||
padding: '0 16px'
|
||||
}} onClick={() => router.back()}>
|
||||
上一级
|
||||
</Button>
|
||||
<Button style={{
|
||||
borderColor: '#d9d9d9',
|
||||
borderRadius: '4px',
|
||||
height: '36px',
|
||||
padding: '0 16px'
|
||||
}} onClick={() => router.push('/')}>
|
||||
返回主页
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="error-support" style={{ textAlign: 'center', marginTop: '16px' }}>
|
||||
<Paragraph style={{ fontSize: '14px', color: '#666' }}>
|
||||
需要帮助吗?您可以联系 <a href="mailto:support@qq.com" style={{ color: '#000', textDecoration: 'none' }}>support@qq.com</a>
|
||||
</Paragraph>
|
||||
<div className="error-support" style={{ textAlign: 'center', marginTop: '16px' }}>
|
||||
<Paragraph style={{ fontSize: '14px', color: '#666' }}>
|
||||
需要帮助吗?您可以联系 <a href="mailto:support@qq.com" style={{ color: '#000', textDecoration: 'none' }}>support@qq.com</a>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user