feat: 论坛新增 & 列表页面修改

This commit is contained in:
Shu Guang 2025-05-16 01:30:16 +08:00
parent 3a1c04686a
commit 664e2950f7
4 changed files with 190 additions and 250 deletions

View File

@ -1,62 +1,83 @@
.forumContainer { .forumContainer {
background: #f6f8fa;
min-height: 100vh;
padding: 32px 0;
}
.searchBar { .searchBar {
background: #fff; background: #fff;
padding: 16px; border-radius: 12px;
border-radius: 8px; margin: 0 auto 24px auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); // max-width: 1100px;
margin-bottom: 16px; 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 { .postItem {
transition: all 0.3s; border-radius: 10px;
margin-bottom: 18px;
transition: box-shadow 0.2s;
background: #fafbfc;
&:hover { &:hover {
background-color: #fafafa; box-shadow: 0 4px 16px #e6e6e6;
background: #fff;
}
} }
.postTitle { .postTitle {
font-size: 18px; font-size: 20px;
font-weight: 500; font-weight: 600;
color: #1a1a1a; color: #222;
&:hover { &: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 { .postMeta {
color: #8c8c8c; color: #888;
font-size: 14px; 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 { .postStats {
color: #595959;
font-size: 14px; font-size: 14px;
color: #888;
gap: 18px;
}
.statItem { .statItem {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; 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;
} }

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, List, Tag, Space, Input, Button, Select, Badge } from 'antd'; import { Card, List, Tag, Space, Input, Button, Select, Badge, Avatar } from 'antd';
import { history } from '@umijs/max'; import { history } from '@umijs/max';
import { import {
PlusOutlined, PlusOutlined,
@ -8,80 +8,34 @@ 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(() => {
id: 1, const fetchPosts = async () => {
title: '使用 GPT-4 进行高级数据分析的实践经验', setLoading(true);
category: '技术讨论', try {
author: '数据专家', const res = await listAllPostsUsingGet();
createTime: '2024-01-15 14:30', if (res && res.code === 0 && Array.isArray(res.data)) {
description: '分享在大规模数据集上使用 GPT-4 进行智能分析的经验,包括提示词工程、数据预处理技巧,以及如何提高分析准确率...', setPostList(res.data);
views: 2150, } else {
comments: 156, setPostList([]);
likes: 342, }
isTop: true, } catch (e) {
cover: 'https://picsum.photos/272/153', setPostList([]);
}, }
{ setLoading(false);
id: 2, };
title: 'AI 辅助数据可视化最佳实践', fetchPosts();
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: '全部' },
@ -91,9 +45,15 @@ 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}>
@ -132,7 +92,7 @@ const ForumList: React.FC = () => {
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => `${total} 条帖子`, showTotal: (total) => `${total} 条帖子`,
}} }}
dataSource={mockPosts} // 使用模拟数据 dataSource={filteredPosts}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
key={item.id} key={item.id}
@ -140,13 +100,13 @@ const ForumList: React.FC = () => {
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.views} <EyeOutlined /> {item.viewNum || 0}
</span> </span>
<span key="comments" className={styles.statItem}> <span key="comments" className={styles.statItem}>
<MessageOutlined /> {item.comments} <MessageOutlined /> {item.commentNum || 0}
</span> </span>
<span key="likes" className={styles.statItem}> <span key="likes" className={styles.statItem}>
<LikeOutlined /> {item.likes} <LikeOutlined /> {item.favourNum || 0}
</span> </span>
</Space>, </Space>,
]} ]}
@ -163,6 +123,13 @@ 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
@ -171,9 +138,12 @@ const ForumList: React.FC = () => {
> >
{item.title} {item.title}
</a> </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> </Tag>
))}
{item.isTop && ( {item.isTop && (
<Badge color="red" text="置顶" /> <Badge color="red" text="置顶" />
)} )}
@ -181,13 +151,15 @@ const ForumList: React.FC = () => {
} }
description={ description={
<Space className={styles.postMeta} size="middle"> <Space className={styles.postMeta} size="middle">
<span>{item.author}</span> <span>{item.user?.userName || '匿名用户'}</span>
<span> {item.createTime}</span> <span>
{dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss')}
</span>
</Space> </Space>
} }
/> />
<div className={styles.postContent}> <div className={styles.postContent}>
{item.description} {item.content}
</div> </div>
</List.Item> </List.Item>
)} )}

