├── static └── .gitkeep ├── config ├── prod.env.js ├── test.env.js ├── dev.env.js └── index.js ├── clipscreen ├── 0.jpg ├── 1.jpg ├── 10.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg └── 9.jpg ├── src ├── assets │ ├── logo.png │ ├── menu.png │ └── vue.png ├── App.vue ├── constants │ └── mutationTypes.js ├── components │ ├── loading.vue │ ├── backTop.vue │ ├── header.vue │ ├── reply.vue │ ├── userInfo.vue │ └── menu.vue ├── main.js ├── style │ ├── login.less │ ├── login.css │ ├── loading.less │ ├── loading.css │ ├── menu.less │ ├── menu.css │ ├── iconfont │ │ └── iconfont.css │ ├── publishTopic.less │ ├── header.less │ ├── publishTopic.css │ ├── header.css │ ├── index.less │ ├── message.css │ ├── index.css │ ├── message.less │ ├── topic.less │ ├── user.less │ ├── user.css │ └── topic.css ├── router │ └── index.js ├── api │ ├── publicApi.js │ └── index.js ├── utils │ └── filter.js ├── page │ ├── publishTopic.vue │ ├── login.vue │ ├── message.vue │ ├── index.vue │ ├── user.vue │ └── topic.vue └── vuex │ └── store.js ├── test ├── unit │ ├── .eslintrc │ ├── specs │ │ └── Hello.spec.js │ ├── index.js │ └── karma.conf.js └── e2e │ ├── specs │ └── test.js │ ├── custom-assertions │ └── elementCount.js │ ├── runner.js │ └── nightwatch.conf.js ├── .gitignore ├── .editorconfig ├── .postcssrc.js ├── .babelrc ├── index.html ├── README.md └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /clipscreen/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/0.jpg -------------------------------------------------------------------------------- /clipscreen/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/1.jpg -------------------------------------------------------------------------------- /clipscreen/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/10.jpg -------------------------------------------------------------------------------- /clipscreen/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/2.jpg -------------------------------------------------------------------------------- /clipscreen/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/3.jpg -------------------------------------------------------------------------------- /clipscreen/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/4.jpg -------------------------------------------------------------------------------- /clipscreen/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/5.jpg -------------------------------------------------------------------------------- /clipscreen/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/6.jpg -------------------------------------------------------------------------------- /clipscreen/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/7.jpg -------------------------------------------------------------------------------- /clipscreen/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/8.jpg -------------------------------------------------------------------------------- /clipscreen/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/clipscreen/9.jpg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/src/assets/menu.png -------------------------------------------------------------------------------- /src/assets/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandisen/cnode-vue/HEAD/src/assets/vue.png -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | test/e2e/reports 9 | selenium-debug.log 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /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'; 9 | 10 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '@/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import $ from 'webpack-zepto' 7 | import filter from './utils/filter.js'; 8 | //注册全局组件 9 | Vue.prototype.$filter = filter; 10 | Vue.config.productionTip = false 11 | 12 | /* eslint-disable no-new */ 13 | new Vue({ 14 | el: '#app', 15 | router, 16 | template: '', 17 | components: { App } 18 | }) 19 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /src/style/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 #80bd01; 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: #80bd01; 18 | font-size: 1.6rem; 19 | margin:15px 0; 20 | color:#fff; 21 | padding:10px; 22 | border-radius: 3px; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/style/login.css: -------------------------------------------------------------------------------- 1 | .login { 2 | padding-top: 40px; 3 | } 4 | .login .login_token { 5 | padding: 0 15px; 6 | margin-top: 50px; 7 | } 8 | .login .login_token input { 9 | padding: 12px 0; 10 | border-bottom: 1px solid #80bd01; 11 | background-color: transparent; 12 | font-size: 1.4rem; 13 | color: #313131; 14 | width: 100%; 15 | } 16 | .login .login_token .btn_login { 17 | width: 100%; 18 | border-bottom: 2px solid #3aa373; 19 | background-color: #80bd01; 20 | font-size: 1.6rem; 21 | margin: 15px 0; 22 | color: #fff; 23 | padding: 10px; 24 | border-radius: 3px; 25 | } 26 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/style/loading.less: -------------------------------------------------------------------------------- 1 | .loading{ 2 | width:120px; 3 | margin:5px auto; 4 | text-align: center; 5 | .icon-loading{ 6 | color:#ccc; 7 | display: inline-block; 8 | font-size: 5rem; 9 | -webkit-animation: loading 1s infinite linear; 10 | animation: loding 1s infinite linear; 11 | } 12 | @keyframes loading{ 13 | 0% { 14 | -webkit-transform:rotate(0deg); 15 | transform: rotate(0deg); 16 | } 17 | 100%{ 18 | -webkit-transform:rotate(360deg); 19 | transform: rotate(360deg); 20 | } 21 | } 22 | @-webkit-keyframes loading{ 23 | 0% { 24 | -webkit-transform:rotate(0deg); 25 | transform: rotate(0deg); 26 | } 27 | 100%{ 28 | -webkit-transform:rotate(360deg); 29 | transform: rotate(360deg); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/style/loading.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | width: 120px; 3 | margin: 5px auto; 4 | text-align: center; 5 | } 6 | .loading .icon-loading { 7 | color: #ccc; 8 | display: inline-block; 9 | font-size: 5rem; 10 | -webkit-animation: loading 1s infinite linear; 11 | animation: loding 1s infinite linear; 12 | } 13 | @keyframes loading { 14 | 0% { 15 | -webkit-transform: rotate(0deg); 16 | transform: rotate(0deg); 17 | } 18 | 100% { 19 | -webkit-transform: rotate(360deg); 20 | transform: rotate(360deg); 21 | } 22 | } 23 | @-webkit-keyframes loading { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(360deg); 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/style/menu.less: -------------------------------------------------------------------------------- 1 | nav.menu_bar{ 2 | position:fixed; 3 | top:40px; 4 | bottom:0; 5 | left:-180px; 6 | width:180px; 7 | background-color: #fff; 8 | transition: all .3s ease; 9 | z-index:10; 10 | &.show{ 11 | transform:translateX(180px); 12 | } 13 | .menu_list{ 14 | borer-top:1px solid #80bd01; 15 | li.menu_item{ 16 | a:hover{ 17 | background-color: #80bd01; 18 | color:#fff; 19 | } 20 | a{ 21 | display: block; 22 | padding:14px 24px; 23 | font-size: 1.4rem; 24 | text-decoration: none; 25 | i.iconfont{ 26 | margin-right:30px; 27 | color:#333; 28 | } 29 | .message_count { 30 | position: absolute; 31 | color: red; 32 | left: 40px; 33 | top: 15px; 34 | } 35 | } 36 | &:nth-of-type(6){ 37 | border-top:1px solid #80bd01; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/style/menu.css: -------------------------------------------------------------------------------- 1 | nav.menu_bar { 2 | position: fixed; 3 | top: 40px; 4 | bottom: 0; 5 | left: -180px; 6 | width: 180px; 7 | background-color: #fff; 8 | transition: all .3s ease; 9 | z-index: 10; 10 | } 11 | nav.menu_bar.show { 12 | transform: translateX(180px); 13 | } 14 | nav.menu_bar .menu_list { 15 | borer-top: 1px solid #80bd01; 16 | } 17 | nav.menu_bar .menu_list li.menu_item a:hover { 18 | background-color: #80bd01; 19 | color: #fff; 20 | } 21 | nav.menu_bar .menu_list li.menu_item a { 22 | display: block; 23 | padding: 14px 24px; 24 | font-size: 1.4rem; 25 | text-decoration: none; 26 | } 27 | nav.menu_bar .menu_list li.menu_item a i.iconfont { 28 | margin-right: 30px; 29 | color: #333; 30 | } 31 | nav.menu_bar .menu_list li.menu_item a .message_count { 32 | position: absolute; 33 | color: red; 34 | left: 40px; 35 | top: 15px; 36 | } 37 | nav.menu_bar .menu_list li.menu_item:nth-of-type(6) { 38 | border-top: 1px solid #80bd01; 39 | } 40 | -------------------------------------------------------------------------------- /src/style/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/style/publishTopic.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-sizing: border-box; 26 | box-shadow: 0 0 2px rgba(60,60,60,.5); 27 | font-size: 1.4rem; 28 | padding:5px; 29 | } 30 | } 31 | .content{ 32 | padding:15px 20px; 33 | textarea{ 34 | width:100%; 35 | border:1px solid rgb(60,60,60); 36 | border-radius: 3px; 37 | box-sizing: border-box; 38 | padding:5px; 39 | font-size: 16px; 40 | } 41 | } 42 | button{ 43 | display: block; 44 | margin:0 20px; 45 | background-color: #80bd01; 46 | padding:8px 15px; 47 | border-radius: 5px; 48 | color:#fff; 49 | } 50 | } -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/style/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,.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 | line-height: 40px; 17 | box-shadow: 0 0 4px rgba(0,0,0,.25); 18 | z-index:6; 19 | background-color: rgba(255,255,255,1); 20 | transition:all .3s ease; 21 | display: flex; 22 | display: -webkit-flex; 23 | align-items: center; 24 | .menu_btn{ 25 | width:44px; 26 | height:40px; 27 | background: url("../assets/menu.png") center center no-repeat; 28 | background-size: 24px; 29 | } 30 | .header_title{ 31 | flex:1; 32 | text-align:center; 33 | display: flex; 34 | display: -webkit-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("../assets/vue.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 | } 53 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import Index from '@/page/index' 5 | import Topic from '@/page/topic' 6 | import PublishTopic from '@/page/publishTopic' 7 | import Login from '@/page/login' 8 | import User from '@/page/user' 9 | import Message from '@/page/message' 10 | Vue.use(Router) 11 | 12 | export default new Router({ 13 | routes: [ 14 | { 15 | path: '/', 16 | redirect: {name: 'index'} 17 | }, 18 | { 19 | path: '/', 20 | name: 'index', 21 | component: Index 22 | }, 23 | { 24 | path: '/topic/:id', 25 | name: 'topic', 26 | component: Topic 27 | }, 28 | { 29 | path: '/create', 30 | name: 'create', 31 | component: PublishTopic, 32 | meta: { requiresAuth: true } 33 | }, 34 | { 35 | path: '/login', 36 | name: 'login', 37 | component: Login 38 | }, 39 | { 40 | path: '/user/:loginname', 41 | name: 'user', 42 | component: User 43 | }, 44 | { 45 | path: '/message', 46 | name: 'message', 47 | component: Message, 48 | meta: { requiresAuth: true } 49 | } 50 | ] 51 | }) 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vuedemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/style/publishTopic.css: -------------------------------------------------------------------------------- 1 | .topic_create { 2 | padding-top: 40px; 3 | } 4 | .topic_create .category { 5 | border-bottom: 1px solid #d4d4d4; 6 | padding: 15px 20px; 7 | } 8 | .topic_create .category span { 9 | display: inline-block; 10 | } 11 | .topic_create .category select { 12 | width: 100px; 13 | border: 1px solid #a9a9a9; 14 | height: 30px; 15 | border-radius: 3px; 16 | font-size: 1.6rem; 17 | padding: 3px; 18 | } 19 | .topic_create .title { 20 | padding: 15px 20px; 21 | border-bottom: 1px solid #d4d4d4; 22 | } 23 | .topic_create .title input { 24 | width: 100%; 25 | height: 30px; 26 | border-radius: 5px; 27 | box-sizing: border-box; 28 | box-shadow: 0 0 2px rgba(60, 60, 60, 0.5); 29 | font-size: 1.4rem; 30 | padding: 5px; 31 | } 32 | .topic_create .content { 33 | padding: 15px 20px; 34 | } 35 | .topic_create .content textarea { 36 | width: 100%; 37 | border: 1px solid #3c3c3c; 38 | border-radius: 3px; 39 | box-sizing: border-box; 40 | padding: 5px; 41 | font-size: 16px; 42 | } 43 | .topic_create button { 44 | display: block; 45 | margin: 0 20px; 46 | background-color: #80bd01; 47 | padding: 8px 15px; 48 | border-radius: 5px; 49 | color: #fff; 50 | } 51 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | server.ready.then(() => { 6 | // 2. run the nightwatch test suite against it 7 | // to run in additional browsers: 8 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 9 | // 2. add it to the --env flag below 10 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 11 | // For more information on Nightwatch's config file, see 12 | // http://nightwatchjs.org/guide#settings-file 13 | var opts = process.argv.slice(2) 14 | if (opts.indexOf('--config') === -1) { 15 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 16 | } 17 | if (opts.indexOf('--env') === -1) { 18 | opts = opts.concat(['--env', 'chrome']) 19 | } 20 | 21 | var spawn = require('cross-spawn') 22 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 23 | 24 | runner.on('exit', function (code) { 25 | server.close() 26 | process.exit(code) 27 | }) 28 | 29 | runner.on('error', function (err) { 30 | server.close() 31 | throw err 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuedemo 2 | 3 | > vue2.0+vue-router2.0+webpack+npm+es6 4 | 5 | ![](./clipscreen/0.jpg) 6 | 7 | 知识点: 8 | * vue2 9 | * vue-router2 10 | * 移动端开发 11 | * es6 12 | * less 13 | 14 | 项目还在持续更新中,以后计划: 15 | 1. 优化css 16 | 2. 优化router 17 | 3. 使用vuex状态管理器 18 | 4. 加入transition效果 19 | 20 | ## Build Setup 21 | 22 | ``` bash 23 | # install dependencies 24 | npm install 25 | 26 | # serve with hot reload at localhost:8080 27 | npm run dev 28 | 29 | # build for production with minification 30 | npm run build 31 | 32 | # build for production and view the bundle analyzer report 33 | npm run build --report 34 | 35 | # run unit tests 36 | npm run unit 37 | 38 | # run e2e tests 39 | npm run e2e 40 | 41 | # run all tests 42 | npm test 43 | ``` 44 | 45 | 访问 localhost:8090 46 | 47 | ![全部](./clipscreen/1.jpg) 48 | ![精华](./clipscreen/3.jpg) 49 | ![详情](./clipscreen/4.jpg) 50 | ![未登录](./clipscreen/7.jpg) 51 | ![登录](./clipscreen/8.jpg) 52 | ![已登录](./clipscreen/2.jpg) 53 | ![点赞评论](./clipscreen/5.jpg) 54 | ![发布新话题](./clipscreen/6.jpg) 55 | ![用户中心](./clipscreen/9.jpg) 56 | ![消息](./clipscreen/10.jpg) 57 | 58 | # vue-cnode 59 | 60 | 学习vue.js前后端分离开发,熟悉并掌握vue2.0+vue-router2.0+webpack+npm+es6快速搭建项目框架,利用cnode社区提供的api实现话题列表,话题详情,发布话题,发表评论,点赞点踩等诸多功能。本人初涉vue,代码拙劣,大神请略过。如果对您有帮助,就给个star鼓励一下吧。 61 | -------------------------------------------------------------------------------- /src/style/header.css: -------------------------------------------------------------------------------- 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 | line-height: 40px; 17 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); 18 | z-index: 6; 19 | background-color: #ffffff; 20 | transition: all .3s ease; 21 | display: flex; 22 | display: -webkit-flex; 23 | align-items: center; 24 | } 25 | .header_bar .menu_btn { 26 | width: 44px; 27 | height: 40px; 28 | background: url("../assets/menu.png") center center no-repeat; 29 | background-size: 24px; 30 | } 31 | .header_bar .header_title { 32 | flex: 1; 33 | text-align: center; 34 | display: flex; 35 | display: -webkit-flex; 36 | align-items: center; 37 | justify-content: center; 38 | font-size: 1.6rem; 39 | } 40 | .header_bar .header_title .vue_logo { 41 | width: 40px; 42 | height: 40px; 43 | background: url("../assets/vue.png") center center no-repeat; 44 | background-size: 24px; 45 | } 46 | .header_bar a.publish_btn { 47 | color: #42b983; 48 | height: 40px; 49 | line-height: 40px; 50 | padding: 0 15px; 51 | display: block; 52 | } 53 | -------------------------------------------------------------------------------- /src/api/publicApi.js: -------------------------------------------------------------------------------- 1 | /****ES6语法尚不熟悉,所以暂不使用,留作以后优化*******/ 2 | 3 | import fetchApi from './index'; 4 | 5 | export const topicList = (data) => { 6 | return fetchApi({ 7 | url: '/v1/topics', 8 | body: data 9 | }) 10 | } 11 | 12 | export const topicInfo = (id) => { 13 | return fetchApi({ 14 | url: '/v1/topic/' + id 15 | }) 16 | } 17 | 18 | export const login = (data) => { 19 | return fetchApi({ 20 | url: 'v1/accesstoken', 21 | method: 'post', 22 | body: data 23 | }) 24 | } 25 | 26 | export const reply = (data, id) => { 27 | return fetchApi({ 28 | url: 'v1/topic/${id}/replies', 29 | method: 'post', 30 | body: data 31 | }) 32 | } 33 | 34 | export const messageCount = (data) => { 35 | return fetchApi({ 36 | url: 'v1/message/count', 37 | body: data 38 | }) 39 | } 40 | 41 | export const messages = (data) => { 42 | return fetchApi({ 43 | url: 'v1/messages', 44 | body: data 45 | }) 46 | } 47 | 48 | export const upReply = (data, id) => { 49 | return fetchApi({ 50 | url: 'v1/reply/${id}/ups', 51 | method: 'post', 52 | body: data 53 | }) 54 | } 55 | 56 | export const addTopic = (data) => { 57 | return fetchApi({ 58 | url: 'v1/topics', 59 | method: 'post', 60 | body: data 61 | }) 62 | } 63 | 64 | export const getUserInfo = (loginname) => { 65 | return fetchApi({ 66 | url: 'v1/user/${loginname}' 67 | }) 68 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8090, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/backTop.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/utils/filter.js: -------------------------------------------------------------------------------- 1 | function formatTime(date) { 2 | var year = date.getFullYear() 3 | var month = date.getMonth() + 1 4 | var day = date.getDate() 5 | 6 | var hour = date.getHours() 7 | var minute = date.getMinutes() 8 | var second = date.getSeconds() 9 | 10 | 11 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 12 | } 13 | 14 | function formatNumber(n) { 15 | n = n.toString() 16 | return n[1] ? n : '0' + n 17 | } 18 | 19 | function getDateDiff(dateTimeStamp){ 20 | var result=""; 21 | var minute = 1000 * 60; 22 | var hour = minute * 60; 23 | var day = hour * 24; 24 | var halfmonth = day * 15; 25 | var month = day * 30; 26 | var year = day * 365; 27 | var now = new Date().getTime(); 28 | var diffValue= now -dateTimeStamp; 29 | if(diffValue < 0){ 30 | return '数据错误'; 31 | } 32 | var yearC =diffValue / year; 33 | var monthC =diffValue / month; 34 | var weekC = diffValue / (7 * day); 35 | var dayC = diffValue / day; 36 | var hourC =diffValue / hour; 37 | var minC = diffValue /minute; 38 | if(yearC >= 1){ 39 | result = parseInt(yearC) + '年以前'; 40 | }else if(monthC >= 1){ 41 | result = parseInt(monthC) + '个月前'; 42 | }else if(weekC >= 1){ 43 | result = parseInt(weekC) + '星期前'; 44 | }else if(dayC >= 1){ 45 | result = parseInt(dayC) + '天前'; 46 | }else if(hourC >= 1){ 47 | result = parseInt(hourC) + '小时前'; 48 | }else if(minC >= 5){ 49 | result = parseInt(minC) + '分钟前'; 50 | }else{ 51 | result = '刚刚发表'; 52 | } 53 | return result; 54 | } 55 | export default { 56 | formatTime: formatTime, 57 | getDateDiff:getDateDiff 58 | } -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | body,html{ 2 | font-family:"Microsoft YaHei",Source Sans Pro, Helvetica Neue,Roboto, Lato, sans-serif; 3 | font-size:10px; 4 | } 5 | body,html,ul,ol,li,dl,dd,dt,h1,h2,h3,h4,h5,h6,p,span,button,a,input,textarea,select{ 6 | margin:0; 7 | padding:0; 8 | list-style-type:none; 9 | text-decoration: none; 10 | color:#000; 11 | border:none; 12 | -webkit-appearance:none; 13 | -webkit-tap-highlight-color: rgba(0,0,0,0); 14 | outline: none; 15 | } 16 | .logo{ 17 | width:100%; 18 | background-color: #444; 19 | display: block;//消除img的缝隙 20 | } 21 | .tabbar{ 22 | height:30px; 23 | line-height: 30px; 24 | font-size: 1.6rem; 25 | background-color: #444; 26 | .tabbar_item{ 27 | width:20%; 28 | display: inline-block; 29 | text-align: center; 30 | color:#80bd01; 31 | } 32 | } 33 | .topic{ 34 | font-size:1.4rem; 35 | margin-top:40px; 36 | .topic_list{ 37 | padding:10px; 38 | border-bottom: 1px solid #80bd01; 39 | overflow: hidden; 40 | .topic_author{ 41 | float:left; 42 | width:50px; 43 | height:50px; 44 | border-radius: 50%; 45 | display: inline-block; 46 | vertical-align: top; 47 | margin-right:10px; 48 | } 49 | .topic_msg{ 50 | color:#778087; 51 | overflow: hidden; 52 | margin:5px 0; 53 | .topic_create_at{ 54 | float: right; 55 | } 56 | } 57 | } 58 | .topic_title{ 59 | .topic_tab_top,.topic_tab{ 60 | font-size:1rem; 61 | float:left; 62 | border:1px solid red; 63 | color:red; 64 | display: inline-block; 65 | padding:2px 5px; 66 | margin-right:5px; 67 | border-radius: 3px; 68 | } 69 | .topic_tab{ 70 | border:1px solid #80bd01; 71 | color:#80bd01; 72 | } 73 | .list_item{ 74 | line-height: 25px; 75 | margin-top:10px; 76 | font-size: 1.5rem; 77 | font-weight: bold; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/page/publishTopic.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/reply.vue: -------------------------------------------------------------------------------- 1 | 7 | 32 | -------------------------------------------------------------------------------- /src/style/message.css: -------------------------------------------------------------------------------- 1 | .message { 2 | padding-top: 40px; 3 | } 4 | .message .tab { 5 | display: flex; 6 | display: -webkit-flex; 7 | border-bottom: 1px solid #d4d4d4; 8 | overflow: hidden; 9 | } 10 | .message .tab .tab_item { 11 | flex: 1; 12 | border-right: 1px solid #d4d4d4; 13 | text-align: center; 14 | font-weight: 700; 15 | font-size: 1.6rem; 16 | padding: 10px 0; 17 | } 18 | .message .tab .tab_item.active { 19 | color: #80bd01; 20 | border-bottom: 2px solid #80bd01; 21 | } 22 | .message .tab .tab_item:last-child { 23 | border-right: none; 24 | } 25 | .message .message_content { 26 | padding: 10px 0; 27 | border-bottom: 1px solid #d4d4d4; 28 | } 29 | .message .message_content .author_info { 30 | display: -webkit-flex; 31 | display: flex; 32 | padding: 0 10px; 33 | margin: 10px 0; 34 | } 35 | .message .message_content .author_info .head { 36 | width: 40px; 37 | height: 40px; 38 | margin-right: 15px; 39 | } 40 | .message .message_content .author_info .info { 41 | flex: 1; 42 | display: -webkit-flex; 43 | display: flex; 44 | } 45 | .message .message_content .author_info .info .left { 46 | flex: 1; 47 | color: #626262; 48 | font-size: 1.6rem; 49 | } 50 | .message .message_content .author_info .info .right { 51 | color: #80bd01; 52 | font-size: 1.2rem; 53 | } 54 | .message .message_content .reply_content { 55 | padding: 0 15px; 56 | } 57 | .message .message_content a { 58 | display: block; 59 | margin: 0 15px; 60 | } 61 | .message .message_content a .topic_title { 62 | padding: 5px; 63 | font-size: 1.8rem; 64 | color: #2c3e50; 65 | background-color: #f0f0f0; 66 | border-radius: 5px; 67 | } 68 | .message .no_data { 69 | height: -webkit-calc(100vh - 90px); 70 | height: calc(100vh - 90px); 71 | text-align: center; 72 | color: #d4d4d4; 73 | font-size: 1.8rem; 74 | display: flex; 75 | justify-content: center; 76 | flex-direction: column; 77 | } 78 | .message .no_data .icon-empty { 79 | display: block; 80 | font-size: 12.5rem; 81 | color: #d4d4d4; 82 | } 83 | -------------------------------------------------------------------------------- /src/page/login.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 17 | 69 | -------------------------------------------------------------------------------- /src/components/userInfo.vue: -------------------------------------------------------------------------------- 1 | 19 | 53 | -------------------------------------------------------------------------------- /src/components/menu.vue: -------------------------------------------------------------------------------- 1 | 39 | 42 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | font-family: "Microsoft YaHei", Source Sans Pro, Helvetica Neue, Roboto, Lato, sans-serif; 4 | font-size: 10px; 5 | } 6 | body, 7 | html, 8 | ul, 9 | ol, 10 | li, 11 | dl, 12 | dd, 13 | dt, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | p, 21 | span, 22 | button, 23 | a, 24 | input, 25 | textarea, 26 | select { 27 | margin: 0; 28 | padding: 0; 29 | list-style-type: none; 30 | text-decoration: none; 31 | color: #000; 32 | border: none; 33 | -webkit-appearance: none; 34 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 35 | outline: none; 36 | } 37 | .logo { 38 | width: 100%; 39 | background-color: #444; 40 | display: block; 41 | } 42 | .tabbar { 43 | height: 30px; 44 | line-height: 30px; 45 | font-size: 1.6rem; 46 | background-color: #444; 47 | } 48 | .tabbar .tabbar_item { 49 | width: 20%; 50 | display: inline-block; 51 | text-align: center; 52 | color: #80bd01; 53 | } 54 | .topic { 55 | font-size: 1.4rem; 56 | margin-top: 40px; 57 | } 58 | .topic .topic_list { 59 | padding: 10px; 60 | border-bottom: 1px solid #80bd01; 61 | overflow: hidden; 62 | } 63 | .topic .topic_list .topic_author { 64 | float: left; 65 | width: 50px; 66 | height: 50px; 67 | border-radius: 50%; 68 | display: inline-block; 69 | vertical-align: top; 70 | margin-right: 10px; 71 | } 72 | .topic .topic_list .topic_msg { 73 | color: #778087; 74 | overflow: hidden; 75 | margin: 5px 0; 76 | } 77 | .topic .topic_list .topic_msg .topic_create_at { 78 | float: right; 79 | } 80 | .topic .topic_title .topic_tab_top, 81 | .topic .topic_title .topic_tab { 82 | font-size: 1rem; 83 | float: left; 84 | border: 1px solid red; 85 | color: red; 86 | display: inline-block; 87 | padding: 2px 5px; 88 | margin-right: 5px; 89 | border-radius: 3px; 90 | } 91 | .topic .topic_title .topic_tab { 92 | border: 1px solid #80bd01; 93 | color: #80bd01; 94 | } 95 | .topic .topic_title .list_item { 96 | line-height: 25px; 97 | margin-top: 10px; 98 | font-size: 1.5rem; 99 | font-weight: bold; 100 | } 101 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | /****ES6语法尚不熟悉,所以暂不使用,留作以后优化*******/ 2 | 3 | require('es6-promise').polyfill(); 4 | require('isomorphic-fetch'); 5 | 6 | const baseOpts = { 7 | method: 'get', 8 | headers: { 9 | 'Accept': 'application/json', 10 | 'Content-Type': 'application/json' 11 | }, 12 | // credentials: 'include', 13 | mode: 'cors' 14 | } 15 | 16 | const isObj = (obj) => { 17 | return obj && Object.prototype.toString.call(obj) === '[object Object]'; 18 | } 19 | 20 | const isFormData = (obj) => { 21 | return obj && Object.prototype.toString.call(obj) === '[object FormData]'; 22 | } 23 | 24 | 25 | 26 | const fetchApi = (cfg) => { 27 | let opts = Object.assign({}, baseOpts, cfg); 28 | const url = opts.url; 29 | delete opts.url; 30 | 31 | 32 | 33 | let fetchUrl = '/api' 34 | if (/^\//.test(url)) { 35 | fetchUrl += url 36 | } else { 37 | fetchUrl += '/' + url 38 | } 39 | 40 | if (opts.method.toLowerCase() !== 'get' && isObj(opts.body) && opts.headers['Content-Type'].indexOf('application/json') > -1) { 41 | opts.body = JSON.stringify(opts.body) 42 | } 43 | 44 | if (opts.method.toLowerCase() === 'get' && isObj(opts.body)) { 45 | fetchUrl += '?'; 46 | for (let key in opts.body) { 47 | let value = opts.body[key]; 48 | 49 | if (value instanceof Array) { 50 | value = JSON.stringify(value); 51 | } 52 | fetchUrl += key + '=' + value + '&'; 53 | } 54 | fetchUrl = fetchUrl.slice(0, -1); 55 | delete opts.body; 56 | } 57 | 58 | // fetchUrl = 'https://cnodejs.org' + fetchUrl; 59 | 60 | 61 | return new Promise((resolve, reject) => { 62 | fetch(fetchUrl, opts).then((res) => { 63 | const isSuccess = res.ok || res.status >= 200 && res.status < 300; 64 | if (isSuccess) { 65 | const data = res.headers.get('content-type') && res.headers.get('content-type').indexOf('json') >= 0 ? res.json() : res.text(); 66 | resolve(data); 67 | } else { 68 | throw res 69 | } 70 | }).catch((err) => { 71 | reject(); 72 | }) 73 | }) 74 | } 75 | 76 | export default fetchApi -------------------------------------------------------------------------------- /src/style/message.less: -------------------------------------------------------------------------------- 1 | .message { 2 | padding-top: 40px; 3 | .tab{ 4 | display: flex; 5 | display: -webkit-flex; 6 | border-bottom: 1px solid #d4d4d4; 7 | overflow: hidden; 8 | .tab_item{ 9 | flex:1; 10 | border-right:1px solid #d4d4d4; 11 | text-align: center; 12 | font-weight:700; 13 | font-size: 1.6rem; 14 | padding:10px 0; 15 | &.active{ 16 | color:#80bd01; 17 | border-bottom: 2px solid #80bd01; 18 | } 19 | &:last-child{ 20 | border-right:none; 21 | } 22 | } 23 | } 24 | .message_content { 25 | padding: 10px 0; 26 | border-bottom: 1px solid #d4d4d4; 27 | .author_info { 28 | display: -webkit-flex; 29 | display: flex; 30 | padding: 0 10px; 31 | margin: 10px 0; 32 | .head { 33 | width: 40px; 34 | height: 40px; 35 | margin-right: 15px; 36 | } 37 | .info { 38 | flex: 1; 39 | display: -webkit-flex; 40 | display: flex; 41 | .left { 42 | flex: 1; 43 | color: #626262; 44 | font-size: 1.6rem; 45 | } 46 | .right { 47 | color: #80bd01; 48 | font-size: 1.2rem; 49 | } 50 | } 51 | } 52 | .reply_content { 53 | padding: 0 15px; 54 | } 55 | a { 56 | display: block; 57 | margin: 0 15px; 58 | .topic_title { 59 | padding: 5px; 60 | font-size: 1.8rem; 61 | color: #2c3e50; 62 | background-color: #f0f0f0; 63 | border-radius: 5px; 64 | } 65 | } 66 | } 67 | .no_data { 68 | height: -webkit-calc(~'100vh - 90px'); 69 | height: calc(~'100vh - 90px'); 70 | text-align: center; 71 | color:#d4d4d4; 72 | font-size: 1.8rem; 73 | display: flex; 74 | justify-content: center; 75 | flex-direction: column; 76 | .icon-empty{ 77 | display: block; 78 | font-size: 12.5rem; 79 | color:#d4d4d4; 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/style/topic.less: -------------------------------------------------------------------------------- 1 | .topic_detail{ 2 | padding-top:40px; 3 | .topic_title{ 4 | padding:5px; 5 | margin:15px 10px; 6 | font-size: 1.8rem; 7 | color:#2c3e50; 8 | line-height: 1.5; 9 | background-color: #f0f0f0; 10 | border-radius: 5px; 11 | border-bottom: 2px solid #ddd; 12 | } 13 | .author_info{ 14 | display: -webkit-flex; 15 | display: flex; 16 | align-items: center; 17 | font-size: 1.2rem; 18 | color:#34495e; 19 | padding:0 10px; 20 | } 21 | img.avatar{ 22 | width:40px; 23 | height:40px; 24 | border-radius: 50%; 25 | margin-right:15px; 26 | } 27 | .center{ 28 | flex:1; 29 | .author,.info{ 30 | display: block; 31 | padding: 5px 0; 32 | } 33 | } 34 | .right{ 35 | .tag{ 36 | color:#fff; 37 | padding:5px 6px; 38 | font-size: 1.2rem; 39 | border-radius: 4px; 40 | text-align: center; 41 | display: block; 42 | background-color: #80bd01; 43 | } 44 | .name{ 45 | padding:5px 0; 46 | display:block; 47 | } 48 | } 49 | .topic_content{ 50 | padding:10px; 51 | margin-top:15px; 52 | border-bottom: 1px solid #d4d4d4; 53 | } 54 | .topic_reply{ 55 | .topic_total{ 56 | padding:10px; 57 | border-bottom: 1px solid #d4d4d4; 58 | font-size:1.6rem; 59 | strong{ 60 | color:#80bd01; 61 | } 62 | } 63 | .reply_list{ 64 | li{ 65 | padding:10px; 66 | border-bottom:1px solid #d4d4d4; 67 | .user{ 68 | display: -webkit-flex; 69 | display: flex; 70 | .head{ 71 | width:40px; 72 | height:40px; 73 | border-radius: 50%; 74 | margin-right:10px; 75 | display: inline-block; 76 | } 77 | .info{ 78 | font-size: 1.3rem; 79 | flex:1; 80 | display: -webkit-flex; 81 | display: flex; 82 | align-items: center; 83 | .left{ 84 | flex:1; 85 | display: -webkit-flex; 86 | display: flex; 87 | line-height: 20px; 88 | .name{ 89 | margin-right:10px; 90 | } 91 | } 92 | .right{ 93 | display: flex; 94 | display: -webkit-flex; 95 | line-height:20px; 96 | .uped{ 97 | color:#80bd01; 98 | } 99 | } 100 | .iconfont{ 101 | font-size: 18px; 102 | color:#333; 103 | } 104 | } 105 | } 106 | .reply_content{ 107 | margin-top:10px; 108 | img{ 109 | max-width: 100%; 110 | border:0; 111 | vertical-align: middle; 112 | } 113 | } 114 | } 115 | &:last-child{ 116 | border-bottom: none; 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/style/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 | border:3px solid #80bd01; 11 | display: block; 12 | margin:0 auto; 13 | } 14 | span.name{ 15 | font-size: 1.4rem; 16 | text-align: center; 17 | margin-top:5px; 18 | display: block; 19 | } 20 | .bottom{ 21 | margin-top: 10px; 22 | display: -webkit-flex; 23 | display: flex; 24 | .time{ 25 | text-align: center; 26 | flex:1; 27 | } 28 | .score{ 29 | text-align: center; 30 | color:#80bd01; 31 | flex:1; 32 | } 33 | } 34 | } 35 | .user_active{ 36 | .tab{ 37 | display: flex; 38 | display: -webkit-flex; 39 | border-bottom: 1px solid #d4d4d4; 40 | overflow: hidden; 41 | .tab_item{ 42 | flex:1; 43 | border-right:1px solid #d4d4d4; 44 | text-align: center; 45 | font-weight:700; 46 | font-size: 1.6rem; 47 | padding:10px 0; 48 | &.active{ 49 | color:#80bd01; 50 | border-bottom: 2px solid #80bd01; 51 | } 52 | &:last-child{ 53 | border-right:none; 54 | } 55 | } 56 | } 57 | .active_content{ 58 | display: -webkit-flex; 59 | display: flex; 60 | padding:10px; 61 | border-bottom: 1px solid #f0f0f0; 62 | .head{ 63 | img{ 64 | width:40px; 65 | height:40px; 66 | border-radius: 50%; 67 | margin-right:15px; 68 | border:2px solid #80bd01; 69 | } 70 | } 71 | .right{ 72 | flex:1; 73 | overflow: hidden; 74 | .topic_title{ 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | display: block; 79 | font-weight:700; 80 | font-size: 1.6rem; 81 | color:#333; 82 | } 83 | .topic_bottom{ 84 | display: -webkit-flex; 85 | display: flex; 86 | margin-top:5px; 87 | .name{ 88 | color:#626262; 89 | flex:1; 90 | } 91 | .time{ 92 | color:#80bd01; 93 | font-size: 1.2rem; 94 | } 95 | } 96 | } 97 | } 98 | .no_data { 99 | height: -webkit-calc(~'100vh - 90px'); 100 | height: calc(~'100vh - 90px'); 101 | text-align: center; 102 | color:#d4d4d4; 103 | font-size: 1.8rem; 104 | display: flex; 105 | justify-content: center; 106 | flex-direction: column; 107 | .icon-empty{ 108 | display: block; 109 | font-size: 12.5rem; 110 | color:#d4d4d4; 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/style/user.css: -------------------------------------------------------------------------------- 1 | .user_page { 2 | padding-top: 40px; 3 | } 4 | .user_page .info { 5 | background-color: #e7e7e7; 6 | padding: 15px 0; 7 | } 8 | .user_page .info img { 9 | border-radius: 50%; 10 | width: 100px; 11 | height: 100px; 12 | border: 3px solid #80bd01; 13 | display: block; 14 | margin: 0 auto; 15 | } 16 | .user_page .info span.name { 17 | font-size: 1.4rem; 18 | text-align: center; 19 | margin-top: 5px; 20 | display: block; 21 | } 22 | .user_page .info .bottom { 23 | margin-top: 10px; 24 | display: -webkit-flex; 25 | display: flex; 26 | } 27 | .user_page .info .bottom .time { 28 | text-align: center; 29 | flex: 1; 30 | } 31 | .user_page .info .bottom .score { 32 | text-align: center; 33 | color: #80bd01; 34 | flex: 1; 35 | } 36 | .user_page .user_active .tab { 37 | display: flex; 38 | display: -webkit-flex; 39 | border-bottom: 1px solid #d4d4d4; 40 | overflow: hidden; 41 | } 42 | .user_page .user_active .tab .tab_item { 43 | flex: 1; 44 | border-right: 1px solid #d4d4d4; 45 | text-align: center; 46 | font-weight: 700; 47 | font-size: 1.6rem; 48 | padding: 10px 0; 49 | } 50 | .user_page .user_active .tab .tab_item.active { 51 | color: #80bd01; 52 | border-bottom: 2px solid #80bd01; 53 | } 54 | .user_page .user_active .tab .tab_item:last-child { 55 | border-right: none; 56 | } 57 | .user_page .user_active .active_content { 58 | display: -webkit-flex; 59 | display: flex; 60 | padding: 10px; 61 | border-bottom: 1px solid #f0f0f0; 62 | } 63 | .user_page .user_active .active_content .head img { 64 | width: 40px; 65 | height: 40px; 66 | border-radius: 50%; 67 | margin-right: 15px; 68 | border: 2px solid #80bd01; 69 | } 70 | .user_page .user_active .active_content .right { 71 | flex: 1; 72 | overflow: hidden; 73 | } 74 | .user_page .user_active .active_content .right .topic_title { 75 | overflow: hidden; 76 | text-overflow: ellipsis; 77 | white-space: nowrap; 78 | display: block; 79 | font-weight: 700; 80 | font-size: 1.6rem; 81 | color: #333; 82 | } 83 | .user_page .user_active .active_content .right .topic_bottom { 84 | display: -webkit-flex; 85 | display: flex; 86 | margin-top: 5px; 87 | } 88 | .user_page .user_active .active_content .right .topic_bottom .name { 89 | color: #626262; 90 | flex: 1; 91 | } 92 | .user_page .user_active .active_content .right .topic_bottom .time { 93 | color: #80bd01; 94 | font-size: 1.2rem; 95 | } 96 | .user_page .user_active .no_data { 97 | height: -webkit-calc(100vh - 90px); 98 | height: calc(100vh - 90px); 99 | text-align: center; 100 | color: #d4d4d4; 101 | font-size: 1.8rem; 102 | display: flex; 103 | justify-content: center; 104 | flex-direction: column; 105 | } 106 | .user_page .user_active .no_data .icon-empty { 107 | display: block; 108 | font-size: 12.5rem; 109 | color: #d4d4d4; 110 | } 111 | -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | Vue.use(Vuex); 4 | 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 //滑动到底部加载时的效果 14 | }, 15 | mutations: { 16 | [GET_TOPIC_LIST](state,data) { 17 | state.topics = data; 18 | }, 19 | [UPDATE_TOPIC_LIST](state,data) { 20 | state.topics = [...state.topics, ...data]; 21 | }, 22 | [GET_TOPIC_INFO](state,data) { 23 | state.topicInfo = data; 24 | }, 25 | [LOGIN](state,data) { 26 | state.userInfo = data; 27 | }, 28 | [LOGIN_OUT](state) { 29 | state.userInfo = {}; 30 | localStorage.removeItem('userInfo'); 31 | }, 32 | [TOOGLE_LOAD](state,data) { 33 | if(data){ 34 | state.showLoad = data; 35 | } else { 36 | state.showLoad = !state.showLoad; 37 | } 38 | }, 39 | [TOOGLE_LIST_LOAD](state,data) { 40 | if(data){ 41 | state.showListLoad = data; 42 | } else { 43 | state.showListLoad = !state.showListLoad; 44 | } 45 | } 46 | }, 47 | actions: { 48 | [GET_TOPIC_LIST]({commit},data) { 49 | commit(TOOGLE_LOAD,true); 50 | return topicList(data).then((res) => { 51 | if(res.success){ 52 | commit(TOOGLE_LOAD,false); 53 | commit(GET_TOPIC_LIST, res.data); 54 | } 55 | }) 56 | }, 57 | 58 | [UPDATE_TOPIC_LIST]({commit},data) { 59 | commit(TOOGLE_LIST_LOAD,true); 60 | return topicList(data).then((res) => { 61 | if(res.success){ 62 | commit(TOOGLE_LIST_LOAD, false); 63 | commit(UPDATE_TOPIC_LIST, res.data); 64 | } 65 | }) 66 | }, 67 | 68 | [GET_TOPIC_INFO]({commit}, data) { 69 | commit(TOOGLE_LOAD, true); 70 | topicInfo(data).then((res) => { 71 | if (res.success) { 72 | commit(TOOGLE_LOAD, false); 73 | commit(GET_TOPIC_INFO, res.data) 74 | } 75 | }) 76 | }, 77 | 78 | [LOGIN]({commit}, data) { 79 | return login(data).then((res) => { 80 | if (res.success) { 81 | const user = { 82 | loginname: res.loginname, 83 | id: res.id, 84 | avatar_url: res.avatar_url, 85 | accesstoken: data.accesstoken 86 | } 87 | localStorage.setItem('userInfo', JSON.stringify(user)); 88 | commit(LOGIN, user); 89 | } 90 | }) 91 | }, 92 | 93 | [REPLY]({commit, dispatch}, data) { 94 | const topicId = data.topicId; 95 | delete data.topicId; 96 | reply(data, topicId).then((res) => { 97 | if (res.success) { 98 | dispatch(GET_TOPIC_INFO, topicId); 99 | } 100 | }) 101 | } 102 | } 103 | }) -------------------------------------------------------------------------------- /src/style/topic.css: -------------------------------------------------------------------------------- 1 | .topic_detail { 2 | padding-top: 40px; 3 | } 4 | .topic_detail .topic_title { 5 | padding: 5px; 6 | margin: 15px 10px; 7 | font-size: 1.8rem; 8 | color: #2c3e50; 9 | line-height: 1.5; 10 | background-color: #f0f0f0; 11 | border-radius: 5px; 12 | border-bottom: 2px solid #ddd; 13 | } 14 | .topic_detail .author_info { 15 | display: -webkit-flex; 16 | display: flex; 17 | align-items: center; 18 | font-size: 1.2rem; 19 | color: #34495e; 20 | padding: 0 10px; 21 | } 22 | .topic_detail img.avatar { 23 | width: 40px; 24 | height: 40px; 25 | border-radius: 50%; 26 | margin-right: 15px; 27 | } 28 | .topic_detail .center { 29 | flex: 1; 30 | } 31 | .topic_detail .center .author, 32 | .topic_detail .center .info { 33 | display: block; 34 | padding: 5px 0; 35 | } 36 | .topic_detail .right .tag { 37 | color: #fff; 38 | padding: 5px 6px; 39 | font-size: 1.2rem; 40 | border-radius: 4px; 41 | text-align: center; 42 | display: block; 43 | background-color: #80bd01; 44 | } 45 | .topic_detail .right .name { 46 | padding: 5px 0; 47 | display: block; 48 | } 49 | .topic_detail .topic_content { 50 | padding: 10px; 51 | margin-top: 15px; 52 | border-bottom: 1px solid #d4d4d4; 53 | } 54 | .topic_detail .topic_reply .topic_total { 55 | padding: 10px; 56 | border-bottom: 1px solid #d4d4d4; 57 | font-size: 1.6rem; 58 | } 59 | .topic_detail .topic_reply .topic_total strong { 60 | color: #80bd01; 61 | } 62 | .topic_detail .topic_reply .reply_list li { 63 | padding: 10px; 64 | border-bottom: 1px solid #d4d4d4; 65 | } 66 | .topic_detail .topic_reply .reply_list li .user { 67 | display: -webkit-flex; 68 | display: flex; 69 | } 70 | .topic_detail .topic_reply .reply_list li .user .head { 71 | width: 40px; 72 | height: 40px; 73 | border-radius: 50%; 74 | margin-right: 10px; 75 | display: inline-block; 76 | } 77 | .topic_detail .topic_reply .reply_list li .user .info { 78 | font-size: 1.3rem; 79 | flex: 1; 80 | display: -webkit-flex; 81 | display: flex; 82 | align-items: center; 83 | } 84 | .topic_detail .topic_reply .reply_list li .user .info .left { 85 | flex: 1; 86 | display: -webkit-flex; 87 | display: flex; 88 | line-height: 20px; 89 | } 90 | .topic_detail .topic_reply .reply_list li .user .info .left .name { 91 | margin-right: 10px; 92 | } 93 | .topic_detail .topic_reply .reply_list li .user .info .right { 94 | display: flex; 95 | display: -webkit-flex; 96 | line-height: 20px; 97 | } 98 | .topic_detail .topic_reply .reply_list li .user .info .right .uped { 99 | color: #80bd01; 100 | } 101 | .topic_detail .topic_reply .reply_list li .user .info .iconfont { 102 | font-size: 18px; 103 | color: #333; 104 | } 105 | .topic_detail .topic_reply .reply_list li .reply_content { 106 | margin-top: 10px; 107 | } 108 | .topic_detail .topic_reply .reply_list li .reply_content img { 109 | max-width: 100%; 110 | border: 0; 111 | vertical-align: middle; 112 | } 113 | .topic_detail .topic_reply .reply_list:last-child { 114 | border-bottom: none; 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuedemo", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "sandisen ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e" 14 | }, 15 | "dependencies": { 16 | "es6-promise": "^4.1.0", 17 | "github-markdown-css": "^2.6.0", 18 | "isomorphic-fetch": "^2.2.1", 19 | "vue": "^2.2.2", 20 | "vue-mui": "^2.0.0", 21 | "vue-resource": "^1.3.1", 22 | "vue-router": "^2.3.0", 23 | "vuex": "^2.2.1", 24 | "webpack-zepto": "^0.0.1" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^6.7.2", 28 | "babel-core": "^6.22.1", 29 | "babel-loader": "^6.2.10", 30 | "babel-plugin-istanbul": "^3.1.2", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-preset-env": "^1.2.1", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "babel-register": "^6.22.0", 35 | "chai": "^3.5.0", 36 | "chalk": "^1.1.3", 37 | "chromedriver": "^2.27.2", 38 | "connect-history-api-fallback": "^1.3.0", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "cross-env": "^3.1.4", 41 | "cross-spawn": "^5.0.1", 42 | "css-loader": "^0.26.1", 43 | "eventsource-polyfill": "^0.9.6", 44 | "express": "^4.14.1", 45 | "extract-text-webpack-plugin": "^2.0.0", 46 | "file-loader": "^0.10.0", 47 | "friendly-errors-webpack-plugin": "^1.1.3", 48 | "html-webpack-plugin": "^2.28.0", 49 | "http-proxy-middleware": "^0.17.3", 50 | "inject-loader": "^2.0.1", 51 | "karma": "^1.4.1", 52 | "karma-coverage": "^1.1.1", 53 | "karma-mocha": "^1.3.0", 54 | "karma-phantomjs-launcher": "^1.0.2", 55 | "karma-phantomjs-shim": "^1.4.0", 56 | "karma-sinon-chai": "^1.2.4", 57 | "karma-sourcemap-loader": "^0.3.7", 58 | "karma-spec-reporter": "0.0.26", 59 | "karma-webpack": "^2.0.2", 60 | "less": "^2.7.2", 61 | "less-loader": "^4.0.3", 62 | "lolex": "^1.5.2", 63 | "mocha": "^3.2.0", 64 | "nightwatch": "^0.9.12", 65 | "node-less": "^1.0.0", 66 | "opn": "^4.0.2", 67 | "optimize-css-assets-webpack-plugin": "^1.3.0", 68 | "ora": "^1.1.0", 69 | "phantomjs-prebuilt": "^2.1.14", 70 | "rimraf": "^2.6.0", 71 | "selenium-server": "^3.0.1", 72 | "semver": "^5.3.0", 73 | "shelljs": "^0.7.6", 74 | "sinon": "^1.17.7", 75 | "sinon-chai": "^2.8.0", 76 | "style-loader": "^0.16.1", 77 | "superagent": "^3.5.2", 78 | "url-loader": "^0.5.7", 79 | "vue-loader": "^11.1.4", 80 | "vue-style-loader": "^2.0.0", 81 | "vue-template-compiler": "^2.2.4", 82 | "webpack": "^2.2.1", 83 | "webpack-bundle-analyzer": "^2.2.1", 84 | "webpack-dev-middleware": "^1.10.0", 85 | "webpack-hot-middleware": "^2.16.1", 86 | "webpack-merge": "^2.6.1" 87 | }, 88 | "engines": { 89 | "node": ">= 4.0.0", 90 | "npm": ">= 3.0.0" 91 | }, 92 | "browserslist": [ 93 | "> 1%", 94 | "last 2 versions", 95 | "not ie <= 8" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/page/message.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /src/page/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 34 | -------------------------------------------------------------------------------- /src/page/user.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 43 | 44 | -------------------------------------------------------------------------------- /src/page/topic.vue: -------------------------------------------------------------------------------- 1 | 62 | 66 | --------------------------------------------------------------------------------