Merge pull request #1351 from baicai99/feat/webui-refactor

feat:重构并改进应用的用户界面组件
This commit is contained in:
HYana
2025-04-29 17:04:31 +08:00
committed by GitHub
17 changed files with 490 additions and 442 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));
+147 -141
View File
@@ -1,162 +1,168 @@
"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() {
const router = useRouter(); const router = useRouter();
const [pageShowRule, setPageShowRule] = useState<BotConfigPageShowRule>(BotConfigPageShowRule.NO_BOT) const [pageShowRule, setPageShowRule] = useState<BotConfigPageShowRule>(
const [modalOpen, setModalOpen] = useState<boolean>(false); BotConfigPageShowRule.NO_BOT
const [botList, setBotList] = useState<BotCardVO[]>([]) );
const [isEditForm, setIsEditForm] = useState(false) const [modalOpen, setModalOpen] = useState<boolean>(false);
const [nowSelectedBotCard, setNowSelectedBotCard] = useState<BotCardVO>() const [botList, setBotList] = useState<BotCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false) const [isEditForm, setIsEditForm] = useState(false);
const [nowSelectedBotCard, setNowSelectedBotCard] = useState<BotCardVO>();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// TODO:补齐加载转圈逻辑
setIsLoading(true);
checkHasLLM().then((hasLLM) => {
if (hasLLM) {
getBotList();
} else {
setPageShowRule(BotConfigPageShowRule.NO_LLM);
setIsLoading(false);
}
});
}, []);
useEffect(() => { async function checkHasLLM(): Promise<boolean> {
// TODO:补齐加载转圈逻辑 // NOT IMPL
setIsLoading(true) return true;
checkHasLLM().then((hasLLM) => { }
if (hasLLM) {
getBotList()
} else {
setPageShowRule(BotConfigPageShowRule.NO_LLM)
setIsLoading(false)
}
})
}, [])
async function checkHasLLM(): Promise<boolean> { function getBotList() {
// NOT IMPL httpClient
return true .getBots()
} .then((resp) => {
const botList: BotCardVO[] = resp.bots.map((bot: Bot) => {
return new BotCardVO({
adapter: bot.adapter,
description: bot.description,
id: bot.uuid || "",
name: bot.name,
updateTime: bot.updated_at || "",
pipelineName: bot.use_pipeline_name || ""
});
});
if (botList.length === 0) {
setPageShowRule(BotConfigPageShowRule.NO_BOT);
} else {
setPageShowRule(BotConfigPageShowRule.HAVE_BOT);
}
setBotList(botList);
})
.catch((err) => {
console.error("get bot list error", err);
// TODO HACK: need refactor to hook mode Notification, but it's not working under render
notification.error({
message: "获取机器人列表失败",
description: err.message,
placement: "bottomRight"
});
})
.finally(() => {
setIsLoading(false);
});
}
function getBotList() { function handleCreateBotClick() {
httpClient.getBots().then((resp) => { setIsEditForm(false);
const botList: BotCardVO[] = resp.bots.map((bot: Bot) => { setNowSelectedCard(undefined);
return new BotCardVO({ setModalOpen(true);
adapter: bot.adapter, }
description: bot.description,
id: bot.uuid || "",
name: bot.name,
updateTime: bot.updated_at || "",
pipelineName: bot.use_pipeline_name || "",
})
})
if (botList.length === 0) {
setPageShowRule(BotConfigPageShowRule.NO_BOT)
} else {
setPageShowRule(BotConfigPageShowRule.HAVE_BOT)
}
setBotList(botList)
}).catch((err) => {
console.error("get bot list error", err)
// TODO HACK: need refactor to hook mode Notification, but it's not working under render
notification.error({
message: "获取机器人列表失败",
description: err.message,
placement: "bottomRight",
})
}).finally(() => {
setIsLoading(false)
})
}
function handleCreateBotClick() { function setNowSelectedCard(cardVO: BotCardVO | undefined) {
setIsEditForm(false) setNowSelectedBotCard(cardVO);
setNowSelectedCard(undefined) }
setModalOpen(true);
}
function setNowSelectedCard(cardVO: BotCardVO | undefined) { function selectBot(cardVO: BotCardVO) {
setNowSelectedBotCard(cardVO) setIsEditForm(true);
} setNowSelectedCard(cardVO);
console.log("set now vo", cardVO);
setModalOpen(true);
}
function selectBot(cardVO: BotCardVO) { return (
setIsEditForm(true) <div className={styles.configPageContainer}>
setNowSelectedCard(cardVO) <Spin spinning={isLoading} tip="加载中..." size="large">
console.log("set now vo", cardVO) <Modal
setModalOpen(true) title={isEditForm ? "编辑机器人" : "创建机器人"}
} centered
open={modalOpen}
onOk={() => setModalOpen(false)}
onCancel={() => setModalOpen(false)}
width={700}
footer={null}
destroyOnClose={true}
>
<BotForm
initBotId={nowSelectedBotCard?.id}
onFormSubmit={() => {
getBotList();
setModalOpen(false);
}}
onFormCancel={() => setModalOpen(false)}
/>
</Modal>
{pageShowRule === BotConfigPageShowRule.NO_LLM && (
<EmptyAndCreateComponent
title={"需要先创建大模型才能配置机器人哦~"}
subTitle={"快去创建一个吧!"}
buttonText={"创建大模型 GO"}
onButtonClick={() => {
router.push("/home/models");
}}
/>
)}
return ( {pageShowRule === BotConfigPageShowRule.NO_BOT && (
<Spin spinning={isLoading}> <EmptyAndCreateComponent
<div className={styles.configPageContainer}> title={"您还未配置机器人哦~"}
<Modal subTitle={"快去创建一个吧!"}
title={isEditForm ? "编辑机器人" : "创建机器人"} buttonText={"创建机器人 +"}
centered onButtonClick={handleCreateBotClick}
open={modalOpen} />
onOk={() => setModalOpen(false)} )}
onCancel={() => setModalOpen(false)} </Spin>
width={700} {/* 注意:其余的返回内容需要保持在Spin组件外部 */}
footer={null} {pageShowRule === BotConfigPageShowRule.HAVE_BOT && (
destroyOnClose={true} <div className={`${styles.botListContainer}`}>
> {botList.map((cardVO) => {
<BotForm return (
initBotId={nowSelectedBotCard?.id} <div
onFormSubmit={() => { key={cardVO.id}
getBotList() onClick={() => {
setModalOpen(false) selectBot(cardVO);
}} }}
onFormCancel={() => setModalOpen(false)} >
/> <BotCard botCardVO={cardVO} />
</Modal> </div>
{pageShowRule === BotConfigPageShowRule.NO_LLM && );
<EmptyAndCreateComponent })}
title={"需要先创建大模型才能配置机器人哦~"} <CreateCardComponent
subTitle={"快去创建一个吧!"} height={200}
buttonText={"创建大模型 GO"} plusSize={90}
onButtonClick={() => { onClick={handleCreateBotClick}
router.push("/home/models"); />
}}
/>
}
{pageShowRule === BotConfigPageShowRule.NO_BOT &&
<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> </div>
</Spin> )}
) </div>
);
} }
enum BotConfigPageShowRule { enum BotConfigPageShowRule {
NO_LLM, NO_LLM,
NO_BOT, NO_BOT,
HAVE_BOT, HAVE_BOT
} }
@@ -67,34 +67,42 @@ export default function HomeSidebar({
} }
} }
return (
<div className={`${styles.sidebarContainer}`}> return (
{/* LangBot、ICON区域 */} <div className={`${styles.sidebarContainer}`}>
<div className={`${styles.langbotIconContainer}`}> {/* LangBot、ICON区域 */}
{/* icon */} <div className={`${styles.langbotIconContainer}`}>
<div className={`${styles.langbotIcon}`}>L</div> {/* icon */}
<div className={`${styles.langbotText}`}>Langbot</div> <div className={`${styles.langbotIcon}`}>
</div> L
{/* 菜单列表,后期可升级成配置驱动 */} </div>
<div> <div className={`${styles.langbotText}`}>
{sidebarConfigList.map((config) => { Langbot
return ( </div>
<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>
</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;
} }
+33 -15
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 onSelectedChange = (child: SidebarChildVO) => { const [title, setTitle] = useState<string>("")
setTitle(child.name); const onSelectedChangeAction = (child: SidebarChildVO) => {
}; 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
onSelectedChangeAction={onSelectedChangeAction}
/>
{/* 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,8 +1,8 @@
"use client"; "use client";
import { useState, useEffect } from "react";
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";
import { useEffect, useState } from "react";
import PluginCardComponent from "@/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent"; import PluginCardComponent from "@/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent";
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";
@@ -66,16 +66,12 @@ export default function PluginInstalledComponent() {
fontSize: "30px", fontSize: "30px",
marginRight: "20px" marginRight: "20px"
}} }}
type="setting"
/> />
<span> GitHub </span>
</div> </div>
} }
centered
open={modalOpen} open={modalOpen}
onOk={() => handleModalConfirm()} onOk={handleModalConfirm}
onCancel={() => setModalOpen(false)} onCancel={() => setModalOpen(false)}
width={500}
destroyOnClose={true} destroyOnClose={true}
> >
<div className={`${styles.modalBody}`}> <div className={`${styles.modalBody}`}>
@@ -95,7 +91,6 @@ export default function PluginInstalledComponent() {
); );
})} })}
<CreateCardComponent <CreateCardComponent
width={360}
height={140} height={140}
plusSize={90} plusSize={90}
onClick={() => { onClick={() => {
@@ -46,19 +46,22 @@ export default function PluginCardComponent({
</div> </div>
{/* footer */} {/* footer */}
<div className={`${styles.cardFooter}`}> <div className={`${styles.cardFooter}`}>
<div className={`${styles.linkSettingContainer}`}> <div className={`${styles.footerContainer}`}>
<div className={`${styles.link}`}> <div className={`${styles.linkAndToolContainer}`}>
<LinkOutlined style={{ fontSize: "22px" }} /> <div className={`${styles.link}`}>
<span>1</span> <LinkOutlined style={{ fontSize: "22px" }} />
<span>1</span>
</div>
<ToolOutlined style={{ fontSize: "22px" }} />
</div>
<div className={`${styles.switchContainer}`}>
<Switch
value={initialized}
onClick={handleEnable}
disabled={!switchEnable}
/>
</div> </div>
<ToolOutlined style={{ fontSize: "22px" }} />
</div> </div>
<Switch
value={initialized}
onClick={handleEnable}
disabled={!switchEnable}
/>
</div> </div>
</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,12 +41,29 @@
.cardFooter { .cardFooter {
width: 90%; width: 90%;
height: 30px; height: 30px;
position: relative;
}
.footerContainer {
width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.linkAndToolContainer {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.switchContainer {
display: flex;
justify-content: flex-end;
}
.fontGray { .fontGray {
color: #6C6C6C; color: #6C6C6C;
@@ -8,80 +8,100 @@ import { Input, Pagination } from "antd";
import { spaceClient } from "@/app/infra/http/HttpClient"; import { spaceClient } from "@/app/infra/http/HttpClient";
export default function PluginMarketComponent() { export default function PluginMarketComponent() {
const [marketPluginList, setMarketPluginList] = useState< const [marketPluginList, setMarketPluginList] = useState<
PluginMarketCardVO[] PluginMarketCardVO[]
>([]); >([]);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [nowPage, setNowPage] = useState(1); const [nowPage, setNowPage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState(""); const [searchKeyword, setSearchKeyword] = useState("");
const [loading, setLoading] = useState(false);
const pageSize = 10;
useEffect(() => { useEffect(() => {
initData(); initData();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function initData() { function initData() {
getPluginList(); getPluginList();
} }
function onInputSearchKeyword(keyword: string) { function onInputSearchKeyword(keyword: string) {
// 这里记得加防抖,暂时没加 // 这里记得加防抖,暂时没加
setSearchKeyword(keyword); setSearchKeyword(keyword);
setNowPage(1); setNowPage(1);
getPluginList(1, keyword); getPluginList(1, keyword);
} }
function getPluginList( function getPluginList(
page: number = nowPage, page: number = nowPage,
keyword: string = searchKeyword keyword: string = searchKeyword
) { ) {
spaceClient.getMarketPlugins(page, 10, keyword).then((res) => { setLoading(true);
setMarketPluginList( spaceClient.getMarketPlugins(page, pageSize, keyword).then((res) => {
res.plugins.map( setMarketPluginList(
(marketPlugin) => res.plugins.map(
new PluginMarketCardVO({ (marketPlugin) =>
author: marketPlugin.author, new PluginMarketCardVO({
description: marketPlugin.description, author: marketPlugin.author,
githubURL: marketPlugin.repository, description: marketPlugin.description,
name: marketPlugin.name, githubURL: marketPlugin.repository,
pluginId: String(marketPlugin.ID), name: marketPlugin.name,
starCount: marketPlugin.stars pluginId: String(marketPlugin.ID),
}) starCount: marketPlugin.stars,
) version: "version" in marketPlugin ? String(marketPlugin.version) : "1.0.0", // Default version if not provided
); })
setTotalCount(res.total); )
console.log("market plugins:", res); );
}); setTotalCount(res.total);
} setLoading(false);
console.log("market plugins:", res);
}).catch(error => {
console.error("获取插件列表失败:", error);
setLoading(false);
});
}
return ( function handlePageChange(page: number) {
<div className={`${styles.marketComponentBody}`}> setNowPage(page);
<Input getPluginList(page);
style={{ }
width: "300px",
marginTop: "10px" return (
}} <div className={`${styles.marketComponentBody}`}>
value={searchKeyword} <Input
placeholder="搜索插件" style={{
onChange={(e) => onInputSearchKeyword(e.target.value)} width: '300px',
/> marginBottom: '10px',
<div className={`${styles.pluginListContainer}`}> }}
{marketPluginList.map((vo, index) => { value={searchKeyword}
return ( placeholder="搜索插件"
<div key={index}> onChange={(e) => onInputSearchKeyword(e.target.value)}
<PluginMarketCardComponent cardVO={vo} /> />
<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>
); {totalCount > 0 && (
})} <div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginTop: '20px' }}>
</div> <Pagination
<Pagination current={nowPage}
defaultCurrent={1} total={totalCount}
total={totalCount} pageSize={pageSize}
onChange={(pageNumber) => { onChange={handlePageChange}
setNowPage(pageNumber); showSizeChanger={false}
getPluginList(pageNumber); />
}} </div>
/> )}
</div> </div>
); )
} }
@@ -1,56 +1,49 @@
import styles from "./pluginMarketCard.module.css" import styles from "./pluginMarketCard.module.css";
import {GithubOutlined, StarOutlined} from '@ant-design/icons'; import { GithubOutlined, StarOutlined } from "@ant-design/icons";
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 {Button} from "antd"; import { Button } from "antd";
export default function PluginMarketCardComponent({ export default function PluginMarketCardComponent({
cardVO cardVO
}: { }: {
cardVO: PluginMarketCardVO cardVO: PluginMarketCardVO;
}) { }) {
function handleInstallClick(pluginId: string) {
console.log("Install plugin: ", pluginId);
}
return (
function handleInstallClick (pluginId: string) { <div className={`${styles.cardContainer}`}>
console.log("Install plugin: ", pluginId) {/* header */}
} <div className={`${styles.cardHeader}`}>
{/* left author */}
return ( <div className={`${styles.fontGray}`}>{cardVO.author}</div>
<div className={`${styles.cardContainer}`}> {/* right icon */}
{/* header */} <GithubOutlined style={{ fontSize: "26px" }} type="setting" />
<div className={`${styles.cardHeader}`}> </div>
{/* left author */} {/* content */}
<div className={`${styles.fontGray}`}>{cardVO.author}</div> <div className={`${styles.cardContent}`}>
{/* right icon */} <div className={`${styles.boldFont}`}>{cardVO.name}</div>
<GithubOutlined <div className={`${styles.fontGray}`}>{cardVO.description}</div>
style={{fontSize: '26px'}} </div>
type="setting" {/* footer */}
/> <div className={`${styles.cardFooter}`}>
</div> <div className={`${styles.linkSettingContainer}`}>
{/* content */} <div className={`${styles.link}`}>
<div className={`${styles.cardContent}`}> <StarOutlined style={{ fontSize: "22px" }} />
<div className={`${styles.boldFont}`}>{cardVO.name}</div> <span style={{ paddingLeft: "5px" }}>{cardVO.starCount}</span>
<div className={`${styles.fontGray}`}>{cardVO.description}</div> </div>
</div>
{/* footer */}
<div className={`${styles.cardFooter}`}>
<div className={`${styles.linkSettingContainer}`}>
<div className={`${styles.link}`}>
<StarOutlined
style={{fontSize: '22px'}}
/>
<span>{cardVO.starCount}</span>
</div>
</div>
<Button
type="primary"
size={"small"}
onClick={() => {
handleInstallClick(cardVO.pluginId)
}}
>
</Button>
</div>
</div> </div>
); <Button
type="primary"
size={"small"}
onClick={() => {
handleInstallClick(cardVO.pluginId);
}}
>
</Button>
</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;
@@ -73,5 +71,6 @@
color: #6062E7; color: #6062E7;
align-self: center; align-self: center;
justify-content: space-between; justify-content: space-between;
align-items: center;
} }
} }
+3 -7
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 {
@@ -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`
}} }}
+9 -11
View File
@@ -26,7 +26,8 @@ import {
ApiRespSystemInfo, ApiRespSystemInfo,
ApiRespAsyncTasks, ApiRespAsyncTasks,
ApiRespAsyncTask, ApiRespAsyncTask,
ApiRespUserToken, MarketPluginResponse ApiRespUserToken,
MarketPluginResponse
} from "../api/api-types"; } from "../api/api-types";
import { notification } from "antd"; import { notification } from "antd";
@@ -50,22 +51,19 @@ export interface RequestConfig extends AxiosRequestConfig {
class HttpClient { class HttpClient {
private instance: AxiosInstance; private instance: AxiosInstance;
private disableToken: boolean = false private disableToken: boolean = false;
// 暂不需要SSR // 暂不需要SSR
// private ssrInstance: AxiosInstance | null = null // private ssrInstance: AxiosInstance | null = null
constructor( constructor(baseURL?: string, disableToken?: boolean) {
baseURL?: string,
disableToken?: boolean
) {
this.instance = axios.create({ this.instance = axios.create({
baseURL: baseURL || this.getBaseUrl(), baseURL: baseURL || this.getBaseUrl(),
timeout: 15000, timeout: 15000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
} }
}); });
this.disableToken = disableToken || false this.disableToken = disableToken || false;
this.initInterceptors(); this.initInterceptors();
} }
@@ -358,7 +356,7 @@ class HttpClient {
public getMarketPlugins( public getMarketPlugins(
page: number, page: number,
page_size: number, page_size: number,
query: string, query: string
): Promise<MarketPluginResponse> { ): Promise<MarketPluginResponse> {
return this.post(`/api/v1/market/plugins`, { return this.post(`/api/v1/market/plugins`, {
page, page,
@@ -366,7 +364,7 @@ class HttpClient {
query, query,
sort_by: "stars", sort_by: "stars",
sort_order: "DESC" sort_order: "DESC"
}) });
} }
public installPluginFromGithub( public installPluginFromGithub(
source: string source: string
@@ -415,4 +413,4 @@ class HttpClient {
export const httpClient = new HttpClient("https://version-4.langbot.dev"); export const httpClient = new HttpClient("https://version-4.langbot.dev");
// 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法 // 临时写法,未来两种Client都继承自HttpClient父类,不允许共享方法
export const spaceClient = new HttpClient("https://space.langbot.app") export const spaceClient = new HttpClient("https://space.langbot.app");
+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>
); );
} }