├── src ├── styles │ ├── atom.less │ ├── login.less │ ├── loading.less │ ├── iconfont │ │ └── iconfont.css │ ├── main.less │ ├── menu.less │ ├── create.less │ ├── header.less │ ├── message.less │ ├── reset.less │ ├── user.less │ └── topic.less ├── images │ ├── logo.png │ └── menu.png ├── constants │ ├── topicInfo.js │ └── mutationTypes.js ├── views │ ├── about.vue │ ├── index.vue │ ├── login.vue │ ├── newTopic.vue │ ├── message.vue │ ├── user.vue │ ├── topic.vue │ └── topicList.vue ├── utils │ └── index.js ├── components │ ├── header.vue │ ├── reply.vue │ ├── loading.vue │ ├── userInfo.vue │ ├── backTop.vue │ └── menu.vue ├── apis │ ├── publicApi.js │ └── index.js ├── app.js ├── configs │ └── routes.js └── vuex │ └── index.js ├── .gitignore ├── snapshoot ├── all.jpg ├── good.jpg ├── login.jpg ├── menu.jpg ├── reply.jpg ├── topic.jpg ├── islogin.jpg └── message.jpg ├── public └── favicon.ico ├── .babelrc ├── template └── index.html ├── index.html ├── server.js ├── LICENSE ├── README.md ├── webpack.config.dev.js ├── webpack.config.base.js ├── webpack.config.prod.js └── package.json /src/styles/atom.less: -------------------------------------------------------------------------------- 1 | .mgr30 { 2 | margin-right: 30px; 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist/ 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /snapshoot/all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/all.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /snapshoot/good.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/good.jpg -------------------------------------------------------------------------------- /snapshoot/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/login.jpg -------------------------------------------------------------------------------- /snapshoot/menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/menu.jpg -------------------------------------------------------------------------------- /snapshoot/reply.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/reply.jpg -------------------------------------------------------------------------------- /snapshoot/topic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/topic.jpg -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /src/images/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/src/images/menu.png -------------------------------------------------------------------------------- /snapshoot/islogin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/islogin.jpg -------------------------------------------------------------------------------- /snapshoot/message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulcm/vue-cnode-mobile/HEAD/snapshoot/message.jpg -------------------------------------------------------------------------------- /src/constants/topicInfo.js: -------------------------------------------------------------------------------- 1 | export const topicTab = { 2 | 'share': '分享', 3 | 'ask': '问答', 4 | 'good': '精华', 5 | 'job': '招聘', 6 | 'top': '置顶', 7 | 'all': "全部" 8 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "env": { 7 | "development": { 8 | "plugins": [ 9 | "transform-runtime" 10 | ] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/views/about.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/constants/mutationTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_TOPIC_LIST = 'GET_TOPIC_LIST'; 2 | export const UPDATE_TOPIC_LIST = 'UPDATE_TOPIC_LIST'; 3 | export const GET_TOPIC_INFO = 'GET_TOPIC_INFO'; 4 | export const LOGIN = 'LOGIN'; 5 | export const LOGIN_OUT = 'LOGIN_OUT'; 6 | export const REPLY = 'REPLY'; 7 | export const TOOGLE_LOAD = 'TOOGLE_LOAD'; 8 | export const TOOGLE_LIST_LOAD = 'TOOGLE_LIST_LOAD'; -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /src/styles/login.less: -------------------------------------------------------------------------------- 1 | .login { 2 | padding-top: 40px; 3 | .login-token { 4 | padding: 0 15px; 5 | margin-top: 50px; 6 | input { 7 | padding: 12px 0; 8 | border-bottom: 1px solid #4fc08d; 9 | background-color: transparent; 10 | font-size: 1.4rem; 11 | color: #313131; 12 | width: 100%; 13 | } 14 | .btn-login { 15 | width: 100%; 16 | border-bottom: 2px solid #3aa373; 17 | background-color: #4fc08d; 18 | font-size: 1.6rem; 19 | margin: 15px 0; 20 | color: #fff; 21 | padding: 10px; 22 | border-radius: 3px; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | cnode 11 | 12 | 13 | 14 |
15 | 16 |
17 | <%=htmlWebpackPlugin.files.webpackManifest%> 18 | 19 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export const getTimeInfo = (str) => { 2 | if (!str) { 3 | return '' 4 | } 5 | const date = new Date(str); 6 | const time = new Date().getTime() - date.getTime(); //现在的时间-传入的时间 = 相差的时间(单位 = 毫秒) 7 | if (time < 0) { 8 | return ''; 9 | } else if (time / 1000 < 60) { 10 | return '刚刚'; 11 | } else if ((time / 60000) < 60) { 12 | return parseInt((time / 60000)) + '分钟前'; 13 | } else if ((time / 3600000) < 24) { 14 | return parseInt(time / 3600000) + '小时前'; 15 | } else if ((time / 86400000) < 31) { 16 | return parseInt(time / 86400000) + '天前'; 17 | } else if ((time / 2592000000) < 12) { 18 | return parseInt(time / 2592000000) + '月前'; 19 | } else { 20 | return parseInt(time / 31536000000) + '年前'; 21 | } 22 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | cnode 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/styles/loading.less: -------------------------------------------------------------------------------- 1 | .loading { 2 | width: 120px; 3 | /*height: 120px;*/ 4 | margin: 5px auto; 5 | text-align: center; 6 | .icon-loading { 7 | color: #ccc; 8 | display: inline-block; 9 | font-size: 5rem; 10 | -webkit-animation: gif 1s infinite linear; 11 | animation: gif 1s infinite linear; 12 | } 13 | 14 | @keyframes gif { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(360deg); 21 | transform: rotate(360deg); 22 | } 23 | } 24 | @-webkit-keyframes gif { 25 | 0% { 26 | -webkit-transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(360deg); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/styles/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('//at.alicdn.com/t/font_flullthqh62vgqfr.eot?t=1478750580998'); /* IE9*/ 4 | src: url('//at.alicdn.com/t/font_flullthqh62vgqfr.eot?t=1478750580998#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('//at.alicdn.com/t/font_flullthqh62vgqfr.woff?t=1478750580998') format('woff'), /* chrome, firefox */ 6 | url('//at.alicdn.com/t/font_flullthqh62vgqfr.ttf?t=1478750580998') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('//at.alicdn.com/t/font_flullthqh62vgqfr.svg?t=1478750580998#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -webkit-text-stroke-width: 0.2px; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | .icon-dianzan:before { content: "\e659"; } 20 | 21 | .icon-menu:before { content: "\e628"; } 22 | -------------------------------------------------------------------------------- /src/styles/main.less: -------------------------------------------------------------------------------- 1 | @import url('reset'); 2 | 3 | #app { 4 | overflow-x: hidden; 5 | } 6 | 7 | .tab { 8 | display: -webkit-flex; 9 | display: flex; 10 | border-bottom: 1px solid #d4d4d4; 11 | height: 42px; 12 | .tab-item { 13 | flex: 1; 14 | border-right: 1px solid #d4d4d4; 15 | text-align: center; 16 | font-weight: 700; 17 | font-size: 1.6rem; 18 | padding: 10px 0; 19 | height: 42px; 20 | &.active { 21 | color: #ff5a5f; 22 | border-bottom: 2px solid #ff5a5f; 23 | } 24 | &:last-child { 25 | border-right: none; 26 | } 27 | } 28 | } 29 | 30 | .no-data { 31 | text-align: center; 32 | color: #d4d4d4; 33 | font-size: 1.8rem; 34 | display: flex; 35 | justify-content: center; 36 | flex-direction: column; 37 | .iconfont.icon-empty { 38 | display: block; 39 | font-size: 12.5rem; 40 | color: #d4d4d4; 41 | } 42 | } -------------------------------------------------------------------------------- /src/styles/menu.less: -------------------------------------------------------------------------------- 1 | nav.menu-bar { 2 | position: fixed; 3 | top: 40px; 4 | bottom: 0; 5 | left: -200px; 6 | width: 200px; 7 | color: #313131; 8 | background-color: #fff; 9 | transition: all .3s ease; 10 | z-index: 10; 11 | &.show { 12 | transform: translateX(200px); 13 | } 14 | 15 | .menu-list { 16 | border-top: 1px solid #d4d4d4; 17 | padding-top: 18px; 18 | li.menu-item { 19 | &:hover { 20 | background-color: #f5f5f5; 21 | } 22 | a { 23 | display: block; 24 | padding: 14px 24px; 25 | font-size: 1.4rem; 26 | color: #313131; 27 | font-weight: 700; 28 | .iconfont { 29 | margin-right: 30px; 30 | } 31 | } 32 | &:nth-of-type(6) { //消息 33 | border-top: 1px solid #d4d4d4; 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var WebpackDevServer = require('webpack-dev-server'); 4 | var config = require('./webpack.config.dev.js'); 5 | var port = 8091; 6 | var favicon = require('serve-favicon'); 7 | 8 | //启动服务 9 | var server = new WebpackDevServer(webpack(config), { 10 | publicPath: 'http://127.0.0.1:8091/dist/', 11 | hot: true, 12 | noInfo: true, 13 | stats: { 14 | colors: true 15 | }, 16 | proxy: { 17 | '/api/*': { 18 | target: 'https://cnodejs.org/', 19 | secure: false 20 | } 21 | }, 22 | historyApiFallback: true 23 | }); 24 | 25 | server.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 26 | 27 | //将其他路由,全部返回index.html 28 | /*server.use('*', function (req,res) { 29 | res.sendFile(__dirname + '/index.html') 30 | });*/ 31 | 32 | server.listen(port, function(err) { 33 | if (err) { 34 | console.log(err); 35 | } else { 36 | console.log('Listening at http://127.0.0.1:' + port); 37 | } 38 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 季陆 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-cnode-mobile 2 | 学习vue,搭建cnode社区,以前有搭过[react-cnode-mobile](https://github.com/soulcm/react-cnode-mobile),但只有一个大的壳子就阉割了,因为很多react坑都已经踩过,就没往下继续了 3 | 4 | 现在学习vue和移动端新知识,重新搭建cnode 5 | 6 | 线上地址: [vue-cnode-mobile](https://soulcm.github.io/vue-cnode-mobile/),当然能不能打开,真的是看脸的 7 | 8 | 你可以将代码down下来,然后本地运行,绝对能打开 9 | 10 | 此版本停止更新,现正在进行重构,开发2.0版本[V2.0](https://github.com/soulcm/vue-cnode-mobile/blob/V2.0/README.md),查看请切换到[V2.0分支](https://github.com/soulcm/vue-cnode-mobile/tree/V2.0) 11 | 12 | 13 | 知识点: 14 | * vue2 15 | * vuex2 16 | * vue-router2 17 | * 移动端开发 18 | * fetch 19 | * es6 es7 20 | * less 不用sass的理由你懂的,那个难安装 21 | 22 | 项目还在持续更新中,以后计划: 23 | 1. 优化css 24 | 2. 优化router 25 | 3. 优化vuex 26 | 4. 加入transition效果 27 | 28 | 29 | 运行步骤 30 | ``` 31 | npm install 32 | 33 | npm run dev 34 | ``` 35 | 36 | 访问 http://127.0.0.1:8091 37 | 38 | 服务启动后,手机端可访问本机ip查看 39 | 40 | 41 | 吐槽: vscode编辑器对vue的支持好差 42 | 43 | 44 | ![全部](./snapshoot/all.jpg) 45 | 46 | ![精华](./snapshoot/good.jpg) 47 | 48 | ![登录](./snapshoot/login.jpg) 49 | 50 | ![菜单](./snapshoot/menu.jpg) 51 | 52 | ![已登录](./snapshoot/islogin.jpg) 53 | 54 | ![文章详情](./snapshoot/topic.jpg) 55 | 56 | ![回复](./snapshoot/reply.jpg) 57 | 58 | ![消息](./snapshoot/message.jpg) 59 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var baseWebpackConfig = require('./webpack.config.base.js'); 4 | var merge = require('webpack-merge'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | // add hot-reload related code to entry chunks 8 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 9 | baseWebpackConfig.entry[name] = ['webpack-dev-server/client?http://127.0.0.1:8091', 'webpack/hot/dev-server'].concat(baseWebpackConfig.entry[name]); 10 | }) 11 | 12 | baseWebpackConfig.plugins.push( 13 | new webpack.DefinePlugin({ 14 | 'process.env': { 15 | NODE_ENV: JSON.stringify('development') 16 | } 17 | }), 18 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js'), 19 | new ExtractTextPlugin('[name].css', {allChunks: true}), 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.HotModuleReplacementPlugin() 22 | ); 23 | 24 | module.exports = merge(baseWebpackConfig, { 25 | devtool: '#source-map', 26 | debug: true, 27 | output: { 28 | path: path.join(__dirname, 'dist'), 29 | filename: '[name].js', 30 | publicPath: '/dist/', 31 | chunkFilename: '[id].build.js' 32 | } 33 | }) -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var entryPath = path.join(__dirname, 'src'); 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | 7 | module.exports = { 8 | entry: { 9 | app: path.join(entryPath, 'app.js'), 10 | vendor: ['vue', 'vuex', 'vue-router'] 11 | }, 12 | 13 | module: { 14 | loaders: [{ 15 | test: /\.vue$/, 16 | exclude: /node_modules/, 17 | loader: 'vue-loader' 18 | }, { 19 | test: /\.js$/, 20 | exclude: /node_modules|vue\/dist/, 21 | loader: 'babel' 22 | }, { 23 | test: /\.(less|css)?$/, 24 | loader: ExtractTextPlugin.extract(['css', 'less']) 25 | }, { 26 | test: /\.(png|jpg)$/, 27 | loader: 'url?limit=25000' 28 | }, { 29 | test: /\.(svg|ttf|eot|svg|woff(\(?2\)?)?)(\?[a-zA-Z_0-9.=&]*)?(#[a-zA-Z_0-9.=&]*)?$/, 30 | loader: "file-loader" 31 | }] 32 | }, 33 | 34 | resolve: { 35 | extensions: ['', '.js', '.vue', '.css', '.less'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.js' 38 | } 39 | }, 40 | 41 | plugins: [ 42 | new webpack.NoErrorsPlugin() 43 | ] 44 | } -------------------------------------------------------------------------------- /src/styles/create.less: -------------------------------------------------------------------------------- 1 | .topic-create { 2 | padding-top: 40px; 3 | .category { 4 | border-bottom: 1px solid #d4d4d4; 5 | padding: 15px 20px; 6 | span { 7 | display: inline-block; 8 | } 9 | select { 10 | width: 100px; 11 | border: 1px solid rgb(169, 169, 169); 12 | height: 30px; 13 | border-radius: 3px; 14 | font-size: 1.6rem; 15 | padding: 3px; 16 | } 17 | } 18 | .title { 19 | padding: 15px 20px; 20 | border-bottom: 1px solid #d4d4d4; 21 | input { 22 | width: 100%; 23 | height: 30px; 24 | border-radius: 5px; 25 | box-shadow: 0 0 2px rgba(60,60,60,.5); 26 | font-size: 1.4rem; 27 | padding: 5px; 28 | } 29 | } 30 | .content { 31 | padding: 15px 20px; 32 | textarea { 33 | width: 100%; 34 | border: 1px solid rgb(169, 169, 169); 35 | border-radius: 3px; 36 | padding: 5px; 37 | font-size: 16px; 38 | } 39 | } 40 | button { 41 | display: block; 42 | margin: 0 20px; 43 | background-color: #80bd01; 44 | padding: 8px 15px; 45 | border-radius: 5px; 46 | color: #fff; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /src/styles/header.less: -------------------------------------------------------------------------------- 1 | .page-cover { 2 | position: fixed; 3 | top: 40px; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background-color: rgba(0, 0, 0, 0.4); 8 | z-index: 7; 9 | } 10 | .header-bar { 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 40px; 16 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); 17 | z-index: 6; 18 | background-color: rgba(255, 255, 255, 0.95); 19 | transition: all .3s ease; 20 | display: -webkit-flex; 21 | display: flex; 22 | align-items: center; 23 | 24 | .menu-btn { 25 | width: 44px; 26 | height: 40px; 27 | background: url("../images/menu.png") center center no-repeat; 28 | background-size: 24px; 29 | } 30 | .info { 31 | flex: 1; 32 | text-align: center; 33 | display: -webkit-flex; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | font-size: 1.6rem; 38 | .vue-logo { 39 | width: 40px; 40 | height: 40px; 41 | background: url("../images/logo.png") center center no-repeat; 42 | background-size: 24px; 43 | } 44 | } 45 | a.publish-btn { 46 | color: #42b983; 47 | height: 40px; 48 | line-height: 40px; 49 | padding: 0 15px; 50 | display: block; 51 | } 52 | } -------------------------------------------------------------------------------- /src/apis/publicApi.js: -------------------------------------------------------------------------------- 1 | import fetchApi from './index'; 2 | 3 | export const topicList = (data) => { 4 | return fetchApi({ 5 | url: '/v1/topics', 6 | body: data 7 | }) 8 | } 9 | 10 | export const topicInfo = (id) => { 11 | return fetchApi({ 12 | url: '/v1/topic/' + id 13 | }) 14 | } 15 | 16 | export const login = (data) => { 17 | return fetchApi({ 18 | url: 'v1/accesstoken', 19 | method: 'post', 20 | body: data 21 | }) 22 | } 23 | 24 | export const reply = (data, id) => { 25 | return fetchApi({ 26 | url: `v1/topic/${id}/replies`, 27 | method: 'post', 28 | body: data 29 | }) 30 | } 31 | 32 | export const messageCount = (data) => { 33 | return fetchApi({ 34 | url: `v1/message/count`, 35 | body: data 36 | }) 37 | } 38 | 39 | export const messages = (data) => { 40 | return fetchApi({ 41 | url: `v1/messages`, 42 | body: data 43 | }) 44 | } 45 | 46 | export const upReply = (data, id) => { 47 | return fetchApi({ 48 | url: `v1/reply/${id}/ups`, 49 | method: 'post', 50 | body: data 51 | }) 52 | } 53 | 54 | export const addTopic = (data) => { 55 | return fetchApi({ 56 | url: `v1/topics`, 57 | method: 'post', 58 | body: data 59 | }) 60 | } 61 | 62 | export const getUserInfo = (loginname) => { 63 | return fetchApi({ 64 | url: `v1/user/${loginname}` 65 | }) 66 | } -------------------------------------------------------------------------------- /src/styles/message.less: -------------------------------------------------------------------------------- 1 | .message { 2 | padding-top: 40px; 3 | .message-content { 4 | padding: 10px 0; 5 | border-bottom: 1px solid #d4d4d4; 6 | .author-info { 7 | display: -webkit-flex; 8 | display: flex; 9 | padding: 0 10px; 10 | margin: 10px 0; 11 | .head { 12 | width: 40px; 13 | height: 40px; 14 | margin-right: 15px; 15 | } 16 | .info { 17 | flex: 1; 18 | display: -webkit-flex; 19 | display: flex; 20 | .left { 21 | flex: 1; 22 | color: #626262; 23 | font-size: 1.6rem; 24 | } 25 | .right { 26 | color: #80bd01; 27 | font-size: 1.2rem; 28 | } 29 | } 30 | } 31 | .reply-content { 32 | padding: 0 15px; 33 | } 34 | a { 35 | display: block; 36 | margin: 0 15px; 37 | .topic-title { 38 | padding: 5px; 39 | font-size: 1.8rem; 40 | color: #2c3e50; 41 | background-color: #f0f0f0; 42 | border-radius: 5px; 43 | } 44 | } 45 | } 46 | .no-data { 47 | height: -webkit-calc(~'100vh - 90px'); 48 | height: calc(~'100vh - 90px'); 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var baseWebpackConfig = require('./webpack.config.base.js'); 4 | var merge = require('webpack-merge'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | var InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin'); 8 | 9 | baseWebpackConfig.plugins = baseWebpackConfig.plugins.concat([ 10 | new webpack.DefinePlugin({ 11 | 'process.env': { 12 | NODE_ENV: JSON.stringify('production') 13 | } 14 | }), 15 | new webpack.optimize.UglifyJsPlugin({ 16 | compress: { 17 | warnings: false 18 | }, 19 | output: {comments: false}, 20 | }), 21 | new webpack.optimize.CommonsChunkPlugin({ 22 | names: ['vendor', 'manifest'] 23 | }), 24 | new ExtractTextPlugin('lib/[name].[contenthash].css', {allChunks: true}), 25 | new HtmlWebpackPlugin({ 26 | title: 'vue-cnode', 27 | template: 'template/index.html', 28 | inject: true, 29 | filename: 'index.html', 30 | chunks: ['vendor', 'app'] 31 | }), 32 | new InlineManifestWebpackPlugin({ 33 | name: 'webpackManifest' 34 | }) 35 | ]); 36 | 37 | module.exports = merge(baseWebpackConfig, { 38 | output: { 39 | path: path.join(__dirname, 'dist'), 40 | filename: 'lib/[name].[chunkhash].js', 41 | chunkFilename: 'lib/[id].build.[chunkhash].js' 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/reply.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/styles/reset.less: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | box-sizing: border-box; 4 | } 5 | 6 | html { 7 | font-size: 62.5%; 8 | } 9 | 10 | html,body,ol,ul,h1,h2,h3,h4,h5,h6,p,th,td,dl,dd,form,fieldset,legend,input,textarea,select,button{margin:0;padding:0;border:0} 11 | 12 | body { 13 | width: 100%; 14 | font-size: 1.4rem; 15 | background-color: #fff; 16 | font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; 17 | -webkit-font-smoothing: antialiased; 18 | } 19 | 20 | input[type=text]:focus, 21 | input[type=password]:focus, 22 | input[type=datetime]:focus, 23 | input[type=datetime-local]:focus, 24 | input[type=date]:focus, 25 | input[type=month]:focus, 26 | input[type=time]:focus, 27 | input[type=week]:focus, 28 | input[type=number]:focus, 29 | input[type=email]:focus, 30 | input[type=url]:focus, 31 | input[type=tel]:focus, 32 | input[type=color]:focus, 33 | input[type=search]:focus, 34 | select, 35 | textarea:focus { 36 | outline: 0; 37 | } 38 | 39 | li { 40 | list-style: none; 41 | } 42 | 43 | a { 44 | text-decoration: none; 45 | &:link { 46 | text-decoration: none; 47 | } 48 | &:visited { 49 | text-decoration: none; 50 | } 51 | &:hover { 52 | text-decoration: none; 53 | } 54 | &:active { 55 | text-decoration: none; 56 | } 57 | &:focus { 58 | text-decoration: none; 59 | } 60 | } 61 | 62 | textarea{ 63 | resize: none; 64 | -webkit-appearance: none; 65 | } 66 | input, button{ 67 | -webkit-appearance: none; 68 | } 69 | select { 70 | /*-webkit-appearance: none;*/ 71 | background-color: #fff; 72 | } -------------------------------------------------------------------------------- /src/views/login.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/userInfo.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | 45 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | import routes from './configs/routes'; 5 | import store from './vuex/index'; 6 | 7 | import Index from './views/index'; 8 | import './styles/main.less'; 9 | import 'github-markdown-css'; //markdown css 10 | // import './styles/iconfont/iconfont.css'; 11 | 12 | // 注册一个全局自定义指令 v-focus 13 | Vue.directive('focus', { 14 | // 当绑定元素插入到 DOM 中。 15 | inserted: function (el, binding) { 16 | // 聚焦元素 17 | if (binding.value) { 18 | el.focus(); 19 | } 20 | } 21 | }) 22 | 23 | Vue.use(VueRouter); 24 | // 实例化VueRouter 25 | const router = new VueRouter({ 26 | mode: 'history', 27 | routes, 28 | scrollBehavior(to, from, savedPosition) { 29 | // if (to.name === 'list' && from.name === 'topic') { 30 | // return {x: 0, y: +sessionStorage.getItem('scrollTop') || 0} 31 | // } else { 32 | if (savedPosition) { 33 | return savedPosition 34 | } else { 35 | return { x: 0, y: 0 } 36 | } 37 | // } 38 | } 39 | }); 40 | 41 | // 处理刷新的时候vuex被清空但是用户已经登录的情况 42 | if (localStorage.getItem('userInfo')) { 43 | store.commit('LOGIN', JSON.parse(localStorage.getItem('userInfo'))); 44 | } 45 | 46 | // 登录验证 47 | router.beforeEach((to, from, next) => { 48 | if (to.matched.some(record => record.meta.requiresAuth)) { 49 | if (store.state.userInfo.loginname) { //已登录 50 | next(); 51 | } else { //未登录 52 | next({ 53 | name: 'login', 54 | query: { redirect: encodeURIComponent(to.name) } //缓存应该跳的页面,方便登录后直接跳转 55 | }); 56 | } 57 | } else { 58 | next(); 59 | } 60 | }); 61 | 62 | new Vue({ 63 | router, 64 | store 65 | }).$mount('#app'); -------------------------------------------------------------------------------- /src/components/backTop.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cnode-mobile", 3 | "version": "1.0.0", 4 | "description": "学习vue", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "cross-env NODE_ENV=development nodemon server.js --ignore src/", 9 | "build": "npm run clean && cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress", 10 | "clean": "rimraf dist/*" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/soulcm/vue-cnode-mobile.git" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "vue-router", 19 | "vuex", 20 | "cnode" 21 | ], 22 | "author": "soulcm", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/soulcm/vue-cnode-mobile/issues" 26 | }, 27 | "homepage": "https://github.com/soulcm/vue-cnode-mobile#readme", 28 | "dependencies": { 29 | "es6-promise": "^4.0.5", 30 | "github-markdown-css": "^2.4.1", 31 | "isomorphic-fetch": "^2.2.1", 32 | "vue": "^2.0.5", 33 | "vue-router": "^2.0.1", 34 | "vuex": "^2.0.0" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.18.2", 38 | "babel-loader": "^6.2.7", 39 | "babel-plugin-transform-runtime": "^6.15.0", 40 | "babel-preset-es2015": "^6.18.0", 41 | "babel-preset-stage-0": "^6.16.0", 42 | "babel-runtime": "^6.18.0", 43 | "cross-env": "^3.1.3", 44 | "css-loader": "^0.25.0", 45 | "extract-text-webpack-plugin": "^1.0.1", 46 | "file-loader": "^0.9.0", 47 | "html-webpack-plugin": "^2.24.1", 48 | "inline-manifest-webpack-plugin": "^3.0.1", 49 | "less": "^2.7.1", 50 | "less-loader": "^2.2.3", 51 | "rimraf": "^2.5.4", 52 | "serve-favicon": "^2.3.0", 53 | "style-loader": "^0.13.1", 54 | "url-loader": "^0.5.7", 55 | "vue-hot-reload-api": "^2.0.6", 56 | "vue-loader": "^9.8.1", 57 | "webpack": "^1.13.3", 58 | "webpack-dev-server": "^1.16.2", 59 | "webpack-merge": "^0.15.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/configs/routes.js: -------------------------------------------------------------------------------- 1 | // import Index from '../views/index'; 2 | import TopicList from '../views/topicList'; 3 | // import Topic from '../views/topic'; 4 | // import NewTopic from '../views/newTopic'; 5 | // import Login from '../views/login'; 6 | // import User from '../views/user'; 7 | // import Message from '../views/message'; 8 | // import About from '../views/about'; 9 | 10 | const Topic = resolve => { 11 | require.ensure(['../views/topic.vue'], () => { 12 | resolve(require('../views/topic.vue')); 13 | }); 14 | }; 15 | 16 | const NewTopic = resolve => { 17 | require.ensure(['../views/newTopic.vue'], () => { 18 | resolve(require('../views/newTopic.vue')); 19 | }); 20 | }; 21 | 22 | const Login = resolve => { 23 | require.ensure(['../views/login.vue'], () => { 24 | resolve(require('../views/login.vue')); 25 | }); 26 | }; 27 | 28 | const User = resolve => { 29 | require.ensure(['../views/user.vue'], () => { 30 | resolve(require('../views/user.vue')); 31 | }); 32 | }; 33 | 34 | const Message = resolve => { 35 | require.ensure(['../views/message.vue'], () => { 36 | resolve(require('../views/message.vue')); 37 | }); 38 | }; 39 | 40 | const About = resolve => { 41 | require.ensure(['../views/about.vue'], () => { 42 | resolve(require('../views/about.vue')); 43 | }); 44 | }; 45 | 46 | const routes = [{ 47 | path: '/', 48 | redirect: {name: 'list'} 49 | }, { 50 | path: '/list', 51 | name: 'list', 52 | component: TopicList 53 | }, { 54 | path: '/topic/:id', 55 | name: 'topic', 56 | component: Topic 57 | }, { 58 | path: '/create', 59 | name: 'create', 60 | component: NewTopic, 61 | meta: { requiresAuth: true } 62 | }, { 63 | path: '/login', 64 | name: 'login', 65 | component: Login 66 | }, { 67 | path: '/user/:loginname', 68 | name: 'user', 69 | component: User 70 | }, { 71 | path: '/message', 72 | name: 'message', 73 | component: Message, 74 | meta: { requiresAuth: true } 75 | }, { 76 | path: '/about', 77 | name: 'about', 78 | component: About 79 | }] 80 | 81 | 82 | export default routes; -------------------------------------------------------------------------------- /src/apis/index.js: -------------------------------------------------------------------------------- 1 | require('es6-promise').polyfill(); 2 | require('isomorphic-fetch'); 3 | 4 | const baseOpts = { 5 | method: 'get', 6 | headers: { 7 | 'Accept': 'application/json', 8 | 'Content-Type': 'application/json' 9 | }, 10 | // credentials: 'include', 11 | mode: 'cors' 12 | } 13 | 14 | const isObj = (obj) => { 15 | return obj && Object.prototype.toString.call(obj) === '[object Object]'; 16 | } 17 | 18 | const isFormData = (obj) => { 19 | return obj && Object.prototype.toString.call(obj) === '[object FormData]'; 20 | } 21 | 22 | 23 | 24 | const fetchApi = (cfg) => { 25 | let opts = Object.assign({}, baseOpts, cfg); 26 | const url = opts.url; 27 | delete opts.url; 28 | 29 | 30 | 31 | let fetchUrl = '/api' 32 | if (/^\//.test(url)) { 33 | fetchUrl += url 34 | } else { 35 | fetchUrl += '/' + url 36 | } 37 | 38 | if (opts.method.toLowerCase() !== 'get' && isObj(opts.body) && opts.headers['Content-Type'].indexOf('application/json') > -1) { 39 | opts.body = JSON.stringify(opts.body) 40 | } 41 | 42 | if (opts.method.toLowerCase() === 'get' && isObj(opts.body)) { 43 | fetchUrl += '?'; 44 | for (let key in opts.body) { 45 | let value = opts.body[key]; 46 | 47 | if (value instanceof Array) { 48 | value = JSON.stringify(value); 49 | } 50 | fetchUrl += key + '=' + value + '&'; 51 | } 52 | fetchUrl = fetchUrl.slice(0, -1); 53 | delete opts.body; 54 | } 55 | 56 | // fetchUrl = 'https://cnodejs.org' + fetchUrl; 57 | 58 | 59 | return new Promise((resolve, reject) => { 60 | fetch(fetchUrl, opts).then((res) => { 61 | const isSuccess = res.ok || res.status >= 200 && res.status < 300; 62 | if (isSuccess) { 63 | const data = res.headers.get('content-type') && res.headers.get('content-type').indexOf('json') >= 0 ? res.json() : res.text(); 64 | resolve(data); 65 | } else { 66 | throw res 67 | } 68 | }).catch((err) => { 69 | reject(); 70 | }) 71 | }) 72 | } 73 | 74 | export default fetchApi -------------------------------------------------------------------------------- /src/styles/user.less: -------------------------------------------------------------------------------- 1 | .user-page { 2 | padding-top: 40px; 3 | .info { 4 | background-color: #e7e7e7; 5 | padding: 15px 0; 6 | img { 7 | border-radius: 50%; 8 | width: 100px; 9 | height: 100px; 10 | display: block; 11 | margin: 0 auto; 12 | } 13 | span.name { 14 | text-align: center; 15 | margin-top: 5px; 16 | display: block; 17 | } 18 | .bottom { 19 | margin-top: 15px; 20 | display: -webkit-flex; 21 | display: flex; 22 | .time { 23 | text-align: center; 24 | flex: 1; 25 | } 26 | .score { 27 | text-align: center; 28 | color: #80bd01; 29 | flex: 1; 30 | } 31 | } 32 | 33 | } 34 | .user-active { 35 | .active-content { 36 | display: -webkit-flex; 37 | display: flex; 38 | padding: 10px; 39 | border-bottom: 1px solid #f0f0f0; 40 | .head { 41 | img { 42 | width: 40px; 43 | height: 40px; 44 | margin-right: 15px; 45 | border-radius: 50%; 46 | border: 2px solid #fff6e6; 47 | } 48 | } 49 | .right { 50 | flex: 1; 51 | overflow: hidden; 52 | .tpoic-title { 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | white-space: nowrap; 56 | display: block; 57 | font-weight: 700; 58 | font-size: 1.8rem; 59 | color: #333; 60 | } 61 | .topic-bottom { 62 | display: -webkit-flex; 63 | display: flex; 64 | margin-top: 5px; 65 | .name { 66 | color: #626262; 67 | flex: 1; 68 | } 69 | .time { 70 | color: #80bd01; 71 | font-size: 1.2rem; 72 | } 73 | } 74 | } 75 | } 76 | .no-data { 77 | height: -webkit-calc(~'100vh - 270px'); 78 | height: calc(~'100vh - 270px'); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/views/newTopic.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/menu.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | -------------------------------------------------------------------------------- /src/vuex/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | Vue.use(Vuex); 4 | import {topicList, topicInfo, login, reply, messages, upReply} from '../apis/publicApi'; 5 | import {GET_TOPIC_LIST, UPDATE_TOPIC_LIST, GET_TOPIC_INFO, LOGIN, LOGIN_OUT, REPLY, TOOGLE_LOAD, TOOGLE_LIST_LOAD} from '../constants/mutationTypes'; 6 | 7 | const store = new Vuex.Store({ 8 | state: { 9 | topics: [], 10 | topicInfo: {}, 11 | userInfo: {}, 12 | showLoad: false, //页面等待效果 13 | showListLoad: false //list划到底后的等待效果 14 | }, 15 | 16 | mutations: { 17 | [GET_TOPIC_LIST](state, data) { 18 | state.topics = data; 19 | }, 20 | 21 | [UPDATE_TOPIC_LIST](state, data) { 22 | state.topics = [...state.topics, ...data]; 23 | }, 24 | 25 | [GET_TOPIC_INFO](state, data) { 26 | state.topicInfo = data; 27 | }, 28 | 29 | [LOGIN](state, data) { 30 | state.userInfo = data; 31 | }, 32 | 33 | [TOOGLE_LOAD](state, data) { 34 | if (data) { 35 | state.showLoad = data; 36 | } else { 37 | state.showLoad = !state.showLoad; 38 | } 39 | }, 40 | 41 | [TOOGLE_LIST_LOAD](state, data) { 42 | if (data) { 43 | state.showListLoad = data; 44 | } else { 45 | state.showListLoad = !state.showListLoad; 46 | } 47 | }, 48 | 49 | [LOGIN_OUT](state) { 50 | state.userInfo = {}; 51 | localStorage.removeItem('userInfo'); 52 | } 53 | }, 54 | 55 | actions: { 56 | [GET_TOPIC_LIST]({commit}, data) { 57 | commit(TOOGLE_LOAD, true); 58 | return topicList(data).then((res) => { 59 | if (res.success) { 60 | commit(TOOGLE_LOAD, false); 61 | commit(GET_TOPIC_LIST, res.data); 62 | } 63 | }) 64 | }, 65 | 66 | [UPDATE_TOPIC_LIST]({commit}, data) { 67 | commit(TOOGLE_LIST_LOAD, true); 68 | 69 | return topicList(data).then((res) => { 70 | if (res.success) { 71 | commit(TOOGLE_LIST_LOAD, false); 72 | commit(UPDATE_TOPIC_LIST, res.data) 73 | } 74 | }) 75 | }, 76 | 77 | [GET_TOPIC_INFO]({commit}, data) { 78 | commit(TOOGLE_LOAD, true); 79 | topicInfo(data).then((res) => { 80 | if (res.success) { 81 | commit(TOOGLE_LOAD, false); 82 | commit(GET_TOPIC_INFO, res.data) 83 | } 84 | }) 85 | }, 86 | 87 | [LOGIN]({commit}, data) { 88 | return login(data).then((res) => { 89 | if (res.success) { 90 | const user = { 91 | loginname: res.loginname, 92 | id: res.id, 93 | avatar_url: res.avatar_url, 94 | accesstoken: data.accesstoken 95 | } 96 | localStorage.setItem('userInfo', JSON.stringify(user)); 97 | commit(LOGIN, user); 98 | } 99 | }) 100 | }, 101 | 102 | [REPLY]({commit, dispatch}, data) { 103 | const topicId = data.topicId; 104 | delete data.topicId; 105 | reply(data, topicId).then((res) => { 106 | if (res.success) { 107 | dispatch(GET_TOPIC_INFO, topicId); 108 | } 109 | }) 110 | }, 111 | } 112 | }) 113 | 114 | export default store; -------------------------------------------------------------------------------- /src/views/message.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | -------------------------------------------------------------------------------- /src/views/user.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | -------------------------------------------------------------------------------- /src/styles/topic.less: -------------------------------------------------------------------------------- 1 | .topic { 2 | /*margin: 40px auto 0;*/ 3 | padding-top: 40px; 4 | .topic-list { 5 | li { 6 | border-bottom: 1px solid #f0f0f0; 7 | &:hover { 8 | background-color: #f5f5f5; 9 | } 10 | a { 11 | padding: 10px 15px; 12 | display: block; 13 | .top { 14 | padding: 5px 0; 15 | display: flex; 16 | span.normal { 17 | flex: 0 0 auto; //TODO:soulcm 为什么最后一个值flex-basis默认不是auto,还得自己设置 18 | background-color: #e5e5e5; 19 | padding: 2px 4px; 20 | border-radius: 3px; 21 | color: #999; 22 | font-size: 1.2rem; 23 | &.color { 24 | background-color: #80bd01; 25 | color: #fff; 26 | } 27 | } 28 | h3 { 29 | color: #000; 30 | flex: 1 1 auto; 31 | margin-left: 5px; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | white-space: nowrap; 35 | font-size: 1.6rem; 36 | } 37 | } 38 | .bottom { 39 | display: flex; 40 | align-items: center; 41 | .author { 42 | flex: 0 0 auto; 43 | width: 38px; 44 | height: 38px; 45 | margin-right: 15px; 46 | border: 1px solid #ddd; 47 | border-radius: 50%; 48 | background-size: cover; 49 | // background-image: url("https://avatars.githubusercontent.com/u/8791709?v=3&s=120"); 50 | } 51 | .info { 52 | flex: 1 1 auto; 53 | p { 54 | display: flex; 55 | font-size: 1.2rem; 56 | color: #778087; 57 | span { 58 | &:first-child { 59 | flex: 1 1 auto; 60 | } 61 | &:last-child { 62 | flex: 0 0 auto; 63 | } 64 | 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | .topic-title { 73 | padding: 5px; 74 | margin: 15px; 75 | font-size: 1.8rem; 76 | color: #2c3e50; 77 | line-height: 1.5; 78 | background-color: #f0f0f0; 79 | border-radius: 5px; 80 | } 81 | .author-info { 82 | display: -webkit-flex; 83 | display: flex; 84 | align-items: center; 85 | font-size: 1.2rem; 86 | color: #34495e; 87 | padding: 0 15px; 88 | img.avatar { 89 | width: 40px; 90 | height: 40px; 91 | border-radius: 50%; 92 | margin-right: 15px; 93 | } 94 | .center { 95 | flex: 1; 96 | .author, .info { 97 | display: block; 98 | padding: 5px 0; 99 | } 100 | } 101 | .right { 102 | .tag { 103 | color: #999; 104 | padding: 5px 6px; 105 | font-size: 1.2rem; 106 | border-radius: 4px; 107 | text-align: center; 108 | display: block; 109 | background-color: #e5e5e5; 110 | &.color { 111 | background-color: #80bd01; 112 | color: #fff; 113 | } 114 | } 115 | .name { 116 | padding: 5px 0; 117 | display: block; 118 | } 119 | } 120 | } 121 | .topic-content { 122 | padding: 15px; 123 | margin-top: 15px; 124 | border-bottom: 1px solid #d4d4d4; 125 | } 126 | .topic-reply { 127 | .topic-total { 128 | padding: 15px; 129 | border-bottom: 1px solid #d4d4d4; 130 | strong { 131 | color: #42b983; 132 | } 133 | } 134 | .reply-list { 135 | margin-top: 15px; 136 | li { 137 | padding: 15px; 138 | border-bottom: 1px solid #d4d4d4; 139 | .user { 140 | display: -webkit-flex; 141 | display: flex; 142 | .head { 143 | width: 45px; 144 | height: 45px; 145 | margin-right: 10px; 146 | display: inline-block; 147 | } 148 | .info { 149 | flex: 1; 150 | display: -webkit-flex; 151 | display: flex; 152 | align-items: center; 153 | .left { 154 | flex: 1; 155 | color: #626262; 156 | } 157 | .right { 158 | display: flex; 159 | align-items: center; 160 | .iconfont { 161 | font-size: 26px; 162 | &.icon-dianzan.uped { 163 | color: #80bd01; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | .reply-content { 170 | margin-top: 15px; 171 | img { 172 | max-width: 100%; 173 | border: 0; 174 | vertical-align: middle; 175 | } 176 | } 177 | &:last-child { 178 | border-bottom:none; 179 | } 180 | } 181 | } 182 | } 183 | .reply { 184 | margin: 0 15px; 185 | textarea { 186 | width: 100%; 187 | background-color: #fff; 188 | font-size: 14px; //设置rem单位对textarea无效 189 | padding: 15px; 190 | color: #313131; 191 | border: 1px solid #d5dbdb; 192 | } 193 | .btn-reply { 194 | border-bottom: 2px solid #3aa373; 195 | background-color: #4fc08d; 196 | font-size: 1.6rem; 197 | margin: 15px 0; 198 | color: #fff; 199 | padding: 10px; 200 | width: 100%; 201 | border-radius: 3px; 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /src/views/topic.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | -------------------------------------------------------------------------------- /src/views/topicList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 76 | 77 | --------------------------------------------------------------------------------