Compare commits

..

No commits in common. "main" and "1.1.0" have entirely different histories.
main ... 1.1.0

22 changed files with 456 additions and 1253 deletions

View File

@ -1,13 +1,11 @@
export default [ export default [
{path: '/user',layout: false,routes: [ {path: '/user',layout: false,routes: [
{ name: '登录', path: '/user/login', component: './User/Login' }, { 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: "/user/access"}, {path:"/", redirect: "/home"},
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage',access: 'canAdmin', }, { path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage' },
// { path: '/access', name :"权限引导", icon: "PieChartOutlined", component: './Access',hidden: true},
{ path: '/add_chart', name :"智能分析", icon: "barChart", component: './AddChart' }, { path: '/add_chart', name :"智能分析", icon: "barChart", component: './AddChart' },
{ path: '/add_async', name: "异步分析", icon: "DotChartOutlined", component: './AddChartAsync' }, { path: '/add_async', name: "异步分析", icon: "DotChartOutlined", component: './AddChartAsync' },
{ path: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' }, { path: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' },
@ -29,13 +27,12 @@ export default [
}, },
{ {
path: '/admin', path: '/admin',
name: '后台管理', name: '管理',
icon: 'crown', icon: 'crown',
access: 'canAdmin', access: 'canAdmin',
routes: [ routes: [
{ path: '/admin', redirect: '/admin/sub-page' }, { 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' }, { path: '/', redirect: '/welcome' },

View File

@ -49,7 +49,6 @@
"@ant-design/icons": "^4.8.1", "@ant-design/icons": "^4.8.1",
"@ant-design/pro-components": "^2.8.7", "@ant-design/pro-components": "^2.8.7",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@uiw/react-md-editor": "^4.0.6",
"@umijs/route-utils": "^2.2.2", "@umijs/route-utils": "^2.2.2",
"antd": "^5.25.0", "antd": "^5.25.0",
"antd-style": "^3.6.1", "antd-style": "^3.6.1",

View File

@ -5,8 +5,7 @@
* */ * */
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) { export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {}; const { currentUser } = initialState ?? {};
console.log('currentUser',currentUser)
return { return {
canAdmin: currentUser && currentUser.userRole === 'admin', canAdmin: currentUser && currentUser.access === 'admin',
}; };
} }

View File

@ -1,22 +0,0 @@
// 负责处理默认重定向
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>;
};

View File

@ -1,194 +1,44 @@
import React, { useEffect, useState } from 'react'; import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { Table, Button, Modal, Form, Input, Tag, Space, Popconfirm, message, Avatar } from 'antd'; import '@umijs/max';
import { listAllPostsUsingGet, updatePostUsingPost, deletePostUsingPost, addPostUsingPost } from '@/services/hebi/postController'; import { Alert, Card, Typography } from 'antd';
import dayjs from 'dayjs'; // 新增 import React from 'react';
const Admin: React.FC = () => {
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 ( return (
<PageContainer title="论坛管理"> <PageContainer content={' 这个页面只有 admin 权限才能查看'}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <Card>
<Button type="primary" onClick={handleAdd}> <Alert
message={'更快更强的重型组件,已经发布。'}
</Button> type="success"
<Input.Search showIcon
allowClear banner
placeholder="搜索标题或标签" style={{
style={{ width: 300 }} margin: -12,
value={search} marginBottom: 48,
onChange={e => setSearch(e.target.value)} }}
/> />
</div> <Typography.Title
<Table level={2}
rowKey="id" style={{
loading={loading} textAlign: 'center',
columns={columns} }}
dataSource={filteredPosts} >
bordered <SmileTwoTone /> AIGC的智能数据分析系统 <HeartTwoTone twoToneColor="#eb2f96" />
pagination={{ pageSize: 8 }} </Typography.Title>
/> </Card>
<Modal <p
title={editingPost ? '编辑帖子' : '新增帖子'} style={{
open={modalVisible} textAlign: 'center',
onOk={handleOk} marginTop: 24,
onCancel={() => setModalVisible(false)} }}
destroyOnClose
> >
<Form form={form} layout="vertical"> Want to add more pages? Please refer to{' '}
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}> <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
<Input /> use block
</Form.Item> </a>
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
<Input.TextArea rows={4} /> </p>
</Form.Item>
<Form.Item name="tags" label="标签(逗号分隔)">
<Input />
</Form.Item>
</Form>
</Modal>
</PageContainer> </PageContainer>
); );
}; };
export default Admin;
export default ForumAdmin;

View File

@ -1,251 +0,0 @@
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;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { useParams, useModel } from '@umijs/max'; import { useParams } from '@umijs/max';
import { import {
Card, Card,
Avatar, Avatar,
@ -19,197 +19,104 @@ import {
StarFilled, StarFilled,
ShareAltOutlined, ShareAltOutlined,
} from '@ant-design/icons'; } 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 } = Typography; const { Title, Paragraph } = Typography;
const { TextArea } = Input; 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 ForumDetail: React.FC = () => {
const { initialState } = useModel('@@initialState');
const currentUser = initialState?.currentUser;
const { id } = useParams(); const { id } = useParams();
// 帖子相关状态
const [post, setPost] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [liked, setLiked] = useState(false); const [liked, setLiked] = useState(false);
const [collected, setCollected] = useState(false); const [collected, setCollected] = useState(false);
// 评论相关状态
const [comments, setComments] = useState<API.CommentVO[]>([]);
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [commentLoading, setCommentLoading] = useState(false); const [comments, setComments] = useState(mockComments);
// 判断是否是当前用户的帖子 const handleSubmitComment = () => {
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()) { if (!comment.trim()) {
message.warning('请输入评论内容'); message.warning('请输入评论内容');
return; return;
} }
setCommentLoading(true); // 提交评论
try { const newComment = {
const res = await addCommentUsingPost({ id: comments.length + 1,
content: comment.trim(), author: '当前用户',
postId: id, avatar: 'https://joeschmoe.io/api/v1/random',
}); content: comment,
if (res && res.code === 0) { createTime: new Date().toLocaleString(),
message.success('评论成功'); likes: 0,
setComment(''); };
// 重新获取评论列表 setComments([newComment, ...comments]);
const commentsRes = await getCommentListByPostIdUsingGet({ postId: id }); message.success('评论成功');
if (commentsRes && commentsRes.code === 0 && commentsRes.data) { setComment('');
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 ( return (
<Card loading={loading}> <Card>
<article> <article>
<header style={{ marginBottom: 24 }}> <header style={{ marginBottom: 24 }}>
<Title level={2}>{post?.title || '帖子标题'}</Title> <Title level={2}></Title>
<Space split={<Divider type="vertical" />}> <Space split={<Divider type="vertical" />}>
<Space> <Space>
<Avatar src={post?.user?.userAvatar || 'https://joeschmoe.io/api/v1/random'} /> <Avatar src="https://joeschmoe.io/api/v1/random" />
<span>{post?.user?.userName || '无'}</span> <span></span>
</Space> </Space>
<span> <span></span>
{post?.createTime ? dayjs(post.createTime).format('YYYY-MM-DD HH:mm:ss') : '发布时间'} <Tag color="blue"></Tag>
</span> <span> 1000</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> </Space>
</header> </header>
{/* Markdown 渲染帖子内容 */} <Paragraph>
<div data-color-mode="light" style={{ background: '#fff' }}>
<MDEditor.Markdown source={post?.content || '帖子内容'} /> </Paragraph>
</div>
<div style={{ marginTop: 24 }}> <div style={{ marginTop: 24 }}>
<Space size="large"> <Space size="large">
<Button <Button
icon={liked ? <LikeFilled style={{ color: '#ff4d4f' }} /> : <LikeOutlined />} icon={liked ? <LikeFilled /> : <LikeOutlined />}
onClick={handleLike} onClick={() => setLiked(!liked)}
style={liked ? { color: '#ff4d4f', borderColor: '#ff4d4f', background: '#fff0f0' } : {}}
> >
{post?.thumbNum ?? 0}
</Button> </Button>
<Button <Button
icon={collected ? <StarFilled style={{ color: '#ff4d4f' }} /> : <StarOutlined />} icon={collected ? <StarFilled /> : <StarOutlined />}
onClick={handleFavour} onClick={() => setCollected(!collected)}
style={collected ? { color: '#ff4d4f', borderColor: '#ff4d4f', background: '#fff0f0' } : {}}
> >
{post?.favourNum ?? 0}
</Button> </Button>
<Button icon={<ShareAltOutlined />}> <Button icon={<ShareAltOutlined />}>
</Button> </Button>
</Space> </Space>
</div> </div>
<Divider /> <Divider />
<div> <div>
@ -221,7 +128,7 @@ const ForumDetail: React.FC = () => {
placeholder="写下你的评论..." placeholder="写下你的评论..."
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<Button type="primary" onClick={handleSubmitComment} loading={commentLoading}> <Button type="primary" onClick={handleSubmitComment}>
</Button> </Button>
@ -230,14 +137,20 @@ const ForumDetail: React.FC = () => {
itemLayout="horizontal" itemLayout="horizontal"
dataSource={comments} dataSource={comments}
renderItem={(item) => ( renderItem={(item) => (
<List.Item> <List.Item
actions={[
<Button type="text" icon={<LikeOutlined />} key="list-loadmore-like">
{item.likes}
</Button>,
]}
>
<List.Item.Meta <List.Item.Meta
avatar={<Avatar src={item.userAvatar || 'https://joeschmoe.io/api/v1/random'} />} avatar={<Avatar src={item.avatar} />}
title={ title={
<Space> <Space>
<span>{item.userName || '匿名用户'}</span> <span>{item.author}</span>
<span style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: '14px' }}> <span style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: '14px' }}>
{item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') : ''} {item.createTime}
</span> </span>
</Space> </Space>
} }

View File

@ -1,83 +1,62 @@
.forumContainer { .forumContainer {
background: #f6f8fa; .searchBar {
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; background: #fff;
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 16px;
} }
}
.postTitle { .postItem {
font-size: 20px; transition: all 0.3s;
font-weight: 600;
color: #222; &:hover {
&:hover { background-color: #fafafa;
color: #1677ff; }
text-decoration: underline;
.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;
} }
}
.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;
} }

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { Card, List, Tag, Space, Input, Button, Select, Badge, Avatar } from 'antd'; import { Card, List, Tag, Space, Input, Button, Select, Badge } from 'antd';
import { history } from '@umijs/max'; import { history } from '@umijs/max';
import { import {
PlusOutlined, PlusOutlined,
@ -8,34 +8,80 @@ import {
LikeOutlined, LikeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import styles from './index.less'; import styles from './index.less';
import { listAllPostsUsingGet } from '@/services/hebi/postController';
import dayjs from 'dayjs';
const { Search } = Input; const { Search } = Input;
const ForumList: React.FC = () => { const ForumList: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState('all'); const [filter, setFilter] = useState('all');
const [postList, setPostList] = useState<any[]>([]);
// 拉取帖子数据 const mockPosts = [
useEffect(() => { {
const fetchPosts = async () => { id: 1,
setLoading(true); title: '使用 GPT-4 进行高级数据分析的实践经验',
try { category: '技术讨论',
const res = await listAllPostsUsingGet(); author: '数据专家',
if (res && res.code === 0 && Array.isArray(res.data)) { createTime: '2024-01-15 14:30',
setPostList(res.data); description: '分享在大规模数据集上使用 GPT-4 进行智能分析的经验,包括提示词工程、数据预处理技巧,以及如何提高分析准确率...',
} else { views: 2150,
setPostList([]); comments: 156,
} likes: 342,
} catch (e) { isTop: true,
setPostList([]); cover: 'https://picsum.photos/272/153',
} },
setLoading(false); {
}; id: 2,
fetchPosts(); 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',
},
];
const categories = [ const categories = [
{ value: 'all', label: '全部' }, { value: 'all', label: '全部' },
@ -45,15 +91,9 @@ const ForumList: React.FC = () => {
]; ];
const handleSearch = (value: string) => { const handleSearch = (value: string) => {
// 可根据 value 进行前端过滤或请求接口
console.log('搜索:', value); console.log('搜索:', value);
}; };
// 分类过滤
const filteredPosts = filter === 'all'
? postList
: postList.filter(item => item.tagList && item.tagList.includes(filter));
return ( return (
<div className={styles.forumContainer}> <div className={styles.forumContainer}>
<Card className={styles.searchBar} bordered={false}> <Card className={styles.searchBar} bordered={false}>
@ -92,21 +132,21 @@ const ForumList: React.FC = () => {
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => `${total} 条帖子`, showTotal: (total) => `${total} 条帖子`,
}} }}
dataSource={filteredPosts} dataSource={mockPosts} // 使用模拟数据
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
key={item.id} key={item.id}
className={styles.postItem} className={styles.postItem}
actions={[ actions={[
<Space key={"actions"} className={styles.postStats}> <Space key={"actions"} className={styles.postStats}>
{/* <span key="views" className={styles.statItem}> <span key="views" className={styles.statItem}>
<EyeOutlined /> {item.viewNum || 0} <EyeOutlined /> {item.views}
</span> */} </span>
<span key="comments" className={styles.statItem}> <span key="comments" className={styles.statItem}>
<MessageOutlined /> {item.commentNum || 0} <MessageOutlined /> {item.comments}
</span> </span>
<span key="likes" className={styles.statItem}> <span key="likes" className={styles.statItem}>
<LikeOutlined /> {item.favourNum || 0} <LikeOutlined /> {item.likes}
</span> </span>
</Space>, </Space>,
]} ]}
@ -123,13 +163,6 @@ const ForumList: React.FC = () => {
} }
> >
<List.Item.Meta <List.Item.Meta
avatar={
item.user?.userAvatar ? (
<Avatar src={item.user.userAvatar} />
) : (
<Avatar>{item.user?.userName?.[0] || '匿'}</Avatar>
)
}
title={ title={
<Space size="middle" align="center"> <Space size="middle" align="center">
<a <a
@ -138,12 +171,9 @@ const ForumList: React.FC = () => {
> >
{item.title} {item.title}
</a> </a>
{/* 展示所有标签 */} <Tag className={styles.categoryTag} color="blue">
{item.tagList && item.tagList.map((tag: string) => ( {item.category}
<Tag className={styles.categoryTag} color="blue" key={tag}> </Tag>
{tag}
</Tag>
))}
{item.isTop && ( {item.isTop && (
<Badge color="red" text="置顶" /> <Badge color="red" text="置顶" />
)} )}
@ -151,21 +181,13 @@ const ForumList: React.FC = () => {
} }
description={ description={
<Space className={styles.postMeta} size="middle"> <Space className={styles.postMeta} size="middle">
<span>{item.user?.userName || '匿名用户'}</span> <span>{item.author}</span>
<span> <span> {item.createTime}</span>
{dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss')}
</span>
</Space> </Space>
} }
/> />
<div className={styles.postContent} style={{ <div className={styles.postContent}>
display: '-webkit-box', {item.description}
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 5,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{item.content}
</div> </div>
</List.Item> </List.Item>
)} )}

View File

@ -1,25 +1,44 @@
.publishContainer { .publishContainer {
width: 80vw; max-width: 800px;
min-height: 100vh; margin: 0 auto;
margin: 0; padding: 24px;
padding: 0;
background: #fff;
}
.publishCard { .publishCard {
width: 80vw; border-radius: 8px;
min-height: 100vh; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin: 0;
padding: 32px 48px 24px 48px; :global {
box-shadow: none; .ant-card-head {
border: none; border-bottom: 1px solid #f0f0f0;
background: #fff; padding: 16px 24px;
}
.buttonGroup { .ant-card-head-title {
display: flex; font-size: 18px;
gap: 16px; font-weight: 500;
margin-top: 24px; }
} }
.formLabel {
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;
}
} }

View File

@ -1,66 +1,39 @@
import React, { useState,useEffect } from 'react'; import React, { useState } from 'react';
import { Card, Form, Input, Button, Select, message, Alert } from 'antd'; 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 { history } from '@umijs/max'; import { history } from '@umijs/max';
import styles from './index.less'; 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 { TextArea } = Input;
const ForumPublish: React.FC = () => { const ForumPublish: React.FC = () => {
const [searchParams] = useSearchParams();
const id = searchParams.get('id'); // 获取帖子 ID
const [form] = Form.useForm(); const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [mdContent, setMdContent] = useState<string>('');
// 如果是编辑模式,加载帖子内容 const categories = [
useEffect(() => { { value: 'tech', label: '技术讨论', description: '分享技术经验和最佳实践' },
const loadPost = async () => { { value: 'share', label: '经验分享', description: '分享数据分析案例和心得' },
if (id) { { value: 'question', label: '问题求助', description: '寻求技术支持和解决方案' },
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) => { const handleSubmit = async (values: any) => {
setSubmitting(true); setSubmitting(true);
try { try {
const postAddRequest = { // 处理表单提交
id: id || undefined, console.log('提交数据:', values);
title: values.title, message.success('发布成功');
content: mdContent, history.push('/forum/list');
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) { } catch (error) {
message.error(id ? '修改失败' : '发布失败'); message.error('发布失败');
} }
setSubmitting(false); setSubmitting(false);
}; };
return ( return (
<div className={styles.publishContainer}> <div className={styles.publishContainer}>
<Card title={id ? '修改帖子' : '发布帖子'} className={styles.publishCard}> <Card title="发布帖子" className={styles.publishCard}>
<Alert <Alert
message="发帖提示" message="发帖提示"
description="请确保发布的内容与 AIGC 数据分析相关,并遵守社区规范。" description="请确保发布的内容与 AIGC 数据分析相关,并遵守社区规范。"
@ -68,6 +41,7 @@ const ForumPublish: React.FC = () => {
showIcon showIcon
style={{ marginBottom: 24 }} style={{ marginBottom: 24 }}
/> />
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
@ -88,41 +62,78 @@ const ForumPublish: React.FC = () => {
maxLength={100} maxLength={100}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="tags" name="category"
label={<span className={styles.formLabel}></span>} 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>}
rules={[ rules={[
{ required: false, type: 'array', message: '请输入标签' } { required: true, message: '请输入内容' },
{ min: 20, message: '内容至少20个字符' }
]} ]}
> >
<Select <TextArea
mode="tags" rows={12}
style={{ width: '100%' }} placeholder="请详细描述您要分享的内容..."
placeholder="请输入标签,回车分隔" showCount
tokenSeparators={[',', '']} maxLength={5000}
/> />
</Form.Item> </Form.Item>
{/* Markdown 编辑器替换内容输入 */}
<Form.Item <Form.Item
label={<span className={styles.formLabel}></span>} label={<span className={styles.formLabel}></span>}
required extra={<div className={styles.uploadHint}> jpgpng 800x450px</div>}
> >
<div data-color-mode="light"> <Upload
<MDEditor listType="picture-card"
value={mdContent} fileList={fileList}
onChange={setMdContent} onChange={({ fileList }) => setFileList(fileList)}
height={400} beforeUpload={(file) => {
preview="edit" const isImage = file.type.startsWith('image/');
placeholder="请使用 Markdown 语法详细描述您要分享的内容..." if (!isImage) {
/> message.error('只能上传图片文件!');
</div> }
return false;
}}
maxCount={1}
>
{fileList.length < 1 && (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}></div>
</div>
)}
</Upload>
</Form.Item> </Form.Item>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<Button onClick={() => history.back()}> <Button onClick={() => history.back()}>
</Button> </Button>
<Button type="primary" htmlType="submit" loading={submitting}> <Button type="primary" htmlType="submit" loading={submitting}>
{id ? '保存修改' : '发布帖子'}
</Button> </Button>
</div> </div>
</Form> </Form>

View File

@ -12,29 +12,10 @@ import {
ClockCircleOutlined, ClockCircleOutlined,
TeamOutlined, TeamOutlined,
} from '@ant-design/icons'; } 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 HomePage: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true); 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({ const [statisticsData] = useState({
totalCharts: 126, totalCharts: 126,
@ -56,51 +37,6 @@ const HomePage: React.FC = () => {
// ECharts准备的数据 // ECharts准备的数据
const chartTypesForEcharts = chartTypes.map(({ value, name }) => ({ value, name })); 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 = { const chartOptions = {
title: { title: {
text: '近期图表生成趋势', text: '近期图表生成趋势',
@ -143,7 +79,7 @@ const HomePage: React.FC = () => {
name: '生成数量', name: '生成数量',
type: 'line', type: 'line',
smooth: true, smooth: true,
data: weekChartData, // 修改为动态数据 data: [15, 22, 18, 25, 20, 30, 28],
areaStyle: { areaStyle: {
opacity: 0.1, opacity: 0.1,
}, },
@ -203,14 +139,8 @@ const HomePage: React.FC = () => {
useEffect(() => { useEffect(() => {
// 模拟数据加载 // 模拟数据加载
setTimeout(() => { setTimeout(() => {
// 随机生成0~60秒的平均生成时间 setLoading(false);
const randomTime = Math.floor(Math.random() * 61); }, 1000);
setAvgGenerationTime(randomTime);
setTimeout(() => {
setLoading(false);
}, 1000);
}, []);
}, []); }, []);
return ( return (
@ -225,7 +155,7 @@ const HomePage: React.FC = () => {
<span></span> <span></span>
</Space> </Space>
} }
value={totalCharts} value={statisticsData.totalCharts}
valueStyle={{ color: '#1890ff' }} valueStyle={{ color: '#1890ff' }}
/> />
<Progress <Progress
@ -246,13 +176,13 @@ const HomePage: React.FC = () => {
<span></span> <span></span>
</Space> </Space>
} }
value={successRate} value={statisticsData.successRate}
suffix="%" suffix="%"
precision={2} precision={2}
valueStyle={{ color: '#52c41a' }} valueStyle={{ color: '#52c41a' }}
/> />
<Progress <Progress
percent={successRate} percent={statisticsData.successRate}
size="small" size="small"
status="active" status="active"
showInfo={false} showInfo={false}
@ -269,7 +199,7 @@ const HomePage: React.FC = () => {
<span></span> <span></span>
</Space> </Space>
} }
value={todayGenerated} value={statisticsData.todayGenerated}
valueStyle={{ color: '#faad14' }} valueStyle={{ color: '#faad14' }}
/> />
<Progress <Progress
@ -290,7 +220,7 @@ const HomePage: React.FC = () => {
<span></span> <span></span>
</Space> </Space>
} }
value={totalUsers} value={statisticsData.totalUsers}
valueStyle={{ color: '#722ed1' }} valueStyle={{ color: '#722ed1' }}
/> />
<Progress <Progress
@ -311,8 +241,8 @@ const HomePage: React.FC = () => {
title="图表生成趋势" title="图表生成趋势"
extra={ extra={
<Space> <Space>
{/* <Tag color="blue"> +12%</Tag> <Tag color="blue"> +12%</Tag>
<Tag color="green"> +5%</Tag> */} <Tag color="green"> +5%</Tag>
</Space> </Space>
} }
> >
@ -343,7 +273,7 @@ const HomePage: React.FC = () => {
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Statistic <Statistic
title="当前活跃用户" title="当前活跃用户"
value={Math.ceil(totalUsers * 0.8)} value={statisticsData.activeUsers}
prefix={<UserOutlined />} prefix={<UserOutlined />}
/> />
<Progress <Progress
@ -358,12 +288,12 @@ const HomePage: React.FC = () => {
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Statistic <Statistic
title="平均生成时间" title="平均生成时间"
value={avgGenerationTime} value={statisticsData.avgGenerationTime}
suffix="秒" suffix="秒"
precision={1} precision={1}
/> />
<Progress <Progress
percent={Math.round((avgGenerationTime / 60) * 100)} percent={Math.round((2.5 / 5) * 100)}
status="active" status="active"
format={(percent) => `${percent}% 优化空间`} format={(percent) => `${percent}% 优化空间`}
/> />

View File

@ -1,4 +1,4 @@
import { listChartByPageUsingPost,listMyChartByPageUsingPost } from '@/services/hebi/chartController'; import { listChartByPageUsingPost } from '@/services/hebi/chartController';
import { useModel } from '@@/exports'; import { useModel } from '@@/exports';
import { Avatar, Card, Input, List, message, Result } from 'antd'; import { Avatar, Card, Input, List, message, Result } from 'antd';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
@ -30,7 +30,7 @@ const MyChartPage: React.FC = () => {
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
try { try {
const res = currentUser?.userRole==='user'?await listMyChartByPageUsingPost(searchParams):await listChartByPageUsingPost(searchParams); const res = await listChartByPageUsingPost(searchParams);
if (res.data) { if (res.data) {
setChartList(res.data.records ?? []); setChartList(res.data.records ?? []);
setTotal(res.data.total ?? 0); setTotal(res.data.total ?? 0);

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { PageContainer, ProCard } from '@ant-design/pro-components'; import { PageContainer, ProCard } from '@ant-design/pro-components';
import { import {
Avatar, Avatar,
@ -23,107 +23,15 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useModel } from '@umijs/max'; import { useModel } from '@umijs/max';
import type { UploadProps } from 'antd/es/upload'; 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 { TabPane } = Tabs;
const UserInfo: React.FC = () => { const UserInfo: React.FC = () => {
const { initialState } = useModel('@@initialState'); const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {}; const { currentUser } = initialState || {};
const [userInfo, setUserInfo] = useState<any>(null);
const [passwordVisible, setPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); 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) => { const handleAvatarUpload: UploadProps['onChange'] = async (info) => {
@ -150,6 +58,17 @@ const UserInfo: React.FC = () => {
} }
}; };
// 处理基本信息更新
const handleInfoUpdate = async (values: any) => {
try {
// 这里应该调用更新用户信息的API
// await updateUserInfo(values);
message.success('信息更新成功');
} catch (error) {
message.error('信息更新失败');
}
};
return ( return (
<PageContainer> <PageContainer>
<ProCard split="vertical"> <ProCard split="vertical">
@ -158,14 +77,12 @@ const UserInfo: React.FC = () => {
<Upload <Upload
name="avatar" name="avatar"
showUploadList={false} showUploadList={false}
// onChange={handleAvatarUpload} onChange={handleAvatarUpload}
customRequest={customRequest}
onChange={handleUploadChange}
> >
<Space direction="vertical" size="large"> <Space direction="vertical" size="large">
<Avatar <Avatar
size={120} size={120}
src={avatarUrl || userInfo?.userAvatar} src={currentUser?.avatar}
icon={<UserOutlined />} icon={<UserOutlined />}
/> />
<Button icon={<UploadOutlined />} loading={loading}> <Button icon={<UploadOutlined />} loading={loading}>
@ -174,8 +91,8 @@ const UserInfo: React.FC = () => {
</Space> </Space>
</Upload> </Upload>
<div style={{ marginTop: '16px' }}> <div style={{ marginTop: '16px' }}>
<h2>{userInfo?.userName}</h2> <h2>{currentUser?.name}</h2>
<Tag color="blue">{userInfo?.userRole || '普通用户'}</Tag> <Tag color="blue">{currentUser?.role || '普通用户'}</Tag>
</div> </div>
</div> </div>
</ProCard> </ProCard>
@ -185,55 +102,66 @@ const UserInfo: React.FC = () => {
<TabPane tab="基本信息" key="1"> <TabPane tab="基本信息" key="1">
<Form <Form
layout="vertical" layout="vertical"
form={form} initialValues={currentUser}
initialValues={{
userName: userInfo?.userName,
email: userInfo?.userAccount,
phone: userInfo?.userProfile,
}}
onFinish={handleInfoUpdate} onFinish={handleInfoUpdate}
> >
<Form.Item <Form.Item
label="用户名" label="用户名"
name="userName" name="username"
rules={[{ required: true }]} rules={[{ required: true }]}
> >
<Input prefix={<UserOutlined />} /> <Input prefix={<UserOutlined />} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="账号" label="邮箱"
name="email" name="email"
// rules={[{ required: true, type: 'email' }]} rules={[{ required: true, type: 'email' }]}
> >
<Input prefix={<MailOutlined />} disabled /> <Input prefix={<MailOutlined />} />
</Form.Item> </Form.Item>
{/* <Form.Item label="" name="phone"> <Form.Item label="手机" name="phone">
<Input prefix={<PhoneOutlined />} /> <Input prefix={<PhoneOutlined />} />
</Form.Item> */} </Form.Item>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" loading={loading}> <Button type="primary" htmlType="submit">
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
</TabPane> </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"> <TabPane tab="使用统计" key="3">
<Card> <Card>
<Descriptions column={2}> <Descriptions column={2}>
<Descriptions.Item label="注册时间"> <Descriptions.Item label="注册时间">
{userInfo?.createTime ? dayjs(userInfo.createTime).format('YYYY-MM-DD HH:mm:ss') : '-'} {currentUser?.registerTime || '-'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="最后登录"> <Descriptions.Item label="最后登录">
{userInfo?.updateTime ? dayjs(userInfo.updateTime).format('YYYY-MM-DD HH:mm:ss') : '-'} {currentUser?.lastLogin || '-'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="API调用次数"> <Descriptions.Item label="API调用次数">
{apiCallCount} {currentUser?.apiCalls || 0}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="分析次数"> <Descriptions.Item label="分析次数">
{apiCallCount*2|| 0} {currentUser?.analysisCounts || 0}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Card> </Card>

View File

@ -94,11 +94,10 @@ useEffect(()=>{
message.success(defaultLoginSuccessMessage); message.success(defaultLoginSuccessMessage);
await fetchUserInfo(); await fetchUserInfo();
// 登录成功后,设置刷新标记并刷新页面 // 登录成功后,设置刷新标记并刷新页面
// const urlParams = new URL(window.location.href).searchParams; const urlParams = new URL(window.location.href).searchParams;
// history.push(urlParams.get('redirect') || '/'); history.push(urlParams.get('redirect') || '/');
sessionStorage.setItem('avatar_refreshed', '1'); sessionStorage.setItem('avatar_refreshed', '1');
window.location.href = '/user/access'; window.location.href = urlParams.get('redirect') || '/';
history.push('/user/access');
return; return;
} else { } else {
message.error(res.message); message.error(res.message);

View File

@ -1,6 +1,5 @@
import { Footer } from '@/components'; import { Footer } from '@/components';
import { userRegisterUsingPost } from '@/services/hebi/userController'; import { userRegisterUsingPost } from '@/services/hebi/userController';
import { uploadFileUsingPost } from '@/services/hebi/fileController';
import { LockOutlined, PictureOutlined, UserOutlined } from '@ant-design/icons'; import { LockOutlined, PictureOutlined, UserOutlined } from '@ant-design/icons';
import { ProForm, ProFormText } from '@ant-design/pro-components'; import { ProForm, ProFormText } from '@ant-design/pro-components';
import { Helmet, history, Link } from '@umijs/max'; import { Helmet, history, Link } from '@umijs/max';
@ -63,7 +62,7 @@ const Register: React.FC = () => {
return; return;
} }
const res = await userRegisterUsingPost({ ...values, userAvatar: 'http://img-oss.shuguangwl.com/2025/05/18/6829ae97cee35.png' }); const res = await userRegisterUsingPost({ ...values, userAvatar: avatarUrl });
if (res.code === 0) { if (res.code === 0) {
message.success('注册成功!'); message.success('注册成功!');
history.push('/user/login'); history.push('/user/login');
@ -82,8 +81,6 @@ const Register: React.FC = () => {
</div> </div>
); );
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Helmet> <Helmet>
@ -114,7 +111,25 @@ 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 <ProFormText
name="userAccount" name="userAccount"

View File

@ -17,14 +17,6 @@ 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 */ /** deleteChart POST /api/chart/delete */
export async function deleteChartUsingPost( export async function deleteChartUsingPost(
body: API.DeleteRequest, body: API.DeleteRequest,
@ -137,22 +129,6 @@ 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 */ /** getChartById GET /api/chart/get */
export async function getChartByIdUsingGet( export async function getChartByIdUsingGet(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -183,22 +159,6 @@ 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 */ /** listMyChartByPage POST /api/chart/my/list/page */
export async function listMyChartByPageUsingPost( export async function listMyChartByPageUsingPost(
body: API.ChartQueryRequest, body: API.ChartQueryRequest,
@ -214,30 +174,6 @@ 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 */ /** updateChart POST /api/chart/update */
export async function updateChartUsingPost( export async function updateChartUsingPost(
body: API.ChartUpdateRequest, body: API.ChartUpdateRequest,
@ -252,11 +188,3 @@ export async function updateChartUsingPost(
...(options || {}), ...(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 || {}),
});
}

View File

@ -1,33 +0,0 @@
// @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 || {}),
});
}

View File

@ -3,7 +3,6 @@
// API 更新时间: // API 更新时间:
// API 唯一标识: // API 唯一标识:
import * as chartController from './chartController'; import * as chartController from './chartController';
import * as commentController from './commentController';
import * as fileController from './fileController'; import * as fileController from './fileController';
import * as postController from './postController'; import * as postController from './postController';
import * as postFavourController from './postFavourController'; import * as postFavourController from './postFavourController';
@ -12,7 +11,6 @@ import * as queueController from './queueController';
import * as userController from './userController'; import * as userController from './userController';
export default { export default {
chartController, chartController,
commentController,
fileController, fileController,
postController, postController,
postFavourController, postFavourController,

View File

@ -14,14 +14,6 @@ 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 */ /** deletePost POST /api/post/delete */
export async function deletePostUsingPost( export async function deletePostUsingPost(
body: API.DeleteRequest, body: API.DeleteRequest,
@ -67,14 +59,6 @@ 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 */ /** listPostByPage POST /api/post/list/page */
export async function listPostByPageUsingPost( export async function listPostByPageUsingPost(
body: API.PostQueryRequest, body: API.PostQueryRequest,
@ -105,14 +89,6 @@ 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 */ /** listMyPostVOByPage POST /api/post/my/list/page/vo */
export async function listMyPostVoByPageUsingPost( export async function listMyPostVoByPageUsingPost(
body: API.PostQueryRequest, body: API.PostQueryRequest,

View File

@ -22,36 +22,12 @@ declare namespace API {
message?: string; message?: string;
}; };
type BaseResponseDouble_ = {
code?: number;
data?: number;
message?: string;
};
type BaseResponseInt_ = { type BaseResponseInt_ = {
code?: number; code?: number;
data?: number; data?: number;
message?: string; 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_ = { type BaseResponseLoginUserVO_ = {
code?: number; code?: number;
data?: LoginUserVO; data?: LoginUserVO;
@ -64,12 +40,6 @@ declare namespace API {
message?: string; message?: string;
}; };
type BaseResponseMapStringObject_ = {
code?: number;
data?: Record<string, any>;
message?: string;
};
type BaseResponsePageChart_ = { type BaseResponsePageChart_ = {
code?: number; code?: number;
data?: PageChart_; data?: PageChart_;
@ -187,18 +157,6 @@ declare namespace API {
userId?: number; userId?: number;
}; };
type CommentAddRequest = {
content?: string;
postId?: number;
};
type CommentVO = {
content?: string;
createTime?: string;
userAvatar?: string;
userName?: string;
};
type DeleteRequest = { type DeleteRequest = {
id?: number; id?: number;
}; };
@ -220,11 +178,6 @@ declare namespace API {
id?: number; id?: number;
}; };
type getCommentListByPostIdUsingGETParams = {
/** postId */
postId: number;
};
type getPostVOByIdUsingGETParams = { type getPostVOByIdUsingGETParams = {
/** id */ /** id */
id?: number; id?: number;
@ -408,6 +361,7 @@ declare namespace API {
type User = { type User = {
createTime?: string; createTime?: string;
id?: number; id?: number;
isDelete?: number;
updateTime?: string; updateTime?: string;
userAccount?: string; userAccount?: string;
userAvatar?: string; userAvatar?: string;

View File

@ -14,14 +14,6 @@ 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 */ /** deleteUser POST /api/user/delete */
export async function deleteUserUsingPost( export async function deleteUserUsingPost(
body: API.DeleteRequest, body: API.DeleteRequest,