feat: 开发平台&代码分析&报告中心接入newapi

This commit is contained in:
Shu Guang 2025-04-19 01:11:03 +08:00
parent 326689155f
commit e851d2ca02
6 changed files with 494 additions and 243 deletions

View File

@ -119,6 +119,12 @@ export default defineConfig({
async: true, async: true,
}, },
], ],
externals: {
'react-markdown': 'ReactMarkdown',
},
scripts: [
'https://unpkg.com/react-markdown@8.0.7/react-markdown.min.js',
],
//================ pro 插件配置 ================= //================ pro 插件配置 =================
presets: ['umi-presets-pro'], presets: ['umi-presets-pro'],
/** /**

View File

@ -53,9 +53,12 @@
"antd": "5.24.5", "antd": "5.24.5",
"antd-style": "^3.6.1", "antd-style": "^3.6.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"docx": "^9.4.1",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"file-saver": "^2.0.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^15.0.8",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"omit.js": "^2.0.2", "omit.js": "^2.0.2",
@ -66,7 +69,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-echarts": "^0.1.1", "react-echarts": "^0.1.1",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-intl": "^7.1.6" "react-intl": "^7.1.6",
"react-markdown": "^10.1.0"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/pro-cli": "^3.3.0", "@ant-design/pro-cli": "^3.3.0",

View File

@ -11,6 +11,15 @@ const { Dragger } = Upload;
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { TextArea } = Input; const { TextArea } = Input;
interface AnalysisResponse {
id: string;
choices: {
message: {
content: string;
};
}[];
}
const CodeAnalysisPage: React.FC = () => { const CodeAnalysisPage: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]); const [fileList, setFileList] = useState<UploadFile[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -33,48 +42,54 @@ const CodeAnalysisPage: React.FC = () => {
} }
setLoading(true); setLoading(true);
try { try {
// 这里应该调用后端API进行分析 const response = await fetch('https://aizex.top/v1/chat/completions', {
// const response = await analyzeCode({ content: codeContent, type: analysisType }); method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: analysisType === 'er'
? '请分析这段代码并生成对应的ER图使用mermaid语法。分析要点1. 实体关系 2. 属性定义 3. 关系类型'
: '请分析这段代码并生成功能模块图使用mermaid语法。分析要点1. 模块划分 2. 依赖关系 3. 调用流程'
},
{
type: 'text',
text: codeContent
}
]
}
],
max_tokens: 2000
})
});
const data: AnalysisResponse = await response.json();
const content = data.choices[0].message.content;
// 模拟API响应 // 解析返回的内容,提取 mermaid 图表代码和分析建议
setTimeout(() => { const [diagramPart, analysisPart] = content.split('分析建议:').map(part => part.trim());
if (analysisType === 'er') {
setDiagramCode(` // 提取 mermaid 代码块
erDiagram const mermaidCode = diagramPart.match(/```mermaid\n([\s\S]*?)\n```/)?.[1] || diagramPart;
USER ||--o{ POST : creates
POST ||--|{ COMMENT : has setDiagramCode(mermaidCode);
USER { setAnalysisResult(analysisPart || '暂无具体分析建议');
string username setActiveTab('result');
string email message.success('分析成功');
timestamp created_at
}
POST {
string title
text content
timestamp published_at
}
COMMENT {
text content
timestamp created_at
}
`);
} else {
setDiagramCode(`
graph TD
A[] --> B[]
A --> C[]
B --> D[]
C --> E[]
`);
}
setAnalysisResult('分析完成,建议优化数据库索引结构,添加适当的外键约束。');
setActiveTab('result');
message.success('分析成功');
}, 1500);
} catch (error) { } catch (error) {
message.error('分析失败'); console.error('分析失败:', error);
message.error('分析失败,请重试');
} finally {
setLoading(false);
} }
setLoading(false);
}; };
// 处理文件上传 // 处理文件上传

View File

@ -1,39 +1,44 @@
import { PageContainer, ProCard } from '@ant-design/pro-components'; import { PageContainer, ProCard } from '@ant-design/pro-components';
import { Button, Card, Col, Descriptions, Input, message, Modal, Row, Space, Table, Tag } from 'antd'; import { Button, Card, Col, Descriptions, Input, message, Modal, Row, Space, Table, Tag } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { CopyOutlined, KeyOutlined, ReloadOutlined } from '@ant-design/icons'; import { CopyOutlined } from '@ant-design/icons';
const OpenPlatform: React.FC = () => { const OpenPlatform: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false); const [keyList] = useState([
const [visible, setVisible] = useState<boolean>(false);
const [apiKey, setApiKey] = useState<string>('');
// TODO:API密钥数据
const [keyList, setKeyList] = useState([
{ {
id: 1, id: 1,
accessKey: 'ak_xxxxxxxxxxxx', accessKey: 'sk-1PBIyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXGh5S',
secretKey: 'sk_xxxxxxxxxxxx', description: 'GPT-3.5-Turbo API支持中英文对话适合日常对话场景',
status: 'active', status: 'active',
createTime: '2024-01-01 12:00:00', qps: '3次/秒',
expiryTime: '2025-01-01 12:00:00', dailyLimit: '1000次/天',
calls: 1234, },
{
id: 2,
accessKey: 'sk-2ABCyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXABCD',
description: 'Claude-2 API支持多语言对话适合学术研究场景',
status: 'active',
qps: '2次/秒',
dailyLimit: '500次/天',
},
{
id: 3,
accessKey: 'sk-3DEFyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXDEF',
description: 'Stable Diffusion API支持文生图、图生图等功能',
status: 'active',
qps: '1次/秒',
dailyLimit: '100次/天',
}, },
]); ]);
const columns = [ const columns = [
{ {
title: 'Access Key', title: 'API Key',
dataIndex: 'accessKey', dataIndex: 'accessKey',
key: 'accessKey', key: 'accessKey',
},
{
title: 'Secret Key',
dataIndex: 'secretKey',
key: 'secretKey',
render: (text: string) => ( render: (text: string) => (
<Space> <Space>
<span>{'*'.repeat(20)}</span> <span>{text}</span>
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
type="link" type="link"
@ -45,112 +50,65 @@ const OpenPlatform: React.FC = () => {
</Space> </Space>
), ),
}, },
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: '40%',
},
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
render: (status: string) => ( render: (status: string) => (
<Tag color={status === 'active' ? 'green' : 'red'}> <Tag color={status === 'active' ? 'green' : 'red'}>
{status === 'active' ? '启用' : '禁用'} {status === 'active' ? '正常' : '维护中'}
</Tag> </Tag>
), ),
}, },
{ {
title: '创建时间', title: 'QPS限制',
dataIndex: 'createTime', dataIndex: 'qps',
key: 'createTime', key: 'qps',
}, },
{ {
title: '过期时间', title: '每日限额',
dataIndex: 'expiryTime', dataIndex: 'dailyLimit',
key: 'expiryTime', key: 'dailyLimit',
},
{
title: '调用次数',
dataIndex: 'calls',
key: 'calls',
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Space>
<Button type="link" danger onClick={() => handleDisableKey(record.id)}>
{record.status === 'active' ? '禁用' : '启用'}
</Button>
<Button type="link" danger onClick={() => handleDeleteKey(record.id)}>
</Button>
</Space>
),
}, },
]; ];
const handleCreateKey = () => {
setLoading(true);
// 这里应该调用后端API创建密钥
setTimeout(() => {
const newKey = {
id: keyList.length + 1,
accessKey: `ak_${Math.random().toString(36).substring(2)}`,
secretKey: `sk_${Math.random().toString(36).substring(2)}`,
status: 'active',
createTime: new Date().toLocaleString(),
expiryTime: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toLocaleString(),
calls: 0,
};
setKeyList([...keyList, newKey]);
setLoading(false);
message.success('创建成功');
}, 1000);
};
const handleDisableKey = (id: number) => {
setKeyList(
keyList.map((key) =>
key.id === id ? { ...key, status: key.status === 'active' ? 'inactive' : 'active' } : key,
),
);
};
const handleDeleteKey = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '删除后无法恢复,是否继续?',
onOk: () => {
setKeyList(keyList.filter((key) => key.id !== id));
message.success('删除成功');
},
});
};
return ( return (
<PageContainer> <PageContainer>
<Row gutter={16}> <Row gutter={16}>
<Col span={24}> <Col span={24}>
<ProCard> <ProCard>
<Descriptions title="接口信息" column={2}> <Descriptions title="公益API说明" column={1}>
<Descriptions.Item label="接口状态"> <Descriptions.Item label="项目介绍">
<Tag color="green"></Tag> AI API服务GPTClaudeStable Diffusion等多个模型使
</Descriptions.Item>
<Descriptions.Item label="使用须知">
1.
2. API资源
3.
</Descriptions.Item>
<Descriptions.Item label="联系方式">
Email: support@example.com
GitHub: https://github.com/your-repo
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="计费方式"></Descriptions.Item>
<Descriptions.Item label="单价">0.01/</Descriptions.Item>
<Descriptions.Item label="总调用次数">1234</Descriptions.Item>
</Descriptions> </Descriptions>
</ProCard> </ProCard>
</Col> </Col>
</Row> </Row>
<Card <Card style={{ marginTop: 16 }} title="可用API列表">
style={{ marginTop: 16 }} <Table
title="API密钥管理" columns={columns}
extra={ dataSource={keyList}
<Button type="primary" icon={<KeyOutlined />} onClick={handleCreateKey} loading={loading}> rowKey="id"
pagination={false}
</Button> />
}
>
<Table columns={columns} dataSource={keyList} rowKey="id" />
</Card> </Card>
<Card style={{ marginTop: 16 }} title="调用示例"> <Card style={{ marginTop: 16 }} title="调用示例">
@ -159,14 +117,17 @@ const OpenPlatform: React.FC = () => {
<pre> <pre>
{`import requests {`import requests
url = "https://api.yourdomain.com/v1/generate" # GPT-3.5
url = "http://api.example.com/v1/chat/completions"
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-API-KEY": "your_api_key" "Authorization": "Bearer ${keyList[0].accessKey}"
} }
data = { data = {
"prompt": "你的提示词", "model": "gpt-3.5-turbo",
"model": "gpt-3.5-turbo" "messages": [
{"role": "user", "content": "你好,请介绍一下你自己"}
]
} }
response = requests.post(url, json=data, headers=headers) response = requests.post(url, json=data, headers=headers)
@ -174,6 +135,29 @@ print(response.json())`}
</pre> </pre>
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
<Descriptions title="Node.js示例" column={1}>
<Descriptions.Item>
<pre>
{`const axios = require('axios');
async function generateImage() {
const response = await axios.post('http://api.example.com/v1/images/generations', {
prompt: '一只可爱的猫咪',
n: 1,
size: '512x512'
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${keyList[2].accessKey}'
}
});
console.log(response.data);
}`}
</pre>
</Descriptions.Item>
</Descriptions>
</Card> </Card>
</PageContainer> </PageContainer>
); );

View File

@ -90,4 +90,153 @@
color: #1890ff; color: #1890ff;
} }
} }
} }
.reportContent {
padding: 24px;
:global {
h1 {
font-size: 28px;
margin-bottom: 24px;
}
h2 {
font-size: 24px;
margin: 20px 0 16px;
}
h3 {
font-size: 20px;
margin: 16px 0 12px;
}
h4 {
font-size: 16px;
margin: 12px 0 8px;
}
p {
margin-bottom: 16px;
line-height: 1.8;
}
ul, ol {
margin-bottom: 16px;
padding-left: 24px;
li {
margin-bottom: 8px;
}
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
th, td {
border: 1px solid #e8e8e8;
padding: 12px;
text-align: left;
}
th {
background: #fafafa;
font-weight: 500;
}
tr:hover {
background: #fafafa;
}
}
}
}
.container {
margin: 24px;
}
.steps {
margin-bottom: 24px;
}
.content {
min-height: 400px;
padding: 24px;
}
.uploadStep {
max-width: 800px;
margin: 0 auto;
}
.previewCard {
margin-top: 24px;
}
.previewTable {
width: 100%;
border-collapse: collapse;
th, td {
border: 1px solid #e8e8e8;
padding: 12px;
text-align: left;
}
th {
background: #fafafa;
font-weight: 500;
}
}
.goalStep {
max-width: 800px;
margin: 0 auto;
.goalInput {
margin: 24px 0;
}
.generateButton {
margin-bottom: 24px;
}
}
.reportStep {
.chartCard {
margin-top: 24px;
}
}
.markdownList {
margin-left: 24px;
margin-bottom: 16px;
li {
margin-bottom: 8px;
}
}
.markdownTable {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
th, td {
border: 1px solid #e8e8e8;
padding: 8px 16px;
text-align: left;
}
th {
background: #fafafa;
font-weight: 500;
}
tr:hover {
background: #fafafa;
}
}

View File

@ -4,12 +4,38 @@ import { InboxOutlined, FileExcelOutlined, BarChartOutlined, FileWordOutlined }
import { Upload, Card, Button, Steps, message, Input, Spin, Result, Space, Progress, Alert, Typography } from 'antd'; import { Upload, Card, Button, Steps, message, Input, Spin, Result, Space, Progress, Alert, Typography } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface'; import type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { marked } from 'marked';
import styles from './index.less'; import styles from './index.less';
import { Document, Packer, Paragraph as DocxParagraph, TextRun } from 'docx';
import { saveAs } from 'file-saver';
const { Dragger } = Upload; const { Dragger } = Upload;
const { TextArea } = Input; const { TextArea } = Input;
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
interface AnalysisResponse {
id: string;
choices: {
message: {
content: string;
};
}[];
}
interface ReportSection {
title: string;
content: string;
}
interface ReportData {
title: string;
summary: string;
sections: ReportSection[];
charts: any[];
markdown: string;
}
const ReportPage: React.FC = () => { const ReportPage: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]); const [fileList, setFileList] = useState<UploadFile[]>([]);
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
@ -23,94 +49,158 @@ const ReportPage: React.FC = () => {
const handleUpload = async (file: File) => { const handleUpload = async (file: File) => {
setLoading(true); setLoading(true);
try { try {
const formData = new FormData(); // 读取文件为 base64
formData.append('file', file); const reader = new FileReader();
reader.readAsDataURL(file);
// 模拟上传进度 reader.onload = async () => {
const timer = setInterval(() => { const base64Image = reader.result as string;
setProgress(prev => {
if (prev >= 99) { // 调用 NewAPI 接口
clearInterval(timer); try {
return 99; const response = await fetch('https://aizex.top/v1/chat/completions', {
} method: 'POST',
return prev + 10; headers: {
}); 'Content-Type': 'application/json',
}, 200); 'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: '请分析这张图表中的数据趋势和关键信息,并给出专业的分析见解。'
},
{
type: 'image_url',
image_url: {
url: base64Image
}
}
]
}
],
max_tokens: 1000
})
});
// 模拟预览数据 const data: AnalysisResponse = await response.json();
setTimeout(() => {
setPreviewData({ setPreviewData({
columns: ['日期', '销售额', '利润'], columns: ['分析结果'],
data: [ data: [[data.choices[0].message.content]]
['2023-01', 1000, 200], });
['2023-02', 1500, 300],
['2023-03', 1200, 250], message.success('图片分析成功');
] setCurrentStep(1);
}); } catch (error) {
setProgress(100); message.error('图片分析失败');
message.success('文件上传成功'); console.error('API调用失败:', error);
setCurrentStep(1); }
setLoading(false); };
clearInterval(timer);
}, 2000);
} catch (error) { } catch (error) {
message.error('文件上传失败'); message.error('文件处理失败');
} finally {
setLoading(false); setLoading(false);
} }
}; };
const generateWordDocument = async (markdown: string) => {
const doc = new Document({
sections: [
{
properties: {},
children: markdown.split('\n').map(line => {
if (line.startsWith('# ')) {
return new DocxParagraph({
text: line.replace('# ', ''),
heading: 'Heading1'
});
}
if (line.startsWith('## ')) {
return new DocxParagraph({
text: line.replace('## ', ''),
heading: 'Heading2'
});
}
return new DocxParagraph({
children: [
new TextRun({
text: line,
size: 24
})
]
});
})
}
]
});
const blob = await Packer.toBlob(doc);
saveAs(blob, '数据分析报告.docx');
};
const handleDownload = async () => {
if (reportData?.markdown) {
try {
await generateWordDocument(reportData.markdown);
message.success('报告下载成功');
} catch (error) {
message.error('报告下载失败');
console.error('下载失败:', error);
}
}
};
const generateReport = async () => { const generateReport = async () => {
setLoading(true); setLoading(true);
try { try {
let progress = 0; const response = await fetch('https://aizex.top/v1/chat/completions', {
const timer = setInterval(() => { method: 'POST',
progress += 20; headers: {
if (progress <= 100) { 'Content-Type': 'application/json',
setProgress(progress); 'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
} },
}, 1000); body: JSON.stringify({
model: 'gpt-4o',
setTimeout(() => { messages: [
clearInterval(timer);
setProgress(100);
setWordUrl('https://example.com/report.docx');
setReportData({
title: '数据分析报告',
summary: '根据您提供的数据,我们生成了详细的分析报告...',
charts: [
{ {
title: '销售趋势', role: 'user',
type: 'line', content: [
data: { {
title: { type: 'text',
text: '销售趋势分析' text: `请基于以下分析目标和数据生成一份详细的markdown格式分析报告包含标题、概述、详细分析等章节${goal}\n${previewData?.data[0][0]}`
}, }
tooltip: { ]
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月']
},
yAxis: {
type: 'value'
},
series: [{
name: '销售额',
type: 'line',
data: [1000, 1500, 1200],
smooth: true
}]
}
} }
] ],
}); max_tokens: 2000
message.success('报告生成成功'); })
setCurrentStep(2); });
setLoading(false);
}, 5000); const data: AnalysisResponse = await response.json();
const markdownContent = data.choices[0].message.content;
// 解析 markdown 内容
const titleMatch = markdownContent.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1] : '数据分析报告';
setReportData({
title,
summary: '',
sections: [],
charts: [],
markdown: markdownContent
});
setWordUrl('https://example.com/report.docx');
message.success('报告生成成功');
setCurrentStep(2);
} catch (error) { } catch (error) {
message.error('报告生成失败'); message.error('报告生成失败');
} finally {
setLoading(false); setLoading(false);
} }
}; };
@ -141,6 +231,17 @@ const ReportPage: React.FC = () => {
); );
}; };
const renderReport = () => {
if (!reportData?.markdown) return null;
return (
<div
className={styles.reportContent}
dangerouslySetInnerHTML={{ __html: marked(reportData.markdown) }}
/>
);
};
const steps = [ const steps = [
{ {
title: '上传文件', title: '上传文件',
@ -157,14 +258,14 @@ const ReportPage: React.FC = () => {
<Dragger <Dragger
fileList={fileList} fileList={fileList}
beforeUpload={(file) => { beforeUpload={(file) => {
const isExcelOrCsv = // const isExcelOrCsv =
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || // file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel' || // file.type === 'application/vnd.ms-excel' ||
file.type === 'text/csv'; // file.type === 'text/csv';
if (!isExcelOrCsv) { // if (!isExcelOrCsv) {
message.error('只支持上传 Excel 或 CSV 文件!'); // message.error('只支持上传 Excel 或 CSV 文件!');
return false; // return false;
} // }
setFileList([file]); setFileList([file]);
handleUpload(file); handleUpload(file);
return false; return false;
@ -242,7 +343,7 @@ const ReportPage: React.FC = () => {
<Button <Button
type="primary" type="primary"
icon={<FileWordOutlined />} icon={<FileWordOutlined />}
onClick={() => window.open(wordUrl)} onClick={handleDownload}
key="download" key="download"
size="large" size="large"
> >
@ -264,17 +365,7 @@ const ReportPage: React.FC = () => {
</Button>, </Button>,
]} ]}
/> />
{reportData && ( {renderReport()}
<div className={styles.reportPreview}>
<Title level={4}>{reportData.title}</Title>
<Paragraph>{reportData.summary}</Paragraph>
{reportData.charts.map((chart: any, index: number) => (
<Card key={index} title={chart.title} className={styles.chartCard}>
<ReactECharts option={chart.data} style={{ height: 300 }} />
</Card>
))}
</div>
)}
</Card> </Card>
</div> </div>
), ),
@ -283,8 +374,8 @@ const ReportPage: React.FC = () => {
return ( return (
<PageContainer <PageContainer
title="智能报告生成" title="智能图表分析"
subTitle="上传数据文件,快速生成专业分析报告" subTitle="上传数据图表,快速获取专业分析见解"
> >
<Card className={styles.container}> <Card className={styles.container}>
<Steps <Steps
@ -300,4 +391,6 @@ const ReportPage: React.FC = () => {
); );
}; };
export default ReportPage;
export default ReportPage;