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

View File

@@ -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>
);
}

View File

@@ -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>
);
)
}

View File

@@ -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>
);
);
}

View File

@@ -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;
}
}
}

View File

@@ -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>
)
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}