feat: 新增右侧侧边栏中的两个组件 智能编排和组件市场

master
钟良源 6 months ago
parent 167c687a5b
commit 9ce06903fc

@ -0,0 +1,194 @@
import React, { useState, useRef, useEffect } from 'react';
import { Avatar, Input, Button, List, Typography, Tabs } from '@arco-design/web-react';
import { IconSend, IconRobot, IconUser } from '@arco-design/web-react/icon';
import styles from './style/chatBox.module.less';
const { TextArea } = Input;
const TabPane = Tabs.TabPane;
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: Date;
}
const ChatBox: React.FC = () => {
const [activeTab, setActiveTab] = useState('1');
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
content: '你好我是AI助手有什么我可以帮助你的吗',
role: 'assistant',
timestamp: new Date()
}
]);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// 滚动到最新消息
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 发送消息
const handleSend = async () => {
if (!inputValue.trim() || loading) return;
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
content: inputValue,
role: 'user',
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setLoading(true);
try {
// 模拟AI回复实际项目中这里会调用API
setTimeout(() => {
let response = '';
switch (activeTab) {
case '1':
response = `关于应用编排,我收到你的消息:"${inputValue}"。应用编排可以帮助你设计和管理复杂的应用流程。`;
break;
case '2':
response = `关于组件编排,我收到你的消息:"${inputValue}"。组件编排可以让你灵活组合和配置各种功能组件。`;
break;
case '3':
response = `关于组件推荐,我收到你的消息:"${inputValue}"。基于你的需求我推荐以下组件1.数据表格组件 2.图表组件 3.表单组件。`;
break;
default:
response = `我收到了你的消息:"${inputValue}"。这是一个模拟回复实际项目中这里会接入AI接口。`;
}
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
content: response,
role: 'assistant',
timestamp: new Date()
};
setMessages(prev => [...prev, aiMessage]);
setLoading(false);
}, 1000);
} catch (error) {
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
content: '抱歉,我遇到了一些问题,请稍后再试。',
role: 'assistant',
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
setLoading(false);
}
};
// 处理回车发送
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
return (
<div className={styles['chat-container']}>
<div className={styles['chat-header']}>
<Tabs
activeTab={activeTab}
onChange={setActiveTab}
type="rounded"
size="small"
>
<TabPane
key="1"
title={
<span>
</span>
}
/>
<TabPane
key="2"
title={
<span>
</span>
}
/>
<TabPane
key="3"
title={
<span>
</span>
}
/>
</Tabs>
</div>
<div className={styles['chat-messages']}>
<List
dataSource={messages}
render={(item) => (
<List.Item key={item.id} className={styles['message-item']}>
<div className={`${styles['message-content']} ${styles[item.role]}`}>
<div className={styles['message-avatar']}>
<Avatar size={28} style={{
backgroundColor: item.role === 'user' ? '#165dff' : '#626aea'
}}>
{item.role === 'user' ? <IconUser /> : <IconRobot />}
</Avatar>
</div>
<div className={styles['message-main']}>
<div className={styles['message-text']}>
{item.content}
</div>
<div className={styles['message-time']}>
{formatTime(item.timestamp)}
</div>
</div>
</div>
</List.Item>
)}
/>
<div ref={messagesEndRef} />
</div>
<div className={styles['chat-input']}>
<TextArea
value={inputValue}
onChange={setInputValue}
onPressEnter={handleKeyPress}
placeholder={`${['应用编排', '组件编排', '组件推荐'][Number(activeTab) - 1]}中输入消息...`}
autoSize={{ minRows: 2, maxRows: 4 }}
disabled={loading}
/>
<Button
type="primary"
icon={<IconSend />}
onClick={handleSend}
loading={loading}
style={{ marginTop: 10 }}
>
</Button>
</div>
</div>
);
};
export default ChatBox;

