feat(componentMarket): 实现组件详情页的 Markdown 渲染与复制功能

master
钟良源 2 months ago
parent a76a8bbdc0
commit 43fbbb6fba

@ -6,6 +6,7 @@ import { isSSR } from '@/utils/is';
interface EditorSectionProps { interface EditorSectionProps {
initialContent?: string; initialContent?: string;
onChange?: (content: string) => void;
} }
// 创建一个 Viewer 组件用于服务端渲染 // 创建一个 Viewer 组件用于服务端渲染
@ -33,9 +34,9 @@ const EditorViewer: React.FC<{ content: string }> = ({ content }) => {
return <div ref={viewerRef} dangerouslySetInnerHTML={{ __html: content || '' }} />; return <div ref={viewerRef} dangerouslySetInnerHTML={{ __html: content || '' }} />;
}; };
export default function EditorSection({ initialContent }: EditorSectionProps) { export default function EditorSection({ initialContent, onChange }: EditorSectionProps) {
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const editorRef = useRef<typeof ToastEditor | null>(null); const editorRef = useRef<any>(null);
useEffect(() => { useEffect(() => {
if (!isSSR) { if (!isSSR) {
@ -63,7 +64,18 @@ export default function EditorSection({ initialContent }: EditorSectionProps) {
return ( return (
<div className="mt-8"> <div className="mt-8">
<DynamicEditor initialValue={initialContent} initialEditType="markdown" /> <DynamicEditor
initialValue={initialContent}
initialEditType="markdown"
onChange={() => {
// 获取编辑器实例并读取内容
const editorInstance = editorRef.current?.getInstance?.();
if (editorInstance && onChange) {
const content = editorInstance.getMarkdown();
onChange(content);
}
}}
/>
</div> </div>
); );
} }

@ -592,7 +592,7 @@ const GlobalVarContainer = () => {
</div> </div>
{/*数据列表*/} {/*数据列表*/}
<div className={styles['comp-list-list']}> {selectedItem !== '组件审核' && (<div className={styles['comp-list-list']}>
<Table <Table
columns={columns} columns={columns}
data={componentData} data={componentData}
@ -612,7 +612,7 @@ const GlobalVarContainer = () => {
onChange={handlePageChange} onChange={handlePageChange}
/> />
</div> </div>
</div> </div>)}
</div> </div>
</div> </div>

@ -3,12 +3,16 @@ import styles from './style/compDetails.module.less';
import { Space, Divider, Button, Typography, Card, Image, Rate, Grid } from '@arco-design/web-react'; import { Space, Divider, Button, Typography, Card, Image, Rate, Grid } from '@arco-design/web-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getComponentMarket } from '@/api/componentMarket'; import { getComponentMarket } from '@/api/componentMarket';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import CopyComponentModal from './copyComponentModal';
const { Row, Col } = Grid; const { Row, Col } = Grid;
const CompDetails = ({ compInfo }) => { const CompDetails = ({ compInfo }) => {
const [componentList, setComponentList] = useState([]); const [componentList, setComponentList] = useState([]);
const [currentCompInfo, setCurrentCompInfo] = useState<any>(compInfo); const [currentCompInfo, setCurrentCompInfo] = useState<any>(compInfo);
const [copyModalVisible, setCopyModalVisible] = useState(false);
// 获取组件市场数据 // 获取组件市场数据
const fetchComponentData = async () => { const fetchComponentData = async () => {
@ -24,160 +28,6 @@ const CompDetails = ({ compInfo }) => {
if (res?.code === 200 && res?.data) { if (res?.code === 200 && res?.data) {
setComponentList(res.data.list || []); setComponentList(res.data.list || []);
setComponentList([
{
'baseConfigJson': [],
'codeLanguage': 'Python',
'collaboratorId': 0,
'componentBaseId': '1993166956502499330',
'componentClassify': '设备数采与控制交互组件',
'componentStatus': 'DEPLOYED',
'componentVersion': 1,
'createBy': '1992851580222222338',
'createTime': 1764228733000,
'definitionJson': '{"apis":[{"apiType":"EVENT","eventApi":{"topic":"add"},"fieldIns":["a"],"fieldOuts":["v"],"id":"add","restApi":{}}],"dataIns":[{"dataType":"INTEGER","desc":"","id":"a","type":"DATA"}],"dataOuts":[{"dataType":"INTEGER","desc":"","id":"v","type":"DATA"}]}',
'deployType': 'Python',
'desc': '',
'dockerImageId': '',
'dockerImageName': '',
'extraSystemId': '',
'filesName': '',
'id': '1993945941293449218',
'identifier': 'admin_add_num_maket1',
'isDeleted': 0,
'localProjectPath': '/000000/admin_add_num_maket1/master',
'logoUrl': '',
'name': '两数之和2',
'operatesJson': [],
'permission': null,
'projectId': 'add_num_maket1',
'publicStatus': 1,
'publishTime': null,
'reviewOpinion': '',
'size': '106.30 KB',
'star': 0,
'status': 1,
'tags': [],
'tenantId': '000000',
'updateBy': '1992851580222222338',
'updateTime': 1764232122000,
'version': 'master'
},
{
'baseConfigJson': [],
'codeLanguage': 'Python',
'collaboratorId': 0,
'componentBaseId': '1993166956502499330',
'componentClassify': '设备数采与控制交互组件',
'componentStatus': 'DEPLOYED',
'componentVersion': 1,
'createBy': '1992851580222222338',
'createTime': 1764228733000,
'definitionJson': '{"apis":[{"apiType":"EVENT","eventApi":{"topic":"add"},"fieldIns":["a"],"fieldOuts":["v"],"id":"add","restApi":{}}],"dataIns":[{"dataType":"INTEGER","desc":"","id":"a","type":"DATA"}],"dataOuts":[{"dataType":"INTEGER","desc":"","id":"v","type":"DATA"}]}',
'deployType': 'Python',
'desc': '',
'dockerImageId': '',
'dockerImageName': '',
'extraSystemId': '',
'filesName': '',
'id': '1993945941293449218',
'identifier': 'admin_add_num_maket1',
'isDeleted': 0,
'localProjectPath': '/000000/admin_add_num_maket1/master',
'logoUrl': '',
'name': '两数之和3',
'operatesJson': [],
'permission': null,
'projectId': 'add_num_maket1',
'publicStatus': 1,
'publishTime': null,
'reviewOpinion': '',
'size': '106.30 KB',
'star': 0,
'status': 1,
'tags': [],
'tenantId': '000000',
'updateBy': '1992851580222222338',
'updateTime': 1764232122000,
'version': 'master'
},
{
'baseConfigJson': [],
'codeLanguage': 'Python',
'collaboratorId': 0,
'componentBaseId': '1993166956502499330',
'componentClassify': '设备数采与控制交互组件',
'componentStatus': 'DEPLOYED',
'componentVersion': 1,
'createBy': '1992851580222222338',
'createTime': 1764228733000,
'definitionJson': '{"apis":[{"apiType":"EVENT","eventApi":{"topic":"add"},"fieldIns":["a"],"fieldOuts":["v"],"id":"add","restApi":{}}],"dataIns":[{"dataType":"INTEGER","desc":"","id":"a","type":"DATA"}],"dataOuts":[{"dataType":"INTEGER","desc":"","id":"v","type":"DATA"}]}',
'deployType': 'Python',
'desc': '',
'dockerImageId': '',
'dockerImageName': '',
'extraSystemId': '',
'filesName': '',
'id': '1993945941293449218',
'identifier': 'admin_add_num_maket1',
'isDeleted': 0,
'localProjectPath': '/000000/admin_add_num_maket1/master',
'logoUrl': '',
'name': '两数之和4',
'operatesJson': [],
'permission': null,
'projectId': 'add_num_maket1',
'publicStatus': 1,
'publishTime': null,
'reviewOpinion': '',
'size': '106.30 KB',
'star': 0,
'status': 1,
'tags': [],
'tenantId': '000000',
'updateBy': '1992851580222222338',
'updateTime': 1764232122000,
'version': 'master'
},
{
'baseConfigJson': [],
'codeLanguage': 'Python',
'collaboratorId': 0,
'componentBaseId': '1993166956502499330',
'componentClassify': '设备数采与控制交互组件',
'componentStatus': 'DEPLOYED',
'componentVersion': 1,
'createBy': '1992851580222222338',
'createTime': 1764228733000,
'definitionJson': '{"apis":[{"apiType":"EVENT","eventApi":{"topic":"add"},"fieldIns":["a"],"fieldOuts":["v"],"id":"add","restApi":{}}],"dataIns":[{"dataType":"INTEGER","desc":"","id":"a","type":"DATA"}],"dataOuts":[{"dataType":"INTEGER","desc":"","id":"v","type":"DATA"}]}',
'deployType': 'Python',
'desc': '',
'dockerImageId': '',
'dockerImageName': '',
'extraSystemId': '',
'filesName': '',
'id': '1993945941293449218',
'identifier': 'admin_add_num_maket1',
'isDeleted': 0,
'localProjectPath': '/000000/admin_add_num_maket1/master',
'logoUrl': '',
'name': '两数之和5',
'operatesJson': [],
'permission': null,
'projectId': 'add_num_maket1',
'publicStatus': 1,
'publishTime': null,
'reviewOpinion': '',
'size': '106.30 KB',
'star': 0,
'status': 1,
'tags': [],
'tenantId': '000000',
'updateBy': '1992851580222222338',
'updateTime': 1764232122000,
'version': 'master'
}
]);
} }
else { else {
setComponentList([]); setComponentList([]);
@ -192,6 +42,11 @@ const CompDetails = ({ compInfo }) => {
fetchComponentData(); fetchComponentData();
}, []); }, []);
// 处理复制组件
const handleCopyComponent = () => {
setCopyModalVisible(true);
};
// 渲染组件外壳 // 渲染组件外壳
const renderCompHousing = () => { const renderCompHousing = () => {
@ -278,11 +133,68 @@ const CompDetails = ({ compInfo }) => {
{currentCompInfo.description ? currentCompInfo.description : ' - '} {currentCompInfo.description ? currentCompInfo.description : ' - '}
</div> </div>
<Divider style={{ borderBottomStyle: 'dashed' }} /> <Divider style={{ borderBottomStyle: 'dashed' }} />
<div>md</div> <div className={styles['markdown-content']}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 自定义组件样式
h1: ({ node, ...props }) => <h1
style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }} {...props} />,
h2: ({ node, ...props }) => <h2
style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '14px' }} {...props} />,
h3: ({ node, ...props }) => <h3
style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '12px' }} {...props} />,
p: ({ node, ...props }) => <p style={{ marginBottom: '12px', lineHeight: '1.6' }} {...props} />,
code: ({ node, inline, ...props }: any) =>
inline
? <code style={{
backgroundColor: '#f6f6f6',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '0.9em'
}} {...props} />
: <code style={{
display: 'block',
backgroundColor: '#f6f6f6',
padding: '12px',
borderRadius: '4px',
overflow: 'auto',
fontSize: '0.9em'
}} {...props} />,
pre: ({ node, ...props }) => <pre style={{
backgroundColor: '#f6f6f6',
padding: '12px',
borderRadius: '4px',
overflow: 'auto'
}} {...props} />,
ul: ({ node, ...props }) => <ul style={{ marginLeft: '20px', marginBottom: '12px' }} {...props} />,
ol: ({ node, ...props }) => <ol style={{ marginLeft: '20px', marginBottom: '12px' }} {...props} />,
li: ({ node, ...props }) => <li style={{ marginBottom: '6px', lineHeight: '1.6' }} {...props} />,
blockquote: ({ node, ...props }) => <blockquote style={{
borderLeft: '4px solid #ddd',
paddingLeft: '16px',
color: '#666',
margin: '12px 0'
}} {...props} />,
a: ({ node, ...props }) => <a style={{ color: '#165DFF', textDecoration: 'none' }} {...props} />,
table: ({ node, ...props }) => <table
style={{ borderCollapse: 'collapse', width: '100%', marginBottom: '12px' }} {...props} />,
th: ({ node, ...props }) => <th style={{
border: '1px solid #ddd',
padding: '8px',
backgroundColor: '#f6f6f6',
fontWeight: 'bold'
}} {...props} />,
td: ({ node, ...props }) => <td style={{ border: '1px solid #ddd', padding: '8px' }} {...props} />
}}
>
{currentCompInfo.readme || currentCompInfo.desc || '暂无详细说明'}
</ReactMarkdown>
</div>
<Divider style={{ borderBottomStyle: 'dashed' }} /> <Divider style={{ borderBottomStyle: 'dashed' }} />
<div className={styles['handel-box']}> <div className={styles['handel-box']}>
<Button type="text">: {dayjs(currentCompInfo.updateTime).format('YYYY-MM-DD HH:mm:ss')}</Button> <Button type="text">: {dayjs(currentCompInfo.updateTime).format('YYYY-MM-DD HH:mm:ss')}</Button>
<Button type="text"></Button> <Button type="text" onClick={handleCopyComponent}></Button>
</div> </div>
<Divider style={{ borderBottomStyle: 'dashed' }} /> <Divider style={{ borderBottomStyle: 'dashed' }} />
</div> </div>
@ -328,12 +240,22 @@ const CompDetails = ({ compInfo }) => {
</Card> </Card>
<div className={styles['comp-card-footer']}> <div className={styles['comp-card-footer']}>
<div className={styles['comp-type']}>{item.componentClassify || '-'}</div> <div className={styles['comp-type']}>{item.componentClassify || '-'}</div>
<div className={styles['comp-language']}>{item.deployType || '-'}</div> <div className={styles['comp-language']}>{item.codeLanguage || '-'}</div>
</div> </div>
</Col> </Col>
))} ))}
</Row> </Row>
</div> </div>
{/* 复制组件弹窗 */}
<CopyComponentModal
visible={copyModalVisible}
componentInfo={currentCompInfo}
onCancel={() => setCopyModalVisible(false)}
onSuccess={() => {
setCopyModalVisible(false);
}}
/>
</> </>
); );
}; };

