5

通过React16.8构建新闻网站第四章:论坛模块和文章模块的实现

 3 years ago
source link: https://zhuanlan.zhihu.com/p/248588587
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

通过React16.8构建新闻网站第四章:论坛模块和文章模块的实现

北京奇观技术有限责任公司 软件开发工程师

专栏地址:

第四章为网站文章页面和论坛页面的构建

实现效果图如下:

文章模块

论坛模块文章详情页面

文章模块和论坛模块都使用的Ant Desgin的List列表,文章模块是通过分页来渲染数据,而论坛模块采用点击加载更多渲染到同一页面上(本质上还是分页),这样是因为看了其他软件的论坛模块和文章模块,论坛一般是采用下来刷新在同一页面上渲染,这个项目以后如果做移动端的适配了,我再做下拉刷新。

点击加载更多loading效果:

点击加载更多

具体实现:

实现的每一步在注释中已标明清楚

1 论坛模块

import React, { useState, useEffect } from 'react';
import {Link} from 'react-router-dom';
import { List, Avatar,Row,Col,Button,Skeleton} from 'antd';
//antd v4升级后 拿不到以前的Icon了 换成了SmileOutlined
import { SmileOutlined } from '@ant-design/icons'
import {api,host} from '../until';
import './index.scss';

//定义icon
const IconText = props => (
    <span>
        <SmileOutlined type={props.type} style={{ marginRight: 8 }} />
        {props.text}
    </span>
);

const Forum = props => {
    const [initLoading,setInitLoading] = useState(true);//页面初始化loading
    const [loading,setLoading] = useState(false);//每次点击加载更多loading
    const [rawData,setRawData] = useState([]);//请求拿到的数据
    const [listData,setListData] = useState([]);//渲染到页面上的全部数据

    //组件加载时调用 相当于componsetNewsentDidMount 
    useEffect(()=>{
         //拿到数据
        setTimeout(()=>{
            api({
                url:host + 'newsSelectContentByType',
                args: {
                    type:2,
                },
                callback: (res) => {
                    //处理拿到的数据并渲染
                    showData(res);
                    //初始化的loading设为false
                    setInitLoading(false);
                }
            });
        },1)
    },[])

    //处理数据
    const showData = (data)=>{
        let listData = [];
        //这里先只取三个数据
        //做数据的拼接
        for (let i = 0; i < 3; i++) {
            let img = JSON.parse(data[i].img);
            listData.push({
                id: data[i].id,
                title: data[i].title + ':' +data[i].subtitle,
                avatar: data[i].avatar,
                description: data[i].title + ':' +data[i].subtitle,
                content:data[i].content,
                img,
            });
        }
        //把这三条数据加到原本的list上
        let tempData = rawData.concat(listData);
        //修改数据
        setRawData(tempData);
        setListData(tempData);
        //loading设为false
        setLoading(false);
    }

    //点击加载更多触发
    const onLoadMore = () => {
        //设置loading
        setLoading(true);
        //设一个loading动画 先渲染空数据
        //拼接三条空数据 并额外设置loading:true
        setListData(listData.concat([...new Array(3)].map(() => ({ loading: true, name: {} ,img:[,,,]}))))
        //因为这里拿数据很快 所以做一个暂停的动画展示
        setTimeout(()=>{
            //这里没有传入页数和要拿的数据数量 因为我把后端做的很简单 每次把全部数据直接拿过来 然后我在拼接三条上去
            //真实情况每次触发需要page+1 然后把要请求page和每页的数量传入服务器 其实和后端实现和分页是没有区别的
            api({
                url:host + 'newsSelectContentByType',
                args: {
                    type:2,
                },
                callback: (res) => {
                    //处理拿到的数据并渲染
                    showData(res);
                }
            });
        },1500)
        
    };

    /**
     * zyx
     * 2020/6/16
     * 点击更多按钮的渲染
     */
    const loadMore = ()=>{
        if(!initLoading && !loading){
            return (
                <div style={{ textAlign: 'center', marginTop: 12, height: 32, ineHeight: '32px', }} >
                    <Button onClick={onLoadMore}>点击加载更多</Button>
                </div>
            )
        }else{
            return null;
        }
    }
    
    return (
        <div className='forum' style={{margin:"30px 100px"}}>
            <List
                loading={initLoading}
                itemLayout="horizontal"
                loadMore={loadMore()}
                size="large"
                dataSource={listData}
                renderItem={item => (
                    <Link to={`tieziDetails/${item.id}`} target='_blank'>
                        <Skeleton avatar title={false} loading={item.loading} active>
                            <List.Item
                                key={item.title}
                                actions={[
                                <IconText type="star-o" text="156" key="list-vertical-star-o" />,
                                <IconText type="like-o" text="156" key="list-vertical-like-o" />,
                                <IconText type="message" text="2" key="list-vertical-message" />,
                                ]}
                            >
                            <List.Item.Meta
                                avatar={<Avatar src={item.avatar} />}
                                description={item.description}
                                />
                                {item.content}
                                <div>
                                    <Row>
                                        <Col span={6}> 
                                            <img width={272} alt="logo" src={item.img[0]} />
                                        </Col>
                                        <Col span={6}> 
                                            <img width={272} alt="logo" src={item.img[1]} />
                                        </Col>
                                        <Col span={6}> 
                                            <img  width={272} alt="logo" src={item.img[2]} />
                                        </Col>
                                    </Row>
                                </div>
                            </List.Item>
                        </Skeleton>
                    </Link>
                )}
            />
        </div>
    )
}
export default Forum;

