Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2db7793ab3 | ||
|
593f921ef4 | ||
|
b763c47e48 | ||
|
0b06d3dd0e | ||
|
adcad7769e | ||
|
55c472b840 | ||
|
33ef6e7d20 | ||
|
b8e50a9511 | ||
|
9a7b889ada | ||
|
279e5d81f7 | ||
|
664e2950f7 | ||
|
3a1c04686a | ||
|
db59c6d95b | ||
|
c32c8d5e4f | ||
|
8926d28a72 | ||
|
8ad4263e2a |
@ -1,11 +1,13 @@
|
||||
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: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' },
|
||||
@ -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,6 +49,7 @@
|
||||
"@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.25.0",
|
||||
"antd-style": "^3.6.1",
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
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',
|
||||
}}
|
||||
</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
|
||||
>
|
||||
<SmileTwoTone /> 基于AIGC的智能数据分析系统 <HeartTwoTone twoToneColor="#eb2f96" />
|
||||
</Typography.Title>
|
||||
</Card>
|
||||
<p
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: 24,
|
||||
}}
|
||||
>
|
||||
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,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]);
|
||||
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 {
|
||||
background: #f6f8fa;
|
||||
min-height: 100vh;
|
||||
padding: 32px 0;
|
||||
}
|
||||
.searchBar {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 16px;
|
||||
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 {
|
||||
transition: all 0.3s;
|
||||
|
||||
border-radius: 10px;
|
||||
margin-bottom: 18px;
|
||||
transition: box-shadow 0.2s;
|
||||
background: #fafbfc;
|
||||
&:hover {
|
||||
background-color: #fafafa;
|
||||
box-shadow: 0 4px 16px #e6e6e6;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.postTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
color: #1677ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.categoryTag {
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.topBadge {
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.postMeta {
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
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 {
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
|
||||
color: #888;
|
||||
gap: 18px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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}
|
||||
{/* 展示所有标签 */}
|
||||
{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;
|
||||
width: 80vw;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.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;
|
||||
width: 80vw;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 32px 48px 24px 48px;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
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('发布成功');
|
||||
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 data-color-mode="light">
|
||||
<MDEditor
|
||||
value={mdContent}
|
||||
onChange={setMdContent}
|
||||
height={400}
|
||||
preview="edit"
|
||||
placeholder="请使用 Markdown 语法详细描述您要分享的内容..."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</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,
|
||||
},
|
||||
@ -138,10 +202,16 @@ const HomePage: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
// 随机生成0~60秒的平均生成时间
|
||||
const randomTime = Math.floor(Math.random() * 61);
|
||||
setAvgGenerationTime(randomTime);
|
||||
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -94,10 +94,11 @@ useEffect(()=>{
|
||||
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 = urlParams.get('redirect') || '/';
|
||||
window.location.href = '/user/access';
|
||||
history.push('/user/access');
|
||||
return;
|
||||
} else {
|
||||
message.error(res.message);
|
||||
|
@ -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