@ -1,10 +1,8 @@
import React, { useState } from 'react';
import { Layout } from '@arco-design/web-react';
import styles from './style/index.module.less';
import SideBar from './sideBar';
import LogBar from './logBar';
const Content = Layout.Content;
import RightSideBar from './rightSideBar';
interface Selected {
currentPath?: string;
@ -53,6 +51,7 @@ function IDEContainer() {
<LogBar></LogBar>
</div>
<RightSideBar></RightSideBar>
</div>
</>
);

@ -0,0 +1,198 @@
import React from 'react';
import { Card, Grid, Input, Tag, Typography, Divider, Avatar } from '@arco-design/web-react';
import {
IconSearch,
IconStar,
IconDownload,
IconUser,
IconCalendar,
IconFilter
} from '@arco-design/web-react/icon';
import styles from './style/market.module.less';
const { Row, Col } = Grid;
const { Title, Text } = Typography;
// 模拟组件数据
const components = [
{
id: 1,
name: '数据表格',
description: '功能强大的数据展示表格,支持排序、筛选、分页等功能',
author: '张三',
avatar: '',
downloads: 12560,
rating: 4.8,
tags: ['数据展示', '表格', '交互'],
category: '数据展示',
updateTime: '2023-10-15'
},
{
id: 2,
name: '表单生成器',
description: '可视化表单构建工具,支持多种表单元素和校验规则',
author: '李四',
avatar: '',
downloads: 8920,
rating: 4.6,
tags: ['表单', '生成器', '可视化'],
category: '表单',
updateTime: '2023-10-18'
},
{
id: 3,
name: '图表组件',
description: '基于ECharts的图表组件库支持多种图表类型',
author: '王五',
avatar: '',
downloads: 15630,
rating: 4.9,
tags: ['图表', '数据可视化', 'ECharts'],
category: '数据可视化',
updateTime: '2023-10-10'
},
{
id: 4,
name: '流程设计器',
description: '可视化流程设计工具,支持拖拽式流程编排',
author: '赵六',
avatar: '',
downloads: 6750,
rating: 4.5,
tags: ['流程', '设计器', '可视化'],
category: '流程',
updateTime: '2023-10-20'
},
{
id: 5,
name: '文件上传',
description: '支持多种文件格式上传,包含进度显示和校验功能',
author: '孙七',
avatar: '',
downloads: 9800,
rating: 4.7,
tags: ['文件', '上传', '工具'],
category: '工具',
updateTime: '2023-10-12'
},
{
id: 6,
name: '通知中心',
description: '系统通知管理组件,支持多种通知类型和样式',
author: '周八',
avatar: '',
downloads: 5420,
rating: 4.3,
tags: ['通知', '消息', '系统'],
category: '反馈',
updateTime: '2023-10-05'
},
];
// 分类数据
const categories = [
{ id: 1, name: '全部组件', count: 24 },
{ id: 2, name: '数据展示', count: 8 },
{ id: 3, name: '表单', count: 6 },
{ id: 4, name: '数据可视化', count: 5 },
{ id: 5, name: '流程', count: 3 },
{ id: 6, name: '工具', count: 7 },
{ id: 7, name: '反馈', count: 4 },
];
const Market: React.FC = () => {
return (
<div className={styles['market-container']}>
{/* 头部搜索区域 */}
<div className={styles['market-header']}>
<Title heading={4}></Title>
<Text type="secondary">使</Text>
<div className={styles['search-section']}>
<Input
size="large"
placeholder="搜索组件名称、功能或标签..."
prefix={<IconSearch />}
style={{ width: 400, marginRight: 16 }}
/>
<div className={styles['filter-tags']}>
<Tag icon={<IconFilter />} color="arcoblue"></Tag>
</div>
</div>
</div>
<Divider style={{ margin: '20px 0' }} />
{/* 分类导航 */}
<div className={styles['category-section']}>
<div className={styles['category-list']}>
{categories.map(category => (
<Tag
key={category.id}
className={styles['category-tag']}
size="large"
>
{category.name} <span className={styles['category-count']}>({category.count})</span>
</Tag>
))}
</div>
</div>
{/* 组件列表 */}
<div className={styles['components-section']}>
<Row gutter={20}>
{components.map(component => (
<Col span={12} key={component.id} className={styles['component-col']}>
<Card className={styles['component-card']} hoverable>
<div className={styles['card-header']}>
<div className={styles['component-title']}>
<Title heading={6}>{component.name}</Title>
<Tag color="arcoblue">{component.category}</Tag>
</div>
<Text type="secondary" ellipsis>{component.description}</Text>
</div>
<div className={styles['card-content']}>
<div className={styles['component-meta']}>
<div className={styles['meta-item']}>
<IconUser />
<span>{component.author}</span>
</div>
<div className={styles['meta-item']}>
<IconCalendar />
<span>{component.updateTime}</span>
</div>
</div>
<div className={styles['component-tags']}>
{component.tags.map((tag, index) => (
<Tag key={index} color="gray">{tag}</Tag>
))}
</div>
</div>
<div className={styles['card-footer']}>
<div className={styles['component-stats']}>
<div className={styles['stat-item']}>
<IconDownload />
<span>{component.downloads.toLocaleString()}</span>
</div>
<div className={styles['stat-item']}>
<IconStar />
<span>{component.rating}</span>
</div>
</div>
<div className={styles['component-actions']}>
<Text className={styles['install-btn']}></Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
</div>
);
};
export default Market;

@ -0,0 +1,112 @@
import React, { useState } from 'react';
import styles from './style/rightSideBar.module.less';
import { Tabs, ResizeBox } from '@arco-design/web-react';
import { IconApps, IconRobot } from '@arco-design/web-react/icon';
import ChatBox from './chatBox';
import Market from './market';
const TabPane = Tabs.TabPane;
const RightSideBar: React.FC = () => {
const [activeTab, setActiveTab] = useState('1');
const [isExpanded, setIsExpanded] = useState(true);
const [manualWidth, setManualWidth] = useState<number | null>(null); // 记录手动调整的宽度
const handleTabClick = (key: string) => {
if (key === activeTab) {
// 如果有手动调整的宽度,点击时重置为自动控制
if (manualWidth !== null) {
setManualWidth(null);
}
setIsExpanded(!isExpanded);
} else {
setActiveTab(key);
setIsExpanded(true);
// 切换标签时重置手动宽度
setManualWidth(null);
}
};
const getResizeBoxWidth = (tabKey: string) => {
// 如果有手动调整的宽度且当前标签是激活状态,则使用手动宽度
if (manualWidth !== null && activeTab === tabKey && isExpanded) {
return manualWidth;
}
if (!isExpanded || activeTab !== tabKey) {
return 0;
}
return 350;
};
// 处理 ResizeBox 拖动事件
const handleResize = (e: MouseEvent, { width }: { width: number }) => {
if (width > 0) {
setManualWidth(width);
// 如果之前是收起状态,拖动后应该展开
if (!isExpanded) {
setIsExpanded(true);
}
}
};
return (
<>
<div className={styles['right-side-bar']}>
<Tabs
key="card"
tabPosition="right"
className={`${styles.verticalTabs} right-side-bar-tabs`}
activeTab={activeTab}
>
<TabPane
key="1"
title={
<span onClick={() => handleTabClick('1')}>
<IconApps style={{ fontSize: 16 }} />
<span></span>
</span>
}
>
<ResizeBox
className={styles['right-resize-box']}
directions={['left']}
style={{
width: getResizeBoxWidth('1'),
maxWidth: '100%',
minWidth: 0
}}
onMoving={handleResize}
>
<ChatBox></ChatBox>
</ResizeBox>
</TabPane>
<TabPane
key="2"
title={
<span onClick={() => handleTabClick('2')}>
<IconRobot style={{ fontSize: 16 }} />
<span></span>
</span>
}
>
<ResizeBox
className={styles['right-resize-box']}
directions={['left']}
style={{
width: getResizeBoxWidth('2'),
maxWidth: '100%',
minWidth: 0
}}
onMoving={handleResize}
>
<Market></Market>
</ResizeBox>
</TabPane>
</Tabs>
</div>
</>
);
};
export default RightSideBar;

@ -0,0 +1,98 @@
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: #fff;
border-left: 1px solid #e5e5e5;
}
.chat-header {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #e5e5e5;
}
.chat-messages {
flex: 1;
overflow-y: auto;
.message-item {
border-bottom: none !important;
padding: 8px 0;
.message-content {
display: flex;
flex-direction: row;
&.user {
flex-direction: row-reverse;
.message-main {
align-items: flex-end;
margin-right: 12px;
margin-left: 60px;
.message-text {
background-color: #626aea;
color: #fff;
border-radius: 16px 4px 16px 16px;
}
.message-time {
text-align: right;
}
}
}
&.assistant {
.message-main {
margin-left: 12px;
margin-right: 60px;
.message-text {
background-color: #f2f3f5;
color: #1d2129;
border-radius: 4px 16px 16px 16px;
}
.message-time {
text-align: left;
}
}
}
.message-avatar {
flex-shrink: 0;
}
.message-main {
display: flex;
flex-direction: column;
flex: 1;
.message-text {
padding: 10px 12px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.message-time {
font-size: 12px;
color: #86909c;
margin-top: 4px;
}
}
}
}
}
.chat-input {
padding: 16px;
border-top: 1px solid #e5e5e5;
.arco-btn {
float: right;
}
}

@ -13,7 +13,7 @@
min-width: @layout-max-width;
transition: padding-left 0.2s;
box-sizing: border-box;
padding-left: 220px;
padding-left: 200px;
.mainContent {

@ -0,0 +1,125 @@
.market-container {
padding: 20px;
height: 100%;
overflow-y: auto;
background-color: #fff;
.market-header {
.search-section {
margin-top: 20px;
display: flex;
align-items: center;
.filter-tags {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.category-section {
margin: 20px 0;
.category-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
.category-tag {
cursor: pointer;
transition: all 0.2s;
&:hover {
transform: translateY(-2px);
}
.category-count {
color: #86909c;
}
}
}
}
.components-section {
.component-col {
margin-bottom: 20px;
}
.component-card {
height: 100%;
display: flex;
flex-direction: column;
.card-header {
flex: 1;
margin-bottom: 16px;
.component-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
}
.card-content {
flex: 1;
margin-bottom: 16px;
.component-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #86909c;
}
}
.component-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #f2f3f5;
padding-top: 16px;
.component-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #86909c;
}
}
.component-actions {
.install-btn {
color: #626aea;
cursor: pointer;
font-weight: 500;
&:hover {
color: #4169e1;
}
}
}
}
}
}
}

@ -0,0 +1,67 @@
.right-side-bar {
height: calc(100% - 60px);
position: absolute;
right: 0;
top: 60px;
z-index: 100;
background-color: #fff;
// 只针对右侧边栏的第一层 Tabs
:global {
.right-side-bar-tabs {
height: 100%;
> .arco-tabs-header-nav {
.arco-tabs-header-title {
padding: 0;
margin-top: 0;
}
.arco-tabs-header-title-text {
padding: 10px;
writing-mode: vertical-rl;
text-orientation: mixed;
display: flex;
align-items: center;
justify-content: center;
span {
padding-top: 5px;
}
.arco-icon {
margin-left: 5px;
}
}
.arco-tabs-tab-active {
font-weight: bold;
}
.arco-tabs-bar-vertical {
width: 40px;
}
}
}
.arco-tabs-vertical {
height: 100%;
.arco-tabs-content {
padding: 0;
.arco-tabs-content-inner {
height: 100%;
.arco-tabs-pane {
height: 100%;
.arco-resizebox {
height: 100%;
}
}
}
}
}
}
}
Loading…
Cancel
Save