2文章模块实现:

import React, { useState, useEffect } from 'react';
import {Link} from 'react-router-dom';
import { List, Avatar} from 'antd';
//antd v4升级后 拿不到以前的Icon了 换成了SmileOutlined
import { SmileOutlined } from '@ant-design/icons'
import {api,host} from '../until';
import './index.scss';

//定义icon
const IconText = props => (
    <span>
        <SmileOutlined type={props.type} style={{ marginRight: 8 }} />
        {props.text}
    </span>
);

const Article = props => {
    //需要渲染的数据
    const [dataList,setDataList] = useState([]);
    //组件加载时调用 相当于componentDidMount 
    useEffect(()=>{
        //拿到数据
        api({
            url:host + 'newsSelectContentByType',
            args: {
                type:1,
            },
            callback: (res) => {
                //处理拿到的数据并渲染
                showData(res);
            }
        });
    },[])

    //处理数据方法
    const showData = (data)=>{
        let listData = [];
        for (let i = 0; i < data.length; i++) {
            //处理一下拿到的img 因为img在数据库中是[xx,xxx,xxx]的方式存的
            let img = JSON.parse(data[i].img);
            //重新拼接
            listData.push({
                id: data[i].id,
                title: data[i].title + ':' +data[i].subtitle,
                avatar: data[i].avatar,
                description: data[i].title + ':' +data[i].subtitle,
                content:data[i].content,
                img,
            });
        }
        //修改数据
        setDataList(listData);
    }

    return (
        <div className='Article'>
            <List
                itemLayout="vertical"
                size="large"
                pagination={{
                    onChange: page => {
                        console.log(page);
                    },
                    pageSize: 5,
                }}
                dataSource={dataList}
                renderItem={item => (
                    <Link to={`details/${item.id}`} target='_blank'>
                        <List.Item
                            key={item.title}
                            actions={[
                            <IconText type="star-o" text="156" key="list-vertical-star-o" />,
                            <IconText type="like-o" text="156" key="list-vertical-like-o" />,
                            <IconText type="message" text="2" key="list-vertical-message" />,
                            ]}
                            extra={ <img width={272} alt="logo" src={item.img[0]}/>}
                        >
                        <List.Item.Meta
                            avatar= {<Avatar src={item.avatar} />}
                            description= {item.description}
                        />
                            {item.content}
                        </List.Item>
                        
                    </Link>
                )}
            />
        </div>
    )
}
export default Article;

3 文章详情页面

这里对详情页面进行简单构建(不区分文章还是帖子)

功能 1.展示具体内容 2.留言评论功能