View File

@ -1,44 +1,25 @@
.publishContainer { .publishContainer {
max-width: 800px; width: 80vw;
margin: 0 auto; min-height: 100vh;
padding: 24px; margin: 0;
padding: 0;
background: #fff;
}
.publishCard { .publishCard {
border-radius: 8px; width: 80vw;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); min-height: 100vh;
margin: 0;
:global { padding: 32px 48px 24px 48px;
.ant-card-head { box-shadow: none;
border-bottom: 1px solid #f0f0f0; border: none;
padding: 16px 24px; background: #fff;
.ant-card-head-title {
font-size: 18px;
font-weight: 500;
} }
}
.ant-card-body {
padding: 24px;
}
}
}
.formLabel {
font-weight: 500;
font-size: 15px;
}
.uploadHint {
color: #666;
font-size: 13px;
margin-top: 8px;
}
.buttonGroup { .buttonGroup {
display: flex; display: flex;
justify-content: flex-end; gap: 16px;
gap: 12px;
margin-top: 24px; margin-top: 24px;
} }
.formLabel {
font-weight: 500;
} }

View File

@ -1,30 +1,34 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Form, Input, Button, Upload, Select, message, Space, Alert } from 'antd'; import { Card, Form, Input, Button, Select, message, 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 } from '@/services/hebi/postController';
const { TextArea } = Input; const { TextArea } = Input;
const ForumPublish: React.FC = () => { const ForumPublish: React.FC = () => {
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 = [
{ value: 'tech', label: '技术讨论', description: '分享技术经验和最佳实践' },
{ value: 'share', label: '经验分享', description: '分享数据分析案例和心得' },
{ value: 'question', label: '问题求助', description: '寻求技术支持和解决方案' },
];
const handleSubmit = async (values: any) => { const handleSubmit = async (values: any) => {
setSubmitting(true); setSubmitting(true);
try { try {
// 处理表单提交 const postAddRequest = {
console.log('提交数据:', values); title: values.title,
content: mdContent, // 使用 Markdown 内容
tags: values.tags || [],
};
const res = await addPostUsingPost(postAddRequest);
if (res && res.code === 0) {
message.success('发布成功'); message.success('发布成功');
history.push('/forum/list'); history.push('/forum/list');
} else {
message.error(res?.message || '发布失败');
}
} catch (error) { } catch (error) {
message.error('发布失败'); message.error('发布失败');
} }
@ -41,7 +45,6 @@ const ForumPublish: React.FC = () => {
showIcon showIcon
style={{ marginBottom: 24 }} style={{ marginBottom: 24 }}
/> />
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
@ -62,72 +65,35 @@ const ForumPublish: React.FC = () => {
maxLength={100} maxLength={100}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="category" name="tags"
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: true, message: '请输入内容' }, { required: false, type: 'array', message: '请输入标签' }
{ min: 20, message: '内容至少20个字符' }
]} ]}
> >
<TextArea <Select
rows={12} mode="tags"
placeholder="请详细描述您要分享的内容..." style={{ width: '100%' }}
showCount placeholder="请输入标签,回车分隔"
maxLength={5000} tokenSeparators={[',', '']}
/> />
</Form.Item> </Form.Item>
{/* Markdown 编辑器替换内容输入 */}
<Form.Item <Form.Item
label={<span className={styles.formLabel}></span>} label={<span className={styles.formLabel}></span>}
extra={<div className={styles.uploadHint}> jpgpng 800x450px</div>} required
> >
<Upload <div data-color-mode="light">
listType="picture-card" <MDEditor
fileList={fileList} value={mdContent}
onChange={({ fileList }) => setFileList(fileList)} onChange={setMdContent}
beforeUpload={(file) => { height={400}
const isImage = file.type.startsWith('image/'); preview="edit"
if (!isImage) { placeholder="请使用 Markdown 语法详细描述您要分享的内容..."
message.error('只能上传图片文件!'); />
}
return false;
}}
maxCount={1}
>
{fileList.length < 1 && (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}></div>
</div> </div>
)}
</Upload>
</Form.Item> </Form.Item>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<Button onClick={() => history.back()}> <Button onClick={() => history.back()}>