感谢无私开源的程序员们~~~代码因为你们更加美腻~
//根index.js
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import './index.js';
import App from './App';
import { GlobalStyle, GithubMarkdownCss, Icon } from './style.js' // 添加全局样式
import Toast from 'react-toast-mobile';
import 'react-toast-mobile/lib/react-toast-mobile.css';
import * as serviceWorker from './serviceWorker';
const Apps = () => {
return (
<Fragment>
<Toast />
<GlobalStyle />
<Icon/>
<GithubMarkdownCss />
<App />
</Fragment>
)
}
ReactDOM.render(<Apps />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
//app.js
import React, { Component, Fragment } from 'react';
//引入redux
import { Provider } from 'react-redux'
//store
import store from './store'
import { BrowserRouter, Route, Switch } from 'react-router-dom'; // BrowserRouter HashRouter
//对应一些页面
import Topic from './pages/topic'
import Detail from './pages/detail'
import User from './pages/user'
import Login from './common/login'
import Create from './pages/create'
import Mine from './pages/mine'
import Message from './pages/message'
import ErrorPage from './common/errorPage'
import Auth from './common/auth'
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Fragment>
<Switch>
<Route path="/" exact component={Topic}></Route>
<Route path="/detail/:id" component={Detail}></Route>
<Route path="/user/:id" component={User}></Route>
<Route path="/login" component={Login}></Route>
<Auth path="/create" component={Create}></Auth>
<Auth path="/mine" component={Mine}></Auth>
<Auth path="/message" component={Message}></Auth>
<Route path="/404" exact component={ErrorPage}/>
<Route path="*" component={ ErrorPage } />
</Switch>
</Fragment>
</BrowserRouter>
</Provider >
);
}
}
export default App;
封装的时间
//index.js
export function formatDate(str) {
var date = new Date(str);
var time = new Date().getTime() - date.getTime(); //现在的时间-传入的时间 = 相差的时间(单位 = 毫秒)
if (time < 0) {
return '';
} else if (time / 1000 < 60) {
return '刚刚';
} else if ((time / 60000) < 60) {
return parseInt((time / 60000)) + ' 分钟前';
} else if ((time / 3600000) < 24) {
return parseInt(time / 3600000) + ' 小时前';
} else if ((time / 86400000) < 31) {
return parseInt(time / 86400000) + ' 天前';
} else if ((time / 2592000000) < 12) {
return parseInt(time / 2592000000) + ' 月前';
} else {
return parseInt(time / 31536000000) + ' 年前';
}
}
//http.js
import axios from 'axios';
import qs from "qs";
import { T } from 'react-toast-mobile';
// axios 配置
axios.defaults.timeout = 10000;
axios.defaults.baseURL = 'https://cnodejs.org/api/v1'
// http request 拦截器
axios.interceptors.request.use(config => {
T.loading()
let user = localStorage.user
if (config.method === 'post') {
config.data = qs.stringify(config.data)
if (user) {
config.data = config.data + `&accesstoken=${JSON.parse(user).accesstoken}`
}
}
if (config.method === 'get') {
if (user) {
config.params = Object.assign(config.params, { accesstoken: JSON.parse(user).accesstoken })
}
}
return config
},
err => {
return Promise.reject(err);
});
// http response 拦截器
axios.interceptors.response.use(response => {
T.loaded()
return response;
},
error => {
T.loaded()
return Promise.reject(error)
});
export default axios;
redux用的是
redux-thunk
//index.js
import { createStore, compose, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk' // 默认action只能是对象,thunk能让action是一个函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
))
export default store
//reducer.js
import { combineReducers } from 'redux-immutable'
import { reducer as header } from './../pages/header/store'
import { reducer as topic } from './../pages/topic/store'
import { reducer as detail } from './../pages/detail/store'
import { reducer as user } from './../pages/user/store'
import { reducer as login } from './../common/login/store'
import { reducer as create } from './../pages/create/store'
import { reducer as message } from './../pages/message/store'
import { reducer as replies } from './../pages/replies/store'
const reducer = combineReducers({
header,
topic,
detail,
user,
login,
create,
message,
replies
})
export default reducer
store的用法,每一处是在对于的组件页面中使用的,没有抽出来
//reducer.js
import { actionTypes } from './index'
import { fromJS } from 'immutable'
// '全部','精华','分享','问答','招聘'
const defaultState = fromJS({
navList: [
{
type: 'all',
text: '全部'
},
{
type: 'good',
text: '精华'
},
{
type: 'share',
text: '分享'
},
{
type: 'ask',
text: '问答'
},
{
type: 'job',
text: '招聘'
},
],
tab: 'all',
})
const reducer = (state = defaultState, action) => {
switch (action.type) {
case actionTypes.CHANGE_TAB:
return state.set('tab', action.data)
default:
return state
}
}
export default reducer
//header/index.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { HeaderWrapper, NavList, NavItem } from "./style";
import { actionCreators } from "./store";
import { actionCreators as topicActionCreators } from "./../topic/store";
import { Link } from "react-router-dom";
class Header extends Component {
render() {
let { navList, changeTab } = this.props;
let newNavList = navList.toJS();
return (
<HeaderWrapper>
<NavList>
{newNavList.map(it => {
return (
<NavItem key={it.type} onClick={() => changeTab(it.type)}>
<Link to={"/?tab=" + it.type}>{it.text}</Link>
</NavItem>
);
})}
</NavList>
</HeaderWrapper>
);
}
}
const mapState = state => {
return {
navList: state.getIn(["header", "navList"])
};
};
const mapDispatch = dispatch => {
return {
changeTab(type) {
// let page = 1, limit = 15
dispatch(topicActionCreators.clearTopicList([]));
dispatch(topicActionCreators.changePage(1));
dispatch(actionCreators.changeTab(type));
// dispatch(topicActionCreators.getTopic(page, limit, type))
}
};
};
export default connect(
mapState,
mapDispatch
)(Header);
//index.js
import React, { Component } from "react";
import { connect } from "react-redux";
import Footer from "./../../common/footer";
import TopNav from "./../../common/topnav";
import {
MessageWrapper,
MessageList,
MessageItem,
MessageItemLeft,
MessageItemRight,
MessageNothing
} from "./style";
import { actionCreators } from "./store";
import { formatDate } from "./../../utils";
import { Link } from "react-router-dom";
class Message extends Component {
render() {
let { messageList } = this.props;
let newMessageList = messageList.toJS();
if (JSON.stringify(newMessageList) === "{}") return null;
return (
<MessageWrapper>
<TopNav title={"消息"} />
{newMessageList.has_read_messages.length ||
newMessageList.hasnot_read_messages.length ? (
<MessageList>
{newMessageList.hasnot_read_messages.map((it, index) => {
return (
<MessageItem key={index}>
<MessageItemLeft>
<Link to={"/user/" + it.author.loginname}>
<img src={it.author.avatar_url} alt="" />
</Link>
</MessageItemLeft>
<MessageItemRight>
<div className="item-hd">
<span className="name">
<Link to={"/user/" + it.author.loginname}>
{it.author.loginname}
</Link>
</span>
<span className="time">
{formatDate(it.reply.create_at)}
</span>
</div>
<div className="item-bd">
在话题{" "}
<Link to={"/detail/" + it.topic.id}>
{it.topic.title}
</Link>
回复了你
</div>
</MessageItemRight>
</MessageItem>
);
})}
{newMessageList.has_read_messages.map((it, index) => {
return (
<MessageItem key={index}>
<MessageItemLeft>
<Link to={"/user/" + it.author.loginname}>
<img src={it.author.avatar_url} alt="" />
</Link>
</MessageItemLeft>
<MessageItemRight>
<div className="item-hd">
<span className="name">
<Link to={"/user/" + it.author.loginname}>
{it.author.loginname}
</Link>
</span>
<span className="time">
{formatDate(it.reply.create_at)}
</span>
</div>
<div className="item-bd">
回复了你的话题{" "}
<Link to={"/detail/" + it.topic.id}>
{it.topic.title}
</Link>
</div>
</MessageItemRight>
</MessageItem>
);
})}
</MessageList>
) : (
<MessageNothing>暂无消息</MessageNothing>
)}
<Footer />
</MessageWrapper>
);
}
componentDidMount() {
let loginState = localStorage.user;
if (this.props.isLogined || loginState) {
this.props.getMessage();
}
}
}
const mapState = state => {
return {
messageList: state.getIn(["message", "messageList"]),
isLogined: state.getIn(["login", "isLogined"])
};
};
const mapDispatch = dispatch => {
return {
getMessage() {
dispatch(actionCreators.getMessageCount());
}
};
};
export default connect(
mapState,
mapDispatch
)(Message);
//src/pages/replies/index.js
import React, { PureComponent, Fragment } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { Link } from "react-router-dom";
import { RepliesWrapper, RepliesTextarea, RepliesButton } from "./style";
import { T } from "react-toast-mobile";
class Replies extends PureComponent {
render() {
let { handleConfirm, id, replyId, author } = this.props;
return (
<RepliesWrapper>
{localStorage.user ? (
<Fragment>
<RepliesTextarea
ref={textarea => {
this.content = textarea;
}}
placeholder={author ? "@" + author : "请输入回复内容"}
/>
<RepliesButton
onClick={() => {
handleConfirm(id, replyId, author, this.content);
}}
>
回复
</RepliesButton>
</Fragment>
) : (
<div className="login">
你丫的先<Link to={"/login"}> 登录</Link> 才能发评论
</div>
)}
</RepliesWrapper>
);
}
}
const mapDispatch = dispatch => {
return {
handleConfirm(id, replyId, author, content) {
if (content.value.length) {
if (replyId !== "") {
dispatch(
actionCreators.sendReplies(
id,
replyId,
`[@${author}](/user/${author}) ${content.value}`
)
);
} else {
dispatch(actionCreators.sendReplies(id, replyId, content.value));
}
content.value = "";
} else {
T.notify("回复内容不能为空");
}
}
};
};
export default connect(
null,
mapDispatch
)(Replies);
//src/common/footer/index.js
import React from "react";
import { NavLink as Link } from "react-router-dom";
import { FooterWrapper, FooterItem } from "./style";
const Footer = () => {
return (
<FooterWrapper>
<FooterItem>
<Link to={"/"} exact>
<i className="iconfont"></i>
<p>首页</p>
</Link>
</FooterItem>
<FooterItem>
<Link to={"/create"}>
<i className="iconfont"></i>
<p>发表</p>
</Link>
</FooterItem>
<FooterItem>
<Link to={"/message"}>
<i className="iconfont"></i>
<p>消息</p>
</Link>
</FooterItem>
<FooterItem>
<Link to={"/mine"}>
<i className="iconfont"></i>
<p>我的</p>
</Link>
</FooterItem>
</FooterWrapper>
);
};
export default Footer;
//src/common/loading/index.js
import React from "react";
import { Spinner, BounceTop, BounceBottom } from "./style";
const Loading = () => {
return (
<Spinner>
<BounceTop />
<BounceBottom />
</Spinner>
);
};
export default Loading;
//src/common/login/index.js
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { Redirect } from "react-router-dom";
import { LoginWrapper, Input, Button, LoginBack } from "./style";
import TopNav from "./../topnav";
class Login extends PureComponent {
render() {
let { isLogined, path } = this.props;
let from = path ? { pathname: path } : { pathname: "/" };
console.log('...from',from);
if (isLogined) return <Redirect to={from} />;
return (
<LoginBack>
<TopNav title={"登录"} />
<LoginWrapper>
<Input
placeholder="accessToken"
ref={input => {
this.username = input;
}}
/>
<Button
onClick={() => {
this.props.login(this.username);
}}
>
登录
</Button>
</LoginWrapper>
</LoginBack>
);
}
}
const mapState = state => {
return {
isLogined: state.getIn(["login", "isLogined"]),
path: state.getIn(["login", "path"])
};
};
const mapDispatch = dispatch => {
return {
login(usernameElem) {
dispatch(actionCreators.login(usernameElem.value));
}
};
};
export default connect(
mapState,
mapDispatch
)(Login);
//src/common/topnav/index.js
import React, { PureComponent, Fragment } from "react";
import { TopNavWarpper, Back } from "./style";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { actionCreators as loginActionCreators } from "./../login/store";
class TopNav extends PureComponent {
render() {
let loginState = localStorage.user;
return (
<Fragment>
<TopNavWarpper>
<Back onClick={() => this.goBack()}>
<i className="iconfont"></i>
</Back>
{this.props.match.path === "/mine" ? (
<span>个人中心</span>
) : (
<span>{this.props.title}</span>
)}
{this.props.match.path === "/mine" && loginState ? (
<span onClick={() => this.quite()}>
<i className="iconfont"></i>
</span>
) : (
<span />
)}
</TopNavWarpper>
</Fragment>
);
}
goBack() {
this.props.history.goBack();
}
quite() {
localStorage.user = "";
this.props.history.push("/");
this.props.logout();
}
}
const mapDispatch = dispatch => {
return {
logout() {
dispatch(loginActionCreators.isLogined(false));
}
};
};
export default connect(
null,
mapDispatch
)(withRouter(TopNav));
//src/pages/header/store/reducer.js
//根据type值加载数据
import { actionTypes } from './index'
import { fromJS } from 'immutable'
// '全部','精华','分享','问答','招聘'
const defaultState = fromJS({
navList: [
{
type: 'all',
text: '全部'
},
{
type: 'good',
text: '精华'
},
{
type: 'share',
text: '分享'
},
{
type: 'ask',
text: '问答'
},
{
type: 'job',
text: '招聘'
},
],
tab: 'all',
})
const reducer = (state = defaultState, action) => {
switch (action.type) {
case actionTypes.CHANGE_TAB:
return state.set('tab', action.data)
default:
return state
}
}
export default reducer
//src/pages/detail/index.js
import React, { PureComponent, Fragment } from "react";
import { connect } from "react-redux";
import {
MianWrapper,
MianContent,
MianTitle,
MianInfo,
ReplyWrapper,
ReplyContent,
ReplyList,
ReplyItem
} from "./style";
import { actionCreators } from "./store";
import { formatDate } from "./../../utils";
import { Link } from "react-router-dom";
import TopNav from "./../../common/topnav";
import Replies from "./../replies";
class Detail extends PureComponent {
constructor(props) {
super(props);
this.state = {
currIndex: -1,
tab: {
good: "精华",
share: "分享",
ask: "问答",
job: "招聘"
}
};
}
render() {
let { topicDetailList } = this.props;
let newList = topicDetailList.toJS();
// 导步加载数据时,newList转为空,render的时候去读一个空对象的属性时会报错,现提供如下解决方案
// 方法一
// let {title="",create_at = "" ,author = "",visit_count = 0 ,replies = [],tab='good',content=''} = newList
// 方法二
// if (JSON.stringify(newList) === "{}") return null;
// 方法三
// 用 && 操作符
return (
<Fragment>
<TopNav title={"详情"} />
<MianWrapper>
<MianTitle>{newList && newList.title}</MianTitle>
<MianInfo>
<span>发布于 {formatDate(newList && newList.create_at)}</span>
<span>
作者 {newList && newList.author && newList.author.loginname}
</span>
<span>阅读 {newList && newList.visit_count}</span>
<span>来自 {this.state.tab[newList && newList.tab]}</span>
</MianInfo>
<MianContent
className="markdown-body"
dangerouslySetInnerHTML={{ __html: newList && newList.content }}
/>
</MianWrapper>
<ReplyWrapper>
<ReplyContent>
全部回复({newList && newList.replies && newList.replies.length})
</ReplyContent>
{newList && newList.replies && newList.replies.length ? (
<ReplyList>
{newList &&
newList.replies &&
newList.replies.map((it, index) => {
return (
<ReplyItem key={index}>
<div className="replyAvuthor">
<Link to={"/user/" + it.author.loginname}>
<img src={it.author.avatar_url} alt="avatar_url" />
</Link>
</div>
<div className="replyContent">
<div className="content-hd">
<p>
<span className="name">
<Link to={"/user/" + it.author.loginname}>
{it.author.loginname}
</Link>
</span>
{formatDate(it.create_at)}
</p>
<p className="r">
<span
className="replies"
onClick={() => this.openReplies(index)}
>
{" "}
<i className="iconfont"></i>{" "}
</span>
<span className="num"># {index + 1}</span>
</p>
</div>
<p
className="markdown-body"
dangerouslySetInnerHTML={{ __html: it.content }}
/>
{this.state.currIndex === index ? (
<Replies
author={it.author.loginname}
id={newList.id}
replyId={it.id}
/>
) : null}
</div>
</ReplyItem>
);
})}
</ReplyList>
) : (
<p className="noReply">暂无回复</p>
)}
</ReplyWrapper>
<Replies id={newList.id} replyId={""} />
</Fragment>
);
}
componentDidMount() {
window.scrollTo(0, 0);
this.props.getTopicDetail(this.props.match.params.id);
}
openReplies(index) {
this.setState(() => {
return {
currIndex: index
};
});
}
}
const mapState = state => {
return {
topicDetailList: state.getIn(["detail", "topicDetailList"])
};
};
const mapDispatch = dispatch => {
return {
getTopicDetail(id) {
dispatch(actionCreators.getTopicDetail(id));
}
};
};
export default connect(
mapState,
mapDispatch
)(Detail);
感谢作者无私开源的精神感恩~!