Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2db7793ab3 | ||
|
593f921ef4 | ||
|
b763c47e48 | ||
|
0b06d3dd0e | ||
|
adcad7769e | ||
|
55c472b840 | ||
|
33ef6e7d20 | ||
|
b8e50a9511 | ||
|
9a7b889ada | ||
|
279e5d81f7 | ||
|
664e2950f7 | ||
|
3a1c04686a | ||
|
db59c6d95b | ||
|
c32c8d5e4f | ||
|
8926d28a72 | ||
|
8ad4263e2a | ||
06e2371843 | |||
|
015fe34820 | ||
|
75a1070afe | ||
|
0ae3e5bdbb | ||
|
99b18a38d5 | ||
|
f35bf3978e | ||
|
9b0db2cd2f | ||
|
cac4384184 | ||
|
c8f251a4a2 | ||
|
c3950fd4c7 | ||
|
57b1582c11 | ||
|
b05921c1a5 | ||
|
23dcdc6ac6 | ||
|
ed5dfcc7dc | ||
|
f6abb7a850 |
@ -1,17 +1,19 @@
|
||||
export default [
|
||||
{path: '/user',layout: false,routes: [
|
||||
{ name: '登录', path: '/user/login', component: './User/Login' },
|
||||
{ name: '注册', path: '/user/register', component: './User/Register' }
|
||||
{ name: '注册', path: '/user/register', component: './User/Register' },
|
||||
{ name :"权限引导",path: '/user/access', component: './Access'},
|
||||
]},
|
||||
{path:"/", redirect: "/home"},
|
||||
{path:"/", redirect: "/user/access"},
|
||||
|
||||
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage' },
|
||||
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage',access: 'canAdmin', },
|
||||
// { path: '/access', name :"权限引导", icon: "PieChartOutlined", component: './Access',hidden: true},
|
||||
{ path: '/add_chart', name :"智能分析", icon: "barChart", component: './AddChart' },
|
||||
{ path: '/add_async', name: "智能分析(异步)", icon: "DotChartOutlined", component: './AddChartAsync' },
|
||||
{ path: '/report', name: "报告中心", icon: "commentOutlined", component: './Report' },
|
||||
{ path: '/code', name: "代码分析", icon: "GithubOutlined", component: './Code' },
|
||||
{ path: '/Forecast', name :"分析中心", icon: "ApiOutlined", component: './Forecast' },
|
||||
{ path: '/add_async', name: "异步分析", icon: "DotChartOutlined", component: './AddChartAsync' },
|
||||
{ path: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' },
|
||||
{ path: '/code', name: "代码分析", icon: "GithubOutlined", component: './Code' },
|
||||
{ path: '/report', name: "数据报告", icon: "commentOutlined", component: './Report' },
|
||||
{ path: '/Forecast', name :"数据分析", icon: "ApiOutlined", component: './Forecast' },
|
||||
{ path: '/open_platform', name :"开放中心", icon: "ApiOutlined", component: './OpenPlatform' },
|
||||
{ path: '/forum', name: "交流论坛", icon: "CrownOutlined", component: './Forum' },
|
||||
{ path: '/user/center', name: "个人中心", icon: "UserOutlined", component: './User/Info' },
|
||||
@ -27,12 +29,13 @@ export default [
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: '管理页',
|
||||
name: '后台管理',
|
||||
icon: 'crown',
|
||||
access: 'canAdmin',
|
||||
routes: [
|
||||
{ path: '/admin', redirect: '/admin/sub-page' },
|
||||
{ path: '/admin/sub-page', name: '二级管理页', component: './Admin' },
|
||||
{ path: '/admin/sub-page', name: '论坛管理', component: './Admin' },
|
||||
{ path: '/admin/user-manage', name: '用户管理', component: './Admin_UserManage' },
|
||||
],
|
||||
},
|
||||
{ path: '/', redirect: '/welcome' },
|
||||
|
@ -49,8 +49,9 @@
|
||||
"@ant-design/icons": "^4.8.1",
|
||||
"@ant-design/pro-components": "^2.8.7",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@uiw/react-md-editor": "^4.0.6",
|
||||
"@umijs/route-utils": "^2.2.2",
|
||||
"antd": "5.24.5",
|
||||
"antd": "^5.25.0",
|
||||
"antd-style": "^3.6.1",
|
||||
"classnames": "^2.5.1",
|
||||
"docx": "^9.4.1",
|
||||
@ -88,7 +89,7 @@
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@umijs/fabric": "^2.14.1",
|
||||
"@umijs/lint": "^4.1.1",
|
||||
"@umijs/max": "^4.1.1",
|
||||
"@umijs/max": "^4.4.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"express": "^4.18.2",
|
||||
|
@ -5,7 +5,8 @@
|
||||
* */
|
||||
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
|
||||
const { currentUser } = initialState ?? {};
|
||||
console.log('currentUser',currentUser)
|
||||
return {
|
||||
canAdmin: currentUser && currentUser.access === 'admin',
|
||||
canAdmin: currentUser && currentUser.userRole === 'admin',
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { AvatarDropdown, AvatarName, Footer, Question } from '@/components';
|
||||
import { SettingDrawer } from '@ant-design/pro-components';
|
||||
import type { RunTimeLayoutConfig } from '@umijs/max';
|
||||
@ -22,6 +23,7 @@ export async function getInitialState(): Promise<{
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 如果不是登录页面,执行
|
||||
const { location } = history;
|
||||
if (location.pathname !== loginPath) {
|
||||
@ -31,10 +33,10 @@ export async function getInitialState(): Promise<{
|
||||
};
|
||||
}
|
||||
return {
|
||||
currentUser: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
|
||||
return {
|
||||
actionsRender: () => [<Question key="doc" />],
|
||||
@ -103,6 +105,8 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @name request 配置,可以配置错误处理
|
||||
* 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。
|
||||
|
@ -8,6 +8,7 @@ import React, { useCallback } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import { userLogoutUsingPost } from '@/services/hebi/userController';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
@ -16,6 +17,7 @@ export type GlobalHeaderRightProps = {
|
||||
|
||||
export const AvatarName = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
console.log('initialState',initialState)
|
||||
const { currentUser } = initialState || {};
|
||||
return <span className="anticon">{currentUser?.userName}</span>;
|
||||
};
|
||||
@ -39,11 +41,23 @@ const useStyles = createStyles(({ token }) => {
|
||||
});
|
||||
|
||||
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
// 移除 useEffect 里的 window.location.reload 相关逻辑
|
||||
// useEffect(() => {
|
||||
// console.log('initialState2',sessionStorage.getItem('avatar_refreshed'))
|
||||
// if (!sessionStorage.getItem('avatar_refreshed')) {
|
||||
// sessionStorage.setItem('avatar_refreshed', '1');
|
||||
// window.location.reload();
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
/**
|
||||
* 退出登录,并且将当前的 url 保存
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
await userLogoutUsingPost();
|
||||
// 退出登录时清除刷新标记
|
||||
sessionStorage.removeItem('avatar_refreshed');
|
||||
const { search, pathname } = window.location;
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
/** redirect */
|
||||
@ -59,7 +73,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, childre
|
||||
};
|
||||
const { styles } = useStyles();
|
||||
|
||||
const { initialState, setInitialState } = useModel('@@initialState');
|
||||
// const { initialState, setInitialState } = useModel('@@initialState');
|
||||
|
||||
const onMenuClick = useCallback(
|
||||
(event: MenuInfo) => {
|
||||
@ -76,6 +90,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, childre
|
||||
[setInitialState],
|
||||
);
|
||||
|
||||
|
||||
const loading = (
|
||||
<span className={styles.action}>
|
||||
<Spin
|
||||
@ -88,13 +103,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, childre
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!initialState) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
const { currentUser } = initialState;
|
||||
|
||||
if (!currentUser || !currentUser.userName) {
|
||||
if (!initialState?.currentUser || !initialState.currentUser.userName) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
|
51
src/hooks/useAIRequest.ts
Normal file
51
src/hooks/useAIRequest.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import useAddress from './useAddress';
|
||||
|
||||
interface AIRequestOptions {
|
||||
model?: string;
|
||||
maxTokens?: number;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
const useAIRequest = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { NewAPiAddress } = useAddress();
|
||||
|
||||
const sendRequest = async (content: any, options: AIRequestOptions = {}) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${NewAPiAddress}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-1PBIyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXGh5S'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: options.model || 'gpt-4o-mini',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: options.systemPrompt || '你是一个智能助手,请根据用户输入进行分析并给出专业的见解。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content
|
||||
}
|
||||
],
|
||||
max_tokens: options.maxTokens || 2000
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { loading, sendRequest };
|
||||
};
|
||||
|
||||
export default useAIRequest;
|
5
src/hooks/useAddress.tsx
Normal file
5
src/hooks/useAddress.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const useAddress = () => {
|
||||
const NewAPiAddress = "https://openai.933999.xyz"
|
||||
return {NewAPiAddress};
|
||||
}
|
||||
export default useAddress;
|
22
src/pages/Access/index.tsx
Normal file
22
src/pages/Access/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
// 负责处理默认重定向
|
||||
import { useEffect } from 'react';
|
||||
import { useModel } from 'umi';
|
||||
import { history } from '@@/core/history';
|
||||
export default () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { currentUser } = initialState || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
// 获取用户角色,优先使用 currentUser.userRole
|
||||
const userRole = currentUser?.userRole || currentUser?.data?.userRole;
|
||||
// 根据用户角色重定向到不同页面
|
||||
history.push(userRole === 'user' ? '/add_chart' : '/home');
|
||||
|
||||
} else {
|
||||
history.push('/user/login');
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
return <div>Loading...</div>;
|
||||
};
|
@ -1,44 +1,194 @@
|
||||
import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import '@umijs/max';
|
||||
import { Alert, Card, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
const Admin: React.FC = () => {
|
||||
import { Table, Button, Modal, Form, Input, Tag, Space, Popconfirm, message, Avatar } from 'antd';
|
||||
import { listAllPostsUsingGet, updatePostUsingPost, deletePostUsingPost, addPostUsingPost } from '@/services/hebi/postController';
|
||||
import dayjs from 'dayjs'; // 新增
|
||||
|
||||
const ForumAdmin: React.FC = () => {
|
||||
const [posts, setPosts] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingPost, setEditingPost] = useState<any>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [search, setSearch] = useState<string>('');
|
||||
|
||||
// 获取帖子列表
|
||||
const fetchPosts = async () => {
|
||||
setLoading(true);
|
||||
const res = await listAllPostsUsingGet();
|
||||
if (res && res.code === 0 && Array.isArray(res.data)) {
|
||||
setPosts(res.data);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPosts();
|
||||
}, []);
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
setEditingPost(record);
|
||||
setModalVisible(true);
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
tags: record.tagList?.join(','),
|
||||
});
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
setEditingPost(null);
|
||||
setModalVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deletePostUsingPost({ id });
|
||||
if (res && res.code === 0) {
|
||||
message.success('删除成功');
|
||||
fetchPosts();
|
||||
} else {
|
||||
message.error(res?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
const tags = values.tags ? values.tags.split(',').map((t: string) => t.trim()) : [];
|
||||
if (editingPost) {
|
||||
// 编辑
|
||||
const res = await updatePostUsingPost({ ...editingPost, ...values, tags });
|
||||
if (res && res.code === 0) {
|
||||
message.success('更新成功');
|
||||
setModalVisible(false);
|
||||
fetchPosts();
|
||||
} else {
|
||||
message.error(res?.message || '更新失败');
|
||||
}
|
||||
} else {
|
||||
// 新增
|
||||
const res = await addPostUsingPost({ ...values, tags });
|
||||
if (res && res.code === 0) {
|
||||
message.success('新增成功');
|
||||
setModalVisible(false);
|
||||
fetchPosts();
|
||||
} else {
|
||||
message.error(res?.message || '新增失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tagList',
|
||||
width: 200,
|
||||
render: (tags: string[]) => (
|
||||
<>
|
||||
{tags?.map(tag => (
|
||||
<Tag color="blue" key={tag}>{tag}</Tag>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: ['user', 'userName'],
|
||||
width: 100,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
{record.user?.userName}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
render: (time: string) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 160,
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除吗?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 前端搜索过滤
|
||||
const filteredPosts = posts.filter(post => {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
if (!keyword) return true;
|
||||
const titleMatch = post.title?.toLowerCase().includes(keyword);
|
||||
const tagsMatch = (post.tagList || []).some((tag: string) =>
|
||||
tag.toLowerCase().includes(keyword)
|
||||
);
|
||||
return titleMatch || tagsMatch;
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer content={' 这个页面只有 admin 权限才能查看'}>
|
||||
<Card>
|
||||
<Alert
|
||||
message={'更快更强的重型组件,已经发布。'}
|
||||
type="success"
|
||||
showIcon
|
||||
banner
|
||||
style={{
|
||||
margin: -12,
|
||||
marginBottom: 48,
|
||||
}}
|
||||
<PageContainer title="论坛管理">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={handleAdd}>
|
||||
新增帖子
|
||||
</Button>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索标题或标签"
|
||||
style={{ width: 300 }}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
<Typography.Title
|
||||
level={2}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<SmileTwoTone /> 基于AIGC的智能数据分析系统 <HeartTwoTone twoToneColor="#eb2f96" />
|
||||
</Typography.Title>
|
||||
</Card>
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: 24,
|
||||
}}
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={filteredPosts}
|
||||
bordered
|
||||
pagination={{ pageSize: 8 }}
|
||||
/>
|
||||
<Modal
|
||||
title={editingPost ? '编辑帖子' : '新增帖子'}
|
||||
open={modalVisible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
destroyOnClose
|
||||
>
|
||||
Want to add more pages? Please refer to{' '}
|
||||
<a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
|
||||
use block
|
||||
</a>
|
||||
。
|
||||
</p>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item name="tags" label="标签(逗号分隔)">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
export default Admin;
|
||||
|
||||
export default ForumAdmin;
|
||||
|
251
src/pages/Admin_UserManage.tsx
Normal file
251
src/pages/Admin_UserManage.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Table, Button, Modal, Form, Input, Space, Popconfirm, message, Avatar, Select } from 'antd';
|
||||
import { listUserByPageUsingPost, updateUserUsingPost, deleteUserUsingPost, addUserUsingPost } from '@/services/hebi/userController';
|
||||
import dayjs from 'dayjs'; // 新增
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const UserManage: React.FC = () => {
|
||||
const [allUsers, setAllUsers] = useState<any[]>([]);
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<any>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [isAdd, setIsAdd] = useState(false); // 新增/编辑标志
|
||||
|
||||
// 拉取所有用户
|
||||
const fetchAllUsers = async () => {
|
||||
setLoading(true);
|
||||
const res = await listUserByPageUsingPost({
|
||||
userQueryRequest: {
|
||||
current: 1,
|
||||
pageSize: 10000, // 假设不会超过1万用户
|
||||
},
|
||||
});
|
||||
if (res && res.code === 0 && res.data) {
|
||||
setAllUsers(res.data.records || []);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 前端过滤和分页
|
||||
const filterAndPaginate = (all: any[], keyword: string, page: number, pageSize: number) => {
|
||||
let filtered = all;
|
||||
if (keyword.trim()) {
|
||||
filtered = all.filter(user =>
|
||||
(user.userName || '').toLowerCase().includes(keyword.trim().toLowerCase())
|
||||
);
|
||||
}
|
||||
const total = filtered.length;
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
return {
|
||||
users: filtered.slice(start, end),
|
||||
total,
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化和数据变动时处理
|
||||
useEffect(() => {
|
||||
fetchAllUsers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { users, total } = filterAndPaginate(allUsers, search, pagination.current, pagination.pageSize);
|
||||
setUsers(users);
|
||||
setPagination(prev => ({ ...prev, total }));
|
||||
}, [allUsers, search, pagination.current, pagination.pageSize]);
|
||||
|
||||
// 搜索时重置到第一页
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
setIsAdd(true);
|
||||
setEditingUser(null);
|
||||
setModalVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
setIsAdd(false);
|
||||
setEditingUser(record);
|
||||
setModalVisible(true);
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
});
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleOk = async () => {
|
||||
const values = await form.validateFields();
|
||||
if (isAdd) {
|
||||
const res = await addUserUsingPost({ ...values });
|
||||
if (res && res.code === 0) {
|
||||
message.success('新增成功');
|
||||
setModalVisible(false);
|
||||
fetchAllUsers();
|
||||
} else {
|
||||
message.error(res?.message || '新增失败');
|
||||
}
|
||||
} else {
|
||||
const res = await updateUserUsingPost({ ...editingUser, ...values });
|
||||
if (res && res.code === 0) {
|
||||
message.success('更新成功');
|
||||
setModalVisible(false);
|
||||
fetchAllUsers();
|
||||
} else {
|
||||
message.error(res?.message || '更新失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id: number) => {
|
||||
const res = await deleteUserUsingPost({ id });
|
||||
if (res && res.code === 0) {
|
||||
message.success('删除成功');
|
||||
fetchAllUsers();
|
||||
} else {
|
||||
message.error(res?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'userAvatar',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (avatar: string) => <Avatar src={avatar} />,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'userName',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'userAccount',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'userRole',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (role: string) => {
|
||||
if (role === 'admin') return <span style={{ color: '#fa541c' }}>管理员</span>;
|
||||
if (role === 'ban') return <span style={{ color: '#bfbfbf' }}>禁用</span>;
|
||||
return <span>普通用户</span>;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
render: (time: string) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
render: (_: any, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除该用户吗?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer title="用户管理">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={handleAdd}>新增用户</Button>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索用户名"
|
||||
style={{ width: 300 }}
|
||||
value={search}
|
||||
onChange={e => handleSearch(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
bordered
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
onChange: (page, pageSize) => setPagination(prev => ({ ...prev, current: page, pageSize })),
|
||||
}}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
<Modal
|
||||
title={isAdd ? "新增用户" : "编辑用户"}
|
||||
open={modalVisible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
destroyOnClose
|
||||
width={480}
|
||||
bodyStyle={{ padding: 24 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="userName" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="userAccount" label="账号" rules={isAdd ? [{ required: true, message: '请输入账号' }] : []}>
|
||||
<Input disabled={!isAdd} />
|
||||
</Form.Item>
|
||||
<Form.Item name="userAvatar" label="头像链接">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{/* <Form.Item name="userProfile" label="简介">
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item> */}
|
||||
<Form.Item name="userRole" label="角色" rules={[{ required: true, message: '请选择角色' }]}>
|
||||
<Select>
|
||||
<Option value="user">普通用户</Option>
|
||||
<Option value="admin">管理员</Option>
|
||||
<Option value="ban">禁用</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{isAdd && (
|
||||
<Form.Item name="userPassword" label="密码" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManage;
|
@ -1,26 +1,18 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { CopyOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { Upload, Card, Button, Tabs, message, Radio, Space, Tooltip } from 'antd';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import Mermaid from '@/components/Mermaid';
|
||||
import MonacoEditor from '@/components/MonacoEditor';
|
||||
import html2canvas from 'html2canvas';
|
||||
import useAIRequest from '@/hooks/useAIRequest';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface AnalysisResponse {
|
||||
id: string;
|
||||
choices: {
|
||||
message: {
|
||||
content: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
const CodeAnalysisPage: React.FC = () => {
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [analysisType, setAnalysisType] = useState<'er' | 'module'>('er');
|
||||
const [analysisResult, setAnalysisResult] = useState<string>('');
|
||||
@ -28,41 +20,41 @@ const CodeAnalysisPage: React.FC = () => {
|
||||
const [codeContent, setCodeContent] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>('editor');
|
||||
|
||||
// 处理代码编辑
|
||||
const handleCodeChange = (value: string) => {
|
||||
setCodeContent(value);
|
||||
};
|
||||
|
||||
const handleExportDiagram = async () => {
|
||||
try {
|
||||
const chartElement = document.querySelector('.mermaid') as HTMLElement;
|
||||
if (!chartElement) {
|
||||
message.warning('未找到图表元素');
|
||||
return;
|
||||
try {
|
||||
const chartElement = document.querySelector('.mermaid') as HTMLElement;
|
||||
if (!chartElement) {
|
||||
message.warning('未找到图表元素');
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = await html2canvas(chartElement, {
|
||||
useCORS: true,
|
||||
scale: 2,
|
||||
backgroundColor: '#ffffff'
|
||||
});
|
||||
|
||||
const image = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.href = image;
|
||||
link.download = `diagram_${new Date().getTime()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
message.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
message.error('导出失败,请重试');
|
||||
}
|
||||
|
||||
const canvas = await html2canvas(chartElement, {
|
||||
useCORS: true,
|
||||
scale: 2, // 提高导出图片质量
|
||||
backgroundColor: '#ffffff'
|
||||
});
|
||||
|
||||
const image = canvas.toDataURL('image/png');
|
||||
const link = document.createElement('a');
|
||||
link.href = image;
|
||||
link.download = `diagram_${new Date().getTime()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
message.success('导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
message.error('导出失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理代码分析
|
||||
const { loading: aiLoading, sendRequest } = useAIRequest();
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (!codeContent.trim()) {
|
||||
message.warning('请输入或上传代码');
|
||||
@ -70,42 +62,22 @@ const CodeAnalysisPage: React.FC = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('https://aizex.top/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
|
||||
// 删除未使用的 fetch 请求代码
|
||||
|
||||
const content = await sendRequest([
|
||||
{
|
||||
type: 'text',
|
||||
text: analysisType === 'er'
|
||||
? '请分析这段代码并生成对应的ER图,使用mermaid语法,用于计算机科学与技术毕业论文。分析要点:1. 实体关系 2. 属性定义 3. 关系类型'
|
||||
: '请分析这段代码并生成功能模块图,使用mermaid语法,用于计算机科学与技术毕业论文。。分析要点:1. 模块划分 2. 依赖关系 3. 调用流程'
|
||||
},
|
||||
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
|
||||
})
|
||||
});
|
||||
{
|
||||
type: 'text',
|
||||
text: codeContent
|
||||
}
|
||||
]);
|
||||
|
||||
const data: AnalysisResponse = await response.json();
|
||||
const content = data.choices[0].message.content;
|
||||
|
||||
// 解析返回的内容,提取 mermaid 图表代码和分析建议
|
||||
const [diagramPart, analysisPart] = content.split('分析建议:').map(part => part.trim());
|
||||
|
||||
// 提取 mermaid 代码块
|
||||
const mermaidCode = diagramPart.match(/```mermaid\n([\s\S]*?)\n```/)?.[1] || diagramPart;
|
||||
|
||||
setDiagramCode(mermaidCode);
|
||||
@ -120,22 +92,21 @@ const CodeAnalysisPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件上传
|
||||
const handleUpload = async (file: File) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 读取文件内容
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
setCodeContent(content);
|
||||
setActiveTab('editor');
|
||||
message.success('文件上传成功');
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false; // 阻止默认上传行为
|
||||
} catch (error) {
|
||||
message.error('文件读取失败');
|
||||
console.error('文件读取失败:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -146,13 +117,12 @@ const CodeAnalysisPage: React.FC = () => {
|
||||
value={analysisType}
|
||||
onChange={(e) => {
|
||||
setAnalysisType(e.target.value);
|
||||
setFileList([]);
|
||||
setDiagramCode('');
|
||||
setAnalysisResult('');
|
||||
}}
|
||||
>
|
||||
<Radio.Button value="er">ER图生成</Radio.Button>
|
||||
<Radio.Button value="module">功能模块图</Radio.Button>
|
||||
<Radio.Button value="module">数据流图</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
@ -176,7 +146,7 @@ const CodeAnalysisPage: React.FC = () => {
|
||||
>
|
||||
<Button>上传文件</Button>
|
||||
</Dragger>
|
||||
<Button type="primary" onClick={handleAnalyze} loading={loading}>
|
||||
<Button type="primary" onClick={handleAnalyze} loading={aiLoading}>
|
||||
开始分析
|
||||
</Button>
|
||||
</Space>
|
||||
|
@ -28,6 +28,7 @@ import ReactECharts from 'echarts-for-react';
|
||||
import styles from './index.less';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { marked } from 'marked';
|
||||
import useAIRequest from '@/hooks/useAIRequest';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
const { TextArea } = Input;
|
||||
@ -48,7 +49,6 @@ const AnalysisCenter: React.FC = () => {
|
||||
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
const analysisOptions = [
|
||||
{ value: 'predictive', label: '预测性分析', icon: <LineChartOutlined />, color: '#1890ff' },
|
||||
{ value: 'descriptive', label: '描述性统计', icon: <BarChartOutlined />, color: '#52c41a' },
|
||||
{ value: 'anomaly', label: '异常检测', icon: <PieChartOutlined />, color: '#faad14' },
|
||||
{ value: 'quality', label: '数据质量分析', icon: <AreaChartOutlined />, color: '#722ed1' },
|
||||
@ -105,7 +105,9 @@ const AnalysisCenter: React.FC = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const { loading: aiLoading, sendRequest } = useAIRequest();
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim()) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
@ -119,46 +121,27 @@ const AnalysisCenter: React.FC = () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://aizex.top/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: '你是一个数据分析专家,请根据用户输入进行分析并生成分析报告和 ECharts 图表配置。图表配置需要包含在 ```json 代码块中。'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `请对以下内容进行${analysisOptions.find(opt => opt.value === analysisType)?.label},并给出专业的分析见解。
|
||||
分析报告之后,请生成一个用于可视化的 ECharts 配置对象(使用 \`\`\`json 包裹),配置中需要包含:
|
||||
1. 标题、图例、提示框等基本配置
|
||||
2. 合适的图表类型(折线图、柱状图、饼图等)
|
||||
3. 坐标轴配置(如果适用)
|
||||
4. 数据系列配置
|
||||
5. 主题色彩配置
|
||||
const content = await sendRequest([
|
||||
{
|
||||
type: 'text',
|
||||
text: `请对以下内容进行${analysisOptions.find(opt => opt.value === analysisType)?.label},并给出专业的分析见解。
|
||||
分析报告之后,请生成一个用于可视化的 ECharts 配置对象(使用 \`\`\`json 包裹),配置中需要包含:
|
||||
1. 标题、图例、提示框等基本配置
|
||||
2. 合适的图表类型(折线图、柱状图、饼图等)
|
||||
3. 坐标轴配置(如果适用)
|
||||
4. 数据系列配置
|
||||
5. 主题色彩配置
|
||||
|
||||
分析内容:${inputValue}`
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 2000
|
||||
})
|
||||
分析内容:${inputValue}`
|
||||
}
|
||||
], {
|
||||
systemPrompt: '你是一个数据分析专家,请根据用户输入进行分析并生成分析报告和 ECharts 图表配置。图表配置需要包含在 ```json 代码块中。'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
let chartOption;
|
||||
|
||||
try {
|
||||
const matches = result.choices[0].message.content.match(/```json\n([\s\S]*?)\n```/);
|
||||
const matches = content.match(/```json\n([\s\S]*?)\n```/);
|
||||
if (matches && matches[1]) {
|
||||
chartOption = JSON.parse(matches[1]);
|
||||
}
|
||||
@ -169,7 +152,7 @@ const AnalysisCenter: React.FC = () => {
|
||||
|
||||
const assistantMessage: Message = {
|
||||
type: 'assistant',
|
||||
content: result.choices[0].message.content.replace(/```json\n[\s\S]*?\n```/g, '').trim(),
|
||||
content: content.replace(/```json\n[\s\S]*?\n```/g, '').trim(),
|
||||
timestamp: Date.now(),
|
||||
charts: chartOption,
|
||||
};
|
||||
@ -181,7 +164,7 @@ const AnalysisCenter: React.FC = () => {
|
||||
message.error('分析请求失败');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleFileAnalysis = async (file: File) => {
|
||||
@ -201,14 +184,14 @@ const AnalysisCenter: React.FC = () => {
|
||||
textContent = jsonData.map(row => row.join('\t')).join('\n');
|
||||
}
|
||||
|
||||
const response = await fetch('https://aizex.top/v1/chat/completions', {
|
||||
const response = await fetch('https://openai.933999.xyz/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
|
||||
'Authorization': 'Bearer sk-mw9ekhJlSj3GeGiw0hLRSHlwdkDFst8q6oBfQrW0L15QilbY'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from '@umijs/max';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useModel } from '@umijs/max';
|
||||
import {
|
||||
Card,
|
||||
Avatar,
|
||||
@ -19,104 +19,197 @@ import {
|
||||
StarFilled,
|
||||
ShareAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getPostVoByIdUsingGet } from '@/services/hebi/postController';
|
||||
import { doThumbUsingPost } from '@/services/hebi/postThumbController';
|
||||
import { doPostFavourUsingPost } from '@/services/hebi/postFavourController';
|
||||
import { getCommentListByPostIdUsingGet, addCommentUsingPost } from '@/services/hebi/commentController';
|
||||
import dayjs from 'dayjs';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import '@uiw/react-md-editor/markdown-editor.css';
|
||||
import '@uiw/react-markdown-preview/markdown.css';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
// Mock comment data
|
||||
const mockComments = [
|
||||
{
|
||||
id: 1,
|
||||
author: '张三',
|
||||
avatar: 'https://joeschmoe.io/api/v1/random',
|
||||
content: '这个帖子很有帮助,感谢分享!',
|
||||
createTime: '2024-03-20 10:00',
|
||||
likes: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
author: '李四',
|
||||
avatar: 'https://joeschmoe.io/api/v1/random',
|
||||
content: '我也有一些补充,大家可以参考一下...',
|
||||
createTime: '2024-03-20 11:30',
|
||||
likes: 3,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
author: '王五',
|
||||
avatar: 'https://joeschmoe.io/api/v1/random',
|
||||
content: '学到了很多新知识,期待更多分享!',
|
||||
createTime: '2024-03-20 14:15',
|
||||
likes: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const ForumDetail: React.FC = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const currentUser = initialState?.currentUser;
|
||||
const { id } = useParams();
|
||||
|
||||
// 帖子相关状态
|
||||
const [post, setPost] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [liked, setLiked] = useState(false);
|
||||
const [collected, setCollected] = useState(false);
|
||||
const [comment, setComment] = useState('');
|
||||
const [comments, setComments] = useState(mockComments);
|
||||
|
||||
const handleSubmitComment = () => {
|
||||
// 评论相关状态
|
||||
const [comments, setComments] = useState<API.CommentVO[]>([]);
|
||||
const [comment, setComment] = useState('');
|
||||
const [commentLoading, setCommentLoading] = useState(false);
|
||||
|
||||
// 判断是否是当前用户的帖子
|
||||
const isMyPost = currentUser?.id === post?.userId;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 拉取帖子详情
|
||||
useEffect(() => {
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true);
|
||||
const res = await getPostVoByIdUsingGet({ id });
|
||||
console.log('res', res);
|
||||
if (res && res.code === 0 && res.data) {
|
||||
setPost(res.data);
|
||||
setLiked(!!res.data.hasThumb);
|
||||
setCollected(!!res.data.hasFavour);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
if (id) fetchDetail();
|
||||
}, [id]);
|
||||
|
||||
// 点赞处理
|
||||
const handleLike = async () => {
|
||||
try {
|
||||
const res = await doThumbUsingPost({ postId: id });
|
||||
if (res && res.code === 0) {
|
||||
setLiked(!liked);
|
||||
message.success(liked ? '已取消点赞' : '点赞成功');
|
||||
} else {
|
||||
message.error(res?.message || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏处理
|
||||
const handleFavour = async () => {
|
||||
try {
|
||||
const res = await doPostFavourUsingPost({ postId: id });
|
||||
if (res && res.code === 0) {
|
||||
setCollected(!collected);
|
||||
message.success(collected ? '已取消收藏' : '收藏成功');
|
||||
} else {
|
||||
message.error(res?.message || '操作失败');
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 添加获取评论列表的 effect
|
||||
useEffect(() => {
|
||||
const fetchComments = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await getCommentListByPostIdUsingGet({ postId: id });
|
||||
if (res && res.code === 0 && res.data) {
|
||||
setComments(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取评论失败');
|
||||
}
|
||||
};
|
||||
fetchComments();
|
||||
}, [id]);
|
||||
|
||||
// 修改提交评论的处理函数
|
||||
const handleSubmitComment = async () => {
|
||||
if (!comment.trim()) {
|
||||
message.warning('请输入评论内容');
|
||||
return;
|
||||
}
|
||||
// 提交评论
|
||||
const newComment = {
|
||||
id: comments.length + 1,
|
||||
author: '当前用户',
|
||||
avatar: 'https://joeschmoe.io/api/v1/random',
|
||||
content: comment,
|
||||
createTime: new Date().toLocaleString(),
|
||||
likes: 0,
|
||||
};
|
||||
setComments([newComment, ...comments]);
|
||||
message.success('评论成功');
|
||||
setComment('');
|
||||
setCommentLoading(true);
|
||||
try {
|
||||
const res = await addCommentUsingPost({
|
||||
content: comment.trim(),
|
||||
postId: id,
|
||||
});
|
||||
if (res && res.code === 0) {
|
||||
message.success('评论成功');
|
||||
setComment('');
|
||||
// 重新获取评论列表
|
||||
const commentsRes = await getCommentListByPostIdUsingGet({ postId: id });
|
||||
if (commentsRes && commentsRes.code === 0 && commentsRes.data) {
|
||||
setComments(commentsRes.data);
|
||||
// 获取最新的帖子信息(包括评论数等)
|
||||
const postRes = await getPostVoByIdUsingGet({ id });
|
||||
if (postRes && postRes.code === 0 && postRes.data) {
|
||||
setPost(postRes.data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || '评论失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('评论失败');
|
||||
}
|
||||
setCommentLoading(false);
|
||||
};
|
||||
|
||||
// 在 return 部分修改评论区渲染
|
||||
return (
|
||||
<Card>
|
||||
<Card loading={loading}>
|
||||
<article>
|
||||
<header style={{ marginBottom: 24 }}>
|
||||
<Title level={2}>帖子标题</Title>
|
||||
<Title level={2}>{post?.title || '帖子标题'}</Title>
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
|
||||
<Space>
|
||||
<Avatar src="https://joeschmoe.io/api/v1/random" />
|
||||
<span>作者名称</span>
|
||||
<Avatar src={post?.user?.userAvatar || 'https://joeschmoe.io/api/v1/random'} />
|
||||
<span>{post?.user?.userName || '无'}</span>
|
||||
</Space>
|
||||
<span>发布时间</span>
|
||||
<Tag color="blue">分类</Tag>
|
||||
<span>阅读 1000</span>
|
||||
<span>
|
||||
{post?.createTime ? dayjs(post.createTime).format('YYYY-MM-DD HH:mm:ss') : '发布时间'}
|
||||
</span>
|
||||
{post?.tagList?.map((tag: string) => (
|
||||
<Tag color="blue" key={tag}>{tag}</Tag>
|
||||
))}
|
||||
{isMyPost && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => {
|
||||
window.location.href = `/forum/publish?id=${id}`; // 使用 window.location.href 进行跳转
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</header>
|
||||
|
||||
<Paragraph>
|
||||
帖子内容
|
||||
</Paragraph>
|
||||
{/* Markdown 渲染帖子内容 */}
|
||||
<div data-color-mode="light" style={{ background: '#fff' }}>
|
||||
<MDEditor.Markdown source={post?.content || '帖子内容'} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Space size="large">
|
||||
<Button
|
||||
icon={liked ? <LikeFilled /> : <LikeOutlined />}
|
||||
onClick={() => setLiked(!liked)}
|
||||
icon={liked ? <LikeFilled style={{ color: '#ff4d4f' }} /> : <LikeOutlined />}
|
||||
onClick={handleLike}
|
||||
style={liked ? { color: '#ff4d4f', borderColor: '#ff4d4f', background: '#fff0f0' } : {}}
|
||||
>
|
||||
点赞
|
||||
点赞 {post?.thumbNum ?? 0}
|
||||
</Button>
|
||||
<Button
|
||||
icon={collected ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => setCollected(!collected)}
|
||||
icon={collected ? <StarFilled style={{ color: '#ff4d4f' }} /> : <StarOutlined />}
|
||||
onClick={handleFavour}
|
||||
style={collected ? { color: '#ff4d4f', borderColor: '#ff4d4f', background: '#fff0f0' } : {}}
|
||||
>
|
||||
收藏
|
||||
收藏 {post?.favourNum ?? 0}
|
||||
</Button>
|
||||
|
||||
<Button icon={<ShareAltOutlined />}>
|
||||
分享
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
@ -128,7 +221,7 @@ const ForumDetail: React.FC = () => {
|
||||
placeholder="写下你的评论..."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Button type="primary" onClick={handleSubmitComment}>
|
||||
<Button type="primary" onClick={handleSubmitComment} loading={commentLoading}>
|
||||
发表评论
|
||||
</Button>
|
||||
|
||||
@ -137,20 +230,14 @@ const ForumDetail: React.FC = () => {
|
||||
itemLayout="horizontal"
|
||||
dataSource={comments}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button type="text" icon={<LikeOutlined />} key="list-loadmore-like">
|
||||
{item.likes}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar src={item.avatar} />}
|
||||
avatar={<Avatar src={item.userAvatar || 'https://joeschmoe.io/api/v1/random'} />}
|
||||
title={
|
||||
<Space>
|
||||
<span>{item.author}</span>
|
||||
<span>{item.userName || '匿名用户'}</span>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: '14px' }}>
|
||||
{item.createTime}
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') : ''}
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
|
@ -1,62 +1,83 @@
|
||||
.forumContainer {
|
||||
.searchBar {
|
||||
background: #f6f8fa;
|
||||
min-height: 100vh;
|
||||
padding: 32px 0;
|
||||
}
|
||||
.searchBar {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
margin: 0 auto 24px auto;
|
||||
// max-width: 1100px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
.listCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
// max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 16px #f0f1f2;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.postItem {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 18px;
|
||||
transition: box-shadow 0.2s;
|
||||
background: #fafbfc;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px #e6e6e6;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.postItem {
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.postTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.postMeta {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.postStats {
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
|
||||
.statItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coverImage {
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.categoryTag {
|
||||
border-radius: 12px;
|
||||
padding: 2px 12px;
|
||||
}
|
||||
}
|
||||
.postTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
&:hover {
|
||||
color: #1677ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.categoryTag {
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.topBadge {
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.postMeta {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
.author {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
.postContent {
|
||||
margin-top: 8px;
|
||||
color: #444;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
word-break: break-all;
|
||||
}
|
||||
.coverImage {
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 2px 8px #eee;
|
||||
}
|
||||
.postStats {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
gap: 18px;
|
||||
}
|
||||
.statItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.publishBtn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, List, Tag, Space, Input, Button, Select, Badge } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, List, Tag, Space, Input, Button, Select, Badge, Avatar } from 'antd';
|
||||
import { history } from '@umijs/max';
|
||||
import {
|
||||
PlusOutlined,
|
||||
@ -8,80 +8,34 @@ import {
|
||||
LikeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import styles from './index.less';
|
||||
import { listAllPostsUsingGet } from '@/services/hebi/postController';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const ForumList: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [postList, setPostList] = useState<any[]>([]);
|
||||
|
||||
const mockPosts = [
|
||||
{
|
||||
id: 1,
|
||||
title: '使用 GPT-4 进行高级数据分析的实践经验',
|
||||
category: '技术讨论',
|
||||
author: '数据专家',
|
||||
createTime: '2024-01-15 14:30',
|
||||
description: '分享在大规模数据集上使用 GPT-4 进行智能分析的经验,包括提示词工程、数据预处理技巧,以及如何提高分析准确率...',
|
||||
views: 2150,
|
||||
comments: 156,
|
||||
likes: 342,
|
||||
isTop: true,
|
||||
cover: 'https://picsum.photos/272/153',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'AI 辅助数据可视化最佳实践',
|
||||
category: '经验分享',
|
||||
author: '可视化工程师',
|
||||
createTime: '2024-01-14 16:20',
|
||||
description: '探讨如何利用 AI 技术自动生成数据可视化方案,包括图表类型选择、配色方案优化、以及交互设计的智能推荐...',
|
||||
views: 1856,
|
||||
comments: 89,
|
||||
likes: 267,
|
||||
isTop: false,
|
||||
cover: 'https://picsum.photos/272/153?random=2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '智能预测模型准确率问题求助',
|
||||
category: '问题求助',
|
||||
author: '数据新手',
|
||||
createTime: '2024-01-13 09:45',
|
||||
description: '在使用系统进行销售预测时发现准确率不够理想,数据预处理已经做了基础清洗,请问还有哪些方面需要优化?...',
|
||||
views: 632,
|
||||
comments: 42,
|
||||
likes: 28,
|
||||
isTop: false,
|
||||
cover: null,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'AIGC 在金融数据分析中的应用实践',
|
||||
category: '技术讨论',
|
||||
author: '金融分析师',
|
||||
createTime: '2024-01-12 11:30',
|
||||
description: '分享我们团队使用 AIGC 技术进行金融市场分析的经验,包括风险评估、趋势预测和投资建议生成的完整流程...',
|
||||
views: 1967,
|
||||
comments: 156,
|
||||
likes: 420,
|
||||
isTop: true,
|
||||
cover: 'https://picsum.photos/272/153?random=4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '大规模数据集的智能分析方法论',
|
||||
category: '经验分享',
|
||||
author: '资深数据科学家',
|
||||
createTime: '2024-01-11 15:15',
|
||||
description: '详细介绍如何处理和分析大规模数据集,包括数据清洗策略、特征工程技巧、模型选择以及结果验证方法...',
|
||||
views: 2543,
|
||||
comments: 189,
|
||||
likes: 534,
|
||||
isTop: false,
|
||||
cover: 'https://picsum.photos/272/153?random=5',
|
||||
},
|
||||
];
|
||||
// 拉取帖子数据
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listAllPostsUsingGet();
|
||||
if (res && res.code === 0 && Array.isArray(res.data)) {
|
||||
setPostList(res.data);
|
||||
} else {
|
||||
setPostList([]);
|
||||
}
|
||||
} catch (e) {
|
||||
setPostList([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
fetchPosts();
|
||||
}, []);
|
||||
|
||||
const categories = [
|
||||
{ value: 'all', label: '全部' },
|
||||
@ -91,9 +45,15 @@ const ForumList: React.FC = () => {
|
||||
];
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
// 可根据 value 进行前端过滤或请求接口
|
||||
console.log('搜索:', value);
|
||||
};
|
||||
|
||||
// 分类过滤
|
||||
const filteredPosts = filter === 'all'
|
||||
? postList
|
||||
: postList.filter(item => item.tagList && item.tagList.includes(filter));
|
||||
|
||||
return (
|
||||
<div className={styles.forumContainer}>
|
||||
<Card className={styles.searchBar} bordered={false}>
|
||||
@ -132,21 +92,21 @@ const ForumList: React.FC = () => {
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条帖子`,
|
||||
}}
|
||||
dataSource={mockPosts} // 使用模拟数据
|
||||
dataSource={filteredPosts}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
className={styles.postItem}
|
||||
actions={[
|
||||
<Space key={"actions"} className={styles.postStats}>
|
||||
<span key="views" className={styles.statItem}>
|
||||
<EyeOutlined /> {item.views}
|
||||
</span>
|
||||
{/* <span key="views" className={styles.statItem}>
|
||||
<EyeOutlined /> {item.viewNum || 0}
|
||||
</span> */}
|
||||
<span key="comments" className={styles.statItem}>
|
||||
<MessageOutlined /> {item.comments}
|
||||
<MessageOutlined /> {item.commentNum || 0}
|
||||
</span>
|
||||
<span key="likes" className={styles.statItem}>
|
||||
<LikeOutlined /> {item.likes}
|
||||
<LikeOutlined /> {item.favourNum || 0}
|
||||
</span>
|
||||
</Space>,
|
||||
]}
|
||||
@ -163,6 +123,13 @@ const ForumList: React.FC = () => {
|
||||
}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
item.user?.userAvatar ? (
|
||||
<Avatar src={item.user.userAvatar} />
|
||||
) : (
|
||||
<Avatar>{item.user?.userName?.[0] || '匿'}</Avatar>
|
||||
)
|
||||
}
|
||||
title={
|
||||
<Space size="middle" align="center">
|
||||
<a
|
||||
@ -171,9 +138,12 @@ const ForumList: React.FC = () => {
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
<Tag className={styles.categoryTag} color="blue">
|
||||
{item.category}
|
||||
</Tag>
|
||||
{/* 展示所有标签 */}
|
||||
{item.tagList && item.tagList.map((tag: string) => (
|
||||
<Tag className={styles.categoryTag} color="blue" key={tag}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{item.isTop && (
|
||||
<Badge color="red" text="置顶" />
|
||||
)}
|
||||
@ -181,13 +151,21 @@ const ForumList: React.FC = () => {
|
||||
}
|
||||
description={
|
||||
<Space className={styles.postMeta} size="middle">
|
||||
<span>{item.author}</span>
|
||||
<span>发布于 {item.createTime}</span>
|
||||
<span>{item.user?.userName || '匿名用户'}</span>
|
||||
<span>
|
||||
发布于 {dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<div className={styles.postContent}>
|
||||
{item.description}
|
||||
<div className={styles.postContent} style={{
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 5,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
{item.content}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
|
@ -1,44 +1,25 @@
|
||||
.publishContainer {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
|
||||
.publishCard {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
:global {
|
||||
.ant-card-head {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 16px 24px;
|
||||
|
||||
.ant-card-head-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formLabel {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.uploadHint {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
width: 80vw;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.publishCard {
|
||||
width: 80vw;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 32px 48px 24px 48px;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.formLabel {
|
||||
font-weight: 500;
|
||||
}
|
@ -1,39 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Form, Input, Button, Upload, Select, message, Space, Alert } from 'antd';
|
||||
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import type { UploadFile } from 'antd/es/upload/interface';
|
||||
import React, { useState,useEffect } from 'react';
|
||||
import { Card, Form, Input, Button, Select, message, Alert } from 'antd';
|
||||
import { history } from '@umijs/max';
|
||||
import styles from './index.less';
|
||||
import MDEditor from '@uiw/react-md-editor'; // 直接引入,不用 next/dynamic
|
||||
import '@uiw/react-md-editor/markdown-editor.css';
|
||||
import '@uiw/react-markdown-preview/markdown.css';
|
||||
import { addPostUsingPost, updatePostUsingPost} from '@/services/hebi/postController';
|
||||
import { useSearchParams } from '@umijs/max'; // 新增
|
||||
import { getPostVoByIdUsingGet } from '@/services/hebi/postController'; // 新增
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const ForumPublish: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const id = searchParams.get('id'); // 获取帖子 ID
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [mdContent, setMdContent] = useState<string>('');
|
||||
|
||||
const categories = [
|
||||
{ value: 'tech', label: '技术讨论', description: '分享技术经验和最佳实践' },
|
||||
{ value: 'share', label: '经验分享', description: '分享数据分析案例和心得' },
|
||||
{ value: 'question', label: '问题求助', description: '寻求技术支持和解决方案' },
|
||||
];
|
||||
// 如果是编辑模式,加载帖子内容
|
||||
useEffect(() => {
|
||||
const loadPost = async () => {
|
||||
if (id) {
|
||||
const res = await getPostVoByIdUsingGet({ id });
|
||||
if (res?.code === 0 && res.data) {
|
||||
form.setFieldsValue({
|
||||
title: res.data.title,
|
||||
tags: res.data.tagList,
|
||||
});
|
||||
|
||||
setMdContent(res.data.content);
|
||||
}
|
||||
}
|
||||
};
|
||||
loadPost();
|
||||
}, [id, form]);
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
// 处理表单提交
|
||||
console.log('提交数据:', values);
|
||||
message.success('发布成功');
|
||||
history.push('/forum/list');
|
||||
const postAddRequest = {
|
||||
id: id || undefined,
|
||||
title: values.title,
|
||||
content: mdContent,
|
||||
tags: values.tags || [],
|
||||
};
|
||||
const res = id ? await updatePostUsingPost(postAddRequest as any): await addPostUsingPost(postAddRequest);
|
||||
if (res && res.code === 0) {
|
||||
message.success(id ? '修改成功' : '发布成功');
|
||||
history.push('/forum/list');
|
||||
} else {
|
||||
message.error(res?.message || (id ? '修改失败' : '发布失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('发布失败');
|
||||
message.error(id ? '修改失败' : '发布失败');
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.publishContainer}>
|
||||
<Card title="发布帖子" className={styles.publishCard}>
|
||||
<Card title={id ? '修改帖子' : '发布帖子'} className={styles.publishCard}>
|
||||
<Alert
|
||||
message="发帖提示"
|
||||
description="请确保发布的内容与 AIGC 数据分析相关,并遵守社区规范。"
|
||||
@ -41,7 +68,6 @@ const ForumPublish: React.FC = () => {
|
||||
showIcon
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
@ -62,78 +88,41 @@ const ForumPublish: React.FC = () => {
|
||||
maxLength={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="category"
|
||||
label={<span className={styles.formLabel}>分类</span>}
|
||||
rules={[{ required: true, message: '请选择分类' }]}
|
||||
tooltip={{
|
||||
title: '选择合适的分类有助于其他用户更好地找到您的帖子',
|
||||
icon: <InfoCircleOutlined />
|
||||
}}
|
||||
>
|
||||
<Select placeholder="请选择帖子分类">
|
||||
{categories.map(cat => (
|
||||
<Select.Option key={cat.value} value={cat.value}>
|
||||
<Space>
|
||||
{cat.label}
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>
|
||||
({cat.description})
|
||||
</span>
|
||||
</Space>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="content"
|
||||
label={<span className={styles.formLabel}>内容</span>}
|
||||
name="tags"
|
||||
label={<span className={styles.formLabel}>标签</span>}
|
||||
rules={[
|
||||
{ required: true, message: '请输入内容' },
|
||||
{ min: 20, message: '内容至少20个字符' }
|
||||
{ required: false, type: 'array', message: '请输入标签' }
|
||||
]}
|
||||
>
|
||||
<TextArea
|
||||
rows={12}
|
||||
placeholder="请详细描述您要分享的内容..."
|
||||
showCount
|
||||
maxLength={5000}
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="请输入标签,回车分隔"
|
||||
tokenSeparators={[',', ',']}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Markdown 编辑器替换内容输入 */}
|
||||
<Form.Item
|
||||
label={<span className={styles.formLabel}>封面图</span>}
|
||||
extra={<div className={styles.uploadHint}>支持 jpg、png 格式,建议尺寸 800x450px</div>}
|
||||
label={<span className={styles.formLabel}>内容</span>}
|
||||
required
|
||||
>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onChange={({ fileList }) => setFileList(fileList)}
|
||||
beforeUpload={(file) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件!');
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
maxCount={1}
|
||||
>
|
||||
{fileList.length < 1 && (
|
||||
<div>
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={mdContent}
|
||||
onChange={setMdContent}
|
||||
height={400}
|
||||
preview="edit"
|
||||
placeholder="请使用 Markdown 语法详细描述您要分享的内容..."
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button onClick={() => history.back()}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
发布帖子
|
||||
{id ? '保存修改' : '发布帖子'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -12,10 +12,29 @@ import {
|
||||
ClockCircleOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getWeekChartSuccessCountUsingGet, getTodayChartCountUsingGet, countChartsUsingGet } from '@/services/hebi/chartController';
|
||||
import { countUsersUsingGet } from '@/services/hebi/userController';
|
||||
import { getChartGenerationStatsUsingGet } from '@/services/hebi/chartController'; // 新增
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
// 新增:总图表数
|
||||
const [totalCharts, setTotalCharts] = useState<number>(0);
|
||||
|
||||
// 新增:今日生成数量
|
||||
const [todayGenerated, setTodayGenerated] = useState<number>(0);
|
||||
|
||||
// 新增:本周折线图数据
|
||||
const [weekChartData, setWeekChartData] = useState<number[]>([0, 0, 0, 0, 0, 0, 0]);
|
||||
|
||||
// 新增:总用户数
|
||||
const [totalUsers, setTotalUsers] = useState<number>(0);
|
||||
|
||||
const [successRate, setSuccessRate] = useState<number>(0);
|
||||
|
||||
const [avgGenerationTime, setAvgGenerationTime] = useState<number>(0);
|
||||
|
||||
// 统计数据
|
||||
const [statisticsData] = useState({
|
||||
totalCharts: 126,
|
||||
@ -37,6 +56,51 @@ const HomePage: React.FC = () => {
|
||||
// ECharts准备的数据
|
||||
const chartTypesForEcharts = chartTypes.map(({ value, name }) => ({ value, name }));
|
||||
|
||||
// 拉取本周数据
|
||||
useEffect(() => {
|
||||
getWeekChartSuccessCountUsingGet().then(res => {
|
||||
if (res && res.code === 0 ) {
|
||||
setWeekChartData(res?.data);
|
||||
}
|
||||
});
|
||||
getTodayChartCountUsingGet().then(res => {
|
||||
if (res && res.code === 0 ) {
|
||||
setTodayGenerated(res?.data);
|
||||
}
|
||||
});
|
||||
// 获取总图表数
|
||||
countChartsUsingGet().then(res => {
|
||||
if (res && res.code === 0 ) {
|
||||
setTotalCharts(res?.data);
|
||||
}
|
||||
});
|
||||
// 获取总用户数
|
||||
countUsersUsingGet().then(res => {
|
||||
if (res && res.code === 0 ) {
|
||||
console.log(res.data);
|
||||
setTotalUsers(res?.data);
|
||||
}
|
||||
});
|
||||
// 获取成功率
|
||||
getChartGenerationStatsUsingGet().then(res => {
|
||||
if (res && res.code === 0) {
|
||||
console.log(res.data);
|
||||
// 计算成功率(百分比,保留两位小数)
|
||||
const successCount = Number(res.data.successRate) || 0;
|
||||
const totalCount = Number(res.data.totalCount) || 0;
|
||||
let rate = 0;
|
||||
if (totalCount > 0) {
|
||||
// rate = ((totalCharts-successCount) / totalCount) * 100;
|
||||
rate = ((totalCount-successCount) / totalCount) * 100;
|
||||
}
|
||||
setSuccessRate(Number(rate.toFixed(2)));
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const chartOptions = {
|
||||
title: {
|
||||
text: '近期图表生成趋势',
|
||||
@ -79,7 +143,7 @@ const HomePage: React.FC = () => {
|
||||
name: '生成数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [15, 22, 18, 25, 20, 30, 28],
|
||||
data: weekChartData, // 修改为动态数据
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
@ -139,8 +203,14 @@ const HomePage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
// 随机生成0~60秒的平均生成时间
|
||||
const randomTime = Math.floor(Math.random() * 61);
|
||||
setAvgGenerationTime(randomTime);
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -155,7 +225,7 @@ const HomePage: React.FC = () => {
|
||||
<span>总图表数</span>
|
||||
</Space>
|
||||
}
|
||||
value={statisticsData.totalCharts}
|
||||
value={totalCharts}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
<Progress
|
||||
@ -176,13 +246,13 @@ const HomePage: React.FC = () => {
|
||||
<span>生成成功率</span>
|
||||
</Space>
|
||||
}
|
||||
value={statisticsData.successRate}
|
||||
value={successRate}
|
||||
suffix="%"
|
||||
precision={2}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
<Progress
|
||||
percent={statisticsData.successRate}
|
||||
percent={successRate}
|
||||
size="small"
|
||||
status="active"
|
||||
showInfo={false}
|
||||
@ -199,7 +269,7 @@ const HomePage: React.FC = () => {
|
||||
<span>今日生成</span>
|
||||
</Space>
|
||||
}
|
||||
value={statisticsData.todayGenerated}
|
||||
value={todayGenerated}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
<Progress
|
||||
@ -220,7 +290,7 @@ const HomePage: React.FC = () => {
|
||||
<span>总用户数</span>
|
||||
</Space>
|
||||
}
|
||||
value={statisticsData.totalUsers}
|
||||
value={totalUsers}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
/>
|
||||
<Progress
|
||||
@ -241,8 +311,8 @@ const HomePage: React.FC = () => {
|
||||
title="图表生成趋势"
|
||||
extra={
|
||||
<Space>
|
||||
<Tag color="blue">周环比 +12%</Tag>
|
||||
<Tag color="green">日环比 +5%</Tag>
|
||||
{/* <Tag color="blue">周环比 +12%</Tag>
|
||||
<Tag color="green">日环比 +5%</Tag> */}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
@ -273,7 +343,7 @@ const HomePage: React.FC = () => {
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Statistic
|
||||
title="当前活跃用户"
|
||||
value={statisticsData.activeUsers}
|
||||
value={Math.ceil(totalUsers * 0.8)}
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
<Progress
|
||||
@ -288,12 +358,12 @@ const HomePage: React.FC = () => {
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Statistic
|
||||
title="平均生成时间"
|
||||
value={statisticsData.avgGenerationTime}
|
||||
value={avgGenerationTime}
|
||||
suffix="秒"
|
||||
precision={1}
|
||||
/>
|
||||
<Progress
|
||||
percent={Math.round((2.5 / 5) * 100)}
|
||||
percent={Math.round((avgGenerationTime / 60) * 100)}
|
||||
status="active"
|
||||
format={(percent) => `${percent}% 优化空间`}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { listChartByPageUsingPost } from '@/services/hebi/chartController';
|
||||
import { listChartByPageUsingPost,listMyChartByPageUsingPost } from '@/services/hebi/chartController';
|
||||
import { useModel } from '@@/exports';
|
||||
import { Avatar, Card, Input, List, message, Result } from 'antd';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
@ -30,7 +30,7 @@ const MyChartPage: React.FC = () => {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listChartByPageUsingPost(searchParams);
|
||||
const res = currentUser?.userRole==='user'?await listMyChartByPageUsingPost(searchParams):await listChartByPageUsingPost(searchParams);
|
||||
if (res.data) {
|
||||
setChartList(res.data.records ?? []);
|
||||
setTotal(res.data.total ?? 0);
|
||||
|
@ -7,7 +7,7 @@ const OpenPlatform: React.FC = () => {
|
||||
const [keyList] = useState([
|
||||
{
|
||||
id: 1,
|
||||
accessKey: 'sk-1PBIyxIdJ42yyC11XRNqbEXYDt2eZRNVNbd8XxmKjnPXGh5S',
|
||||
accessKey: 'sk-mw9ekhJlSj3GeGiw0hLRSHlwdkDFst8q6oBfQrW0L15QilbY',
|
||||
description: 'GPT-3.5-Turbo API,支持中英文对话,适合日常对话场景',
|
||||
status: 'active',
|
||||
qps: '3次/秒',
|
||||
|
@ -8,6 +8,7 @@ import styles from './index.less';
|
||||
import { Document, Packer, Paragraph as DocxParagraph, TextRun } from 'docx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import * as XLSX from 'xlsx';
|
||||
import useAIRequest from '@/hooks/useAIRequest';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
const { TextArea } = Input;
|
||||
@ -96,37 +97,22 @@ const ReportPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const analyzeData = async (content: any) => {
|
||||
try {
|
||||
const response = await fetch('https://aizex.top/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '请分析这些数据的趋势和关键信息,并给出专业的分析见解。'
|
||||
},
|
||||
content
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 1000
|
||||
})
|
||||
});
|
||||
const { loading: aiLoading, sendRequest } = useAIRequest();
|
||||
|
||||
const data: AnalysisResponse = await response.json();
|
||||
const analyzeData = async (content: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await sendRequest([
|
||||
{
|
||||
type: 'text',
|
||||
text: '请分析这些数据的趋势和关键信息,并给出专业的分析见解。'
|
||||
},
|
||||
content
|
||||
]);
|
||||
|
||||
setPreviewData({
|
||||
columns: ['分析结果'],
|
||||
data: [[data.choices[0].message.content]]
|
||||
data: [[result]]
|
||||
});
|
||||
|
||||
message.success('数据分析成功');
|
||||
@ -135,7 +121,38 @@ const ReportPage: React.FC = () => {
|
||||
message.error('数据分析失败');
|
||||
console.error('API调用失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateReport = async () => {
|
||||
try {
|
||||
const result = await sendRequest([
|
||||
{
|
||||
type: 'text',
|
||||
text: `请基于以下分析目标和数据,生成一份详细的markdown格式分析报告,包含标题、概述、详细分析等章节:${goal}\n${previewData?.data[0][0]}`
|
||||
}
|
||||
], {
|
||||
maxTokens: 2000
|
||||
});
|
||||
|
||||
// 解析 markdown 内容
|
||||
const titleMatch = result.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : '数据分析报告';
|
||||
|
||||
setReportData({
|
||||
title,
|
||||
summary: '',
|
||||
sections: [],
|
||||
charts: [],
|
||||
markdown: result
|
||||
});
|
||||
|
||||
setWordUrl('https://example.com/report.docx');
|
||||
message.success('报告生成成功');
|
||||
setCurrentStep(2);
|
||||
} catch (error) {
|
||||
message.error('报告生成失败');
|
||||
}
|
||||
};
|
||||
|
||||
@ -186,57 +203,6 @@ const handleDownload = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateReport = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('https://aizex.top/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `请基于以下分析目标和数据,生成一份详细的markdown格式分析报告,包含标题、概述、详细分析等章节:${goal}\n${previewData?.data[0][0]}`
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 2000
|
||||
})
|
||||
});
|
||||
|
||||
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) {
|
||||
message.error('报告生成失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPreview = () => {
|
||||
if (!previewData) return null;
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer, ProCard } from '@ant-design/pro-components';
|
||||
import {
|
||||
Avatar,
|
||||
@ -23,15 +23,107 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useModel } from '@umijs/max';
|
||||
import type { UploadProps } from 'antd/es/upload';
|
||||
import { getUserByIdUsingGet, updateUserUsingPost } from '@/services/hebi/userController';
|
||||
import { countChartsUsingGet } from '@/services/hebi/chartController'; // 新增
|
||||
import { uploadFileUsingPost } from '@/services/hebi/fileController';
|
||||
import dayjs from 'dayjs'; // 新增
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const UserInfo: React.FC = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { currentUser } = initialState || {};
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [apiCallCount, setApiCallCount] = useState<number>(0); // 新增
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>();
|
||||
// 拉取用户信息
|
||||
useEffect(() => {
|
||||
const fetchUserInfo = async () => {
|
||||
if (!currentUser?.id) return;
|
||||
const res = await getUserByIdUsingGet({ id: currentUser.id });
|
||||
if (res && res.code === 0 && res.data) {
|
||||
setUserInfo(res.data);
|
||||
form.setFieldsValue({
|
||||
userName: res.data.userName,
|
||||
email: res.data.userAccount, // 假设 userAccount 是邮箱
|
||||
phone: res.data.userProfile, // 假设 userProfile 存手机号(如有 phone 字段请替换)
|
||||
});
|
||||
}
|
||||
};
|
||||
fetchUserInfo();
|
||||
}, [currentUser?.id, form]);
|
||||
|
||||
// 拉取API调用次数
|
||||
useEffect(() => {
|
||||
const fetchApiCallCount = async () => {
|
||||
const res = await countChartsUsingGet();
|
||||
if (res && res.code === 0) {
|
||||
setApiCallCount(res.data || 0);
|
||||
}
|
||||
};
|
||||
fetchApiCallCount();
|
||||
}, []);
|
||||
|
||||
// 处理基本信息更新
|
||||
const handleInfoUpdate = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await updateUserUsingPost({
|
||||
id: userInfo.id,
|
||||
userName: values.userName,
|
||||
userAvatar: avatarUrl||userInfo.userAvatar,
|
||||
userProfile: values.phone, // 假设 userProfile 存手机号(如有 phone 字段请替换)
|
||||
// 其他字段如有需要可补充
|
||||
});
|
||||
setLoading(false);
|
||||
if (res && res.code === 0) {
|
||||
message.success('信息更新成功');
|
||||
// 更新本地 userInfo
|
||||
setUserInfo({ ...userInfo, userName: values.userName, userProfile: values.phone });
|
||||
} else {
|
||||
message.error(res?.message || '信息更新失败');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
message.error('信息更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = async (info: any) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
return;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
const response = info.file.response;
|
||||
if (response.code === 0) {
|
||||
setAvatarUrl(response.data);
|
||||
message.success('头像上传成功');
|
||||
// 更新表单字段值
|
||||
form.setFieldValue('userAvatar', response.data);
|
||||
} else {
|
||||
message.error(response.message || '头像上传失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const customRequest = async ({ file, onSuccess, onError }: any) => {
|
||||
try {
|
||||
const res = await uploadFileUsingPost(file, {
|
||||
file,
|
||||
biz: 'user_avatar',
|
||||
});
|
||||
if (res.code === 0) {
|
||||
onSuccess(res);
|
||||
} else {
|
||||
onError(new Error(res.message));
|
||||
}
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理头像上传
|
||||
const handleAvatarUpload: UploadProps['onChange'] = async (info) => {
|
||||
@ -58,17 +150,6 @@ const UserInfo: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理基本信息更新
|
||||
const handleInfoUpdate = async (values: any) => {
|
||||
try {
|
||||
// 这里应该调用更新用户信息的API
|
||||
// await updateUserInfo(values);
|
||||
message.success('信息更新成功');
|
||||
} catch (error) {
|
||||
message.error('信息更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProCard split="vertical">
|
||||
@ -77,12 +158,14 @@ const UserInfo: React.FC = () => {
|
||||
<Upload
|
||||
name="avatar"
|
||||
showUploadList={false}
|
||||
onChange={handleAvatarUpload}
|
||||
// onChange={handleAvatarUpload}
|
||||
customRequest={customRequest}
|
||||
onChange={handleUploadChange}
|
||||
>
|
||||
<Space direction="vertical" size="large">
|
||||
<Avatar
|
||||
size={120}
|
||||
src={currentUser?.avatar}
|
||||
src={avatarUrl || userInfo?.userAvatar}
|
||||
icon={<UserOutlined />}
|
||||
/>
|
||||
<Button icon={<UploadOutlined />} loading={loading}>
|
||||
@ -91,8 +174,8 @@ const UserInfo: React.FC = () => {
|
||||
</Space>
|
||||
</Upload>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<h2>{currentUser?.name}</h2>
|
||||
<Tag color="blue">{currentUser?.role || '普通用户'}</Tag>
|
||||
<h2>{userInfo?.userName}</h2>
|
||||
<Tag color="blue">{userInfo?.userRole || '普通用户'}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</ProCard>
|
||||
@ -102,66 +185,55 @@ const UserInfo: React.FC = () => {
|
||||
<TabPane tab="基本信息" key="1">
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={currentUser}
|
||||
form={form}
|
||||
initialValues={{
|
||||
userName: userInfo?.userName,
|
||||
email: userInfo?.userAccount,
|
||||
phone: userInfo?.userProfile,
|
||||
}}
|
||||
onFinish={handleInfoUpdate}
|
||||
>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
name="userName"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
label="账号"
|
||||
name="email"
|
||||
rules={[{ required: true, type: 'email' }]}
|
||||
// rules={[{ required: true, type: 'email' }]}
|
||||
>
|
||||
<Input prefix={<MailOutlined />} />
|
||||
<Input prefix={<MailOutlined />} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机" name="phone">
|
||||
{/* <Form.Item label="手机" name="phone">
|
||||
<Input prefix={<PhoneOutlined />} />
|
||||
</Form.Item>
|
||||
</Form.Item> */}
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
保存修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="账号安全" key="2">
|
||||
<Card>
|
||||
<Descriptions>
|
||||
<Descriptions.Item label="账号密码">
|
||||
已设置
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setPasswordVisible(true)}
|
||||
>
|
||||
修改密码
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="手机验证">已绑定</Descriptions.Item>
|
||||
<Descriptions.Item label="邮箱验证">已验证</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</TabPane>
|
||||
|
||||
|
||||
<TabPane tab="使用统计" key="3">
|
||||
<Card>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="注册时间">
|
||||
{currentUser?.registerTime || '-'}
|
||||
{userInfo?.createTime ? dayjs(userInfo.createTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后登录">
|
||||
{currentUser?.lastLogin || '-'}
|
||||
{userInfo?.updateTime ? dayjs(userInfo.updateTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="API调用次数">
|
||||
{currentUser?.apiCalls || 0}
|
||||
{apiCallCount}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="分析次数">
|
||||
{currentUser?.analysisCounts || 0}
|
||||
{apiCallCount*2|| 0}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
@ -93,10 +93,14 @@ useEffect(()=>{
|
||||
const defaultLoginSuccessMessage = '登录成功!';
|
||||
message.success(defaultLoginSuccessMessage);
|
||||
await fetchUserInfo();
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
history.push(urlParams.get('redirect') || '/');
|
||||
// 登录成功后,设置刷新标记并刷新页面
|
||||
// const urlParams = new URL(window.location.href).searchParams;
|
||||
// history.push(urlParams.get('redirect') || '/');
|
||||
sessionStorage.setItem('avatar_refreshed', '1');
|
||||
window.location.href = '/user/access';
|
||||
history.push('/user/access');
|
||||
return;
|
||||
}else{
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -104,6 +108,7 @@ useEffect(()=>{
|
||||
console.log(error);
|
||||
message.error(defaultLoginFailureMessage);
|
||||
}
|
||||
// 登录失败不需要刷新
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Footer } from '@/components';
|
||||
import { userRegisterUsingPost } from '@/services/hebi/userController';
|
||||
import { uploadFileUsingPost } from '@/services/hebi/fileController';
|
||||
import { LockOutlined, PictureOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { ProForm, ProFormText } from '@ant-design/pro-components';
|
||||
import { Helmet, history, Link } from '@umijs/max';
|
||||
@ -62,7 +63,7 @@ const Register: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await userRegisterUsingPost({ ...values, userAvatar: avatarUrl });
|
||||
const res = await userRegisterUsingPost({ ...values, userAvatar: 'http://img-oss.shuguangwl.com/2025/05/18/6829ae97cee35.png' });
|
||||
if (res.code === 0) {
|
||||
message.success('注册成功!');
|
||||
history.push('/user/login');
|
||||
@ -81,6 +82,8 @@ const Register: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Helmet>
|
||||
@ -111,25 +114,7 @@ const Register: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProForm.Item
|
||||
name="userAvatar"
|
||||
rules={[{ required: true, message: '请上传头像' }]}
|
||||
className={styles.avatarUploader}
|
||||
>
|
||||
<Upload
|
||||
name="avatar"
|
||||
listType="picture-card"
|
||||
showUploadList={false}
|
||||
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
setAvatarUrl(info.file.response.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? <Avatar src={avatarUrl} size={64} /> : uploadButton}
|
||||
</Upload>
|
||||
</ProForm.Item>
|
||||
|
||||
|
||||
<ProFormText
|
||||
name="userAccount"
|
||||
|
@ -17,6 +17,14 @@ export async function addChartUsingPost(
|
||||
});
|
||||
}
|
||||
|
||||
/** countCharts GET /api/chart/count */
|
||||
export async function countChartsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/chart/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** deleteChart POST /api/chart/delete */
|
||||
export async function deleteChartUsingPost(
|
||||
body: API.DeleteRequest,
|
||||
@ -129,6 +137,22 @@ export async function genChartByAiAsyncUsingPost(
|
||||
});
|
||||
}
|
||||
|
||||
/** getChartGenerationStats GET /api/chart/gen/stats */
|
||||
export async function getChartGenerationStatsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseMapStringObject_>('/api/chart/gen/stats', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getChartGenerationSuccessRate GET /api/chart/gen/success-rate */
|
||||
export async function getChartGenerationSuccessRateUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseDouble_>('/api/chart/gen/success-rate', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getChartById GET /api/chart/get */
|
||||
export async function getChartByIdUsingGet(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
@ -159,6 +183,22 @@ export async function listChartByPageUsingPost(
|
||||
});
|
||||
}
|
||||
|
||||
/** countMyCharts GET /api/chart/my/count */
|
||||
export async function countMyChartsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/chart/my/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getMyChartGenerationSuccessRate GET /api/chart/my/gen/success-rate */
|
||||
export async function getMyChartGenerationSuccessRateUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseDouble_>('/api/chart/my/gen/success-rate', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** listMyChartByPage POST /api/chart/my/list/page */
|
||||
export async function listMyChartByPageUsingPost(
|
||||
body: API.ChartQueryRequest,
|
||||
@ -174,6 +214,30 @@ export async function listMyChartByPageUsingPost(
|
||||
});
|
||||
}
|
||||
|
||||
/** getMyTodayChartCount GET /api/chart/my/today/count */
|
||||
export async function getMyTodayChartCountUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/chart/my/today/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getMyWeekChartSuccessCount GET /api/chart/my/week/success/count */
|
||||
export async function getMyWeekChartSuccessCountUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseListInt_>('/api/chart/my/week/success/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getTodayChartCount GET /api/chart/today/count */
|
||||
export async function getTodayChartCountUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/chart/today/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** updateChart POST /api/chart/update */
|
||||
export async function updateChartUsingPost(
|
||||
body: API.ChartUpdateRequest,
|
||||
@ -188,3 +252,11 @@ export async function updateChartUsingPost(
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** getWeekChartSuccessCount GET /api/chart/week/success/count */
|
||||
export async function getWeekChartSuccessCountUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseListInt_>('/api/chart/week/success/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
33
src/services/hebi/commentController.ts
Normal file
33
src/services/hebi/commentController.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// @ts-ignore
|
||||
/* eslint-disable */
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** getCommentListByPostId GET /api/comment/comments */
|
||||
export async function getCommentListByPostIdUsingGet(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.getCommentListByPostIdUsingGETParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.BaseResponseListCommentVO_>('/api/comment/comments', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** addComment POST /api/comment/sendcomment */
|
||||
export async function addCommentUsingPost(
|
||||
body: API.CommentAddRequest,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.BaseResponseLong_>('/api/comment/sendcomment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
import * as chartController from './chartController';
|
||||
import * as commentController from './commentController';
|
||||
import * as fileController from './fileController';
|
||||
import * as postController from './postController';
|
||||
import * as postFavourController from './postFavourController';
|
||||
@ -11,6 +12,7 @@ import * as queueController from './queueController';
|
||||
import * as userController from './userController';
|
||||
export default {
|
||||
chartController,
|
||||
commentController,
|
||||
fileController,
|
||||
postController,
|
||||
postFavourController,
|
||||
|
@ -14,6 +14,14 @@ export async function addPostUsingPost(body: API.PostAddRequest, options?: { [ke
|
||||
});
|
||||
}
|
||||
|
||||
/** countPosts GET /api/post/count */
|
||||
export async function countPostsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/post/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** deletePost POST /api/post/delete */
|
||||
export async function deletePostUsingPost(
|
||||
body: API.DeleteRequest,
|
||||
@ -59,6 +67,14 @@ export async function getPostVoByIdUsingGet(
|
||||
});
|
||||
}
|
||||
|
||||
/** listAllPosts GET /api/post/list/all */
|
||||
export async function listAllPostsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseListPostVO_>('/api/post/list/all', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** listPostByPage POST /api/post/list/page */
|
||||
export async function listPostByPageUsingPost(
|
||||
body: API.PostQueryRequest,
|
||||
@ -89,6 +105,14 @@ export async function listPostVoByPageUsingPost(
|
||||
});
|
||||
}
|
||||
|
||||
/** countMyPosts GET /api/post/my/count */
|
||||
export async function countMyPostsUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/post/my/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** listMyPostVOByPage POST /api/post/my/list/page/vo */
|
||||
export async function listMyPostVoByPageUsingPost(
|
||||
body: API.PostQueryRequest,
|
||||
|
48
src/services/hebi/typings.d.ts
vendored
48
src/services/hebi/typings.d.ts
vendored
@ -22,12 +22,36 @@ declare namespace API {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseDouble_ = {
|
||||
code?: number;
|
||||
data?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseInt_ = {
|
||||
code?: number;
|
||||
data?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseListCommentVO_ = {
|
||||
code?: number;
|
||||
data?: CommentVO[];
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseListInt_ = {
|
||||
code?: number;
|
||||
data?: number[];
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseListPostVO_ = {
|
||||
code?: number;
|
||||
data?: PostVO[];
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseLoginUserVO_ = {
|
||||
code?: number;
|
||||
data?: LoginUserVO;
|
||||
@ -40,6 +64,12 @@ declare namespace API {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponseMapStringObject_ = {
|
||||
code?: number;
|
||||
data?: Record<string, any>;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type BaseResponsePageChart_ = {
|
||||
code?: number;
|
||||
data?: PageChart_;
|
||||
@ -157,6 +187,18 @@ declare namespace API {
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
type CommentAddRequest = {
|
||||
content?: string;
|
||||
postId?: number;
|
||||
};
|
||||
|
||||
type CommentVO = {
|
||||
content?: string;
|
||||
createTime?: string;
|
||||
userAvatar?: string;
|
||||
userName?: string;
|
||||
};
|
||||
|
||||
type DeleteRequest = {
|
||||
id?: number;
|
||||
};
|
||||
@ -178,6 +220,11 @@ declare namespace API {
|
||||
id?: number;
|
||||
};
|
||||
|
||||
type getCommentListByPostIdUsingGETParams = {
|
||||
/** postId */
|
||||
postId: number;
|
||||
};
|
||||
|
||||
type getPostVOByIdUsingGETParams = {
|
||||
/** id */
|
||||
id?: number;
|
||||
@ -361,7 +408,6 @@ declare namespace API {
|
||||
type User = {
|
||||
createTime?: string;
|
||||
id?: number;
|
||||
isDelete?: number;
|
||||
updateTime?: string;
|
||||
userAccount?: string;
|
||||
userAvatar?: string;
|
||||
|
@ -14,6 +14,14 @@ export async function addUserUsingPost(body: API.UserAddRequest, options?: { [ke
|
||||
});
|
||||
}
|
||||
|
||||
/** countUsers GET /api/user/count */
|
||||
export async function countUsersUsingGet(options?: { [key: string]: any }) {
|
||||
return request<API.BaseResponseLong_>('/api/user/count', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** deleteUser POST /api/user/delete */
|
||||
export async function deleteUserUsingPost(
|
||||
body: API.DeleteRequest,
|
||||
|
Loading…
x
Reference in New Issue
Block a user