Router

{
    path: '/details/:uniquekey',
    exact: true,
    component: Detail
},

Detail组件

import React, {useState, useEffect } from 'react';
import './index.scss';
import { Comment, Avatar, Form, Button, List, Input, message } from 'antd';
import {api,host} from '../until';
const { TextArea } = Input;

//输入评论列表
const CommentList = ({ comments }) => (
    <List
        dataSource={comments}
        header={`${comments.length} ${comments.length > 1 ? 'replies' : 'reply'}`}
        itemLayout="horizontal"
        renderItem={props => <Comment {...props} />}
    />
);
//输入评论框
const Editor = ({ onChange, onSubmit, submitting, value }) => (
    <div>
        <Form.Item>
            <TextArea rows={4} onChange={onChange} value={value} />
        </Form.Item>
        <Form.Item>
            <Button htmlType="submit" loading={submitting} onClick={onSubmit} type="primary">
                Add Comment
            </Button>
        </Form.Item>
    </div>
);

//拿出来当前登录用户的数据
const {userId,userName,userAvatar } = localStorage;

//文章和帖子的详情页面
const Detail = props => {

    const [comments,setComments] = useState([]);//全部的评论内容
    const [submitting,setSubmitting] = useState(false);//点击评论提交
    const [value,setValue] = useState('');//提交的评论内容
    const [data,setData] = useState({});//文章或是帖子的详情内容
    
    //componentDidmount
    useEffect(()=>{
        //把页面导航去掉
        let nav = document.getElementsByClassName('nav');
        nav[0].style.display='none';
        //拿到帖子数据
        console.log(props);
        let id = props.match.params.uniquekey;
        api({
            url:host + 'newsSelectContentByType',
            args: {
                id,
            },
            callback: (res) => {
                showData(res);
            }
        });
        getCommentData();
    },[])

     /**
     * zyx
     * 2020/6/28
     * 处理数据文章数据
     */
    const showData = (data)=>{
        //临时存放数据
        let handleData = {};
        handleData.content =data[0].content;
        setData(handleData)
    }

    /**
     * zyx
     * 2020/6/18
     * 拿评论数据
     */
    const getCommentData = ()=>{
        let pid = props.match.params.uniquekey;
        api({
            url:host + 'newsSelectAllComment',
            args: {
                pid,
            },
            callback: (res) => {
                showCommentData(res);
            }
        });
    }

     /**
     * zyx
     * 2020/6/18
     * 处理评论数据
     */
    const showCommentData = (data) =>{
        let tempData = [];
        for (let i = 0; i < data.length; i++) {
            tempData.push({
                id: data[i].id,
                author: data[i].name,
                avatar: data[i].avatar,
                content: data[i].content,
                datatime:data[i].create_time,
            });
        }

        setComments(tempData);
    }


    //插入文章评论
    const insertCommentData = ()=>{
        let pid = props.match.params.uniquekey;
        let comment = value;
        if(!userId){
            message.warn("请先登录");
            return 0;
        }
        api({
            url:host + 'newsInsertComment',
            args: {
                user_id:userId,
                pid,
                content:comment,
            },
            callback: (res) => {
                getCommentData();
                setSubmitting(false);
                setValue('')
            }
        });
    }

    //点击提交评论
    const handleSubmit = () => {
        if (!value) {
            return;
        }
        setSubmitting(true);
        //动画暂停
        setTimeout(() => {
            insertCommentData();
        }, 1000);
    };
    
    //评论框内容填写
    const handleChange = e => {
        setValue(e.target.value);
    };

    return (
        <div className='detail'>
                <div style={{marginBottom:'30px'}}>
                    {data.content}
                </div>
                {comments.length > 0 && <CommentList comments={comments} />}
                <Comment
                    avatar={
                        <Avatar
                        src={userAvatar}
                        alt={userName}
                        />
                    }
                    content={
                        <Editor
                        onChange={handleChange}
                        onSubmit={handleSubmit}
                        submitting={submitting}
                        value={value}
                        />
                    }
                />
            </div>
    );
}

export default Detail;

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK