Repository: lumia2046/cnode
Branch: master
Commit: a1a4487dbf23
Files: 71
Total size: 138.9 KB
Directory structure:
gitextract_305ial8u/
├── .babelrc
├── .gitignore
├── README.md
├── index.html
├── package.json
├── redBoxBlackStyle.js
├── src/
│ ├── actions/
│ │ ├── fetchError.js
│ │ └── hashUrl.js
│ ├── actions.js
│ ├── components/
│ │ ├── Article/
│ │ │ ├── Content/
│ │ │ │ ├── Content.js
│ │ │ │ └── styles.scss
│ │ │ └── Reply/
│ │ │ ├── Reply.js
│ │ │ └── styles.scss
│ │ ├── HomePage/
│ │ │ ├── Drawer/
│ │ │ │ ├── Drawer.js
│ │ │ │ └── styles.scss
│ │ │ ├── FloatingActionButton.js
│ │ │ ├── Header/
│ │ │ │ ├── Header.js
│ │ │ │ └── styles.scss
│ │ │ └── Lists/
│ │ │ ├── Lists.js
│ │ │ └── styles.scss
│ │ ├── Message/
│ │ │ └── Content/
│ │ │ ├── Content.js
│ │ │ └── styles.scss
│ │ ├── PublishTopic/
│ │ │ └── Form/
│ │ │ ├── Form.js
│ │ │ └── styles.scss
│ │ └── common/
│ │ ├── AsyncContainer.js
│ │ ├── CircleLoading.js
│ │ ├── Dialog.js
│ │ ├── Header/
│ │ │ ├── Header.js
│ │ │ └── styles.scss
│ │ ├── LinkToLogin/
│ │ │ ├── LinkToLogin.js
│ │ │ └── styles.scss
│ │ ├── Profile/
│ │ │ ├── Profile.js
│ │ │ └── styles.scss
│ │ ├── Snackbar.js
│ │ └── react-pullrefresh.js
│ ├── configureStore.js
│ ├── containers/
│ │ ├── App.js
│ │ ├── Article.js
│ │ ├── HomePage.js
│ │ ├── Login.js
│ │ ├── Message.js
│ │ ├── Profile.js
│ │ └── PublishTopic.js
│ ├── index.js
│ ├── reducers/
│ │ ├── article.js
│ │ ├── collectedTopics.js
│ │ ├── fetchError.js
│ │ ├── hashUrl.js
│ │ ├── homePage.js
│ │ ├── index.js
│ │ ├── login.js
│ │ ├── message.js
│ │ ├── profile.js
│ │ └── publishTopic.js
│ ├── routes.js
│ ├── styles/
│ │ ├── iconfont/
│ │ │ ├── demo.css
│ │ │ ├── demo_fontclass.html
│ │ │ ├── demo_symbol.html
│ │ │ ├── demo_unicode.html
│ │ │ ├── iconfont.css
│ │ │ └── iconfont.js
│ │ └── index.css
│ └── utils/
│ ├── getOS.js
│ ├── getPosition.js
│ ├── getSize.js
│ ├── getStrLength.js
│ ├── myFetch.js
│ ├── routePrefix.js
│ ├── transformDate.js
│ └── urlPrefix.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
//babel配置文件,不需要做修改,因为都配置好了
{
"presets": [
["env", {"modules": false}],
"react",
"stage-0"
],
"plugins": [
"react-hot-loader/babel",
["transform-runtime", {
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}],
// ['import', {libraryName: 'antd', style:true}],
"transform-decorators-legacy",
"transform-async-to-generator",
"transform-do-expressions",
"syntax-do-expressions",
["babel-plugin-react-transform", {
transforms: [
{
transform: 'react-transform-catch-errors',
imports: ['react','redbox-react','redBoxBlackStyle']
}
],
}]
]
}
================================================
FILE: .gitignore
================================================
/node_modules/
================================================
FILE: README.md
================================================
## 项目简介
一个WebApp版的cnode客户端,项目采用react技术栈构建。组件选用的是[Material-UI](http://www.material-ui.com/),让界面更适合触控操作。
- 感谢来自[cnodejs论坛](https://cnodejs.org/)官方提供的api!
## 功能
- 首页列表,下拉时自动加载下一页,在顶端上拉刷新
- 主题详情,登陆后能够收藏,评论和点赞
- 消息提醒,能查看消息详情和清空所有未读消息
- 个人主页,包括最近参与,回复,以及收藏的主题
- 发表主题,成功后能跳转到相应主题页面
- 页面后退,能还原数据和滚动位置
## 运用的技术主要有:
- 采用react技术栈,通过Redux来管理页面状态,通过Router来设置页面路由
- 组件选用的是Material-UI,不再自己造轮子,既美观又能方便触控操作
- 使用react-route v4 和 react原生的react-transition-group v2 来实现路由切换动画
- 使用react-flip-move插件来实现list的加载动画
- 应用`isomorphic-fetch`库代替`XMLHttpRequest`实现网络请求
- 使用`PostCSS`对CSS进行预处理
- 通过`CSSModules`处理模块内部的类名
## 预览
[DEMO](https://lumia2046.github.io/cnode/)
## 运行项目
```
git clone https://github.com/lumia2046/cnode.git
cd cnode
npm install
npm start
打开浏览器访问:http://localhost:5678
```
## 生产项目
```
windows下
npm run build-win
linux、mac下
npm run build-win
```
<!--
## 状态树
本项目使用redux管理状态,状态树如图:

基本思路是每个页面对应一个reducer,管理本页面的状态。其中:
- `homePage`对应主页信息,还包括了浏览的主题类别等状态
- `article`对应文章内容页面,能缓存多篇,所以状态信息中提供了当前正在阅读的主题信息
- `login`对应登陆账号的信息页面,包括是否登录成功等状态
- `profile`代表用户的信息页面,比如用户名,积分情况等,还包括发表、回复和收藏的主题
- `publishTopic`对应发表主题页面,包括主题是否发送成功等状态
- `message`对应登陆账号的消息界面,还包括了未读消息是否被标记已读等状态
## 总结
- 对react组件及其生命周期有了更深入的了解
- 如果需要在组件更新的生命周期里setState(),应该在componentWillReceiveProps(或者componentWillUpdate)里通过对this.props和newProps里面的属性做出准确判断后再去setState(),否则会导致组件更新死循环以致页面卡死
- 在组件的生命周期里调用dispatch发送不带异步的action时,每发送一个action都会更新一次store。但是在事件回调的方法里多次调用dispatch发送不带异步的action时,只会在最后一个action发送完毕才更新一次store,想要每次action都更新store,那么必须手动将其封装为异步操作
- 公共组件最好不要设置自己的状态,应该由父组件管理其状态
- 对es6有了更深入的了解
- 当采用es5写法时,React 自动将组件绑定给所有的事件回调方法中的this,这种自动绑定的行为只适用于当组件是用 React.createClass 创建时。如果用 ES6 的类来定义组件,那么事件方法中 this 的值就是 undefined,除非你自己显式绑定它
- 为了省略es6中事件方法的绑定,可以将事件方法写成箭头函数的形式,这种写法在react文档中标注为es7+,eg: 把handleClick(){}写成handleClick = () => {}即可,但是react生命周期函数不支es7+这种写法
- 利用扩展运算符和对象的解构赋值极大的简化了react组件间props的传递的书写。
eg: < Reply {...({login,dispatch,profile})} / >只需这样,就可以把login,dispatch,profile三个变量当作props传递给Reply组件,需要注意的是解构对象本身时必须用括号括起来,否则解析器会把{}里的内容理解成一个代码块,而不是赋值语句
- 对React技术栈有了深入的了解
- react-router的history属性,当设置为browerHistory时,需要对服务器改造。否则用户直接向服务器请求某个子路由,会显示网页找不到的404错误。
- redux通过connect连接react组件,只有mapStateToProps里return的对象的属性发生改变,组件才会去更新。store中有的但是return的对象里不存在的属性改变时,组件不会去更新,因为这样没有意义
- 使用了模块化编程后,页面整体逻辑变得清晰很多,每个模块里的css和js都只负责管理一个对应的组件。尤其是用了css这一块,用css-loader开启CSS-Modules后,就再也不用为类名的语义化和重复去烦恼 -->
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cnode</title>
<!-- <link rel="stylesheet" href="./build/ui.css"> -->
<link rel="stylesheet" href="./build/module.css">
<link rel="stylesheet" href="./build/global.css">
</head>
<body>
<div id="root"></div>
<script src="./build/vendor.js"></script>
<script src="./build/app.js"></script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "cnode",
"version": "2.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"lint": "eslint src",
"build-mac": "export NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js",
"build-win": "set NODE_ENV=production && webpack --progress --hide-modules --config webpack.config.js",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"babel-polyfill": "^6.20.0",
"github-markdown-css": "^2.4.1",
"history": "^4.6.1",
"isomorphic-fetch": "^2.2.1",
"lazy-load-component": "^1.1.2",
"material-ui": "^0.20.0",
"normalize.css": "^7.0.0",
"pullhelper": "^1.1.8",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-flip-move": "^3.0.0",
"react-modal": "^3.1.0",
"react-motion": "^0.5.2",
"react-redux": "^5.0.6",
"react-router": "^4.2.0",
"react-router-dom": "^4.1.2",
"react-router-redux": "^4.0.8",
"react-swipeable-views": "^0.12.9",
"react-tap-event-plugin": "^3.0.2",
"react-transition-group": "^2.2.1",
"redux": "^3.6.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.1.0"
},
"devDependencies": {
"autoprefixer": "^7.1.6",
"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-plugin-import": "^1.2.1",
"babel-plugin-react-transform": "^3.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-hmre": "^1.1.1",
"babel-preset-stage-0": "^6.16.0",
"babel-preset-stage-1": "^6.16.0",
"babel-preset-stage-2": "^6.17.0",
"babel-preset-stage-3": "^6.17.0",
"cross-env": "^5.1.1",
"css-loader": "^0.28.7",
"eventsource-polyfill": "^0.9.6",
"express": "^4.13.3",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^2.24.1",
"node-sass": "^4.6.0",
"open": "0.0.5",
"postcss-loader": "^2.0.8",
"postcss-scss": "^1.0.2",
"react-hot-loader": "^4.0.0-beta.14",
"react-transform-catch-errors": "^1.0.2",
"redbox-react": "^1.3.3",
"redux-devtools-extension": "^2.13.2",
"rimraf": "^2.4.3",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.4",
"moment": "^2.18.1"
}
}
================================================
FILE: redBoxBlackStyle.js
================================================
module.exports = {
style: {
redbox: {
boxSizing: 'border-box',
fontFamily: 'sans-serif',
position: 'fixed',
padding: 10,
top: '0px',
left: '0px',
bottom: '0px',
right: '0px',
width: '100%',
background: 'rgba(0, 0, 0, 0.75)',
color: 'red',
zIndex: 9999,
textAlign: 'left',
fontSize: '16px',
lineHeight: 1.2
}
}
};
================================================
FILE: src/actions/fetchError.js
================================================
import urlPrefix from '../utils/urlPrefix'
export const FETCH_ERROR = 'FETCH_ERROR'
export const CLEAR_ERROR = 'CLEAR_ERROR'
export const FETCH_START = 'FETCH_START'
export const FETCH_END = 'FETCH_END'
export const fetchStart = () => ({
type: FETCH_START
})
export const fetchEnd = () => ({
type: FETCH_END
})
export const fetchError = e => ({
type: FETCH_ERROR,
data: e
})
export const clearError = () => ({
type: CLEAR_ERROR
})
================================================
FILE: src/actions/hashUrl.js
================================================
export const SET_TRANSITION = 'SET_TRANSITION'
export const SET_HASH_URL = 'SET_HASH_URL'
export const setHashUrl = (hashUrl) => {
return {
type:SET_HASH_URL,
data:hashUrl
}
}
export const setTransition = (transiton) => {
return {
type:SET_TRANSITION,
data:transiton
}
}
================================================
FILE: src/actions.js
================================================
import fetch from 'isomorphic-fetch'
export const REQUEST_TOPICS = 'REQUEST_TOPICS'
export const RECEIVE_TOPICS = 'RECEIVE_TOPICS'
export const SELECT_TAB= 'SELECT_TAB'
export const RECORD_SCROLLT='RECORD_SCROLLT'
export const REQUEST_ARTICLE = 'REQUEST_ARTICLE'
export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE'
export const CHANGE_CURRENT_TOPICID = 'CHANGE_CURRENT_TOPICID'
export const CURRENT_ROUTER = 'CURRENT_ROUTER'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAILED = 'LOGIN_FAILED'
export const LOGOUT = 'LOGOUT'
export const REQUEST_PROFILE = 'REQUEST_PROFILE'
export const RECEIVE_PROFILE = 'RECEIVE_PROFILE'
export const SWITCH_SUPPORT = 'SWITCH_SUPPORT'
export const FETCH_COMMENT = 'FETCH_COMMENT'
export const SWITCH_COLLECTED = 'SWITCH_COLLECTED'
export const RECORD_ARTICLE_SCROLLT = 'RECORD_ARTICLE_SCROLLT'
export const PUBLISH_TOPIC = 'PUBLISH_TOPIC'
export const FETCH_MESSAGE = 'FETCH_MESSAGE'
export const MARK_ALL_MESSAGES = 'MARK_ALL_MESSAGES'
export const GET_COLLECTED_TOPICS = 'GET_COLLECTED_TOPICS'
// HomePage
export const selectTab = tab => ({
type:SELECT_TAB,
tab
})
const requestTopics = tab => ({
type:REQUEST_TOPICS,
tab
})
const receiveTopics = (tab,topics,page,limit) => ({
type:RECEIVE_TOPICS,
tab,
topics,
page,
limit
})
export const fetchTopics = (tab,page=1,limit=20) => {
return dispatch => {
dispatch(requestTopics(tab))
fetch(`https://cnodejs.org/api/v1/topics?tab=${tab}&page=${page}&limit=${limit}`)
.then(response => response.json())
.then(json => dispatch(receiveTopics(tab,json.data,page,limit)))
}
}
export const recordScrollT = (tab,scrollT) => {
return ({
type:RECORD_SCROLLT,
tab,
scrollT
})
}
// Article
const requestArticle = (topicId) => ({
type:REQUEST_ARTICLE,
topicId
})
const receiveArticle = (topicId,article) => ({
type:RECEIVE_ARTICLE,
topicId,
article
})
const changeCurrentTopicId = topicId => ({
type:CHANGE_CURRENT_TOPICID,
topicId
})
export const recordArticleScrollT = (topicId,scrollT) => ({
type:RECORD_ARTICLE_SCROLLT,
topicId,
scrollT
})
export const fetchArticle = (topicId,request=true) => {
return dispatch => {
if(request){
dispatch(requestArticle(topicId))
fetch(`https://cnodejs.org/api/v1/topic/${topicId}`)
.then(response => response.json())
.then(json => dispatch(receiveArticle(topicId,json.data)))
}else{
dispatch(changeCurrentTopicId(topicId))
}
}
}
export const switchSupport = (accessToken,replyId,index) => {
return dispatch => {
fetch(`https://cnodejs.org/api/v1/reply/${replyId}/ups`, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `accesstoken=${accessToken}`
})
.then(response => response.json())
.then(json => dispatch({
type:SWITCH_SUPPORT,
replyId,
index,
success:json.success,
action:json.action
}))
}
}
export const fetchComment = (accessToken,topicId,content,replyId) => {
return dispatch => {
const postConent = replyId ? `accesstoken=${accessToken}&content=${content}&replyId=${replyId}`:`accesstoken=${accessToken}&content=${content}`
fetch(`https://cnodejs.org/api/v1/topic/${topicId}/replies`, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: postConent
})
.then(response => response.json())
.then(json => dispatch({
type:FETCH_COMMENT,
success:json.success,
replyId:json.reply_id
}))
}
}
export const switchCollected = (isCollected,accessToken,articleId) => {
return dispatch => {
fetch(`https://cnodejs.org/api/v1/topic_collect/${isCollected?'de_collect':'collect'}`, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `accesstoken=${accessToken}&topic_Id=${articleId}`
})
.then(response => response.json())
.then(json => dispatch({
type:SWITCH_COLLECTED,
success:json.success
}))
}
}
// Footer
export const setCurrentRouter = router => ({
type:CURRENT_ROUTER,
router
})
// Login
export const fetchAccess = accessToken => {
return dispatch => {
fetch('https://cnodejs.org/api/v1/accesstoken', {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `accesstoken=${accessToken}`
})
.then(response => response.json())
.then(json => {
if (json.success){
dispatch(loginSucceed(json.loginname,json.id,accessToken))
} else {
dispatch(loginFailed(json.error_msg));
}
})
}
}
const loginSucceed = (loginName,loginId,accessToken) => ({
type:LOGIN_SUCCESS,
loginName,
loginId,
accessToken
})
const loginFailed = failedMessage => ({
type:LOGIN_FAILED,
failedMessage
})
export const logout = () => ({
type:LOGOUT
})
// profile
const requestProfile = loginname => ({
type:REQUEST_PROFILE,
loginname
})
const receiveProfile = (loginname,profile) => ({
type:RECEIVE_PROFILE,
loginname,
profile
})
const getCollectedTopics = (userName) => {
return dispatch => {
fetch(`https://cnodejs.org/api/v1/topic_collect/${userName}`)
.then(response => response.json())
.then(json => dispatch({
type:GET_COLLECTED_TOPICS,
success:json.success,
data:json.data
}))
}
}
export const fetchProfile = (loginname) => {
return dispatch => {
dispatch(requestProfile(loginname))
dispatch(getCollectedTopics(loginname))
fetch(`https://cnodejs.org/api/v1/user/${loginname}`)
.then(response => response.json())
.then(json => dispatch(receiveProfile(loginname,json.data)))
}
}
// publishTopic
export const fetchPublishTopic = (accessToken,tab,title,content) => {
return dispatch => {
const postConent = `accesstoken=${accessToken}&tab=${tab}&content=${content}&title=${title}`
fetch(`https://cnodejs.org/api/v1/topics`, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: postConent
})
.then(response => response.json())
.then(json => dispatch({
type:PUBLISH_TOPIC,
success:json.success,
topicId:json.topic_id
}))
}
}
// message
export const fetchMessage = (accessToken) => {
return dispatch => {
fetch(`https://cnodejs.org/api/v1/messages?accesstoken=${accessToken}`)
.then(response => response.json())
.then(json => dispatch({
type:FETCH_MESSAGE,
hasReadMessage:json.data.has_read_messages,
hasNotReadMessage:json.data.hasnot_read_messages
}))
}
}
export const markAllMessages = (accessToken) => {
return dispatch => {
fetch(`https://cnodejs.org/api/v1/message/mark_all`, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `accesstoken=${accessToken}`
})
.then(response => response.json())
.then(json => dispatch({
type:MARK_ALL_MESSAGES,
isMarked:json.success
}))
}
}
================================================
FILE: src/components/Article/Content/Content.js
================================================
import React, { PropTypes, Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import prefix from '../../../utils/routePrefix'
import { setTransition } from '../../../actions/hashUrl'
import { fetchProfile, switchCollected } from '../../../actions'
import styles from './styles.scss'
import LinkToLogin from '../../common/LinkToLogin/LinkToLogin'
import transformDate from '../../../utils/transformDate'
import classnames from 'classnames'
import { List, ListItem } from 'material-ui/List'
import Divider from 'material-ui/Divider'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import fetch from 'isomorphic-fetch'
@connect()
class Content extends Component {
constructor() {
super()
this.state = {
isCollected: false
}
}
componentWillMount() {
this.update(this.props)
}
componentWillReceiveProps(newProps) {
if (this.props.collectedTopics.userName !== newProps.collectedTopics.userName) {
this.update(newProps);
}
}
update(props) {
const { article, login, collectedTopics } = props
if (login.succeed && collectedTopics.length !== 0) {
let isCollected = collectedTopics.some(topic => {
return article.id === topic.id
})
this.setState({
isCollected: isCollected
})
}
}
render() {
const { article, dispatch, fetchProfile, login, collectedTopics, profile } = this.props
return (
<MuiThemeProvider>
<List>
<div style={{ margin: '-8px 0' }}>
<ListItem>
<div style={{ margin: -16 }}>
<div className={styles.head}>
<div className={styles.imgbox}>
<img src={article.author.avatar_url} alt={article.author.loginname} />
</div>
<div className={styles.info}>
<Link to={`${prefix}/profile`} onClick={e => {
this.props.dispatch(setTransition({ transition: 'up' }))
if (profile.loginname !== article.author.loginname) {
dispatch(fetchProfile(article.author.loginname))
}
}}>
<span>{article.author.loginname}</span>
</Link>
<span style={{ float: 'right' }}>发表于{transformDate(article.create_at)}</span>
</div>
<div className={styles.info}>
{login.succeed &&
<span>
<span style={{ marginRight: 5 }}>收藏</span>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }} style={{
height: 16, lineHeight: '16px', fontSize: '15px',
color: this.state.isCollected ? 'red' : 'black'
}} onClick={e => {
this.setState({
isCollected: !this.state.isCollected
})
dispatch(switchCollected(this.state.isCollected, login.accessToken, article.id))
// fetch(`https://cnodejs.org/api/v1/topic_collect/${this.state.isCollected?'de_collect':'collect'}`, {
// method: 'POST',
// headers: {
// "Content-Type": "application/x-www-form-urlencoded"
// },
// body: `accesstoken=${login.accessToken}&topic_Id=${article.id}`
// })
}}></i>
</span>
}
<span style={{ float: 'right' }}>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }} style={{ fontSize: '18px', color: 'grey' }}></i>
{article.visit_count}
</span>
<span style={{ float: 'right', marginRight: 10 }}>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }} style={{ fontSize: '14px', color: 'grey' }}></i>
{article.reply_count}
</span>
</div>
</div>
</div>
</ListItem>
<ListItem>
<div className={styles.title}>{article.title}</div>
</ListItem>
<Divider />
<ListItem style={{ lineHeight: 'auto' }}>
<div style={{ margin: -16 }}>
<div className={`${styles.main} markdown-body`} dangerouslySetInnerHTML={{ __html: article.content }}></div>
</div>
</ListItem>
</div>
</List>
</MuiThemeProvider>
)
}
}
export default Content
================================================
FILE: src/components/Article/Content/styles.scss
================================================
.head{
margin-top: 64px;
background: #ccc;
overflow: hidden;
padding: 10px;
img{
width: 50px;
height: 50px;
border-radius: 50px;
}
}
.imgbox{
float: left;
text-align: center;
margin-right: 10px;
}
.info{
height: 25px;
line-height: 25px;
}
.title{
text-align: center;
font-weight: bold;
font-size: 24px;
line-height: 32px;
}
.main{
padding: 20px;
p{
text-indent: 2em;
}
img{
display: block;
margin: 20px auto;
width: 600px;
}
}
@media (max-width: 750px){
.main{
img{
width: 80%;
}
}
}
================================================
FILE: src/components/Article/Reply/Reply.js
================================================
import React, { PropTypes,Component } from 'react'
import {Link} from 'react-router-dom'
import { connect } from 'react-redux'
import prefix from '../../../utils/routePrefix'
import styles from './styles.scss'
import { setTransition } from '../../../actions/hashUrl'
import {fetchComment,fetchArticle,recordArticleScrollT,switchSupport,fetchProfile} from '../../../actions'
import transformDate from '../../../utils/transformDate'
import getSize from '../../../utils/getSize'
import LinkToLogin from '../../common/LinkToLogin/LinkToLogin'
import Dialog from '../../common/Dialog'
import classnames from 'classnames'
import {List, ListItem} from 'material-ui/List';
import Divider from 'material-ui/Divider';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
@connect()
class Reply extends Component {
constructor(){
super();
this.state = {
isSupported:[],
supportNum:[],
height:[],
name:[],
openDialog:false
}
}
supportState = (replies,login) => {
let isSupported = replies.map(reply => {
return reply.ups.some(up => up === login.loginId)
})
let supportNum = replies.map(reply => reply.ups.length)
this.setState({isSupported,supportNum})
}
closeDialog = () => {
this.setState({
openDialog:false
})
}
componentWillMount(){
const {replies,login} = this.props
this.supportState(replies,login)
}
componentWillReceiveProps(newProps){
const {switchSupportInfo,replies,login,isCommented,currentTopicId,dispatch} = newProps
if(this.state.height.length !== 0){
this.setState({
height:[],
name:[]
})
}
if(replies.length !== this.props.replies.length){
this.supportState(replies,login)
}
if(isCommented && this.props.isCommented !== isCommented){
dispatch(fetchArticle(currentTopicId))
}
// if(switchSupportInfo){
// if(!this.props.switchSupportInfo || !(this.props.switchSupportInfo.action===switchSupportInfo.action && this.props.switchSupportInfo.index===switchSupportInfo.index)){
// if(switchSupportInfo.success){
// let isSupported = this.state.isSupported;
// let supportNum = this.state.supportNum;
// isSupported[switchSupportInfo.index] = switchSupportInfo.action === 'up';
// switchSupportInfo.action === 'up' ? ++supportNum[switchSupportInfo.index] : --supportNum[switchSupportInfo.index]
// this.setState({
// isSupported,
// supportNum
// })
// }else{
// }
// }
// }
}
render(){
let {profile,replies,dispatch,login,switchSupportInfo,currentTopicId}= this.props
return (
<div className={styles.reply}>
<Dialog isOpen={this.state.openDialog} close={this.closeDialog} singleButton={true}>
不能给自己点赞!
</Dialog>
<div style={{margin:'-8px 0'}}>
<MuiThemeProvider>
<List>
<ListItem>
<div style={{margin:-16}}>
<h2>共有{replies.length}条回复</h2>
</div>
</ListItem>
{replies.map((reply,index) => (
<div key={index}>
<ListItem>
<div className={styles.author}>
<img src={reply.author.avatar_url} alt={reply.author.loginname}/>
<div style={{textAlign:'center',paddingTop:10}}>{index+1}楼</div>
</div>
<div className={styles.main}>
<div className={styles.item}>
<Link to={`${prefix}/profile`} onClick={e => {
this.props.dispatch(setTransition({ transition: 'up' }))
if(profile.loginname !== reply.author.loginname){
dispatch(fetchProfile(reply.author.loginname))
}
}}>
{reply.author.loginname}
</Link>
<span style={{float:'right'}}>{transformDate(reply.create_at)}</span>
</div>
<div className={`${styles.item} markdown-text`} dangerouslySetInnerHTML={{__html: reply.content}} style={{lineHeight:'21px'}}></div>
<div className={styles.item}>
<div style={{float:'right'}}>
<span onClick={e => {
let heightArr = [];
let nameArr = [];
heightArr[index] = this.state.height[index] ? 0 : 120
nameArr[index] = `@${reply.author.loginname} `
this.setState({
height:heightArr,
name:nameArr
})
}} style={{cursor:'pointer',marginRight:10}}>
回复
</span>
<span>
<i className="iconfont" dangerouslySetInnerHTML={{__html: ''}} onClick={ e => {
e.stopPropagation()
if(login.loginId){
if(reply.author.loginname !== login.loginName){
const {scrollT} = getSize()
// 点赞的时候也会发送数据请求,所以Article组件会刷新,如果不保存位置的话,Article的位置会变成上次记录的位置或者默认位置0
dispatch(recordArticleScrollT(currentTopicId,scrollT))
let isSupported = this.state.isSupported;
let supportNum = this.state.supportNum;
isSupported[index]? --supportNum[index] : ++supportNum[index]
isSupported[index] = !isSupported[index]
this.setState({
isSupported,
supportNum
})
dispatch(switchSupport(login.accessToken,reply.id,index))
}else{
this.setState({
openDialog:true
})
}
}else{
let heightArr = [];
heightArr[index] = this.state.height[index] ? 0 : 150
this.setState({
height:heightArr
})
}
}} style={{color:this.state.isSupported[index] ? 'red':'black',cursor:'pointer'}}
></i>
{this.state.supportNum[index]}
</span>
</div>
</div>
<NeedComment {...({login,dispatch,currentTopicId})}
pHeight={this.state.height[index]} defaultValue={this.state.name[index]}/>
</div>
</ListItem>
<Divider/>
</div>
))}
</List>
</MuiThemeProvider>
</div>
<MuiThemeProvider>
<ListItem>
<NeedComment {...({login,dispatch,currentTopicId})} pHeight='150px'/>
</ListItem>
</MuiThemeProvider>
</div>
)
}
}
class NeedComment extends Component{
render(){
const {login,dispatch,currentTopicId,pHeight} = this.props
const sHeight = pHeight ? pHeight : 0
const style = pHeight ? {overflow:'hidden',minHeight:pHeight} : {overflow:'hidden',height:0}
// const tail = '<b>—— —— 来自lumia2046专版客户端</b>'
const tail = '<p style="text-align:right"><a href="https://github.com/lumia2046/cnode"> — — 来自lumia2046-react-cnode</a></p>'
if(login.loginId){
return (<div style={style} className={styles.textarea}>
<MuiThemeProvider>
<form className={styles.form} >
<TextField hintText={this.props.defaultValue || '请输入内容'} multiLine={true} ref='textarea'
underlineStyle={{color:'#00BCD4'}} onClick={e => {
const defaultValue = this.props.defaultValue || ''
e.target.value = e.target.value || defaultValue
}}
/><br />
<RaisedButton label="回复" primary={true} onClick={e => {
e.preventDefault();
const textarea = this.refs.textarea.input.refs.input.value + tail
if(!textarea.trim()){
return null;
}
const {scrollT,contentH,windowH} = getSize()
dispatch(fetchComment(login.accessToken,currentTopicId,textarea))
if(pHeight === 120){
dispatch(recordArticleScrollT(currentTopicId,contentH-windowH))
}else{
dispatch(recordArticleScrollT(currentTopicId,scrollT))
}
this.refs.textarea.input.refs.input.value = ''
}}/>
</form>
</MuiThemeProvider>
</div>)
}else{
return (<div style={{overflow:'hidden',height:sHeight,lineHeight:`${sHeight}px`}} className={styles.textarea}>
<LinkToLogin dispatch={dispatch}/>
</div>)
}
}
}
export default Reply
================================================
FILE: src/components/Article/Reply/styles.scss
================================================
.reply{
li{
padding: 10px 20px;
overflow: hidden;
border-bottom: 1px solid #ccc;
}
h2{
padding: 10px 20px;
font-size: 18px;
background: #ccc;
}
img{
width: 50px;
height: 50px;
border-radius: 50%;
}
b{
display: block;
text-align: right;
}
}
.author{
float: left;
text-align: center;
padding-top: 10px;
}
.main{
padding: 0 0 0 60px;
}
.item{
padding: 5px;
overflow: hidden;
}
.form{
padding: 20px;
}
.textarea{
transition: all 0.3s ease-out;
}
================================================
FILE: src/components/HomePage/Drawer/Drawer.js
================================================
import React from 'react';
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import prefix from '../../../utils/routePrefix'
import Drawer from 'material-ui/Drawer'
import MenuItem from 'material-ui/MenuItem'
import RaisedButton from 'material-ui/RaisedButton'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import Divider from 'material-ui/Divider'
import Avatar from 'material-ui/Avatar'
import { logout, fetchProfile } from '../../../actions'
import { setTransition } from '../../../actions/hashUrl'
import styles from './styles.scss'
import getSize from '../../../utils/getSize'
import transformDate from '../../../utils/transformDate'
import Dialog from '../../common/Dialog'
@connect()
export default class myDrawer extends React.Component {
constructor() {
super();
this.state = {
isOpen: false
}
}
excuteLogout = () => {
const { dispatch } = this.props
window.localStorage.removeItem('masterInfo')
window.sessionStorage.removeItem('masterProfile')
dispatch(logout())
}
close = () => {
this.setState({
isOpen: false
})
}
componentWillUpdate(nextProps) {
}
render() {
let { contentW } = getSize()
if (contentW > 800) {
contentW = 640
} else {
contentW = 0.8 * contentW
}
let { login, profile } = this.props
const { succeed } = login
if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) {
profile = JSON.parse(window.sessionStorage.masterProfile)
}
const { avatar_url, loginname, score, create_at } = profile
return (
<MuiThemeProvider>
<Drawer
docked={false}
width={contentW}
open={this.props.openDrawer}
onRequestChange={this.props.toggleDrawer}
>
{succeed && <div>
<div className={styles.header}>
<Link to={`${prefix}/login`} className={styles.link}>
<Avatar src={avatar_url} size={80} />
<p>{loginname}</p>
</Link>
<p>积分:{score}</p>
<p>注册于:{transformDate(create_at)}</p>
<RaisedButton label="注销登陆" primary={true} onClick={() => {
this.setState({
isOpen: true
})
}} />
<Dialog isOpen={this.state.isOpen} action={this.excuteLogout} close={this.close}>
确定要注销登陆?
</Dialog>
</div>
<MenuItem onTouchTap={this.props.toggleDrawer}>
<Link to={`${prefix}/login`} className={styles.link}>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }} style={{ color: '#00BCD4' }}></i>
<span style={{ color: '#00BCD4' }}> 个人主页</span>
</Link>
</MenuItem>
<Divider />
<MenuItem onTouchTap={this.props.toggleDrawer}>
<Link to={`${prefix}/message`} className={styles.link}>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }} style={{ color: '#00BCD4' }}></i>
<span style={{ color: '#00BCD4' }}> 消息</span>
</Link>
</MenuItem>
<Divider />
</div>}
{!succeed && <div className={styles.header}>
<Link to={`${prefix}/login`} className={styles.link} onClick={() => this.props.dispatch(setTransition({ transition: 'up' }))}>
<Avatar backgroundColor={'rgb(0,188,212)'} size={80}>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }}></i>
</Avatar>
</Link>
<p>点击头像登陆</p>
</div>}
</Drawer>
</MuiThemeProvider>
);
}
}
================================================
FILE: src/components/HomePage/Drawer/styles.scss
================================================
.header{
text-align: center;
padding: 100px 10px 50px;
background: #DCE775;
p{
margin: 10px;
}
}
.link{
vertical-align: center;
display: block;
border-bottom: 1px solid #DCE775;
}
================================================
FILE: src/components/HomePage/FloatingActionButton.js
================================================
import React from 'react'
import { connect } from 'react-redux'
import { setTransition } from '../../actions/hashUrl'
import FloatingActionButton from 'material-ui/FloatingActionButton'
import ContentAdd from 'material-ui/svg-icons/content/add'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import { Link } from 'react-router-dom'
import prefix from '../../utils/routePrefix'
const style = {
position: 'fixed',
bottom: 50,
right: 50
}
@connect()
class FloatActionButton extends React.Component {
render() {
return (
<Link to={`${prefix}/publishTopic`} onClick={() => this.props.dispatch(setTransition({ transition: 'up' }))}>
<MuiThemeProvider>
<FloatingActionButton style={style} secondary={true}>
<ContentAdd />
</FloatingActionButton>
</MuiThemeProvider>
</Link>
)
}
}
export default FloatActionButton
================================================
FILE: src/components/HomePage/Header/Header.js
================================================
import React, { Component } from 'react'
import { connect } from 'react-redux'
import styles from './styles.scss'
import { setTransition } from '../../../actions/hashUrl'
import { Link } from 'react-router-dom'
import prefix from '../../../utils/routePrefix'
import { Tabs, Tab } from 'material-ui/Tabs'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import AppBar from 'material-ui/AppBar'
import Badge from 'material-ui/Badge'
import NotificationsIcon from 'material-ui/svg-icons/social/notifications'
import IconButton from 'material-ui/IconButton'
import SwipeableViews from 'react-swipeable-views'
import getSize from '../../../utils/getSize'
@connect()
class Header extends Component {
constructor(props) {
super(props);
this.state = {
slideIndex: 0
}
}
componentWillMount() {
const { tabs, filter } = this.props;
let slideIndex;
tabs.forEach((tab, index) => {
if (tab.filter === filter) {
slideIndex = index;
}
})
this.setState({
slideIndex: slideIndex
})
}
handleChange = (value) => {
this.setState({
slideIndex: value,
});
this.props.onClick(this.props.tabs[value].filter)
};
render() {
// this.props.tabs.forEach(tab => {
// tab.cn = classnames({[styles.active] : tab.filter === this.props.filter})
// })
return (
<MuiThemeProvider>
<div>
<div className={styles.header} style={{ top: -this.props.fixedTop, width: window.width||'100%' }}>
<AppBar title={<p style={{ textAlign: 'center' }}>NodeJS论坛</p>} onLeftIconButtonClick={this.props.toggleDrawer}
iconElementRight={<div style={{ marginTop: -8 }}>
<Badge badgeContent={this.props.unreadMessageCount} secondary={true} style={{ top: 3 }}>
<Link to={`${prefix}/message`} onClick={() => this.props.dispatch(setTransition({ transition: 'up' }))}>
<IconButton tooltip="未读消息" style={{ padding: 0, width: 25, height: 25 }}>
<div>
<NotificationsIcon style={{ color: 'white' }} />
</div>
</IconButton>
</Link>
</Badge>
</div>} />
<Tabs onChange={this.handleChange} value={this.state.slideIndex}>
{this.props.tabs.map((tab, i) =>
<Tab key={i} label={tab.title} value={i}>
</Tab>
)}
</Tabs>
</div>
<SwipeableViews index={this.state.slideIndex} onChangeIndex={this.handleChange}>
{this.props.children}
</SwipeableViews>
</div>
</MuiThemeProvider>
)
}
}
// Header.propTypes = {
// filter: PropTypes.string.isRequired,
// onClick: PropTypes.func.isRequired
// }
export default Header
================================================
FILE: src/components/HomePage/Header/styles.scss
================================================
.header{
position: fixed;
overflow: hidden;
z-index: 10;
// width: 100%;
transition: all 0.3s ;
}
================================================
FILE: src/components/HomePage/Lists/Lists.js
================================================
import React from 'react'
import FlipMove from 'react-flip-move'
import transformDate from '../../../utils/transformDate'
import styles from './styles.scss'
import { setTransition } from '../../../actions/hashUrl'
import { Link } from 'react-router-dom'
import prefix from '../../../utils/routePrefix'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import { List, ListItem } from 'material-ui/List'
import Divider from 'material-ui/Divider'
import Avatar from 'material-ui/Avatar'
import getSize from '../../../utils/getSize'
const Lists = props => {
const tabChn = { all: '全部', good: '精华', share: '分享', ask: '问答', job: '招聘' }
const { topics, fetchArticle, dispatch, article, isFetching, selectedTab, history } = props
let disableAllAnimations = topics.length === 20 ? false : true
// disableAllAnimations从启用到禁用时enterAnimation设定的动画会不起作用,原因不明。
let enterAnimation = {
from: { transform: 'translateY(-80px)', opacity: 0 },
to: { transform: 'translateY(0)', opacity: 1 }
}
return (
<div style={{ position: 'relative' }}>
<div className={styles.lists}>
<MuiThemeProvider>
<List>
<FlipMove disableAllAnimations={disableAllAnimations} enterAnimation={enterAnimation}
easing='ease-out' duration='400' staggerDelayBy='40' staggerDurationBy='4'>
{topics.map((topic, i) =>
<Link to={`${prefix}/topic/${topic.id}`} key={i} onClick={() => {
dispatch(setTransition({ transition: 'move' }))
if (!article[topic.id]) {
dispatch(fetchArticle(topic.id))
} else if (article.currentTopicId !== topic.id) {
dispatch(fetchArticle(topic.id, false))
}
}}>
<ListItem
leftAvatar={<Avatar src={topic.author.avatar_url} />}
primaryText={
<div className={styles.text}>
{topic.top && <span style={{ color: 'blue' }}>顶</span>}
{topic.good && <span style={{ color: 'red' }}>精</span>}
<span className={styles.title}>{topic.title}</span>
</div>
}
secondaryText={
<div className={styles.text}>
<span>{topic.reply_count + '/' + topic.visit_count}</span>
<span>{tabChn[topic.tab]}</span>
<span style={{ float: 'right' }}>{transformDate(topic.create_at)}</span>
</div>
}
/>
<Divider inset={true} />
</Link>
)}
</FlipMove>
</List>
</MuiThemeProvider>
<div className={styles.spinner}>
<div className={styles.bounce1}></div>
<div className={styles.bounce2}></div>
<div className={styles.bounce3}></div>
</div>
</div>
</div>
)
}
// Lists.propTypes = {
// topics: PropTypes.array.isRequired,
// fetchArticle: PropTypes.func.isRequired
// }
export default Lists
================================================
FILE: src/components/HomePage/Lists/styles.scss
================================================
.lists{
position: relative;
margin-top: 100px;
}
.li{
position: relative;
top:0;
height: 61px;
padding: 5px 15px;
border-bottom: 1px solid #ccc;
}
.link{
display: block;
}
.text{
margin: 0 0 10px;
span{
padding-right: 10px;
vertical-align: middle;
font-weight:bold;
}
.title{
display: inline-block;
padding: 0;
width:80%;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
}
.spinner {
margin: 24px auto;
width: 100%;
text-align: center;
}
.spinner > div {
width: 30px;
height: 30px;
margin: 0 20px;
background-color: #00BCD4;
border-radius: 100%;
display: inline-block;
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
animation: bouncedelay 1.4s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
================================================
FILE: src/components/Message/Content/Content.js
================================================
import React, { PropTypes, Component } from 'react'
import { connect } from 'react-redux'
import transformDate from '../../../utils/transformDate'
import styles from './styles.scss'
import classnames from 'classnames'
import { Link } from 'react-router-dom'
import { setTransition } from '../../../actions/hashUrl'
import prefix from '../../../utils/routePrefix'
import { Tabs, Tab } from 'material-ui/Tabs';
import SwipeableViews from 'react-swipeable-views';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { List, ListItem } from 'material-ui/List';
import Divider from 'material-ui/Divider';
import Avatar from 'material-ui/Avatar';
import RaisedButton from 'material-ui/RaisedButton';
import Dialog from '../../common/Dialog'
import CircleLoading from '../../common/CircleLoading'
import { markAllMessages, fetchMessage } from '../../../actions'
@connect()
class Content extends Component {
constructor(props) {
super(props);
this.state = {
slideIndex: 0,
isOpen: false,
isUpdating: false
}
}
handleChange = (value) => {
this.setState({
slideIndex: value,
});
};
markMessages = () => {
let { dispatch, login } = this.props
let accessToken = login.accessToken
dispatch(markAllMessages(accessToken))
this.setState({
isUpdating: true
})
}
close = () => {
this.setState({
isOpen: false
})
}
componentWillUpdate(newProps) {
let { dispatch, login, isMarked, hasNotReadMessage } = newProps
let accessToken = login.accessToken
if (isMarked && isMarked !== this.props.isMarked) {
dispatch(fetchMessage(accessToken))
}
if (isMarked && hasNotReadMessage.length === 0 &&
hasNotReadMessage.length !== this.props.hasNotReadMessage.length) {
this.setState({
isUpdating: false
})
}
}
render() {
const { dispatch, hasNotReadMessage, hasReadMessage, article, fetchArticle } = this.props
return (
<div>
{this.state.isUpdating && <div><CircleLoading /></div>}
{!this.state.isUpdating && <MuiThemeProvider>
<div>
<Tabs onChange={this.handleChange} value={this.state.slideIndex}>
<Tab label={<h2>未读消息:{hasNotReadMessage && hasNotReadMessage.length}</h2>} value={0} />
<Tab label={<h2>已读消息:{hasReadMessage && hasReadMessage.length}</h2>} value={1} />
</Tabs>
<SwipeableViews index={this.state.slideIndex} onChangeIndex={this.handleChange}>
<div>
{hasNotReadMessage && hasNotReadMessage.length === 0 &&
<div>
<div className={styles.msg}>暂无未读消息</div>
</div>
}
{hasNotReadMessage.length > 0 &&
<div>
<List>
{hasNotReadMessage.map((msg, index) =>
<Link key={index} to={`${prefix}/topic/${msg.topic.id}`} className={styles.link} onClick={(e) => {
dispatch(setTransition({ transition: 'up' }))
if (!article[msg.topic.id]) {
dispatch(fetchArticle(msg.topic.id))
} else if (article.currentTopicId !== topic.id) {
dispatch(fetchArticle(msg.topic.id, false))
}
}}>
<ListItem
leftAvatar={<Avatar src={msg.author.avatar_url} />}
primaryText={msg.author.loginname}
secondaryText={
<div>
<div className={styles.oneline} dangerouslySetInnerHTML={{ __html: msg.reply.content }}></div>
<p style={{ fontSize: '14px' }}>
<span>来自:{msg.topic.title}</span>
<span style={{ float: 'right' }}>{transformDate(msg.reply.create_at)}</span>
</p>
</div>
}
secondaryTextLines={2}
/>
<Divider inset={true} />
</Link>)}
</List>
<div style={{ textAlign: 'center' }}>
<RaisedButton label="清空未读消息" primary={true} onClick={e => {
this.setState({
isOpen: true
})
}} />
</div>
<Dialog isOpen={this.state.isOpen} title='注意' action={this.markMessages} close={this.close}>
是否将所有未读消息标记为已读?
</Dialog>
</div>
}
</div>
<div>
{hasReadMessage.length === 0 &&
<div className={styles.msg}>您还没有查看过任何消息哦</div>
}
{hasReadMessage.length > 0 &&
<List>
{hasReadMessage.map((msg, index) =>
<Link key={index} to={`${prefix}/topic/${msg.topic.id}`} className={styles.link} onClick={(e) => {
dispatch(setTransition({ transition: 'up' }))
if (!article[msg.topic.id]) {
dispatch(fetchArticle(msg.topic.id))
} else if (article.currentTopicId !== topic.id) {
dispatch(fetchArticle(msg.topic.id, false))
}
}}>
<ListItem
leftAvatar={<Avatar src={msg.author.avatar_url} />}
primaryText={msg.author.loginname}
secondaryText={
<div>
<div className={styles.oneline} dangerouslySetInnerHTML={{ __html: msg.reply.content }}></div>
<p style={{ fontSize: '14px' }}>
<span>来自:{msg.topic.title}</span>
<span style={{ float: 'right' }}>{transformDate(msg.reply.create_at)}</span>
</p>
</div>
}
secondaryTextLines={2}
/>
<Divider inset={true} />
</Link>
)}
</List>}
</div>
</SwipeableViews>
</div>
</MuiThemeProvider>}
</div>
);
}
}
export default Content
================================================
FILE: src/components/Message/Content/styles.scss
================================================
.content{
margin-top: 64px;
overflow: hidden;
}
.link{
display: block;
}
.msg{
padding: 20px;
text-align: center;
font-size: 24px;
}
.oneline{
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
================================================
FILE: src/components/PublishTopic/Form/Form.js
================================================
import React, { PropTypes,Component } from 'react'
import getStrLength from '../../../utils/getStrLength'
import styles from './styles.scss'
import classnames from 'classnames'
import TextField from 'material-ui/TextField';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import RaisedButton from 'material-ui/RaisedButton'
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
class Form extends Component {
constructor(props) {
super(props);
this.state = {value: "ask"};
}
handleChange = (event, index, value) => this.setState({value});
render(){
const {ifTitleErr,ifContentErr,showDialog,fetchPublishTopic,dispatch,login,state} = this.props
return (
<MuiThemeProvider>
<form className={styles.form}>
<div className={styles.content}>
<span className={styles.title}>请选择主题类别:</span>
<DropDownMenu value={this.state.value} onChange={this.handleChange} ref='select'>
<MenuItem value="ask" primaryText="问答" />
<MenuItem value="share" primaryText="分享" />
<MenuItem value="job" primaryText="招聘" />
</DropDownMenu>
</div>
<div className={styles.content}>
<TextField ref='input' hintText=' 请输入标题,不少于十个字符' floatingLabelText=' 请输入标题,不少于十个字符' onChange={e => {
let titleErr = getStrLength(e.target.value)<10 ? true:false
ifTitleErr(titleErr)
}}/>
<div style={{height:state.titleErr ? 20 : 0 }} className={styles.errorInfo}>标题不得少于十个字符!</div>
</div>
<div className={styles.content}>
<TextField ref='textarea' hintText=' 请输入内容' floatingLabelText='请输入内容' style={{textAlign:'left'}}
multiLine={true} rows={2} onChange={e => {
let contentErr = getStrLength(e.target.value)===0 ? true:false
ifContentErr(contentErr)
}}/>
<div style={{height:state.contentErr ? 20 : 0 }} className={styles.errorInfo}>内容不能为空!</div>
</div>
<RaisedButton label="提交" primary={true} onClick={e => {
e.preventDefault()
const input = this.refs.input.input.value
const textarea = this.refs.textarea.input.refs.input.value
const select = this.refs.select.props.value
if(getStrLength(input) < 10 || !textarea.trim()){
return null
}
dispatch(fetchPublishTopic(login.accessToken,select,input,textarea))
showDialog()
}} />
</form>
</MuiThemeProvider>
)
}
}
export default Form
================================================
FILE: src/components/PublishTopic/Form/styles.scss
================================================
.form{
text-align: center;
margin-top: 80px;
}
.errorInfo{
color: red;
margin: 5px;
overflow: hidden;
transition: all 0.3s;
}
.content{
margin: 10px;
}
.title{
position: relative;
top: -20px;
/*vertical-align: top;*/
}
================================================
FILE: src/components/common/AsyncContainer.js
================================================
import React, { Component } from 'react'
import CircleLoading from './CircleLoading'
class AsyncContainer extends Component {
state = { mounted: false }
componentDidMount() {
this.timeOut = setTimeout(() => {
this.setState({ mounted: true })
}, this.props.delay || 500)
}
componentWillUnmount() {
clearTimeout(this.timeOut)
}
render() {
return (
<div>
<div style={{ height: this.state.mounted ? 0 : 'auto', overflow: 'hidden' }}>
<CircleLoading />
</div>
{this.state.mounted && this.props.children}
</div>
)
}
componentWillUnmount() {
clearTimeout(this.timeOut)
}
}
export default AsyncContainer
================================================
FILE: src/components/common/CircleLoading.js
================================================
import React from 'react'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import CircularProgress from 'material-ui/CircularProgress'
import getSize from '../../utils/getSize'
const { windowH, windowW } = getSize()
const style = {
position:'relative',
paddingTop: 0.5 * windowH - 40,
textAlign: 'center'
}
const CircleLoading = () => (
<div style={style}>
<MuiThemeProvider>
<CircularProgress size={60} thickness={7} />
</MuiThemeProvider>
</div>
);
export default CircleLoading;
================================================
FILE: src/components/common/Dialog.js
================================================
import React from 'react';
// import { hashHistory,browserHistory } from 'react-router';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
const DialogExample = (props) => {
const handleClose = () => {
props.close()
};
const handleJump = () => {
props.close()
if(props.link){
// hashHistory.push(props.link)
// browserHistory.push(props.link)
// pros.location = props.link
alert('a')
}
if(props.action){
props.action()
}
}
const actions = props.singleButton ? [<FlatButton label="确定" primary={true} onTouchTap={handleClose}/>] :
[<FlatButton label="取消" primary={true} onTouchTap={handleClose}/>,
<FlatButton label="确定" primary={true} onTouchTap={handleJump}/>];
return (
<MuiThemeProvider>
<Dialog
title={props.title || ''}
actions={actions}
modal={false}
open={props.isOpen}
onRequestClose={handleClose}>
{props.children}
</Dialog>
</MuiThemeProvider>
);
}
export default DialogExample
================================================
FILE: src/components/common/Header/Header.js
================================================
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { setTransition } from '../../../actions/hashUrl'
import styles from './styles.scss'
import AppBar from 'material-ui/AppBar'
import IconButton from 'material-ui/IconButton'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import getSize from '../../../utils/getSize'
// @withRouter
@connect(store => ({ hashUrl: store.hashUrl }))
class Header extends Component {
constructor() {
super()
this.state = { position: 'static' }
}
componentWillMount() {
if (this.props.position) {
this.state.position = 'absolute'
this.state.left = '100%'
}
}
componentDidMount() {
if (this.props.position) {
this.setState({ left: 0 })
setTimeout(() => this.setState({ position: 'fixed' }), 500)
}
}
componentWillUnmount() {
}
render() {
const { isFetching, title, history, hashUrl } = this.props
return (
<div className={styles.header} style={{ position: this.state.position, left: this.state.left, width: window.width || '100%' }}>
<MuiThemeProvider>
<AppBar
title={<p className={styles.title}>
{isFetching ? '加载中' : title}
</p>}
iconElementLeft={<IconButton>
<i className="iconfont" dangerouslySetInnerHTML={{ __html: '' }}></i>
</IconButton>}
onLeftIconButtonClick={() => {
this.props.dispatch(setTransition({ transition: 'left' }))
history.goBack()
if (this.props.position) {
setTimeout(() => this.setState({ left: '100%' }), 450)
}
// window.location.href = `${window.location.href.split('#')[0]}#${hashUrl.oldUrl}`
}}
/>
</MuiThemeProvider>
</div>
)
}
}
export default withRouter(Header)
================================================
FILE: src/components/common/Header/styles.scss
================================================
.header{
// position: fixed;
width: 100%;
z-index: 10;
transition: all 0.5s ease-out;
}
.title{
padding-right: 30px;
text-align: center;
}
================================================
FILE: src/components/common/LinkToLogin/LinkToLogin.js
================================================
import React, { PropTypes } from 'react'
import { Link } from 'react-router-dom'
import { setTransition } from '../../../actions/hashUrl'
import prefix from '../../../utils/routePrefix'
import classnames from 'classnames'
import { setCurrentRouter } from '../../../actions'
import styles from './styles.scss'
import FlatButton from 'material-ui/FlatButton';
import RefreshIndicator from 'material-ui/RefreshIndicator';
import CircularProgress from 'material-ui/CircularProgress';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
const LinkToLogin = props => {
let { dispatch } = props
const masterInfo = window.localStorage.getItem('masterInfo') ? true : false
return (
<div className={styles.linkToLogin} >
{!masterInfo &&
<Link to={`${prefix}/login`} className={styles.link} onClick={() => {
dispatch(setTransition({ transition: 'up' }))
dispatch(setCurrentRouter('login'))
}}>
<MuiThemeProvider>
<FlatButton label="点击登陆" primary={true} />
</MuiThemeProvider>
</Link>
}
{masterInfo &&
<div style={{ paddingTop: 100 }}>
<MuiThemeProvider>
<CircularProgress size={60} thickness={7} />
</MuiThemeProvider>
</div>
}
</div>
)
}
export default LinkToLogin
================================================
FILE: src/components/common/LinkToLogin/styles.scss
================================================
.linkToLogin{
display: block;
text-align: center;
}
================================================
FILE: src/components/common/Profile/Profile.js
================================================
import React from 'react'
import {Link} from 'react-router-dom'
import prefix from '../../../utils/routePrefix'
import styles from './styles.scss'
import { setTransition } from '../../../actions/hashUrl'
import transformDate from '../../../utils/transformDate'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import Avatar from 'material-ui/Avatar';
import {List, ListItem} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
import Divider from 'material-ui/Divider';
const Profile = props => {
let {collectedTopics,profile} = props
let {avatar_url,create_at,loginname,recent_replies,recent_topics,score} = profile;
recent_replies = recent_replies ? recent_replies : [];
recent_topics = recent_topics ? recent_topics : [];
return (
<div className={styles.profile}>
<div className={styles.header}>
<img src={avatar_url} alt={loginname}/>
<p>{loginname}</p>
<p>积分:{score}</p>
<p>注册于:{transformDate(create_at)}</p>
</div>
<div className={styles.boxs}>
<div className={styles.box}>
<MuiThemeProvider>
<List>
<Subheader>收藏的话题</Subheader>
<Divider/>
<TopicList {...props} topics={collectedTopics}/>
</List>
</MuiThemeProvider>
</div>
<div className={styles.box}>
<MuiThemeProvider>
<List>
<Subheader>最近参与的话题</Subheader>
<Divider/>
<TopicList {...props} topics={recent_replies}/>
</List>
</MuiThemeProvider>
</div>
<div className={styles.box}>
<MuiThemeProvider>
<List>
<Subheader>最近创建的话题</Subheader>
<Divider/>
<TopicList {...props} topics={recent_topics}/>
</List>
</MuiThemeProvider>
</div>
</div>
</div>
)
}
const TopicList = props => {
const {dispatch,article,fetchArticle,topics} = props;
return (
<div>
{topics.length === 0 && <ListItem primaryText="还没有相关话题" />}
{topics.length > 0 &&
topics.map((topic,index) =>
<Link key={index} to={`${prefix}/topic/${topic.id}`} className={styles.link} onClick={() => {
dispatch(setTransition({ transition: 'up' }))
if(!article[topic.id]){
dispatch(fetchArticle(topic.id))
}else if(article.currentTopicId !== topic.id){
dispatch(fetchArticle(topic.id,false))
}
}}>
<ListItem primaryText={topic.title} leftAvatar={<Avatar src={topic.author.avatar_url} />}/>
<Divider/>
</Link>
)
}
</div>
)
}
const ListExampleChat = () => (
<List>
<Subheader>Recent chats</Subheader>
<ListItem
primaryText="Brendan Lim"
leftAvatar={<Avatar src="images/ok-128.jpg" />}
/>
<Divider />
</List>
);
export default Profile
================================================
FILE: src/components/common/Profile/styles.scss
================================================
.header{
text-align: center;
padding: 0 20px 20px;
img{
width: 50px;
height: 50px;
border-radius: 50%;
}
p{
margin: 10px;
}
}
.boxs{
padding: 20px;
overflow: hidden;
}
.box{
float: left;
margin: 1.6%;
width: 30%;
/*padding: 0 20px 20px;*/
box-shadow: 0 0 5px grey;
background: white;
h2{
padding: 10px;
}
}
.title{
font-weight:bold;
padding: 5px 10px;
border-bottom: 1px solid grey;
text-align: left;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
.link{
display: block;
}
@media only screen and (max-width: 768px) {
.box {
margin: 5%;
width: 90%;
}
}
================================================
FILE: src/components/common/Snackbar.js
================================================
import React from 'react';
import Snackbar from 'material-ui/Snackbar';
import RaisedButton from 'material-ui/RaisedButton';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
const SnackbarExample = (props) => {
return (
<MuiThemeProvider>
<Snackbar
open={props.isOpened}
message={props.message}
autoHideDuration={3000}
style={{textAlign:'center'}}
/>
</MuiThemeProvider>
);
}
export default SnackbarExample
================================================
FILE: src/components/common/react-pullrefresh.js
================================================
import React, { Component } from 'react'
import RefreshIndicator from 'material-ui/RefreshIndicator';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
// import './rotate.less'
// import image from './pull.svg'
const MAX_DEFAULT = 100
class Pull extends Component {
static defaultProps = {
max: MAX_DEFAULT
}
constructor(props) {
super(props)
this.state = {
pulled:0
}
}
componentDidMount() {
this.pullhelper = require('pullhelper')
let { disabled,onRefresh,max } = this.props
let maxPull = max || MAX_DEFAULT
let that= this
this.pullhelper
.on('start',function(pulled) {
that.setState({
pulling:true
})
})
.on('stepback',function(pulled,next) {
that.setState({
pulled:pulled
})
let nextPulled = Math.min(pulled - Math.min(pulled/2,10),max)
next(nextPulled)
})
.on('step',function(pulled) {
that.setState({
pulled:pulled
})
})
.on('pull',function(pulled,next) {
that.setState({
pulling:false
})
if(!onRefresh || pulled < maxPull) {
next()
return
}
that.setState({
loading:true
})
onRefresh(_ => {
that.setState({
loading:false
})
next()
})
})
.load()
if(disabled) {
this.pullhelper.pause()
}
}
componentWillReceiveProps(nextProps) {
let { disabled } = this.props
if(disabled !== nextProps.disabled) {
if(nextProps.disabled) {
this.pullhelper.pause()
} else {
this.pullhelper.resume()
}
}
}
componentWillUnmount() {
this.pullhelper.unload()
}
render() {
let { pulling,loading,pulled } = this.state
let maxPull = this.props.max || MAX_DEFAULT
let size = this.props.size || 30
let color = this.props.color || '#00BCD4'
let style = this.props.style || {}
return (
<div>
<div style={{
display:pulling ? 'block' : 'none',
position:'fixed',
top:0,
left:0,
right:0,
bottom:0,
zIndex:this.props.zIndex,
userSelect:'none'
}} />
<div style={Object.assign({
background:'white',
width: size,
height: size,
position:'fixed',
zIndex:this.props.zIndex,
top:-size+Math.min(pulled,maxPull),
left:'50%',
borderRadius:size/2,
transform:'translate(-50%,-50%)',
boxShadow:'0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2)',
userSelect:'none'
},style)}>
<MuiThemeProvider>
<RefreshIndicator
percentage={80}
size={size}
left={0}
top={0}
color={color}
loadingColor={color}
status= {pulled/maxPull > 0.9999 ? 'loading':'ready'}
style={{display:'inline-block',
position:'relative',
opacity:pulled/maxPull}}
/>
</MuiThemeProvider>
</div>
</div>
)
}
}
export default Pull;
// <i style={{
// display:'block',
// width:'100%',
// height:'100%',
// lineHeight:`${size}px`,
// fontSize:`${size*0.6}px`,
// color: color,
// opacity:pulled/maxPull,
// textAlign:'center',
// transform:`rotate(${pulled/maxPull*360}deg)`,
// animation:'rotating 2s linear infinite'
// }} className="iconfont" dangerouslySetInnerHTML={{__html: ''}}>
// </i>
================================================
FILE: src/configureStore.js
================================================
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import rootReducer from './reducers/index'
const history = createHistory();
const middleware = routerMiddleware(history)
const logger = createLogger({ collapsed: true })
const middlewares = [thunk, middleware]
if(process.env.NODE_ENV === 'development') {
middlewares.push(logger)
}
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middlewares)))
export default store
================================================
FILE: src/containers/App.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {fetchAccess,fetchMessage,fetchProfile} from '../actions'
import Header from '../components/common/Header/Header'
import Content from '../components/Article/Content/Content'
import Reply from '../components/Article/Reply/Reply'
import getSize from '../utils/getSize'
class App extends Component {
componentWillMount(){
// console.log('componentWillMount')
const {dispatch} = this.props;
const LoadingAction = (accessToken,loginName) => {
dispatch(fetchAccess(accessToken))
dispatch(fetchMessage(accessToken))
dispatch(fetchProfile(loginName))
}
if(window.localStorage.getItem('masterInfo')){
let masterInfo = window.localStorage.getItem('masterInfo')
masterInfo = JSON.parse(masterInfo)
const accessToken = masterInfo.accessToken
const loginName = masterInfo.loginName
LoadingAction(accessToken,loginName)
}else{
// const accessToken = '1cbc2a58-6c1b-426f-971d-070676fb849d'
// const loginName = 'lumia2046'
// LoadingAction(accessToken,loginName)
}
}
render() {
return (
<div>{this.props.children}</div>
)
}
}
App.propTypes = {
// currentTopicId: PropTypes.string.isRequired,
// article: PropTypes.object.isRequired,
// isFetching: PropTypes.bool.isRequired,
// dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state) {
const {login,profile} = state
return {login,profile}
}
export default connect(mapStateToProps)(App)
================================================
FILE: src/containers/Article.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { switchSupport, fetchComment, fetchArticle, recordArticleScrollT, fetchProfile } from '../actions'
import Header from '../components/common/Header/Header'
import CircleLoading from '../components/common/CircleLoading'
import AsyncContainer from '../components/common/AsyncContainer'
import Content from '../components/Article/Content/Content'
import Reply from '../components/Article/Reply/Reply'
import getSize from '../utils/getSize'
class Article extends Component {
constructor() {
super()
this.state = {
fadeIn: true
}
}
componentWillMount() {
const { scrollT, dispatch, article, isFetching } = this.props
if (scrollT) {
// window.scrollTo(0, scrollT)
}
// window.scrollTo(0, scrollT)
if (!article.author && !isFetching) {
const topicId = window.location.href.split('topic/')[1].split('?_')[0]
dispatch(fetchArticle(topicId))
}
}
componentWillReceiveProps(newProps) {
const { scrollT } = newProps
document.getElementById('articleContainer').scrollTop = scrollT
}
componentDidMount() {
const { scrollT } = this.props
document.getElementById('articleContainer').scrollTop = scrollT
}
componentWillUnmount() {
const { currentTopicId, dispatch, profile, login } = this.props
dispatch(recordArticleScrollT(currentTopicId, document.getElementById('articleContainer').scrollTop))
if (!window.sessionStorage.masterProfile && login.loginName === profile.loginname) {
window.sessionStorage.masterProfile = JSON.stringify(profile)
}
}
render() {
let { isFetching, article, currentTopicId, login, switchSupportInfo, isCommented, dispatch, collectedTopics, profile } = this.props
if (login.loginName !== profile.loginname && window.sessionStorage.masterProfile) {
collectedTopics = JSON.parse(window.sessionStorage.masterProfile).collectedTopics
}
return (
<div id='articleContainer' style={{ width: window.width, height: window.height, overflow: 'auto' }}>
<Header isFetching={isFetching} title='详情' showBack={true} position={true} />
{Object.keys(article).length === 0 && <CircleLoading />}
{Object.keys(article).length !== 0 &&
<div>
<Content {...({ article, dispatch, fetchProfile, login, collectedTopics, profile })} />
<Reply replies={article.replies}
{...({ login, dispatch, switchSupportInfo, currentTopicId, profile, isCommented })} />
</div>
}
</div>
)
}
}
function mapStateToProps(state) {
const { currentRouter, login, profile } = state;
const { currentTopicId, switchSupportInfo, isCommented } = state.article;
const { collectedTopics } = profile
const isFetching = state.article[currentTopicId] ? state.article[currentTopicId].isFetching : false;
const scrollT = state.article[currentTopicId] ? state.article[currentTopicId].scrollT : '0';
const article = state.article[currentTopicId] && state.article[currentTopicId].article ? state.article[currentTopicId].article : {};
return { isFetching, scrollT, article, currentTopicId, login, switchSupportInfo, currentRouter, collectedTopics, profile, isCommented }
}
export default connect(mapStateToProps)(Article)
================================================
FILE: src/containers/HomePage.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { selectTab, fetchTopics, recordScrollT, fetchArticle, fetchAccess, fetchMessage } from '../actions'
import Header from '../components/HomePage/Header/Header'
import FloatingActionButton from '../components/HomePage/FloatingActionButton'
import CircleLoading from '../components/common/CircleLoading'
import Drawer from '../components/HomePage/Drawer/Drawer'
import Lists from '../components/HomePage/Lists/Lists'
import Snackbar from '../components/common/Snackbar'
import getSize from '../utils/getSize'
import { setTransition } from '../actions/hashUrl'
import Pull from '../components/common/react-pullrefresh'
// @withRouter
@connect(state => {
const { homePage, article, login, profile, message, hashUrl } = state
const { selectedTab, tabData } = homePage;
const unreadMessageCount = message.hasNotReadMessage.length
// 当组件第一次render时,还未进行任何action,初始化的state里没有tabData[selectedTab],所以这里需要初始化
const { isFetching, page, scrollT, topics } = tabData[selectedTab] || { isFetching: false, page: 0, scrollT: 0, topics: [] }
return { isFetching, page, scrollT, topics, selectedTab, article, login, profile, tabData, unreadMessageCount, hashUrl }
})
class HomePage extends Component {
constructor() {
super()
this.state = {
fadeIn: true,
openDrawer: false,
openSnackbar: false,
isFreshing: false,
fixedTop: 0,
scrollT: 0
}
}
onRefresh = (next) => {
if (!this.state.isFreshing) {
this.setState({
isFreshing: true
})
this.fresh()
setTimeout(_ => {
next()
this.openSnackbar()
this.setState({
isFreshing: false
})
}, 3000)
}
}
fresh = () => {
const { selectedTab, login, dispatch } = this.props;
dispatch(fetchTopics(selectedTab, 1))
dispatch(fetchMessage(login.accessToken))
}
openSnackbar = () => {
this.setState({
openSnackbar: true
})
setTimeout(() => {
this.setState({
openSnackbar: false
})
}, 2500)
}
handleClick = (tab) => {
let homeContainerDom = document.getElementById('homeContainer')
let scrollT = homeContainerDom.scrollTop
const { selectedTab, dispatch, tabData } = this.props
dispatch(recordScrollT(selectedTab, scrollT))
dispatch(selectTab(tab))
homeContainerDom.scrollTop = tabData[tab].scrollT || 0
const ua = navigator.userAgent
if (!tabData[tab] && ua.indexOf('Mobile') === -1) {
if (scrollT >= 64) {
dispatch(recordScrollT(tab, 64))
this.setState({
scrollT: 64
})
} else {
dispatch(recordScrollT(tab, scrollT))
this.setState({
scrollT: scrollT
})
}
}
// 在事件的回调函数中的action,是将action都发送并改变state完毕,再更新state
// 也就是说事件回调函数中发送多个不带异步的action,只在最后一个action更新完state进行一次状态更新
// 要想每次action更新state后,都更新一次状态(方便在更新时做一些操作),就要把action封装成异步操作
// 但是在react生命周期里的一个函数里发送多个action,每个action更新state都会对相应组件进行一次状态更新,不需要封装成异步操作
// let asyncDispatch = async function(){
// await dispatch(recordScrollT(selectedTab,scrollT))
// await dispatch(selectTab(tab))
// }
// asyncDispatch()
}
loadMore = () => {
const { selectedTab, page, isFetching, dispatch } = this.props;
let ipage = page
if (!isFetching) {
dispatch(fetchTopics(selectedTab, ++ipage))
}
}
toggleDrawer = () => {
this.setState({
openDrawer: !this.state.openDrawer
})
}
componentWillMount() {
const { scrollT } = this.props
}
componentWillUpdate(newProps, newState) {
const { topics, isFetching, selectedTab, scrollT, dispatch } = newProps
// 去除刷新时记住的滚动条位置
if (topics.length === 0 && this.props.scrollT === 0) {
window.scrollTo(0, 0)
}
// fetchTopics开始后会先发送一个request的action,这个ation也会改变state从而触发该方法。如果不对isFetching进行判断,会再次进行fetchTopics从而进行了不必要的重复数据请求
if (!isFetching && topics.length === 0) {
dispatch(fetchTopics(selectedTab));
}
if (selectedTab !== this.props.selectedTab) {
if (scrollT) {
// console.log(scrollT, " window.scrollTo",'componentWillUpdate')
window.scrollTo(0, scrollT)
}
}
}
tabs = [
{
title: '全部',
filter: 'all',
},
{
title: '精华',
filter: 'good',
},
{
title: '分享',
filter: 'share',
},
{
title: '问答',
filter: 'ask',
},
{
title: '招聘',
filter: 'job',
}
]
render() {
const { selectedTab, isFetching, page, topics, dispatch, article, currentRouter, login, profile, unreadMessageCount, tabData, history } = this.props
return (
<div id='homeContainer' style={{ width: window.width, height: window.height, overflow: 'auto' }}>
<Header filter={selectedTab} onClick={this.handleClick} toggleDrawer={this.toggleDrawer}
fixedTop={this.state.fixedTop} unreadMessageCount={unreadMessageCount} tabs={this.tabs}>
{this.tabs.map((tab, index) =>
<div key={index}>
{((isFetching && page === 0) || (tab.filter !== selectedTab && !tabData[tab.filter])) && <CircleLoading />}
{tab.filter === selectedTab && <div style={{ opacity: (!isFetching || page >= 1) ? 1 : 0 }}>
<Lists topics={topics} fetchArticle={fetchArticle} dispatch={dispatch} article={article} isFetching={isFetching} selectedTab={selectedTab} history={history} />
</div>}
</div>
)}
</Header>
{!isFetching && <FloatingActionButton />}
<Drawer toggleDrawer={this.toggleDrawer} openDrawer={this.state.openDrawer}
{...({ login, dispatch, profile })} />
<Snackbar isOpened={this.state.openSnackbar} message='刷新成功' />
</div>
)
}
componentDidMount() {
const { selectedTab, page, scrollT, dispatch } = this.props
if (page === 0) {
dispatch(fetchTopics(selectedTab))
}
let homeContainerDom = document.getElementById('homeContainer')
if (scrollT) {
homeContainerDom.scrollTop = scrollT
}
homeContainerDom.onscroll = () => {
let scrollT = homeContainerDom.scrollTop
let contentH = homeContainerDom.scrollHeight
let { windowH } = getSize()
if (windowH + scrollT + 10 > contentH) {
this.loadMore()
}
// 由于下面的操作比较费cpu,所以进行判断是否为手机端
let lastScrollY = this.lastScrollY
const ua = navigator.userAgent;
if (ua.indexOf('Mobile') === -1) {
if (!lastScrollY || !scrollT) {
lastScrollY = scrollT
}
let diff = scrollT - lastScrollY
if (diff >= 0) {
if (scrollT > 64 && this.state.fixedTop !== 64) {
this.setState({
fixedTop: 64
})
}
if (scrollT <= 64) {
this.setState({
fixedTop: scrollT
})
}
} else {
this.setState({
scrollT: 0
})
if (scrollT > 64 && this.state.fixedTop !== 0) {
this.setState({
fixedTop: 0
})
}
}
lastScrollY = scrollT
}
}
}
componentDidUpdate() {
let { windowH, contentH, scrollT } = getSize();
const { topics } = this.props
// 第一次切换到没有加载数据的tab时,在willReceiveProp中已经将页面滚动了一定距离,在render中打印scrollT也不为0
// 但是一进入这个函数scrollT就变为0,而且也未触发onscroll事件(问题待解决),所以目前只能去重新判断这种情况
if (scrollT === 0 && this.state.scrollT > 0) {
// window.scrollTo(0, this.state.scrollT)
}
// 判断内容加载后,内容的高度是否填满屏幕,若网页太高,加载一次内容的高度不能填满整个网页,则继续请求数据
if (topics.length > 0 && windowH + 200 > contentH) {
// this.loadMore()
}
}
componentWillUnmount() {
const { selectedTab, dispatch } = this.props
dispatch(recordScrollT(selectedTab, document.getElementById('homeContainer').scrollTop))
// 必须解绑事件,否则当组件再次被加载时,该事件会监听两个组件
window.onscroll = null
}
}
// 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的
// connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch
export default withRouter(HomePage)
================================================
FILE: src/containers/Login.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {fetchAccess,fetchMessage,logout,fetchArticle,fetchProfile} from '../actions'
import Header from '../components/common/Header/Header'
import Profile from '../components/common/Profile/Profile'
import getSize from '../utils/getSize'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import CircularProgress from 'material-ui/CircularProgress';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';
import Toggle from 'material-ui/Toggle';
class Login extends Component {
constructor(){
super();
this.state = {
toggleOn:true
}
}
componentWillReceiveProps(newProps){
let {succeed,loginName,accessToken,dispatch,profile} = newProps;
if(succeed && !profile.isFetching && profile.loginname !== loginName){
if(this.state.toggleOn && !window.localStorage.getItem('masterInfo')){
accessToken = accessToken.trim()
loginName = loginName.trim()
let masterInfo = {accessToken,loginName}
masterInfo = JSON.stringify(masterInfo)
window.localStorage.setItem('masterInfo',masterInfo)
}
dispatch(fetchProfile(loginName))
dispatch(fetchMessage(accessToken))
}
}
onToggle = () => {
this.setState({
toggleOn:!this.state.toggleOn
})
}
render(){
let {dispatch,article,profile,failedMessage,succeed,loginName,loginId,accessToken,collectedTopics} = this.props;
if(loginName !== profile.loginname && window.sessionStorage.masterProfile){
profile = JSON.parse(window.sessionStorage.masterProfile)
collectedTopics = profile.collectedTopics
}
const masterInfo = window.localStorage.getItem('masterInfo') ? true : false
return (
<div>
<Header isFetching={profile.loginname?false:true} title='个人中心' showBack={false}/>
<div style={{textAlign:'center',paddingTop:100}}>
{!masterInfo && !succeed &&
<MuiThemeProvider>
<div>
<div>
<TextField hintText="请输入Access Token" floatingLabelText="请输入Access Token" ref='input'/>
</div>
<div style={{display:'inline-block',margin:'0 auto'}}>
<Toggle label="记住登陆信息" defaultToggled={true} onToggle={this.onToggle} style={{maxWidth: 200}}/>
</div>
<div>
<RaisedButton label="登陆" primary={true} onClick={() => {
const input = this.refs.input.input.value
if(!input.trim()){
return null;
}
dispatch(fetchAccess(input))
}}/>
</div>
</div>
</MuiThemeProvider>
}
{!succeed && failedMessage &&
<h2 style={{color:'red'}}>{failedMessage}</h2>
}
{succeed && !profile.loginname &&
<MuiThemeProvider>
<CircularProgress size={60} thickness={7} />
</MuiThemeProvider>
}
{succeed && profile.loginname &&
<div>
<Profile {...({collectedTopics,profile,dispatch,fetchArticle,article})}/>
</div>
}
</div>
</div>
)
}
}
function mapStateToProps(state) {
const {article,profile,login,currentRouter} = state;
const {failedMessage,succeed,loginName,loginId,accessToken} = login;
const {collectedTopics} = profile
return {article,profile,succeed,loginName,loginId,accessToken,failedMessage,collectedTopics}
}
// 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的
// connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch
export default connect(mapStateToProps)(Login)
================================================
FILE: src/containers/Message.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {fetchMessage,fetchArticle} from '../actions'
import Content from '../components/Message/Content/Content'
import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin'
import Header from '../components/common/Header/Header'
import getSize from '../utils/getSize'
class Message extends Component {
componentDidMount(){
const {login,dispatch,message} = this.props
if(login.accessToken && message.hasReadMessage.length === 0){
dispatch(fetchMessage(login.accessToken))
}
}
render(){
const {dispatch,currentRouter,message,article,login} = this.props;
return (
<div>
<div>
<Header title='详情'/>
<div style={{paddingTop:64}}>
{login.succeed && <Content {...message} {...({dispatch,fetchArticle,article,login})}/>}
{!login.succeed && <LinkToLogin dispatch={dispatch}/>}
</div>
</div>
</div>
)
}
}
function mapStateToProps(state) {
const {login,message,article} = state;
return {login,message,article}
}
export default connect(mapStateToProps)(Message)
================================================
FILE: src/containers/Profile.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {fetchArticle} from '../actions'
import Header from '../components/common/Header/Header'
import ProfileComponent from '../components/common/Profile/Profile'
import getSize from '../utils/getSize'
class Profile extends Component {
render(){
const {profile} = this.props
return (
<div>
<div>
<Header title='详情' showBack={true}/>
{profile.loginname &&
<div style={{paddingTop:100}}>
<ProfileComponent {...({...this.props,fetchArticle})}/>
</div>
}
</div>
</div>
)
}
}
// HomePage.propTypes = {
// selectedTab: PropTypes.string.isRequired,
// topics: PropTypes.array.isRequired,
// isFetching: PropTypes.bool.isRequired,
// page:PropTypes.number.isRequired,
// scrollT:PropTypes.number.isRequired,
// dispatch: PropTypes.func.isRequired
// }
function mapStateToProps(state) {
const {profile,article} = state;
const {collectedTopics} = profile;
return {profile,article,collectedTopics}
}
export default connect(mapStateToProps)(Profile)
================================================
FILE: src/containers/PublishTopic.js
================================================
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {Link} from 'react-router-dom'
import prefix from '../utils/routePrefix'
import {fetchArticle,fetchPublishTopic} from '../actions'
import Header from '../components/common/Header/Header'
import Dialog from '../components/common/Dialog'
import LinkToLogin from '../components/common/LinkToLogin/LinkToLogin'
import Form from '../components/PublishTopic/Form/Form'
class PublishTopic extends Component {
constructor(){
super();
this.state = {
isOpen:false,
isFetching: false,
titleErr:false,
contentErr:false
}
}
componentWillReceiveProps(newProps){
const {publishTopic,dispatch} = newProps;
if(!this.props.publishTopic.topicId || this.props.publishTopic.topicId !== publishTopic.topicId){
this.setState({isFetching:false})
dispatch(fetchArticle(publishTopic.topicId))
}
}
showDialog() {
this.setState({
isOpen:true,
isFetching:true
})
}
close = () => {
this.setState({
isOpen:false
})
}
ifTitleErr(boolean) {
this.setState({
titleErr: boolean
})
}
ifContentErr(boolean) {
this.setState({
contentErr: boolean
})
}
render(){
const {dispatch,publishTopic,currentRouter,login} = this.props;
const ifTitleErr = this.ifTitleErr.bind(this)
const ifContentErr = this.ifContentErr.bind(this)
const showDialog = this.showDialog.bind(this)
const state = this.state
return (
<div>
<Header title='发布新话题'/>
<div style={{paddingTop:100}}>
{login.succeed && <Form {...({ifTitleErr,ifContentErr,showDialog,fetchPublishTopic,dispatch,login,state})}/>}
{!login.succeed && <LinkToLogin dispatch={dispatch}/>}
</div>
<Dialog isOpen={this.state.isOpen} link={`${prefix}/topic/${publishTopic.topicId}`} close={this.close}>
{state.isFetching && <div>加载中</div>}
{!state.isFetching && <div>发送成功,去查看</div>}
</Dialog>
</div>
)
}
}
// HomePage.propTypes = {
// selectedTab: PropTypes.string.isRequired,
// topics: PropTypes.array.isRequired,
// isFetching: PropTypes.bool.isRequired,
// page:PropTypes.number.isRequired,
// scrollT:PropTypes.number.isRequired,
// dispatch: PropTypes.func.isRequired
// }
function mapStateToProps(state) {
const {publishTopic,login} = state;
// const {selectedTab,tabData} = state.homePage;
// // 当组件第一次render时,还未进行任何action,初始化的state里没有tabData[selectedTab],所以这里需要初始化
// const {isFetching,page,scrollT,topics} = tabData[selectedTab] || {isFetching:false,page:0,scrollT:0,topics:[]}
return {publishTopic,login}
}
// 用connect方法连接HomePage组件,实际上是在HomePage组件上加上了Connect(HomePage)这个父组件,HomePage里有关Connect的state变化的props就是通过mapStateToProps设置的
// connect方法的第二个参数如果不传的话就会默认将store.dispatch默认作为了dispatch参数传进HomePage的props,所以HomePage的props里就有一个dispatch
export default connect(mapStateToProps)(PublishTopic)
================================================
FILE: src/index.js
================================================
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom'
import store from './configureStore'
import { Provider } from 'react-redux'
// import { Router,hashHistory } from 'react-router'
import Routes from './Routes'
import './styles/index.css'
import { AppContainer } from 'react-hot-loader'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<Component />
</Provider>
</AppContainer>,
document.getElementById('root')
)
}
render(Routes)
if(module.hot) {
module.hot.accept('./Routes', () => { render(Routes) });
}
// ReactDOM.render(
// <AppContainer>
// <Provider store={store}>
// <div>aaaaaaaa</div>
// </Provider>
// </AppContainer>,
// document.getElementById('root')
// )
================================================
FILE: src/reducers/article.js
================================================
import {
REQUEST_ARTICLE, RECEIVE_ARTICLE, CHANGE_CURRENT_TOPICID, SWITCH_SUPPORT, FETCH_COMMENT, RECORD_ARTICLE_SCROLLT
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).article : {
currentTopicId: ''
}
const article = (state = initState, action) => {
let stateItem = state[action.topicId] || {}
switch (action.type) {
case CHANGE_CURRENT_TOPICID:
return { ...state, currentTopicId: action.topicId }
case SWITCH_SUPPORT:
return { ...state, switchSupportInfo: { replyId: action.replyId, index: action.index, success: action.success, action: action.action } }
case FETCH_COMMENT:
return { ...state, isCommented: action.success }
case RECORD_ARTICLE_SCROLLT:
stateItem = { ...stateItem, scrollT: action.scrollT }
return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId }
case REQUEST_ARTICLE:
stateItem = { ...stateItem, isFetching: true }
return { ...state, [action.topicId]: stateItem, currentTopicId: action.topicId, isCommented: false }
case RECEIVE_ARTICLE:
stateItem = { ...stateItem, isFetching: false, article: action.article }
return { ...state, [action.topicId]: stateItem }
default:
return state
}
}
export default article
================================================
FILE: src/reducers/collectedTopics.js
================================================
import {
GET_COLLECTED_TOPICS
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).collectedTopics : {
success: false
}
const collectedTopics = (state = initState, action) => {
switch (action.type) {
case GET_COLLECTED_TOPICS:
return { ...state, success: action.success, data: action.data, userName: action.userName }
default:
return state
}
}
export default collectedTopics
================================================
FILE: src/reducers/fetchError.js
================================================
import {
FETCH_START, FETCH_END, FETCH_ERROR, CLEAR_ERROR
} from '../actions/fetchError'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).fetchError : {
error: null, fetched: null
}
const fetchError = (state = initState, action) => {
switch (action.type) {
case FETCH_START:
return { ...state, fetched: 'start' }
case FETCH_END:
return { ...state, fetched: 'end' }
case FETCH_ERROR:
return { ...state, error: action.data, fetched: 'failed' }
case CLEAR_ERROR:
return { ...state, error: null }
default:
return state
}
}
export default fetchError
================================================
FILE: src/reducers/hashUrl.js
================================================
import {
SET_HASH_URL, SET_TRANSITION
} from '../actions/hashUrl'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).hashUrl : {
oldUrl: '/', currentUrl: '/', transition: 'none'
}
const hashUrl = (state = initState, action) => {
switch (action.type) {
case SET_HASH_URL:
case SET_TRANSITION:
return { ...state, ...action.data }
default:
return state
}
}
export default hashUrl;
================================================
FILE: src/reducers/homePage.js
================================================
import {
SELECT_TAB, RECORD_SCROLLT,
REQUEST_TOPICS, RECEIVE_TOPICS
} from '../actions'
const selectedTab = (state, action) => {
switch (action.type) {
case SELECT_TAB:
return action.tab
default:
return state
}
}
// 当组件第一次发出REQUEST_TOPICS后,需要对其返回的state进行初始化,否则没有topics等属性会报错
function tabDataItem(state = { isFetching: false, page: 0, scrollT: 0, topics: [] }, action) {
switch (action.type) {
case REQUEST_TOPICS:
return {
...state,
isFetching: true
}
case RECEIVE_TOPICS:
if (state.page < action.page) {
let topics = state.topics
action.topics = topics.concat(action.topics)
}
return {
...state,
isFetching: false,
page: action.page,
topics: action.topics,
limit: action.limit
}
case RECORD_SCROLLT:
return {
...state,
scrollT: action.scrollT
}
default:
return state
}
}
const tabData = (state = {}, action) => {
switch (action.type) {
case RECEIVE_TOPICS:
case REQUEST_TOPICS:
case RECORD_SCROLLT:
return {
...state,
[action.tab]: tabDataItem(state[action.tab], action)
}
default:
return state
}
}
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).homePage : {
selectedTab: 'all', tabData: {}
}
const homePage = (state = initState, action) => {
if (state) {
const sTab = selectedTab(state.selectedTab, action);
const tData = tabData(state.tabData, action);
// 返回的一定要是一个新的对象,如果只是改变原来的state,返回的还是原来的state对象,就无法被store.subscribe监听到,从而不会对组件状态进行更新
return { ...state, selectedTab: sTab, tabData: tData }
}
return state
}
export default homePage;
================================================
FILE: src/reducers/index.js
================================================
import { combineReducers } from 'redux'
import homePage from './homePage'
import article from './article'
import login from './login'
import profile from './profile'
import message from './message'
import publishTopic from './publishTopic'
import hashUrl from './hashUrl'
import fetchError from './fetchError'
const rootReducer = combineReducers({
homePage,
article,
login,
profile,
publishTopic,
message,
hashUrl,
fetchError
})
export default rootReducer
================================================
FILE: src/reducers/login.js
================================================
import {
LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).login : {
succeed: false
}
const login = (state = initState, action) => {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, succeed: true, loginName: action.loginName, loginId: action.loginId, accessToken: action.accessToken }
case LOGIN_FAILED:
return { ...state, succeed: false, failedMessage: action.failedMessage }
case LOGOUT:
return { succeed: false }
default:
return state
}
}
export default login
================================================
FILE: src/reducers/message.js
================================================
import {
FETCH_MESSAGE, MARK_ALL_MESSAGES
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).message : {
isMarked: false, hasReadMessage: [], hasNotReadMessage: []
}
const message = (state = initState, action) => {
switch (action.type) {
case FETCH_MESSAGE:
return { ...state, hasReadMessage: action.hasReadMessage, hasNotReadMessage: action.hasNotReadMessage }
case MARK_ALL_MESSAGES:
return { ...state, isMarked: action.isMarked }
default:
return state
}
}
export default message
================================================
FILE: src/reducers/profile.js
================================================
import {
REQUEST_PROFILE, RECEIVE_PROFILE, GET_COLLECTED_TOPICS
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).profile : {
isFetching: false, collectedTopics: []
}
const profile = (state = initState, action) => {
switch (action.type) {
case REQUEST_PROFILE:
return { ...state, isFetching: true }
case RECEIVE_PROFILE:
return { ...state, ...action.profile, isFetching: false }
case GET_COLLECTED_TOPICS:
return { ...state, collectedTopics: action.data }
default:
return state
}
}
export default profile
================================================
FILE: src/reducers/publishTopic.js
================================================
import {
PUBLISH_TOPIC
} from '../actions'
const initState = sessionStorage.getItem('store') ? JSON.parse(sessionStorage.getItem('store')).publishTopic : {
success: false
}
const publishTopic = (state = initState, action) => {
switch (action.type) {
case PUBLISH_TOPIC:
return { ...state, success: action.success, topicId: action.topicId }
default:
return state
}
}
export default publishTopic
================================================
FILE: src/routes.js
================================================
import React, { Component } from 'react'
import { Route, Router, Redirect, Switch, withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import createHistory from 'history/createHashHistory'
import lazyLoadComponent from 'lazy-load-component'
import App from './containers/App'
import HomePage from './containers/HomePage'
import Article from './containers/Article'
import Message from './containers/Message'
import Login from './containers/Login'
import Profile from './containers/Profile'
import PublishTopic from './containers/PublishTopic'
import prefix from './utils/routePrefix'
import getSize from './utils/getSize'
import { setHashUrl, setTransition } from './actions/hashUrl'
// import { clearUserInfo } from './actions/login'
import { clearError } from './actions/fetchError'
import { CSSTransition, TransitionGroup, Transition } from 'react-transition-group'
const history = createHistory()
// const Article = lazyLoadComponent(() => import(/*webpackChunkName:"Article" */'./containers/Article'))
// const Message = lazyLoadComponent(() => import(/*webpackChunkName:"Message" */'./containers/Message'))
// const Login = lazyLoadComponent(() => import(/*webpackChunkName:"Login" */'./containers/Login'))
// const Profile = lazyLoadComponent(() => import(/*webpackChunkName:"Profile" */'./containers/Profile'))
// const PublishTopic = lazyLoadComponent(() => import(/*webpackChunkName:"PublishTopic" */'./containers/PublishTopic'))
@connect(store => ({ store }))
class Routes extends Component {
constructor() {
super()
this.state = {}
}
hashChange = (ev) => {
if (this.props.store.hashUrl.oldURL != '/') {
this.setState({ overflow: 'hidden' })
setTimeout(() => this.setState({ overflow: 'visible' }), 500)
}
const dispatch = this.props.dispatch
let hashUrl = null;
if (ev.oldURL) {
hashUrl = { oldUrl: ev.oldURL.split('#')[1], currentUrl: ev.newURL.split('#')[1] }
} else {
this.oldUrl = this.currentUrl
this.currentUrl = window.location.href.split('#')[1]
hashUrl = { oldUrl: this.oldUrl, currentUrl: this.currentUrl }
}
dispatch(setHashUrl(hashUrl))
// if (this.props.hashUrl.transition != 'none') {
// clearTimeout(this.transitionTimeOut)
// this.transitionTimeOut = setTimeout(() => {
// dispatch(setTransition({ transition: 'none' }))
// }, 50)
// }
}
oldUrl = '/'
currentUrl = '/'
// changeTransition = (transition) => {
// this.setState({ transition: transition })
// }
componentWillMount() {
let dispatch = this.props.dispatch
window.myDispatch = dispatch
window.width = getSize().windowW
window.height = getSize().windowH
let menu = window.location.href.split('#')[1].split('/')
if (menu[1]) {
this.currentUrl = window.location.href.split('#')[1]
dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: window.location.href.split('#')[1] }))
} else {
dispatch(setHashUrl({ oldUrl: this.oldUrl, currentUrl: this.currentUrl }))
}
window.addEventListener('hashchange', this.hashChange)
// 由于头部组件fix定位,在路由切换时,width:100%在手机上的判定会有问题,暂时采取全局变量储存页面加载时的宽度
// window.width = document.getElementById('root').offsetWidth
// console.log('****************',document.getElementById('root').offsetWidth)
}
saveState = () => {
let store = this.props.store
sessionStorage.setItem('store', JSON.stringify(store))
}
componentWillUpdate(nextProps) {
// if (this.props.store.hashUrl.oldUrl == nextProps.store.hashUrl.currentUrl) {
// this.props.dispatch(setTransition({ transition: 'left' }))
// }
}
render() {
this.transition = this.props.store.hashUrl.transition
// let transition = this.props.store.hashUrl.transition
return (
<Router path={`${prefix}/`} history={history}>
<Route render={({ location }) => (
<div style={{ position: 'relative', width: window.width, height: window.height, overflow: 'hidden' }}>
<TransitionGroup>
<Transition timeout={500} key={location.pathname}
onEnter={() => this.enterCN = `${this.transition}-enter`}
onEntering={() => this.enterCN = `${this.transition}-enter ${this.transition}-enter-active`}
onEntered={() => this.enterCN = ``}
onExit={() => this.exitCN = `${this.transition}-exit`}
onExiting={() => this.exitCN = `${this.transition}-exit ${this.transition}-exit-active`}
onExited={() => this.exitCN = ``}
>
{(status) => (
<div className={status.includes('enter') ? this.enterCN : this.exitCN} style={{ width: window.width, height: window.height, overflow: 'auto' }}>
<Switch location={location}>
<Route exact path='/' render={() => <Redirect to='/home' />} />
<Route path='/home' render={() => <HomePage />} />
<Route path='/topic/:id' render={() => <Article />} />
<Route path='/message' render={() => <Message />} />
<Route path='/login' render={() => <Login />} />
<Route path='/profile' render={() => <Profile />} />
<Route path='/publishTopic' render={() => <PublishTopic />} />
</Switch>
</div>
)}
</Transition>
</TransitionGroup>
</div>
)} />
</Router>
)
}
componentDidMount() {
window.width = document.getElementById('root').offsetWidth
window.addEventListener('beforeunload', this.saveState)
}
componentWillUnmount() {
window.removeEventListener('beforeunload', this.saveState)
window.removeEventListener('hashchange', () => { })
}
}
export default Routes
================================================
FILE: src/styles/iconfont/demo.css
================================================
*{margin: 0;padding: 0;list-style: none;}
/*
KISSY CSS Reset
理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。
2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。
3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。
特色:1. 适应中文;2. 基于最新主流浏览器。
维护:玉伯<lifesinger@gmail.com>, 正淳<ragecarrier@gmail.com>
*/
/** 清除内外边距 **/
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */
dl, dt, dd, ul, ol, li, /* list elements 列表元素 */
pre, /* text formatting elements 文本格式元素 */
form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */
th, td /* table elements 表格元素 */ {
margin: 0;
padding: 0;
}
/** 设置默认字体 **/
body,
button, input, select, textarea /* for ie */ {
font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif;
}
h1, h2, h3, h4, h5, h6 { font-size: 100%; }
address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */
code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */
small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
/** 重置列表元素 **/
ul, ol { list-style: none; }
/** 重置文本格式元素 **/
a { text-decoration: none; }
a:hover { text-decoration: underline; }
/** 重置表单元素 **/
legend { color: #000; } /* for ie6 */
fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */
button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */
/* 注:optgroup 无法扶正 */
/** 重置表格元素 **/
table { border-collapse: collapse; border-spacing: 0; }
/* 清除浮动 */
.ks-clear:after, .clear:after {
content: '\20';
display: block;
height: 0;
clear: both;
}
.ks-clear, .clear {
*zoom: 1;
}
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;}
.helps{margin-top:40px;}
.helps pre{
padding:20px;
margin:10px 0;
border:solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists{
width: 100% !important;
}
.icon_lists li{
float:left;
width: 100px;
height:180px;
text-align: center;
list-style: none !important;
}
.icon_lists .icon{
font-size: 42px;
line-height: 100px;
margin: 10px 0;
color:#333;
-webkit-transition: font-size 0.25s ease-out 0s;
-moz-transition: font-size 0.25s ease-out 0s;
transition: font-size 0.25s ease-out 0s;
}
.icon_lists .icon:hover{
font-size: 100px;
}
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p,
.markdown pre {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown pre {
border-radius: 6px;
background: #f7f7f7;
padding: 20px;
}
.markdown pre code {
border: none;
background: #f7f7f7;
margin: 0;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown > table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown > table th,
.markdown > table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown > table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
font-style: italic;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
pre{
background: #fff;
}
================================================
FILE: src/styles/iconfont/demo_fontclass.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>IconFont</title>
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
</head>
<body>
<div class="main markdown">
<h1>IconFont 图标</h1>
<ul class="icon_lists clear">
<li>
<i class="icon iconfont icon-xiai"></i>
<div class="name">喜爱</div>
<div class="fontclass">.icon-xiai</div>
</li>
<li>
<i class="icon iconfont icon-ding"></i>
<div class="name">顶</div>
<div class="fontclass">.icon-ding</div>
</li>
<li>
<i class="icon iconfont icon-user"></i>
<div class="name">用户</div>
<div class="fontclass">.icon-user</div>
</li>
<li>
<i class="icon iconfont icon-back"></i>
<div class="name">返回</div>
<div class="fontclass">.icon-back</div>
</li>
<li>
<i class="icon iconfont icon-informatiom"></i>
<div class="name">信息</div>
<div class="fontclass">.icon-informatiom</div>
</li>
<li>
<i class="icon iconfont icon-huifu"></i>
<div class="name">回复</div>
<div class="fontclass">.icon-huifu</div>
</li>
<li>
<i class="icon iconfont icon-chakanguo"></i>
<div class="name">查看过</div>
<div class="fontclass">.icon-chakanguo</div>
</li>
</ul>
<h2 id="font-class-">font-class引用</h2>
<hr>
<p>font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。</p>
<p>与unicode使用方式相比,具有如下特点:</p>
<ul>
<li>兼容性良好,支持ie8+,及所有现代浏览器。</li>
<li>相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。</li>
<li>因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。</li>
<li>不过因为本质上还是使用的字体,所以多色图标还是不支持的。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的fontclass代码:</h3>
<pre><code class="lang-js hljs javascript"><span class="hljs-comment"><link rel="stylesheet" type="text/css" href="./iconfont.css"></span></code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="lang-css hljs"><<span class="hljs-selector-tag">i</span> <span class="hljs-selector-tag">class</span>="<span class="hljs-selector-tag">iconfont</span> <span class="hljs-selector-tag">icon-xxx</span>"></<span class="hljs-selector-tag">i</span>></code></pre>
<blockquote>
<p>"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。</p>
</blockquote>
</div>
</body>
</html>
================================================
FILE: src/styles/iconfont/demo_symbol.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>IconFont</title>
<link rel="stylesheet" href="demo.css">
<script src="iconfont.js"></script>
<style type="text/css">
.icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em; height: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
</style>
</head>
<body>
<div class="main markdown">
<h1>IconFont 图标</h1>
<ul class="icon_lists clear">
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xiai"></use>
</svg>
<div class="name">喜爱</div>
<div class="fontclass">#icon-xiai</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-ding"></use>
</svg>
<div class="name">顶</div>
<div class="fontclass">#icon-ding</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-user"></use>
</svg>
<div class="name">用户</div>
<div class="fontclass">#icon-user</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-back"></use>
</svg>
<div class="name">返回</div>
<div class="fontclass">#icon-back</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-informatiom"></use>
</svg>
<div class="name">信息</div>
<div class="fontclass">#icon-informatiom</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-huifu"></use>
</svg>
<div class="name">回复</div>
<div class="fontclass">#icon-huifu</div>
</li>
<li>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-chakanguo"></use>
</svg>
<div class="name">查看过</div>
<div class="fontclass">#icon-chakanguo</div>
</li>
</ul>
<h2 id="symbol-">symbol引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过<code>font-size</code>,<code>color</code>来调整样式。</li>
<li>兼容性较差,支持 ie9+,及现代浏览器。</li>
<li>浏览器渲染svg的性能一般,还不如png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的symbol代码:</h3>
<pre><code class="lang-js hljs javascript"><span class="hljs-comment"><script src="./iconfont.js"></script></span></code></pre>
<h3 id="-css-">第二步:加入通用css代码(引入一次就行):</h3>
<pre><code class="lang-js hljs javascript"><style type=<span class="hljs-string">"text/css"</span>>
.icon {
width: <span class="hljs-number">1</span>em; height: <span class="hljs-number">1</span>em;
vertical-align: <span class="hljs-number">-0.15</span>em;
fill: currentColor;
overflow: hidden;
}
<<span class="hljs-regexp">/style></span></code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="lang-js hljs javascript"><svg <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"icon"</span> aria-hidden=<span class="hljs-string">"true"</span>><span class="xml"><span class="hljs-tag">
<<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#icon-xxx"</span>></span><span class="hljs-tag"></<span class="hljs-name">use</span>></span>
</span><<span class="hljs-regexp">/svg>
</span></code></pre>
</div>
</body>
</html>
================================================
FILE: src/styles/iconfont/demo_unicode.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>IconFont</title>
<link rel="stylesheet" href="demo.css">
<style type="text/css">
@font-face {font-family: "iconfont";
src: url('iconfont.eot'); /* IE9*/
src: url('iconfont.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont.woff') format('woff'), /* chrome, firefox */
url('iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
</style>
</head>
<body>
<div class="main markdown">
<h1>IconFont 图标</h1>
<ul class="icon_lists clear">
<li>
<i class="icon iconfont"></i>
<div class="name">喜爱</div>
<div class="code">&#xe600;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">顶</div>
<div class="code">&#xe610;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">用户</div>
<div class="code">&#xe60f;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">返回</div>
<div class="code">&#xe611;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">信息</div>
<div class="code">&#xe617;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">回复</div>
<div class="code">&#xe63f;</div>
</li>
<li>
<i class="icon iconfont"></i>
<div class="name">查看过</div>
<div class="code">&#xe6ae;</div>
</li>
</ul>
<h2 id="unicode-">unicode引用</h2>
<hr>
<p>unicode是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>兼容性最好,支持ie6+,及所有现代浏览器。</li>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式</p>
</blockquote>
<p>unicode使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的font-face</h3>
<pre><code class="lang-js hljs javascript">@font-face {
font-family: <span class="hljs-string">'iconfont'</span>;
src: url(<span class="hljs-string">'iconfont.eot'</span>);
src: url(<span class="hljs-string">'iconfont.eot?#iefix'</span>) format(<span class="hljs-string">'embedded-opentype'</span>),
url(<span class="hljs-string">'iconfont.woff'</span>) format(<span class="hljs-string">'woff'</span>),
url(<span class="hljs-string">'iconfont.ttf'</span>) format(<span class="hljs-string">'truetype'</span>),
url(<span class="hljs-string">'iconfont.svg#iconfont'</span>) format(<span class="hljs-string">'svg'</span>);
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用iconfont的样式</h3>
<pre><code class="lang-js hljs javascript">.iconfont{
font-family:<span class="hljs-string">"iconfont"</span> !important;
font-size:<span class="hljs-number">16</span>px;font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: <span class="hljs-number">0.2</span>px;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre><code class="lang-js hljs javascript"><i <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"iconfont"</span>>&#x33;<span class="xml"><span class="hljs-tag"></<span class="hljs-name">i</span>></span></span></code></pre>
<blockquote>
<p>"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。</p>
</blockquote>
</div>
</body>
</html>
================================================
FILE: src/styles/iconfont/iconfont.css
================================================
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1481708482838'); /* IE9*/
src: url('iconfont.eot?t=1481708482838#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont.woff?t=1481708482838') format('woff'), /* chrome, firefox */
url('iconfont.ttf?t=1481708482838') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont.svg?t=1481708482838#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiai:before { content: "\e600"; }
.icon-ding:before { content: "\e610"; }
.icon-user:before { content: "\e60f"; }
.icon-back:before { content: "\e611"; }
.icon-informatiom:before { content: "\e617"; }
.icon-huifu:before { content: "\e63f"; }
.icon-chakanguo:before { content: "\e6ae"; }
================================================
FILE: src/styles/iconfont/iconfont.js
================================================
;(function(window) {
var svgSprite = '<svg>' +
'' +
'<symbol id="icon-xiai" viewBox="0 0 1024 1024">' +
'' +
'<path d="M533.504 268.288q33.792-41.984 71.68-75.776 32.768-27.648 74.24-50.176t86.528-19.456q63.488 5.12 105.984 30.208t67.584 63.488 34.304 87.04 6.144 99.84-17.92 97.792-36.864 87.04-48.64 74.752-53.248 61.952q-40.96 41.984-85.504 78.336t-84.992 62.464-73.728 41.472-51.712 15.36q-20.48 1.024-52.224-14.336t-69.632-41.472-79.872-61.952-82.944-75.776q-26.624-25.6-57.344-59.392t-57.856-74.24-46.592-87.552-21.504-100.352 11.264-99.84 39.936-83.456 65.536-61.952 88.064-35.328q24.576-5.12 49.152-1.536t48.128 12.288 45.056 22.016 40.96 27.648q45.056 33.792 86.016 80.896z" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-ding" viewBox="0 0 1024 1024">' +
'' +
'<path d="M898.799192 421.647515 652.021657 421.647515C747.635071 68.468364 586.127515 50.424242 586.127515 50.424242c-68.477414 0-54.265535 54.137535-59.434667 63.158303 0 172.721131-183.462788 308.06497-183.462788 308.06497l0 489.810747c0 48.333576 65.887677 65.737697 91.729455 65.737697L805.774222 977.19596c34.885818 0 63.308283-91.52 63.308283-91.52C960.822303 573.74901 960.822303 480.943838 960.822303 480.943838 960.822303 416.496485 898.799192 421.647515 898.799192 421.647515L898.799192 421.647515M898.799192 421.647515 898.799192 421.647515zM241.961374 421.807838 94.18602 421.807838c-30.526061 0-31.008323 29.972687-31.008323 29.972687l30.526061 493.994667c0 31.420768 31.489293 31.420768 31.489293 31.420768l127.910788 0c26.647273 0 26.406788-20.783838 26.406788-20.783838L279.510626 459.267879C279.511919 421.326869 241.961374 421.807838 241.961374 421.807838L241.961374 421.807838M241.961374 421.807838 241.961374 421.807838z" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-user" viewBox="0 0 1024 1024">' +
'' +
'<path d="M512 239.36m-239.36 0a239.36 239.36 0 1 0 478.72 0 239.36 239.36 0 1 0-478.72 0Z" fill="" ></path>' +
'' +
'<path d="M512 478.72a512 512 0 0 0-512 512V1024h1024v-33.28a512 512 0 0 0-512-512z" fill="" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-back" viewBox="0 0 1024 1024">' +
'' +
'<path d="M691.84 140.16a40.96 40.96 0 0 0-58.24 0L295.04 483.2a40.96 40.96 0 0 0 0 58.24l338.56 342.4a40.96 40.96 0 0 0 58.24-58.24L384 512l307.84-314.24a40.96 40.96 0 0 0 0-57.6z" fill="" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-informatiom" viewBox="0 0 1024 1024">' +
'' +
'<path d="M512 640L0 320v576c0-29.44 28.16 0 64 0h896a64 64 0 0 0 64-64V320z" fill="" ></path>' +
'' +
'<path d="M960 192H64a64 64 0 0 0-64 64l512 315.52 512-320A64 64 0 0 0 960 192z" fill="" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-huifu" viewBox="0 0 1024 1024">' +
'' +
'<path d="M979.861 238.897l0 394.758c0 76.758-62.138 138.896-138.896 138.896l-208.346 0-10.964 179.102-204.69-175.449-230.275 0c-76.758 0-138.896-62.138-138.896-138.896l0-398.412c0-76.758 62.138-138.896 138.896-138.896l654.274 0c76.758 0 138.896 62.138 138.896 138.896z" ></path>' +
'' +
'</symbol>' +
'' +
'<symbol id="icon-chakanguo" viewBox="0 0 1024 1024">' +
'' +
'<path d="M509.618255 415.794891c-42.695405 0-77.253478 38.371936-77.253478 85.73771 0 47.352471 34.558074 86.22071 77.253478 86.22071 42.666752 0 76.716242-38.867216 76.716242-86.22071C586.333474 454.181153 552.285007 415.794891 509.618255 415.794891zM511.518534 237.895155c-211.54809 0-383.266034 159.858848-383.266034 274.586822 0 114.726951 171.717943 273.623891 383.266034 273.623891 212.484416 0 384.227941-162.52149 384.227941-273.623891C895.746476 401.393903 724.00295 237.895155 511.518534 237.895155zM510.071579 689.900759c-89.698928 0-162.133657-81.708953-162.133657-181.90291 0-101.156888 72.434729-182.397167 162.133657-182.397167 90.182952 0 163.099658 81.241302 163.099658 182.397167C673.171238 608.204086 600.254531 689.900759 510.071579 689.900759z" ></path>' +
'' +
'</symbol>' +
'' +
'</svg>'
var script = function() {
var scripts = document.getElementsByTagName('script')
return scripts[scripts.length - 1]
}()
var shouldInjectCss = script.getAttribute("data-injectcss")
/**
* document ready
*/
var ready = function(fn) {
if (document.addEventListener) {
if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) {
setTimeout(fn, 0)
} else {
var loadFn = function() {
document.removeEventListener("DOMContentLoaded", loadFn, false)
fn()
}
document.addEventListener("DOMContentLoaded", loadFn, false)
}
} else if (document.attachEvent) {
IEContentLoaded(window, fn)
}
function IEContentLoaded(w, fn) {
var d = w.document,
done = false,
// only fire once
init = function() {
if (!done) {
done = true
fn()
}
}
// polling for no errors
var polling = function() {
try {
// throws errors until after ondocumentready
d.documentElement.doScroll('left')
} catch (e) {
setTimeout(polling, 50)
return
}
// no errors, fire
init()
};
polling()
// trying to always fire before onload
d.onreadystatechange = function() {
if (d.readyState == 'complete') {
d.onreadystatechange = null
init()
}
}
}
}
/**
* Insert el before target
*
* @param {Element} el
* @param {Element} target
*/
var before = function(el, target) {
target.parentNode.insertBefore(el, target)
}
/**
* Prepend el to target
*
* @param {Element} el
* @param {Element} target
*/
var prepend = function(el, target) {
if (target.firstChild) {
before(el, target.firstChild)
} else {
target.appendChild(el)
}
}
function appendSvg() {
var div, svg
div = document.createElement('div')
div.innerHTML = svgSprite
svgSprite = null
svg = div.getElementsByTagName('svg')[0]
if (svg) {
svg.setAttribute('aria-hidden', 'true')
svg.style.position = 'absolute'
svg.style.width = 0
svg.style.height = 0
svg.style.overflow = 'hidden'
prepend(svg, document.body)
}
}
if (shouldInjectCss && !window.__iconfont__svg__cssinject__) {
window.__iconfont__svg__cssinject__ = true
try {
document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>");
} catch (e) {
console && console.log(e)
}
}
ready(appendSvg)
})(window)
================================================
FILE: src/styles/index.css
================================================
/*Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性*/
@import '~normalize.css/normalize.css';
@import "~github-markdown-css/github-markdown.css";
@font-face {font-family: "iconfont";
src: url('iconfont/iconfont.eot'); /* IE9*/
src: url('iconfont/iconfont.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('iconfont/iconfont.woff') format('woff'), /* chrome, firefox */
url('iconfont/iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('iconfont/iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:20px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
body,h2,p,ul,li{
margin:0;
padding:0;
}
ul{
padding-left:0;
}
li{
list-style-type: none;
}
a:hover,a:link,a:visited,a:active{
text-decoration: none;
}
/* .fade-in{
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn{
0%{opacity:0;}
100%{opacity:1;}
} */
/* .move-appear {
transform: scale(50);
background: grey;
}
.move-appear.move-appear-active {
transform: scale(1);
background: grey;
transition: 60s;
} */
.move-enter {
position: absolute;
z-index: 100;
width: 100%;
height: 100%;
background: white;
top: 0;
left: 100%;
}
.move-enter.move-enter-active {
left: 0%;
transition: 0.5s;
}
.move-exit {
position: absolute;
z-index: 50;
width: 100%;
height: 100%;
opacity: 50;
background: white;
top: 0;
left: 0;
}
.move-exit.move-exit-active {
opacity: 50;
left: -100%;
transition: 0.5s;
}
.left-enter {
position: absolute;
z-index: 100;
width: 100%;
height: 100%;
background: white;
top: 0;
left: 0;
}
.left-enter.left-enter-active {
left: 0;
}
.left-exit {
position: absolute;
z-index: 100;
width: 100%;
height: 100%;
background: white;
top: 0;
left: 0;
}
.left-exit.left-exit-active {
left: 100%;
transition: 0.5s;
}
.up-enter {
position: absolute;
z-index: 10000;
width: 100%;
height: 100%;
background: white;
top: 100%;
left: 0;
}
.up-enter.up-enter-active {
top: 0;
transition: 0.5s;
}
.up-exit {
position: absolute;
z-index: 100;
width: 100%;
height: 100%;
background: white;
top: 0;
left: 0;
}
.up-exit.up-exit-active {
top: 0;
}
================================================
FILE: src/utils/getOS.js
================================================
export const os = 'win32';export const host = '192.168.6.183'
================================================
FILE: src/utils/getPosition.js
================================================
const getPosition = (direction, DOMNode, className) => {
switch(document.getElementsByClassName(className).length){
case 0:
alert('注意:传入的className对应的元素不存在!')
break
case 1:
break
default:
alert('注意:传入的className对应多个元素!请给该元素唯一className')
}
let offset = DOMNode[`offset${direction}`]
let parent = DOMNode.offsetParent
if (parent) {
if (className) {
if (parent.className != className) {
offset += getPosition(direction, parent, className);
}
} else {
if (parent.nodeName != 'BODY') {
offset += getPosition(direction, parent);
}
}
}
return offset
}
export const getTop = (DOMNode, className) => {
return getPosition('Top', DOMNode, className)
}
export const getLeft = (DOMNode, className) => {
return getPosition('Left', DOMNode, className)
}
export const getBottom = (DOMNode, className) => {
let containerHeight = 0;
let selfHeight = DOMNode.offsetHeight
if (className) {
containerHeight = document.getElementsByClassName(className)[0].offsetHeight
} else {
containerHeight = document.getElementsByTagName('body')[0].offsetHeight
}
return containerHeight - selfHeight - getTop(DOMNode, className)
}
export const getRight = (DOMNode, className) => {
let containerWidth = 0;
let selfWidth = DOMNode.offsetWidth
if (className) {
containerWidth = document.getElementsByClassName(className)[0].offsetWidth
} else {
containerWidth = document.getElementsByTagName('body')[0].offsetWidth
}
return containerWidth - selfWidth - getLeft(DOMNode, className)
}
================================================
FILE: src/utils/getSize.js
================================================
const getSize = () => {
let windowW,windowH,contentH,contentW,scrollT;
windowH = window.innerHeight;
windowW = window.innerWidth;
scrollT = document.documentElement.scrollTop || document.body.scrollTop;
contentH = (document.documentElement.scrollHeight > document.body.scrollHeight) ? document.documentElement.scrollHeight : document.body.scrollHeight;
contentW = (document.documentElement.scrollWidth > document.body.scrollWidth) ? document.documentElement.scrollWidth : document.body.scrollWidth;
return {windowW,windowH,contentH,contentW,scrollT}
}
export default getSize;
================================================
FILE: src/utils/getStrLength.js
================================================
// GBK字符集实际长度计算
const getStrLength = str => {
let realLength = 0;
let len = str.length;
let charCode = -1;
for(let i = 0; i < len; i++){
charCode = str.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
}else{
// 如果是中文则长度加2
realLength += 2;
}
}
return realLength;
}
export default getStrLength
================================================
FILE: src/utils/myFetch.js
================================================
import originFetch from 'isomorphic-fetch'
import { fetchError, fetchStart, fetchEnd } from '../actions/fetchError'
const getData = async (url, option) => {
try {
let response = await originFetch(url, option)
if (response.ok) {
return response
} else {
let error = await response.json()
throw error
}
} catch (error) {
myDispatch(fetchError(error))
}
return new Promise(() => { })
}
const myFetch = (url, option) => {
let myOption = {
credentials: 'include',
headers: {
"Content-type": "application/json;charset=UTF-8"
}
}
option = option || {}
option = { ...myOption, ...option }
// 方案1
// return new Promise((resolve, reject) => {
// originFetch(url, option)
// .then(response => {
// if (response.ok) {
// resolve(response)
// } else {
// response.json().then(json => {
// //response的then里产生或抛出的异常,无法直接抛出到该链式调用外面去被originFetch的catch捕获,除非获取到了originFetch的reject
// reject(json)//直接拿到这个return的new Promise的reject方法,能被它的catch方法(即最外面那个catch方法)捕捉到
// })
// }
// })
// .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常
// })
// .catch(error => { //获取的是reject(json)里的json数据
// myDispatch(fetchError(error))
// return new Promise(() => { }) //catch后面如果有then,then里面的方法执行可能会报错,要想其不执行的办法是返回一个promise对象携带空的方法
// })
//方案2
// return new Promise((resolve, reject) => {
// originFetch(url, option)
// .then(response => {
// if (response.ok) {
// resolve(response)
// } else {
// response.json().then(json => {
// throw json
// })
// .catch(error => myDispatch(fetchError(error)))//也可以直接在这里获取response里抛出的错误
// //promise对象如果不调用resolve方法,后面的then链路里的函数是不会被执行的
// }
// })
// .catch(error => myDispatch(fetchError(error)))//获取originFetch链式调用里产生的异常,这里能获取到的是fetch请求失败,无返回response,服务器无响应的异常
// })
//方案3
//getData方法在最上面定义
return getData(url, option)
}
export default myFetch
================================================
FILE: src/utils/routePrefix.js
================================================
// 使用browserHistory需要进行判断,在生产环境下,如果编译的文件不是根目录文件,而是在子文件夹内,子文件夹的地址部分会被browserHistory解析成路由
// 比如:编译的文件放在站点www文件夹的cnode文件夹里,访问时用github.com/cnode/,但是此时cnode/被browserHistory解析成路由,这个路由不存在所以会出问题
// let prefix = process.env.NODE_ENV === 'production' ? '/cnode' : '';
// 使用hashHistory则不需要这样的判断,因为router被放在hash中,而浏览器能正确解析hash
let prefix = '';
export default prefix;
================================================
FILE: src/utils/transformDate.js
================================================
export default function (date) {
var createAt = new Date(date);
var time = new Date().getTime() - createAt.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) + '年前';
}
}
================================================
FILE: src/utils/urlPrefix.js
================================================
import { os, host } from './getOS'
let prefix = os == 'win32' ? `http://${host}:8081` : '/api'
// let prefix = 'http://192.168.30.90:8080/s';
// let prefix = '/api'
export default prefix
================================================
FILE: webpack.config.js
================================================
var path = require('path');
var webpack = require('webpack')
var autoprefixer = require('autoprefixer')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var redBox = require('redbox-react')
var host = 'localhost'
var port = '5678'
//判断当前运行环境是开发模式还是生产模式
const nodeEnv = process.env.NODE_ENV || 'development';
const isPro = nodeEnv !== 'development';
const os = require('os').platform()
const network = require('os').networkInterfaces()
console.log("当前运行系统:", os)
console.log("当前运行环境:", isPro ? 'production' : 'development')
// Object.keys(network).forEach(item => {
// if (network[item] && network[item][0] && network[item][0].internal == false && ['以太网', '本地连接'].includes(item)) {
// network[item].forEach(ips => {
// if (ips.family == 'IPv4') {
// host = ips.address
// }
// })
// }
// })
// var fs = require("fs")
// var data = `export const os = '${os}';export const host = '${host}'`
// var writerStream = fs.createWriteStream('src/utils/getOS.js')
// writerStream.write(data, 'UTF8')
// writerStream.end()
// writerStream.on('finish', () => {
// console.log("成功写入:src/utils/getOS.js")
// })
// writerStream.on('error', err => {
// console.log(err.stack)
// })
const moduleCss = new ExtractTextPlugin('module.css')
const globalCss = new ExtractTextPlugin('global.css')
const uiCss = new ExtractTextPlugin('ui.css')
var plugins = [
globalCss,
moduleCss,
uiCss,
// 这个插件的作用是将打包的js文件拆分,默认拆分出2个
new webpack.optimize.CommonsChunkPlugin({
// 这个vendor就是打包后的文件名字,需要写在index.html里面
name: 'vendor',
minChunks: function (module) {
// 该配置假定你引入的 vendor 存在于 node_modules 目录中
return module.context && module.context.indexOf('node_modules') !== -1;
}
}),
//DefinePlugin 在原始的源码中执行查找和替换操作,在导入的代码中,
// 任何出现 process.env.NODE_ENV的地方都会被替换为nodeEnv变量转成的json字符串
new webpack.DefinePlugin({
// 定义全局变量
'process.env': {
'NODE_ENV': JSON.stringify(nodeEnv)
}
})
]
var app = ['./index.js']
if (isPro) {
plugins.push(
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
comments: false,
compress: {
warnings: false,
// 去掉debugger和console
drop_debugger: true,
drop_console: true
}
})
)
} else {
app.unshift('react-hot-loader/patch', `webpack-dev-server/client?http://${host}:${port}`, 'webpack/hot/only-dev-server')
plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
}
// // 开启服务器后不能用相对路径
// var outpath = path.resolve(__dirname, 'build');
module.exports = {
context: path.resolve(__dirname, 'src'),
devtool: isPro ? 'source-map' : 'inline-source-map',
entry: {
app: app
},
output: {
filename: '[name].js',
path: path.join(__dirname, 'build'),
publicPath: isPro ? './build/' : '/build/',
chunkFilename: '[name].js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader?cacheDirectory=true']
},
// {
// test: /\.scss$/,
// // exclude: [nodeModulesPath]用来排除不处理的目录
// exclude: path.resolve(__dirname, 'src/styles'),
// // webpack配置loader时是可以不写loader的后缀明-loader,因此css-loader可以写为css
// // css-loader使你能够使用类似@import 和 url(...)的方法实现 require()的功能
// // style-loader将所有的计算后的样式通过<style>标签的形式加入到dom的head中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中
// // CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。
// // 产生局部作用域的唯一方法,就是使用一个独一无二的class的名字,不会与其他选择器重名(或者在css中类名前面加:local())。这就是 CSS Modules 的做法。
// // css-loader支持CSS Modules,在css-loader后面加了一个查询参数modules,表示打开 CSS Modules 功能
// // 使用css-loader就可以支持css模块化,但此时css中的类名还是不能重复,必须加上参数modules,编译时就会自动将类名换成哈希值类型的类名,不担心重复了
// // loader处理文件的加载顺序是从右到左,即先通过sass-loader将scss转成css,然后再用postcss预处理,添加上css3动画兼容性前缀等
// // 然后在通过css-loader将css引入js文件,如果css-loader加了一个查询参数modules,则会将css中类名MD5化,使其变成唯一
// // 最后通过styleloader将计算好的样式文件以style标签的形式加入到dom头部
// use: ['style-loader', 'css-loader?modules', 'sass-loader']
// },
{
test: /\.scss$/,
// linux中
include: os === 'win32' ? /(src\\components|src\\containers)/ : /(src\/components|src\/containers)/,
use: moduleCss.extract({
use: [
{
loader: "css-loader?modules&localIdentName=[path][name]-[local]-[hash:base64:5]"
}, {
loader: 'sass-loader'
}]
})
}, {
test: /\.css$/,
include: os === 'win32' ? /(src\\styles)/ : /(src\/styles)/,
use: globalCss.extract({
use: [
{
loader: "css-loader"
}
// , {
// loader: "postcss-loader"
// }
]
})
}, {
test: /\.css$/,
include: /node_modules/,
use: uiCss.extract({
use: [
{
loader: "css-loader" // translates CSS into CommonJS
}
// , {
// loader: "less-loader" // compiles Less to CSS
// }
]
})
}, {
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
use: ['url-loader?limit=50000&name=[path][name].[ext]']
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.less', '.scss', '.css'],
// alias里面设置的文件跟node_modules里面的文件等价,直接通过文件名导入,webpack就能找到相应文件。
alias: {
//当import 'redBoxBlackStyle' 时,webpack就会自动找根文件夹下面的redBoxBlackStyle.js
redBoxBlackStyle: path.join(__dirname, 'redBoxBlackStyle.js')
}
},
plugins: plugins,
// postcss: [autoprefixer],
devServer: {
hot: true,
host: host,
compress: true,
port,
historyApiFallback: true,
contentBase: path.resolve(__dirname),
publicPath: '/build/',
stats: {
modules: false,
chunks: false
},
},
};
gitextract_305ial8u/ ├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── package.json ├── redBoxBlackStyle.js ├── src/ │ ├── actions/ │ │ ├── fetchError.js │ │ └── hashUrl.js │ ├── actions.js │ ├── components/ │ │ ├── Article/ │ │ │ ├── Content/ │ │ │ │ ├── Content.js │ │ │ │ └── styles.scss │ │ │ └── Reply/ │ │ │ ├── Reply.js │ │ │ └── styles.scss │ │ ├── HomePage/ │ │ │ ├── Drawer/ │ │ │ │ ├── Drawer.js │ │ │ │ └── styles.scss │ │ │ ├── FloatingActionButton.js │ │ │ ├── Header/ │ │ │ │ ├── Header.js │ │ │ │ └── styles.scss │ │ │ └── Lists/ │ │ │ ├── Lists.js │ │ │ └── styles.scss │ │ ├── Message/ │ │ │ └── Content/ │ │ │ ├── Content.js │ │ │ └── styles.scss │ │ ├── PublishTopic/ │ │ │ └── Form/ │ │ │ ├── Form.js │ │ │ └── styles.scss │ │ └── common/ │ │ ├── AsyncContainer.js │ │ ├── CircleLoading.js │ │ ├── Dialog.js │ │ ├── Header/ │ │ │ ├── Header.js │ │ │ └── styles.scss │ │ ├── LinkToLogin/ │ │ │ ├── LinkToLogin.js │ │ │ └── styles.scss │ │ ├── Profile/ │ │ │ ├── Profile.js │ │ │ └── styles.scss │ │ ├── Snackbar.js │ │ └── react-pullrefresh.js │ ├── configureStore.js │ ├── containers/ │ │ ├── App.js │ │ ├── Article.js │ │ ├── HomePage.js │ │ ├── Login.js │ │ ├── Message.js │ │ ├── Profile.js │ │ └── PublishTopic.js │ ├── index.js │ ├── reducers/ │ │ ├── article.js │ │ ├── collectedTopics.js │ │ ├── fetchError.js │ │ ├── hashUrl.js │ │ ├── homePage.js │ │ ├── index.js │ │ ├── login.js │ │ ├── message.js │ │ ├── profile.js │ │ └── publishTopic.js │ ├── routes.js │ ├── styles/ │ │ ├── iconfont/ │ │ │ ├── demo.css │ │ │ ├── demo_fontclass.html │ │ │ ├── demo_symbol.html │ │ │ ├── demo_unicode.html │ │ │ ├── iconfont.css │ │ │ └── iconfont.js │ │ └── index.css │ └── utils/ │ ├── getOS.js │ ├── getPosition.js │ ├── getSize.js │ ├── getStrLength.js │ ├── myFetch.js │ ├── routePrefix.js │ ├── transformDate.js │ └── urlPrefix.js └── webpack.config.js
SYMBOL INDEX (125 symbols across 23 files)
FILE: src/actions.js
constant REQUEST_TOPICS (line 3) | const REQUEST_TOPICS = 'REQUEST_TOPICS'
constant RECEIVE_TOPICS (line 4) | const RECEIVE_TOPICS = 'RECEIVE_TOPICS'
constant SELECT_TAB (line 5) | const SELECT_TAB= 'SELECT_TAB'
constant RECORD_SCROLLT (line 6) | const RECORD_SCROLLT='RECORD_SCROLLT'
constant REQUEST_ARTICLE (line 7) | const REQUEST_ARTICLE = 'REQUEST_ARTICLE'
constant RECEIVE_ARTICLE (line 8) | const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE'
constant CHANGE_CURRENT_TOPICID (line 9) | const CHANGE_CURRENT_TOPICID = 'CHANGE_CURRENT_TOPICID'
constant CURRENT_ROUTER (line 10) | const CURRENT_ROUTER = 'CURRENT_ROUTER'
constant LOGIN_SUCCESS (line 11) | const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
constant LOGIN_FAILED (line 12) | const LOGIN_FAILED = 'LOGIN_FAILED'
constant LOGOUT (line 13) | const LOGOUT = 'LOGOUT'
constant REQUEST_PROFILE (line 14) | const REQUEST_PROFILE = 'REQUEST_PROFILE'
constant RECEIVE_PROFILE (line 15) | const RECEIVE_PROFILE = 'RECEIVE_PROFILE'
constant SWITCH_SUPPORT (line 16) | const SWITCH_SUPPORT = 'SWITCH_SUPPORT'
constant FETCH_COMMENT (line 17) | const FETCH_COMMENT = 'FETCH_COMMENT'
constant SWITCH_COLLECTED (line 18) | const SWITCH_COLLECTED = 'SWITCH_COLLECTED'
constant RECORD_ARTICLE_SCROLLT (line 19) | const RECORD_ARTICLE_SCROLLT = 'RECORD_ARTICLE_SCROLLT'
constant PUBLISH_TOPIC (line 20) | const PUBLISH_TOPIC = 'PUBLISH_TOPIC'
constant FETCH_MESSAGE (line 21) | const FETCH_MESSAGE = 'FETCH_MESSAGE'
constant MARK_ALL_MESSAGES (line 22) | const MARK_ALL_MESSAGES = 'MARK_ALL_MESSAGES'
constant GET_COLLECTED_TOPICS (line 23) | const GET_COLLECTED_TOPICS = 'GET_COLLECTED_TOPICS'
FILE: src/actions/fetchError.js
constant FETCH_ERROR (line 3) | const FETCH_ERROR = 'FETCH_ERROR'
constant CLEAR_ERROR (line 4) | const CLEAR_ERROR = 'CLEAR_ERROR'
constant FETCH_START (line 5) | const FETCH_START = 'FETCH_START'
constant FETCH_END (line 6) | const FETCH_END = 'FETCH_END'
FILE: src/actions/hashUrl.js
constant SET_TRANSITION (line 1) | const SET_TRANSITION = 'SET_TRANSITION'
constant SET_HASH_URL (line 2) | const SET_HASH_URL = 'SET_HASH_URL'
FILE: src/components/Article/Content/Content.js
class Content (line 16) | @connect()
method constructor (line 18) | constructor() {
method componentWillMount (line 24) | componentWillMount() {
method componentWillReceiveProps (line 27) | componentWillReceiveProps(newProps) {
method update (line 32) | update(props) {
method render (line 43) | render() {
FILE: src/components/Article/Reply/Reply.js
class Reply (line 19) | @connect()
method constructor (line 21) | constructor(){
method componentWillMount (line 43) | componentWillMount(){
method componentWillReceiveProps (line 48) | componentWillReceiveProps(newProps){
method render (line 79) | render(){
class NeedComment (line 185) | class NeedComment extends Component{
method render (line 186) | render(){
FILE: src/components/HomePage/Drawer/Drawer.js
class myDrawer (line 19) | class myDrawer extends React.Component {
method constructor (line 20) | constructor() {
method componentWillUpdate (line 40) | componentWillUpdate(nextProps) {
method render (line 44) | render() {
FILE: src/components/HomePage/FloatingActionButton.js
class FloatActionButton (line 16) | @connect()
method render (line 18) | render() {
FILE: src/components/HomePage/Header/Header.js
class Header (line 16) | @connect()
method constructor (line 18) | constructor(props) {
method componentWillMount (line 25) | componentWillMount() {
method render (line 44) | render() {
FILE: src/components/Message/Content/Content.js
class Content (line 20) | @connect()
method constructor (line 22) | constructor(props) {
method componentWillUpdate (line 49) | componentWillUpdate(newProps) {
method render (line 62) | render() {
FILE: src/components/PublishTopic/Form/Form.js
class Form (line 11) | class Form extends Component {
method constructor (line 12) | constructor(props) {
method render (line 17) | render(){
FILE: src/components/common/AsyncContainer.js
class AsyncContainer (line 4) | class AsyncContainer extends Component {
method componentDidMount (line 7) | componentDidMount() {
method componentWillUnmount (line 13) | componentWillUnmount() {
method render (line 17) | render() {
method componentWillUnmount (line 27) | componentWillUnmount() {
FILE: src/components/common/Header/Header.js
class Header (line 12) | @connect(store => ({ hashUrl: store.hashUrl }))
method constructor (line 14) | constructor() {
method componentWillMount (line 19) | componentWillMount() {
method componentDidMount (line 26) | componentDidMount() {
method componentWillUnmount (line 33) | componentWillUnmount() {
method render (line 36) | render() {
FILE: src/components/common/react-pullrefresh.js
constant MAX_DEFAULT (line 7) | const MAX_DEFAULT = 100
class Pull (line 9) | class Pull extends Component {
method constructor (line 14) | constructor(props) {
method componentDidMount (line 21) | componentDidMount() {
method componentWillReceiveProps (line 69) | componentWillReceiveProps(nextProps) {
method componentWillUnmount (line 79) | componentWillUnmount() {
method render (line 82) | render() {
FILE: src/containers/App.js
class App (line 10) | class App extends Component {
method componentWillMount (line 11) | componentWillMount(){
method render (line 32) | render() {
function mapStateToProps (line 46) | function mapStateToProps(state) {
FILE: src/containers/Article.js
class Article (line 12) | class Article extends Component {
method constructor (line 13) | constructor() {
method componentWillMount (line 19) | componentWillMount() {
method componentWillReceiveProps (line 34) | componentWillReceiveProps(newProps) {
method componentDidMount (line 39) | componentDidMount() {
method componentWillUnmount (line 44) | componentWillUnmount() {
method render (line 52) | render() {
function mapStateToProps (line 77) | function mapStateToProps(state) {
FILE: src/containers/HomePage.js
class HomePage (line 16) | @connect(state => {
method constructor (line 25) | constructor() {
method componentWillMount (line 113) | componentWillMount() {
method componentWillUpdate (line 116) | componentWillUpdate(newProps, newState) {
method render (line 159) | render() {
method componentDidMount (line 183) | componentDidMount() {
method componentDidUpdate (line 237) | componentDidUpdate() {
method componentWillUnmount (line 252) | componentWillUnmount() {
FILE: src/containers/Login.js
class Login (line 13) | class Login extends Component {
method constructor (line 14) | constructor(){
method componentWillReceiveProps (line 20) | componentWillReceiveProps(newProps){
method render (line 39) | render(){
function mapStateToProps (line 91) | function mapStateToProps(state) {
FILE: src/containers/Message.js
class Message (line 10) | class Message extends Component {
method componentDidMount (line 12) | componentDidMount(){
method render (line 18) | render(){
function mapStateToProps (line 36) | function mapStateToProps(state) {
FILE: src/containers/Profile.js
class Profile (line 9) | class Profile extends Component {
method render (line 10) | render(){
function mapStateToProps (line 37) | function mapStateToProps(state) {
FILE: src/containers/PublishTopic.js
class PublishTopic (line 12) | class PublishTopic extends Component {
method constructor (line 13) | constructor(){
method componentWillReceiveProps (line 23) | componentWillReceiveProps(newProps){
method showDialog (line 31) | showDialog() {
method ifTitleErr (line 43) | ifTitleErr(boolean) {
method ifContentErr (line 48) | ifContentErr(boolean) {
method render (line 53) | render(){
function mapStateToProps (line 84) | function mapStateToProps(state) {
FILE: src/reducers/homePage.js
function tabDataItem (line 17) | function tabDataItem(state = { isFetching: false, page: 0, scrollT: 0, t...
FILE: src/routes.js
class Routes (line 33) | @connect(store => ({ store }))
method constructor (line 35) | constructor() {
method componentWillMount (line 72) | componentWillMount() {
method componentWillUpdate (line 97) | componentWillUpdate(nextProps) {
method render (line 103) | render() {
method componentDidMount (line 141) | componentDidMount() {
method componentWillUnmount (line 146) | componentWillUnmount() {
FILE: src/styles/iconfont/iconfont.js
function IEContentLoaded (line 76) | function IEContentLoaded(w, fn) {
function appendSvg (line 137) | function appendSvg() {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (166K chars).
[
{
"path": ".babelrc",
"chars": 740,
"preview": "//babel配置文件,不需要做修改,因为都配置好了\n{\n \"presets\": [\n [\"env\", {\"modules\": false}],\n \"react\",\n \"stage-0\"\n ],\n \"plugins\""
},
{
"path": ".gitignore",
"chars": 15,
"preview": "/node_modules/ "
},
{
"path": "README.md",
"chars": 2450,
"preview": "## 项目简介\n一个WebApp版的cnode客户端,项目采用react技术栈构建。组件选用的是[Material-UI](http://www.material-ui.com/),让界面更适合触控操作。\n- 感谢来自[cnodejs论坛]"
},
{
"path": "index.html",
"chars": 540,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-cn\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"I"
},
{
"path": "package.json",
"chars": 2550,
"preview": "{\n \"name\": \"cnode\",\n \"version\": \"2.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"webpa"
},
{
"path": "redBoxBlackStyle.js",
"chars": 516,
"preview": "module.exports = {\n style: {\n redbox: {\n boxSizing: 'border-box',\n fontFamily: 'sans-ser"
},
{
"path": "src/actions/fetchError.js",
"chars": 455,
"preview": "import urlPrefix from '../utils/urlPrefix'\n\nexport const FETCH_ERROR = 'FETCH_ERROR'\nexport const CLEAR_ERROR = 'CLEAR_E"
},
{
"path": "src/actions/hashUrl.js",
"chars": 299,
"preview": "export const SET_TRANSITION = 'SET_TRANSITION'\nexport const SET_HASH_URL = 'SET_HASH_URL'\n\n\nexport const setHashUrl = (h"
},
{
"path": "src/actions.js",
"chars": 7503,
"preview": "import fetch from 'isomorphic-fetch'\n\nexport const REQUEST_TOPICS = 'REQUEST_TOPICS'\nexport const RECEIVE_TOPICS = 'RECE"
},
{
"path": "src/components/Article/Content/Content.js",
"chars": 4204,
"preview": "import React, { PropTypes, Component } from 'react'\nimport { Link } from 'react-router-dom'\nimport { connect } from 'rea"
},
{
"path": "src/components/Article/Content/styles.scss",
"chars": 530,
"preview": ".head{\n\tmargin-top: 64px;\n\tbackground: #ccc;\n\toverflow: hidden;\n\tpadding: 10px;\n\timg{\n\t\twidth: 50px;\n\t\theight: 50px;\n\t\tb"
},
{
"path": "src/components/Article/Reply/Reply.js",
"chars": 8616,
"preview": "import React, { PropTypes,Component } from 'react'\nimport {Link} from 'react-router-dom'\nimport { connect } from 'react-"
},
{
"path": "src/components/Article/Reply/styles.scss",
"chars": 479,
"preview": ".reply{\n\tli{\n\tpadding: 10px 20px;\n\toverflow: hidden;\n\tborder-bottom: 1px solid #ccc;\n\t}\n\th2{\n\t\tpadding: 10px 20px;\n\t\tfon"
},
{
"path": "src/components/HomePage/Drawer/Drawer.js",
"chars": 3786,
"preview": "import React from 'react';\nimport { Link } from 'react-router-dom'\nimport { connect } from 'react-redux'\nimport prefix f"
},
{
"path": "src/components/HomePage/Drawer/styles.scss",
"chars": 190,
"preview": ".header{\n\ttext-align: center;\n\tpadding: 100px 10px 50px;\n\tbackground: #DCE775;\n\tp{\n\t\tmargin: 10px;\n\t}\n}\n\n.link{\n\tvertica"
},
{
"path": "src/components/HomePage/FloatingActionButton.js",
"chars": 908,
"preview": "import React from 'react'\nimport { connect } from 'react-redux'\nimport { setTransition } from '../../actions/hashUrl'\nim"
},
{
"path": "src/components/HomePage/Header/Header.js",
"chars": 2864,
"preview": "import React, { Component } from 'react'\nimport { connect } from 'react-redux'\nimport styles from './styles.scss'\nimport"
},
{
"path": "src/components/HomePage/Header/styles.scss",
"chars": 105,
"preview": ".header{\n\tposition: fixed;\n\toverflow: hidden;\n\tz-index: 10;\n\t// width: 100%;\n\ttransition: all 0.3s ;\n}\n\n\n"
},
{
"path": "src/components/HomePage/Lists/Lists.js",
"chars": 2818,
"preview": "import React from 'react'\nimport FlipMove from 'react-flip-move'\nimport transformDate from '../../../utils/transformDate"
},
{
"path": "src/components/HomePage/Lists/styles.scss",
"chars": 1400,
"preview": ".lists{\n position: relative;\n\tmargin-top: 100px;\n}\n.li{\n position: relative;\n top:0;\n\theight: 61px;\n\tpadding: 5px 15p"
},
{
"path": "src/components/Message/Content/Content.js",
"chars": 5622,
"preview": "import React, { PropTypes, Component } from 'react'\nimport { connect } from 'react-redux'\nimport transformDate from '../"
},
{
"path": "src/components/Message/Content/styles.scss",
"chars": 239,
"preview": ".content{\n\tmargin-top: 64px;\n\toverflow: hidden;\n}\n\n.link{\n\tdisplay: block;\n}\n.msg{\n\tpadding: 20px;\n\ttext-align: center;\n"
},
{
"path": "src/components/PublishTopic/Form/Form.js",
"chars": 2477,
"preview": "import React, { PropTypes,Component } from 'react'\nimport getStrLength from '../../../utils/getStrLength'\nimport styles "
},
{
"path": "src/components/PublishTopic/Form/styles.scss",
"chars": 230,
"preview": ".form{\n\ttext-align: center;\n\tmargin-top: 80px;\n}\n\n.errorInfo{\n\tcolor: red;\n\tmargin: 5px;\n\toverflow: hidden;\n\ttransition:"
},
{
"path": "src/components/common/AsyncContainer.js",
"chars": 788,
"preview": "import React, { Component } from 'react'\nimport CircleLoading from './CircleLoading'\n\nclass AsyncContainer extends Compo"
},
{
"path": "src/components/common/CircleLoading.js",
"chars": 514,
"preview": "import React from 'react'\nimport MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'\nimport CircularProgress fro"
},
{
"path": "src/components/common/Dialog.js",
"chars": 1197,
"preview": "import React from 'react';\n// import { hashHistory,browserHistory } from 'react-router';\nimport Dialog from 'material-ui"
},
{
"path": "src/components/common/Header/Header.js",
"chars": 1793,
"preview": "import React, { Component } from 'react'\nimport { connect } from 'react-redux'\nimport { withRouter } from 'react-router-"
},
{
"path": "src/components/common/Header/styles.scss",
"chars": 148,
"preview": ".header{\n\t// position: fixed;\n\twidth: 100%;\n\tz-index: 10;\n\ttransition: all 0.5s ease-out;\n}\n\n.title{\n\tpadding-right: 30p"
},
{
"path": "src/components/common/LinkToLogin/LinkToLogin.js",
"chars": 1255,
"preview": "import React, { PropTypes } from 'react'\nimport { Link } from 'react-router-dom'\nimport { setTransition } from '../../.."
},
{
"path": "src/components/common/LinkToLogin/styles.scss",
"chars": 53,
"preview": ".linkToLogin{\n\tdisplay: block;\n\ttext-align: center;\n}"
},
{
"path": "src/components/common/Profile/Profile.js",
"chars": 2746,
"preview": "import React from 'react'\nimport {Link} from 'react-router-dom'\nimport prefix from '../../../utils/routePrefix'\nimport s"
},
{
"path": "src/components/common/Profile/styles.scss",
"chars": 630,
"preview": ".header{\n\ttext-align: center;\n\tpadding: 0 20px 20px;\n\timg{\n\t\twidth: 50px;\n\t\theight: 50px;\n\t\tborder-radius: 50%;\n\t}\n\tp{\n"
},
{
"path": "src/components/common/Snackbar.js",
"chars": 504,
"preview": "import React from 'react';\nimport Snackbar from 'material-ui/Snackbar';\nimport RaisedButton from 'material-ui/RaisedButt"
},
{
"path": "src/components/common/react-pullrefresh.js",
"chars": 3681,
"preview": "import React, { Component } from 'react'\nimport RefreshIndicator from 'material-ui/RefreshIndicator';\nimport MuiThemePro"
},
{
"path": "src/configureStore.js",
"chars": 760,
"preview": "import { createStore, applyMiddleware } from 'redux';\nimport { composeWithDevTools } from 'redux-devtools-extension';\nim"
},
{
"path": "src/containers/App.js",
"chars": 1574,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport {fetchAccess,fetchMessa"
},
{
"path": "src/containers/Article.js",
"chars": 3343,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport { switchSupport, fetchC"
},
{
"path": "src/containers/HomePage.js",
"chars": 8294,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport { withRouter } from 're"
},
{
"path": "src/containers/Login.js",
"chars": 3921,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport {fetchAccess,fetchMessa"
},
{
"path": "src/containers/Message.js",
"chars": 1207,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport {fetchMessage,fetchArti"
},
{
"path": "src/containers/Profile.js",
"chars": 1176,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport {fetchArticle} from '.."
},
{
"path": "src/containers/PublishTopic.js",
"chars": 2992,
"preview": "import React, { Component, PropTypes } from 'react'\nimport { connect } from 'react-redux'\nimport {Link} from 'react-rout"
},
{
"path": "src/index.js",
"chars": 954,
"preview": "import 'babel-polyfill';\nimport React from 'react';\nimport ReactDOM from 'react-dom'\nimport store from './configureStore"
},
{
"path": "src/reducers/article.js",
"chars": 1272,
"preview": "import {\n\tREQUEST_ARTICLE, RECEIVE_ARTICLE, CHANGE_CURRENT_TOPICID, SWITCH_SUPPORT, FETCH_COMMENT, RECORD_ARTICLE_SCROLL"
},
{
"path": "src/reducers/collectedTopics.js",
"chars": 455,
"preview": "import {\n\tGET_COLLECTED_TOPICS\n} from '../actions'\n\nconst initState = sessionStorage.getItem('store') ? JSON.parse(sessi"
},
{
"path": "src/reducers/fetchError.js",
"chars": 653,
"preview": "import {\n FETCH_START, FETCH_END, FETCH_ERROR, CLEAR_ERROR\n} from '../actions/fetchError'\n\nconst initState = sessionSto"
},
{
"path": "src/reducers/hashUrl.js",
"chars": 458,
"preview": "import {\n SET_HASH_URL, SET_TRANSITION\n} from '../actions/hashUrl'\n\nconst initState = sessionStorage.getItem('store') ?"
},
{
"path": "src/reducers/homePage.js",
"chars": 1764,
"preview": "import {\n SELECT_TAB, RECORD_SCROLLT,\n REQUEST_TOPICS, RECEIVE_TOPICS\n} from '../actions'\n\n\nconst selectedTab = (state"
},
{
"path": "src/reducers/index.js",
"chars": 489,
"preview": "import { combineReducers } from 'redux'\nimport homePage from './homePage'\nimport article from './article'\nimport login f"
},
{
"path": "src/reducers/login.js",
"chars": 605,
"preview": "import {\n\tLOGIN_SUCCESS, LOGIN_FAILED, LOGOUT\n} from '../actions'\n\n\nconst initState = sessionStorage.getItem('store') ? "
},
{
"path": "src/reducers/message.js",
"chars": 568,
"preview": "import {\n\tFETCH_MESSAGE, MARK_ALL_MESSAGES\n} from '../actions'\n\nconst initState = sessionStorage.getItem('store') ? JSON"
},
{
"path": "src/reducers/profile.js",
"chars": 597,
"preview": "import {\n\tREQUEST_PROFILE, RECEIVE_PROFILE, GET_COLLECTED_TOPICS\n} from '../actions'\n\nconst initState = sessionStorage.g"
},
{
"path": "src/reducers/publishTopic.js",
"chars": 415,
"preview": "import {\n\tPUBLISH_TOPIC\n} from '../actions'\n\nconst initState = sessionStorage.getItem('store') ? JSON.parse(sessionStora"
},
{
"path": "src/routes.js",
"chars": 6640,
"preview": "import React, { Component } from 'react'\nimport { Route, Router, Redirect, Switch, withRouter } from 'react-router-dom'\n"
},
{
"path": "src/styles/iconfont/demo.css",
"chars": 5746,
"preview": "*{margin: 0;padding: 0;list-style: none;}\n/*\nKISSY CSS Reset\n理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。\n2. reset "
},
{
"path": "src/styles/iconfont/demo_fontclass.html",
"chars": 3017,
"preview": "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\"/>\n <title>IconFont</title>\n <link rel=\"stylesheet\" href=\""
},
{
"path": "src/styles/iconfont/demo_symbol.html",
"chars": 4584,
"preview": "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\"/>\n <title>IconFont</title>\n <link rel=\"stylesheet\" href=\""
},
{
"path": "src/styles/iconfont/demo_unicode.html",
"chars": 4544,
"preview": "\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\"/>\n <title>IconFont</title>\n <link rel=\"stylesheet\" href=\""
},
{
"path": "src/styles/iconfont/iconfont.css",
"chars": 921,
"preview": "\n@font-face {font-family: \"iconfont\";\n src: url('iconfont.eot?t=1481708482838'); /* IE9*/\n src: url('iconfont.eot?t=14"
},
{
"path": "src/styles/iconfont/iconfont.js",
"chars": 6775,
"preview": ";(function(window) {\n\n var svgSprite = '<svg>' +\n '' +\n '<symbol id=\"icon-xiai\" viewBox=\"0 0 1024 1024\">' +\n '"
},
{
"path": "src/styles/index.css",
"chars": 2378,
"preview": "/*Normalize.css 只是一个很小的CSS文件,但它在默认的HTML元素样式上提供了跨浏览器的高度一致性*/\n@import '~normalize.css/normalize.css';\n@import \"~github-mar"
},
{
"path": "src/utils/getOS.js",
"chars": 61,
"preview": "export const os = 'win32';export const host = '192.168.6.183'"
},
{
"path": "src/utils/getPosition.js",
"chars": 1725,
"preview": "const getPosition = (direction, DOMNode, className) => {\n switch(document.getElementsByClassName(className).length){\n"
},
{
"path": "src/utils/getSize.js",
"chars": 583,
"preview": "const getSize = () => {\n\tlet windowW,windowH,contentH,contentW,scrollT;\n\twindowH = window.innerHeight;\n\twindowW = window"
},
{
"path": "src/utils/getStrLength.js",
"chars": 411,
"preview": "// GBK字符集实际长度计算\nconst getStrLength = str => {\n let realLength = 0;\n let len = str.length;\n let charCode = -1;\n "
},
{
"path": "src/utils/myFetch.js",
"chars": 2459,
"preview": "import originFetch from 'isomorphic-fetch'\nimport { fetchError, fetchStart, fetchEnd } from '../actions/fetchError'\n\ncon"
},
{
"path": "src/utils/routePrefix.js",
"chars": 355,
"preview": "// 使用browserHistory需要进行判断,在生产环境下,如果编译的文件不是根目录文件,而是在子文件夹内,子文件夹的地址部分会被browserHistory解析成路由\n// 比如:编译的文件放在站点www文件夹的cnode文件夹里,"
},
{
"path": "src/utils/transformDate.js",
"chars": 684,
"preview": "export default function (date) {\n var createAt = new Date(date);\n var time = new Date().getTime() - createAt.getTi"
},
{
"path": "src/utils/urlPrefix.js",
"chars": 189,
"preview": "import { os, host } from './getOS'\n\nlet prefix = os == 'win32' ? `http://${host}:8081` : '/api'\n// let prefix = 'http://"
},
{
"path": "webpack.config.js",
"chars": 6802,
"preview": "var path = require('path');\nvar webpack = require('webpack')\nvar autoprefixer = require('autoprefixer')\nvar ExtractTextP"
}
]
About this extraction
This page contains the full source code of the lumia2046/cnode GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (138.9 KB), approximately 41.0k tokens, and a symbol index with 125 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.