@ -0,0 +1,271 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
Form,
Input,
Select,
Space,
Button,
Message
} from '@arco-design/web-react';
import { getComponentClassify } from '@/api/componentClassify';
import { copyDesign } from '@/api/componentMarket';
import { copyAll } from '@/api/componentBase';
import EditorSection from '@/components/EditorSection';
const FormItem = Form.Item;
const Option = Select.Option;
interface CopyComponentModalProps {
visible: boolean;
componentInfo: any;
onCancel: () => void;
onSuccess?: () => void;
}
const CopyComponentModal: React.FC<CopyComponentModalProps> = ({
visible,
componentInfo,
onCancel,
onSuccess
}) => {
const [form] = Form.useForm();
const [classifyList, setClassifyList] = useState([]);
const [loading, setLoading] = useState(false);
const [description, setDescription] = useState('');
// 组件语言选项
const codeLanguageOptions = [
{ label: 'Java:8', value: 'Java' },
{ label: 'Python:3.10.12', value: 'Python' }
];
// 组件类型选项
const componentTypeOptions = [
{ label: '普通组件', value: 'normal' },
{ label: '监听组件', value: 'loop' }
];
// 获取组件分类列表
const getComponentClassifyList = async () => {
try {
const res: any = await getComponentClassify('component');
if (res.code === 200) {
const data = res.data.map((item) => ({
label: item.classifyName,
value: item.id
}));
setClassifyList(data);
}
} catch (error) {
console.error('获取组件分类失败:', error);
}
};
useEffect(() => {
getComponentClassifyList();
}, []);
// 当弹窗打开时,设置表单初始值
useEffect(() => {
if (visible && componentInfo) {
form.setFieldsValue({
name: componentInfo.name || '',
projectId: '', // 复制时项目标识需要重新填写
componentClassify: componentInfo.componentClassify || '',
codeLanguage: componentInfo.codeLanguage || '',
type: componentInfo.type || 'normal'
});
setDescription(componentInfo.desc || '');
}
}, [visible, componentInfo, form]);
// 仅复制设计
const handleCopyDesign = async () => {
try {
await form.validate();
const formData = form.getFields();
setLoading(true);
const params = {
id: componentInfo.id,
name: formData.name,
projectId: formData.projectId,
componentClassify: formData.componentClassify,
codeLanguage: formData.codeLanguage,
type: formData.type,
desc: description
};
const res: any = await copyDesign(params);
if (res.code === 200) {
Message.success('仅复制设计成功');
onSuccess?.();
handleClose();
}
else {
Message.error(res.message || '复制失败');
}
} catch (error) {
console.error('复制失败:', error);
Message.error('复制失败');
} finally {
setLoading(false);
}
};
// 复制设计和代码
const handleCopyAll = async () => {
try {
await form.validate();
const formData = form.getFields();
setLoading(true);
const params = {
id: componentInfo.id,
name: formData.name,
projectId: formData.projectId,
componentClassify: formData.componentClassify,
codeLanguage: formData.codeLanguage,
type: formData.type,
desc: description
};
const res: any = await copyAll(params);
if (res.code === 200) {
Message.success('复制设计和代码成功');
onSuccess?.();
handleClose();
}
else {
Message.error(res.message || '复制失败');
}
} catch (error) {
console.error('复制失败:', error);
Message.error('复制失败');
} finally {
setLoading(false);
}
};
const handleClose = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="复制组件到我的组件"
visible={visible}
onCancel={handleClose}
footer={
<Space>
<Button onClick={handleClose}></Button>
<Button
type="primary"
onClick={handleCopyDesign}
loading={loading}
>
</Button>
<Button
type="primary"
status="success"
onClick={handleCopyAll}
loading={loading}
>
</Button>
</Space>
}
style={{ width: '70%', maxWidth: 900 }}
>
<Form
form={form}
layout="vertical"
autoComplete="off"
>
<FormItem
label="组件名称"
field="name"
rules={[{ required: true, message: '请输入组件名称' }]}
>
<Input placeholder="请输入组件名称" />
</FormItem>
<FormItem
label="组件标识"
field="projectId"
rules={[
{ required: true, message: '请输入组件标识' },
{
validator: (value, callback) => {
if (value && !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) {
callback('组件标识必须以字母开头,只能包含字母、数字和下划线');
}
else {
callback();
}
}
}
]}
>
<Input placeholder="请输入组件标识my_component" />
</FormItem>
<FormItem
label="组件分类"
field="componentClassify"
rules={[{ required: true, message: '请选择组件分类' }]}
>
<Select placeholder="请选择组件分类">
{classifyList.map((item) => (
<Option key={item.value} value={item.label}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem
label="代码语言"
field="codeLanguage"
rules={[{ required: true, message: '请选择代码语言' }]}
>
<Select placeholder="请选择代码语言" disabled>
{codeLanguageOptions.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem
label="组件类型"
field="type"
rules={[{ required: true, message: '请选择组件类型' }]}
>
<Select placeholder="请选择组件类型">
{componentTypeOptions.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
<FormItem label="描述">
<EditorSection
initialContent={description}
onChange={(content) => setDescription(content)}
/>
</FormItem>
</Form>
</Modal>
);
};
export default CopyComponentModal;

@ -11,9 +11,6 @@
width: 345px; width: 345px;
height: 100%; height: 100%;
margin-right: 68px; margin-right: 68px;
padding: 20px 25px;
border-radius: 8px;
box-shadow: 2px 2px 20px 0 rgba(0, 0, 0, .25);
.comp-housing { .comp-housing {
width: 95%; width: 95%;
@ -55,8 +52,15 @@
.comp-info { .comp-info {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.header { .header {
flex-shrink: 0;
margin-bottom: 20px;
.title { .title {
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
@ -68,6 +72,8 @@
} }
.extra { .extra {
flex-shrink: 0;
.extra-font { .extra-font {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
@ -75,8 +81,133 @@
} }
.description { .description {
max-height: 15%; flex-shrink: 0;
max-height: 80px;
overflow-y: auto;
margin-bottom: 16px;
}
.markdown-content {
flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
min-height: 200px;
// Markdown 样式优化
:global {
// 标题样式
h1, h2, h3, h4, h5, h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
h1 {
padding-bottom: 0.3em;
border-bottom: 1px solid #eaecef;
}
// 段落样式
p {
margin-bottom: 16px;
line-height: 1.6;
}
// 代码块样式
code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
pre {
margin: 16px 0;
overflow: auto;
}
pre code {
background: transparent !important;
padding: 0 !important;
}
// 列表样式
ul, ol {
padding-left: 2em;
margin-bottom: 16px;
}
li {
margin-bottom: 0.25em;
}
// 引用样式
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 16px 0;
}
// 链接样式
a {
color: #165DFF;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// 表格样式
table {
border-spacing: 0;
border-collapse: collapse;
margin: 16px 0;
width: 100%;
}
table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
table tr:nth-child(2n) {
background-color: #f6f8fa;
}
table th,
table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
table th {
font-weight: 600;
background-color: #f6f6f6;
}
// 水平分割线
hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
// 图片样式
img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
}
}
.handel-box {
flex-shrink: 0;
padding: 16px 0;
} }
.params { .params {

Loading…
Cancel
Save