├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── configs ├── base.js ├── dev.js └── index.js ├── package-lock.json ├── package.json ├── server.js ├── shot └── QR-code.png ├── src ├── app.vue ├── components │ ├── content.vue │ ├── data-null.vue │ ├── footer.vue │ ├── header.vue │ ├── index.js │ └── loading.vue ├── css │ └── common.css ├── filters │ └── index.js ├── http │ └── index.js ├── iconfont │ ├── demo.css │ ├── demo_fontclass.html │ ├── demo_symbol.html │ ├── demo_unicode.html │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.js │ ├── iconfont.svg │ ├── iconfont.ttf │ └── iconfont.woff ├── less │ ├── common.less │ └── config.less ├── main.js ├── pages │ ├── about │ │ └── index.vue │ ├── index │ │ └── index.vue │ ├── self │ │ ├── home │ │ │ ├── headimg-bg.jpg │ │ │ └── index.vue │ │ └── messages │ │ │ └── index.vue │ ├── signin │ │ └── index.vue │ ├── signout │ │ └── index.vue │ ├── topic │ │ ├── create │ │ │ └── index.vue │ │ └── detail │ │ │ ├── index.vue │ │ │ └── reply-box.vue │ └── user │ │ └── username │ │ ├── headimg-bg.jpg │ │ ├── index.vue │ │ └── list.vue ├── router │ ├── index.js │ ├── router.js │ └── routes.js ├── template │ └── index.html ├── utils │ ├── index.js │ └── is-seeing.js └── vuet │ ├── index.js │ └── vuet.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "compact": true 7 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['standard'], 4 | env: { 5 | browser: true, 6 | }, 7 | plugins: [ 8 | 'html' 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | npm-debug.log 3 | package-lock.json 4 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 此项目除了正常的bug修复,不再进行功能更新 3 | 如果对状态管理感兴趣,可以看下 [Tms](https://github.com/FollowmeTech/tms),文档更齐全 4 | 5 | ### 前言 6 | 7 | 项目灵感的最初来源是[@shinygang](https://github.com/shinygang/Vue-cnodejs)来自的Vue-cnodejs, 8 | 感谢[cnodejs](https://cnodejs.org/)社区提供的API。 9 | github:[https://github.com/lzxb/vue-cnode](https://github.com/lzxb/vue-cnode) 10 | ### 感悟 11 | ``` 12 | 在vue-cnode升级vue2的时候,在公司内部已经有两个正式项目使用vue2, 13 | 遇到的一个最难的问题,就是如何能在页面后退时还原数据和滚动条位置, 14 | 虽然vue2内置了keep-alive组件,vue-router也提供了scrollBehavior方法进行设置, 15 | 但是仍然无法满足需求,后来阅读vue-router的源码发现, 16 | 每个页面都会自动在history.state对象中存储一个对应的key值, 17 | 便利用这个特性实现了页面后退时,数据和滚动条还原, 18 | 不过目前只是实现了页面的顶级组件还原, 19 | 如果需要对顶级组件下的子组件实现数据还原, 20 | 可以利用$options._scopeId来实现。 21 | 哈哈,具体如何实现就要靠大家自己发挥想象力了 22 | ``` 23 | 24 | ### 技术栈 25 | ``` 26 | 基于vue2 + vue-router + vuet + ES6 + less + flex.css重写vue版cnode社区,使用webpack打包 27 | ``` 28 | 29 | ### 使用项目 30 | ``` 31 | 1.克隆项目: git clone https://github.com/lzxb/vue-cnode.git 32 | 2.安装nodejs 33 | 3.安装依赖:     npm install 34 | 4.启动服务: npm run dev 35 | 5.发布代码:     npm run build 36 | ``` 37 | 38 | ### 功能 39 | - [x] 首页列表,上拉加载 40 | - [x] 主题详情,回复,点赞 41 | - [x] 消息列表 42 | - [x] 消息提醒 43 | - [x] 消息标记为已读 44 | - [x] 个人主页 45 | - [x] 用户信息 46 | - [x] 登录 47 | - [x] 退出 48 | - [x] 关于 49 | - [x] 页面后退,数据还原 50 | - [x] 页面后退,滚动位置还原 51 | - [x] ajax请求拦截器 52 | - [x] 页面跳转,不再执行此页面的ajax请求回调方法 53 | - [x] 启动图 54 | 55 | 56 | 57 | ### 项目目录说明 58 | ``` 59 | . 60 | |-- config // 项目开发环境配置 61 | | |-- index.js // 项目打包部署配置 62 | |-- src // 源码目录 63 | | |-- components // 公共组件 64 | |       |-- content.vue             // 页面内容公共组件 65 | |       |-- data-null.vue           // 数据为空时公共组件 66 | |       |-- footer.vue               // 底部导航栏公共组件 67 | |       |-- header.vue               // 页面头部公共组件 68 | |       |-- index.js                 // 加载各种公共组件 69 | |       |-- loading.vue              // 页面数据加载公共组件 70 | |   |-- config                       // 路由配置和程序的基本信息配置 71 | |       |-- config.js               // 配置项目的基本信息 72 | |       |-- routes.js               // 配置页面路由 73 | | |-- css // 各种css文件 74 | | |-- common.css // 全局通用css文件 75 | | |-- iconfont // 各种字体图标 76 | | |-- images // 公共图片 77 | | |-- less // 各种less文件 78 | | |-- common.less // 全局通用less文件 79 | |       |-- config.less             // 全局通用less配置文件 80 | |   |-- lib                        // 各种插件 81 | |       |-- route-data             // 实现页面后退数据还原,滚动位置还原 82 | |   |-- mixins                       // 各种全局mixins 83 | |       |-- pull-list.js             // 上拉加载 84 | | |-- pages // 各种页面组件 85 | | |-- about // 关于 86 | | |-- index // 首页 87 | | |-- login // 登录 88 | | |-- my // 我的主页,和消息列表 89 | | |-- signout // 退出 90 | | |-- topic // 主题详情,主题新建 91 | | |-- user // 查看用户资料 92 | | |-- store // vuex的状态管理 93 | | |-- index.js // 加载各种store模块 94 | | |-- user.js // 用户store 95 | | |-- template // 各种html文件 96 | | |-- index.html // 程序入口html文件 97 | | |-- utils // 公共的js方法 98 | | |-- app.vue // 页面入口文件 99 | | |-- main.js // 程序入口文件,加载各种公共组件 100 | |-- .babelrc // ES6语法编译配置 101 | |-- webpack.config.js // 程序打包配置 102 | |-- server.js // 开发时使用的服务器 103 | |-- README.md // 项目说明 104 | |-- package.json // 配置项目相关信息,通过执行 npm init 命令创建 105 | . 106 | ``` 107 | 108 | ### [扫一扫二维码查看效果](http://lzxb.github.io/vue-cnode/) 109 | [![扫一扫二维码查看效果](https://github.com/lzxb/vue-cnode/raw/master/shot/QR-code.png)](http://lzxb.github.io/vue-cnode/) 110 | -------------------------------------------------------------------------------- /configs/base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: 'https://cnodejs.org/', // api请求的目标网站 3 | base: '/vue-cnode/', // 路由根路径 4 | publicPath: '/vue-cnode/static/', // 程序文件在服务器所在的路径 5 | title: 'vue-cnode 中国最专业的 Node.js 开源技术社区' 6 | } 7 | -------------------------------------------------------------------------------- /configs/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dest: './dist/' // 程序打包后导出的目录 3 | } 4 | -------------------------------------------------------------------------------- /configs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.assign({}, require('./base'), require('./dev')) 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cnode", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "cross-env": "node_modules/.bin/cross-env", 8 | "dev": "node server", 9 | "build": "npm run cross-env NODE_ENV=production webpack -p" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/lzxb/vue-cnode.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/lzxb/vue-cnode/issues" 19 | }, 20 | "homepage": "https://github.com/lzxb/vue-cnode#readme", 21 | "devDependencies": { 22 | "autoprefixer": "^6.7.6", 23 | "babel-core": "^6.23.1", 24 | "babel-loader": "^6.3.2", 25 | "babel-preset-es2015": "^6.22.0", 26 | "babel-preset-stage-0": "^6.22.0", 27 | "connect-history-api-fallback": "^1.3.0", 28 | "cross-env": "^3.1.4", 29 | "css-loader": "^0.26.2", 30 | "eslint": "^3.17.0", 31 | "eslint-config-standard": "^8.0.0-beta.0", 32 | "eslint-loader": "^1.6.3", 33 | "eslint-plugin-html": "^2.0.1", 34 | "eslint-plugin-import": "^2.2.0", 35 | "eslint-plugin-node": "^4.1.0", 36 | "eslint-plugin-promise": "^3.5.0", 37 | "eslint-plugin-standard": "^2.1.1", 38 | "express": "^4.15.0", 39 | "extract-text-webpack-plugin": "^2.0.0", 40 | "file-loader": "^0.10.1", 41 | "html-webpack-plugin": "^2.28.0", 42 | "less": "^2.7.2", 43 | "less-loader": "^2.2.3", 44 | "postcss-loader": "^1.3.3", 45 | "url-loader": "^0.5.8", 46 | "vue-loader": "^11.1.4", 47 | "vue-style-loader": "^2.0.3", 48 | "vue-template-compiler": "^2.2.1", 49 | "webpack": "^2.2.1", 50 | "webpack-dev-middleware": "^1.10.1" 51 | }, 52 | "dependencies": { 53 | "babel-polyfill": "^6.23.0", 54 | "flex.css": "^1.1.7", 55 | "github-markdown-css": "^2.7.0", 56 | "is": "^3.2.1", 57 | "normalize.css": "^5.0.0", 58 | "vue": "^2.3.4", 59 | "vue-router": "^2.5.3", 60 | "vuet": "^1.0.0-rc.3", 61 | "vuet-route": "0.0.8", 62 | "vuet-scroll": "0.0.2", 63 | "whatwg-fetch": "^2.0.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const configs = require('./configs/') 2 | const express = require('express') 3 | const webpack = require('webpack') 4 | const webpackConfig = require('./webpack.config') 5 | 6 | const app = express() 7 | const compiler = webpack(webpackConfig) 8 | 9 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 10 | publicPath: webpackConfig.output.publicPath, 11 | stats: 'minimal' 12 | }) 13 | 14 | app.use(require('connect-history-api-fallback')({ 15 | index: `${configs.publicPath}../index.html` 16 | })) 17 | app.use(devMiddleware) 18 | 19 | app.listen(3000, (err) => { 20 | if (err) return console.log(err) 21 | console.log('http://localhost:3000/') 22 | }) -------------------------------------------------------------------------------- /shot/QR-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/shot/QR-code.png -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 3 | 6 | 28 | -------------------------------------------------------------------------------- /src/components/content.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 22 | -------------------------------------------------------------------------------- /src/components/data-null.vue: -------------------------------------------------------------------------------- 1 | 4 | 15 | 27 | -------------------------------------------------------------------------------- /src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 14 | 52 | 108 | -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 12 | 23 | 47 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import header from './header' 2 | import content from './content' 3 | import footer from './footer' 4 | import dataNull from './data-null' 5 | import loading from './loading' 6 | export default { header, content, footer, dataNull, loading } 7 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 8 | 35 | 69 | -------------------------------------------------------------------------------- /src/css/common.css: -------------------------------------------------------------------------------- 1 | .common-toast { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | z-index: 11000; 6 | width: auto; 7 | padding: 0 8px; 8 | line-height: 34px; 9 | border-radius: 5px; 10 | text-align: center; 11 | font-size: 14px; 12 | color: white; 13 | background: rgba(0, 0, 0, 0.7); 14 | display: none; 15 | } 16 | 17 | .common-toast-show { 18 | animation: commonToastShow ease 300ms; 19 | } 20 | 21 | .common-toast-hide { 22 | animation: commonToastHide ease 500ms; 23 | } 24 | 25 | @keyframes commonToastShow { 26 | 0% { 27 | opacity: 0; 28 | transform: translate3d(0, 0, 0) scale(0); 29 | } 30 | 100% { 31 | opacity: 1; 32 | transform: translate3d(0, 0, 0) scale(1); 33 | } 34 | } 35 | 36 | @keyframes commonToastHide { 37 | 0% { 38 | opacity: 1; 39 | transform: translate3d(0, 0, 0) scale(1); 40 | } 41 | 100% { 42 | opacity: 0; 43 | transform: translate3d(0, 0, 0) scale(0); 44 | } 45 | } -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | export const formatDate = (str) => { 2 | if (!str) return '' 3 | var date = new Date(str) 4 | var time = new Date().getTime() - date.getTime() // 现在的时间-传入的时间 = 相差的时间(单位 = 毫秒) 5 | if (time < 0) { 6 | return '' 7 | } else if ((time / 1000 < 30)) { 8 | return '刚刚' 9 | } else if (time / 1000 < 60) { 10 | return parseInt((time / 1000)) + '秒前' 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 | } 23 | -------------------------------------------------------------------------------- /src/http/index.js: -------------------------------------------------------------------------------- 1 | const API = 'https://cnodejs.org/api/v1' 2 | 3 | let accesstoken = () => (localStorage.getItem('vue_cnode_accesstoken') || '') 4 | 5 | const filter = (str) => { // 特殊字符转义 6 | str += '' // 隐式转换 7 | str = str.replace(/%/g, '%25') 8 | str = str.replace(/\+/g, '%2B') 9 | str = str.replace(/ /g, '%20') 10 | str = str.replace(/\//g, '%2F') 11 | str = str.replace(/\?/g, '%3F') 12 | str = str.replace(/&/g, '%26') 13 | str = str.replace(/=/g, '%3D') 14 | str = str.replace(/#/g, '%23') 15 | return str 16 | } 17 | const queryStr = (data) => { 18 | const query = [] 19 | if (!data.accesstoken) { 20 | data.accesstoken = accesstoken() 21 | } 22 | Object.keys(data).forEach((k) => query.push(`${k}=${filter(data[k])}`)) 23 | return query.join('&') 24 | } 25 | 26 | export default { 27 | async get (url, data = {}) { 28 | const search = queryStr(data) 29 | const arr = [`${API}${url}`] 30 | if (search) { 31 | arr.push(search) 32 | } 33 | return fetch(arr.join('?')).then(response => response.json()) 34 | }, 35 | async post (url, data = {}) { 36 | const body = queryStr(data) 37 | const arr = [`${API}${url}`] 38 | return fetch(arr.join('?'), { 39 | body, 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/x-www-form-urlencoded' 43 | } 44 | }).then(response => response.json()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/iconfont/demo.css: -------------------------------------------------------------------------------- 1 | *{margin: 0;padding: 0;list-style: none;} 2 | /* 3 | KISSY CSS Reset 4 | 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 5 | 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 6 | 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 7 | 特色:1. 适应中文;2. 基于最新主流浏览器。 8 | 维护:玉伯, 正淳 9 | */ 10 | 11 | /** 清除内外边距 **/ 12 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ 13 | dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ 14 | pre, /* text formatting elements 文本格式元素 */ 15 | form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ 16 | th, td /* table elements 表格元素 */ { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | /** 设置默认字体 **/ 22 | body, 23 | button, input, select, textarea /* for ie */ { 24 | font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; 25 | } 26 | h1, h2, h3, h4, h5, h6 { font-size: 100%; } 27 | address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ 28 | code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ 29 | small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ 30 | 31 | /** 重置列表元素 **/ 32 | ul, ol { list-style: none; } 33 | 34 | /** 重置文本格式元素 **/ 35 | a { text-decoration: none; } 36 | a:hover { text-decoration: underline; } 37 | 38 | 39 | /** 重置表单元素 **/ 40 | legend { color: #000; } /* for ie6 */ 41 | fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ 42 | button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ 43 | /* 注:optgroup 无法扶正 */ 44 | 45 | /** 重置表格元素 **/ 46 | table { border-collapse: collapse; border-spacing: 0; } 47 | 48 | /* 清除浮动 */ 49 | .ks-clear:after, .clear:after { 50 | content: '\20'; 51 | display: block; 52 | height: 0; 53 | clear: both; 54 | } 55 | .ks-clear, .clear { 56 | *zoom: 1; 57 | } 58 | 59 | .main { 60 | padding: 30px 100px; 61 | width: 960px; 62 | margin: 0 auto; 63 | } 64 | .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} 65 | 66 | .helps{margin-top:40px;} 67 | .helps pre{ 68 | padding:20px; 69 | margin:10px 0; 70 | border:solid 1px #e7e1cd; 71 | background-color: #fffdef; 72 | overflow: auto; 73 | } 74 | 75 | .icon_lists{ 76 | width: 100% !important; 77 | 78 | } 79 | 80 | .icon_lists li{ 81 | float:left; 82 | width: 100px; 83 | height:180px; 84 | text-align: center; 85 | list-style: none !important; 86 | } 87 | .icon_lists .icon{ 88 | font-size: 42px; 89 | line-height: 100px; 90 | margin: 10px 0; 91 | color:#333; 92 | -webkit-transition: font-size 0.25s ease-out 0s; 93 | -moz-transition: font-size 0.25s ease-out 0s; 94 | transition: font-size 0.25s ease-out 0s; 95 | 96 | } 97 | .icon_lists .icon:hover{ 98 | font-size: 100px; 99 | } 100 | 101 | 102 | 103 | .markdown { 104 | color: #666; 105 | font-size: 14px; 106 | line-height: 1.8; 107 | } 108 | 109 | .highlight { 110 | line-height: 1.5; 111 | } 112 | 113 | .markdown img { 114 | vertical-align: middle; 115 | max-width: 100%; 116 | } 117 | 118 | .markdown h1 { 119 | color: #404040; 120 | font-weight: 500; 121 | line-height: 40px; 122 | margin-bottom: 24px; 123 | } 124 | 125 | .markdown h2, 126 | .markdown h3, 127 | .markdown h4, 128 | .markdown h5, 129 | .markdown h6 { 130 | color: #404040; 131 | margin: 1.6em 0 0.6em 0; 132 | font-weight: 500; 133 | clear: both; 134 | } 135 | 136 | .markdown h1 { 137 | font-size: 28px; 138 | } 139 | 140 | .markdown h2 { 141 | font-size: 22px; 142 | } 143 | 144 | .markdown h3 { 145 | font-size: 16px; 146 | } 147 | 148 | .markdown h4 { 149 | font-size: 14px; 150 | } 151 | 152 | .markdown h5 { 153 | font-size: 12px; 154 | } 155 | 156 | .markdown h6 { 157 | font-size: 12px; 158 | } 159 | 160 | .markdown hr { 161 | height: 1px; 162 | border: 0; 163 | background: #e9e9e9; 164 | margin: 16px 0; 165 | clear: both; 166 | } 167 | 168 | .markdown p, 169 | .markdown pre { 170 | margin: 1em 0; 171 | } 172 | 173 | .markdown > p, 174 | .markdown > blockquote, 175 | .markdown > .highlight, 176 | .markdown > ol, 177 | .markdown > ul { 178 | width: 80%; 179 | } 180 | 181 | .markdown ul > li { 182 | list-style: circle; 183 | } 184 | 185 | .markdown > ul li, 186 | .markdown blockquote ul > li { 187 | margin-left: 20px; 188 | padding-left: 4px; 189 | } 190 | 191 | .markdown > ul li p, 192 | .markdown > ol li p { 193 | margin: 0.6em 0; 194 | } 195 | 196 | .markdown ol > li { 197 | list-style: decimal; 198 | } 199 | 200 | .markdown > ol li, 201 | .markdown blockquote ol > li { 202 | margin-left: 20px; 203 | padding-left: 4px; 204 | } 205 | 206 | .markdown code { 207 | margin: 0 3px; 208 | padding: 0 5px; 209 | background: #eee; 210 | border-radius: 3px; 211 | } 212 | 213 | .markdown pre { 214 | border-radius: 6px; 215 | background: #f7f7f7; 216 | padding: 20px; 217 | } 218 | 219 | .markdown pre code { 220 | border: none; 221 | background: #f7f7f7; 222 | margin: 0; 223 | } 224 | 225 | .markdown strong, 226 | .markdown b { 227 | font-weight: 600; 228 | } 229 | 230 | .markdown > table { 231 | border-collapse: collapse; 232 | border-spacing: 0px; 233 | empty-cells: show; 234 | border: 1px solid #e9e9e9; 235 | width: 95%; 236 | margin-bottom: 24px; 237 | } 238 | 239 | .markdown > table th { 240 | white-space: nowrap; 241 | color: #333; 242 | font-weight: 600; 243 | 244 | } 245 | 246 | .markdown > table th, 247 | .markdown > table td { 248 | border: 1px solid #e9e9e9; 249 | padding: 8px 16px; 250 | text-align: left; 251 | } 252 | 253 | .markdown > table th { 254 | background: #F7F7F7; 255 | } 256 | 257 | .markdown blockquote { 258 | font-size: 90%; 259 | color: #999; 260 | border-left: 4px solid #e9e9e9; 261 | padding-left: 0.8em; 262 | margin: 1em 0; 263 | font-style: italic; 264 | } 265 | 266 | .markdown blockquote p { 267 | margin: 0; 268 | } 269 | 270 | .markdown .anchor { 271 | opacity: 0; 272 | transition: opacity 0.3s ease; 273 | margin-left: 8px; 274 | } 275 | 276 | .markdown .waiting { 277 | color: #ccc; 278 | } 279 | 280 | .markdown h1:hover .anchor, 281 | .markdown h2:hover .anchor, 282 | .markdown h3:hover .anchor, 283 | .markdown h4:hover .anchor, 284 | .markdown h5:hover .anchor, 285 | .markdown h6:hover .anchor { 286 | opacity: 1; 287 | display: inline-block; 288 | } 289 | 290 | .markdown > br, 291 | .markdown > p > br { 292 | clear: both; 293 | } 294 | 295 | 296 | .hljs { 297 | display: block; 298 | background: white; 299 | padding: 0.5em; 300 | color: #333333; 301 | overflow-x: auto; 302 | } 303 | 304 | .hljs-comment, 305 | .hljs-meta { 306 | color: #969896; 307 | } 308 | 309 | .hljs-string, 310 | .hljs-variable, 311 | .hljs-template-variable, 312 | .hljs-strong, 313 | .hljs-emphasis, 314 | .hljs-quote { 315 | color: #df5000; 316 | } 317 | 318 | .hljs-keyword, 319 | .hljs-selector-tag, 320 | .hljs-type { 321 | color: #a71d5d; 322 | } 323 | 324 | .hljs-literal, 325 | .hljs-symbol, 326 | .hljs-bullet, 327 | .hljs-attribute { 328 | color: #0086b3; 329 | } 330 | 331 | .hljs-section, 332 | .hljs-name { 333 | color: #63a35c; 334 | } 335 | 336 | .hljs-tag { 337 | color: #333333; 338 | } 339 | 340 | .hljs-title, 341 | .hljs-attr, 342 | .hljs-selector-id, 343 | .hljs-selector-class, 344 | .hljs-selector-attr, 345 | .hljs-selector-pseudo { 346 | color: #795da3; 347 | } 348 | 349 | .hljs-addition { 350 | color: #55a532; 351 | background-color: #eaffea; 352 | } 353 | 354 | .hljs-deletion { 355 | color: #bd2c00; 356 | background-color: #ffecec; 357 | } 358 | 359 | .hljs-link { 360 | text-decoration: underline; 361 | } 362 | 363 | pre{ 364 | background: #fff; 365 | } 366 | 367 | 368 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /src/iconfont/demo_fontclass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 11 |
12 |

IconFont 图标

13 |
    14 | 15 |
  • 16 | 17 |
    首页
    18 |
    .icon-index
    19 |
  • 20 | 21 |
  • 22 | 23 |
    28 退出
    24 |
    .icon-signout
    25 |
  • 26 | 27 |
  • 28 | 29 |
    返回
    30 |
    .icon-back
    31 |
  • 32 | 33 |
  • 34 | 35 |
    分享
    36 |
    .icon-share
    37 |
  • 38 | 39 |
  • 40 | 41 |
    消息
    42 |
    .icon-msg
    43 |
  • 44 | 45 |
  • 46 | 47 |
    发表
    48 |
    .icon-edit
    49 |
  • 50 | 51 |
  • 52 | 53 |
    评论
    54 |
    .icon-comment
    55 |
  • 56 | 57 |
  • 58 | 59 |
    关于
    60 |
    .icon-about
    61 |
  • 62 | 63 |
  • 64 | 65 |
    回复
    66 |
    .icon-comment-topic
    67 |
  • 68 | 69 |
  • 70 | 71 |
    登录
    72 |
    .icon-user
    73 |
  • 74 | 75 |
  • 76 | 77 |
    问答
    78 |
    .icon-ask
    79 |
  • 80 | 81 |
  • 82 | 83 |
    招聘
    84 |
    .icon-job
    85 |
  • 86 | 87 |
  • 88 | 89 |
    置顶
    90 |
    .icon-topic-top
    91 |
  • 92 | 93 |
  • 94 | 95 |
    精华
    96 |
    .icon-topic-good
    97 |
  • 98 | 99 |
  • 100 | 101 |
    阅读
    102 |
    .icon-click
    103 |
  • 104 | 105 |
  • 106 | 107 |
    赞赞
    108 |
    .icon-comment-fabulous
    109 |
  • 110 | 111 |
  • 112 | 113 |
    精华1
    114 |
    .icon-good
    115 |
  • 116 | 117 |
  • 118 | 119 |
    返回
    120 |
    .icon-arrow-right
    121 |
  • 122 | 123 |
124 | 125 |

font-class引用

126 |
127 | 128 |

font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。

129 |

与unicode使用方式相比,具有如下特点:

130 |
    131 |
  • 兼容性良好,支持ie8+,及所有现代浏览器。
  • 132 |
  • 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
  • 133 |
  • 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
  • 134 |
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • 135 |
136 |

使用步骤如下:

137 |

第一步:引入项目下面生成的fontclass代码:

138 | 139 | 140 |
<link rel="stylesheet" type="text/css" href="./iconfont.css">
141 |

第二步:挑选相应图标并获取类名,应用于页面:

142 |
<i class="iconfont icon-xxx"></i>
143 |
144 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

145 |
146 |
147 | 148 | 149 | -------------------------------------------------------------------------------- /src/iconfont/demo_symbol.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 24 | 25 | 26 |
27 |

IconFont 图标

28 |
    29 | 30 |
  • 31 | 34 |
    首页
    35 |
    #icon-index
    36 |
  • 37 | 38 |
  • 39 | 42 |
    28 退出
    43 |
    #icon-signout
    44 |
  • 45 | 46 |
  • 47 | 50 |
    返回
    51 |
    #icon-back
    52 |
  • 53 | 54 |
  • 55 | 58 |
    分享
    59 |
    #icon-share
    60 |
  • 61 | 62 |
  • 63 | 66 |
    消息
    67 |
    #icon-msg
    68 |
  • 69 | 70 |
  • 71 | 74 |
    发表
    75 |
    #icon-edit
    76 |
  • 77 | 78 |
  • 79 | 82 |
    评论
    83 |
    #icon-comment
    84 |
  • 85 | 86 |
  • 87 | 90 |
    关于
    91 |
    #icon-about
    92 |
  • 93 | 94 |
  • 95 | 98 |
    回复
    99 |
    #icon-comment-topic
    100 |
  • 101 | 102 |
  • 103 | 106 |
    登录
    107 |
    #icon-user
    108 |
  • 109 | 110 |
  • 111 | 114 |
    问答
    115 |
    #icon-ask
    116 |
  • 117 | 118 |
  • 119 | 122 |
    招聘
    123 |
    #icon-job
    124 |
  • 125 | 126 |
  • 127 | 130 |
    置顶
    131 |
    #icon-topic-top
    132 |
  • 133 | 134 |
  • 135 | 138 |
    精华
    139 |
    #icon-topic-good
    140 |
  • 141 | 142 |
  • 143 | 146 |
    阅读
    147 |
    #icon-click
    148 |
  • 149 | 150 |
  • 151 | 154 |
    赞赞
    155 |
    #icon-comment-fabulous
    156 |
  • 157 | 158 |
  • 159 | 162 |
    精华1
    163 |
    #icon-good
    164 |
  • 165 | 166 |
  • 167 | 170 |
    返回
    171 |
    #icon-arrow-right
    172 |
  • 173 | 174 |
175 | 176 | 177 |

symbol引用

178 |
179 | 180 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 181 | 这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:

182 |
    183 |
  • 支持多色图标了,不再受单色限制。
  • 184 |
  • 通过一些技巧,支持像字体那样,通过font-size,color来调整样式。
  • 185 |
  • 兼容性较差,支持 ie9+,及现代浏览器。
  • 186 |
  • 浏览器渲染svg的性能一般,还不如png。
  • 187 |
188 |

使用步骤如下:

189 |

第一步:引入项目下面生成的symbol代码:

190 |
<script src="./iconfont.js"></script>
191 |

第二步:加入通用css代码(引入一次就行):

192 |
<style type="text/css">
193 | .icon {
194 |    width: 1em; height: 1em;
195 |    vertical-align: -0.15em;
196 |    fill: currentColor;
197 |    overflow: hidden;
198 | }
199 | </style>
200 |

第三步:挑选相应图标并获取类名,应用于页面:

201 |
<svg class="icon" aria-hidden="true">
202 |   <use xlink:href="#icon-xxx"></use>
203 | </svg>
204 |         
205 |
206 | 207 | 208 | -------------------------------------------------------------------------------- /src/iconfont/demo_unicode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 29 | 30 | 31 |
32 |

IconFont 图标

33 |
    34 | 35 |
  • 36 | 37 |
    首页
    38 |
    &#xe61b;
    39 |
  • 40 | 41 |
  • 42 | 43 |
    28 退出
    44 |
    &#xe679;
    45 |
  • 46 | 47 |
  • 48 | 49 |
    返回
    50 |
    &#xe604;
    51 |
  • 52 | 53 |
  • 54 | 55 |
    分享
    56 |
    &#xe627;
    57 |
  • 58 | 59 |
  • 60 | 61 |
    消息
    62 |
    &#xe64c;
    63 |
  • 64 | 65 |
  • 66 | 67 |
    发表
    68 |
    &#xe60d;
    69 |
  • 70 | 71 |
  • 72 | 73 |
    评论
    74 |
    &#xe613;
    75 |
  • 76 | 77 |
  • 78 | 79 |
    关于
    80 |
    &#xe6ce;
    81 |
  • 82 | 83 |
  • 84 | 85 |
    回复
    86 |
    &#xe63c;
    87 |
  • 88 | 89 |
  • 90 | 91 |
    登录
    92 |
    &#xe603;
    93 |
  • 94 | 95 |
  • 96 | 97 |
    问答
    98 |
    &#xe605;
    99 |
  • 100 | 101 |
  • 102 | 103 |
    招聘
    104 |
    &#xe645;
    105 |
  • 106 | 107 |
  • 108 | 109 |
    置顶
    110 |
    &#xe68e;
    111 |
  • 112 | 113 |
  • 114 | 115 |
    精华
    116 |
    &#xe690;
    117 |
  • 118 | 119 |
  • 120 | 121 |
    阅读
    122 |
    &#xe601;
    123 |
  • 124 | 125 |
  • 126 | 127 |
    赞赞
    128 |
    &#xe62f;
    129 |
  • 130 | 131 |
  • 132 | 133 |
    精华1
    134 |
    &#xe612;
    135 |
  • 136 | 137 |
  • 138 | 139 |
    返回
    140 |
    &#xe6cf;
    141 |
  • 142 | 143 |
144 |

unicode引用

145 |
146 | 147 |

unicode是字体在网页端最原始的应用方式,特点是:

148 |
    149 |
  • 兼容性最好,支持ie6+,及所有现代浏览器。
  • 150 |
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 151 |
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
  • 152 |
153 |
154 |

注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式

155 |
156 |

unicode使用步骤如下:

157 |

第一步:拷贝项目下面生成的font-face

158 |
@font-face {
159 |   font-family: 'iconfont';
160 |   src: url('iconfont.eot');
161 |   src: url('iconfont.eot?#iefix') format('embedded-opentype'),
162 |   url('iconfont.woff') format('woff'),
163 |   url('iconfont.ttf') format('truetype'),
164 |   url('iconfont.svg#iconfont') format('svg');
165 | }
166 | 
167 |

第二步:定义使用iconfont的样式

168 |
.iconfont{
169 |   font-family:"iconfont" !important;
170 |   font-size:16px;font-style:normal;
171 |   -webkit-font-smoothing: antialiased;
172 |   -webkit-text-stroke-width: 0.2px;
173 |   -moz-osx-font-smoothing: grayscale;
174 | }
175 | 
176 |

第三步:挑选相应图标并获取字体编码,应用于页面

177 |
<i class="iconfont">&#x33;</i>
178 | 179 |
180 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

181 |
182 |
183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1480843971278'); /* IE9*/ 4 | src: url('iconfont.eot?t=1480843971278#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff?t=1480843971278') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf?t=1480843971278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1480843971278#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-index:before { content: "\e61b"; } 20 | 21 | .icon-signout:before { content: "\e679"; } 22 | 23 | .icon-back:before { content: "\e604"; } 24 | 25 | .icon-share:before { content: "\e627"; } 26 | 27 | .icon-msg:before { content: "\e64c"; } 28 | 29 | .icon-edit:before { content: "\e60d"; } 30 | 31 | .icon-comment:before { content: "\e613"; } 32 | 33 | .icon-about:before { content: "\e6ce"; } 34 | 35 | .icon-comment-topic:before { content: "\e63c"; } 36 | 37 | .icon-user:before { content: "\e603"; } 38 | 39 | .icon-ask:before { content: "\e605"; } 40 | 41 | .icon-job:before { content: "\e645"; } 42 | 43 | .icon-topic-top:before { content: "\e68e"; } 44 | 45 | .icon-topic-good:before { content: "\e690"; } 46 | 47 | .icon-click:before { content: "\e601"; } 48 | 49 | .icon-comment-fabulous:before { content: "\e62f"; } 50 | 51 | .icon-good:before { content: "\e612"; } 52 | 53 | .icon-arrow-right:before { content: "\e6cf"; } 54 | 55 | -------------------------------------------------------------------------------- /src/iconfont/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/src/iconfont/iconfont.eot -------------------------------------------------------------------------------- /src/iconfont/iconfont.js: -------------------------------------------------------------------------------- 1 | ;(function(window) { 2 | 3 | var svgSprite = '' + 4 | ''+ 5 | ''+ 6 | ''+ 7 | ''+ 8 | ''+ 9 | ''+ 10 | ''+ 11 | ''+ 12 | ''+ 13 | ''+ 14 | ''+ 15 | ''+ 16 | ''+ 17 | ''+ 18 | ''+ 19 | ''+ 20 | ''+ 21 | ''+ 22 | ''+ 23 | ''+ 24 | ''+ 25 | ''+ 26 | ''+ 27 | ''+ 28 | ''+ 29 | ''+ 30 | ''+ 31 | ''+ 32 | ''+ 33 | ''+ 34 | ''+ 35 | ''+ 36 | ''+ 37 | ''+ 38 | ''+ 39 | ''+ 40 | ''+ 41 | ''+ 42 | ''+ 43 | ''+ 44 | ''+ 45 | ''+ 46 | ''+ 47 | ''+ 48 | ''+ 49 | ''+ 50 | ''+ 51 | ''+ 52 | ''+ 53 | ''+ 54 | ''+ 55 | ''+ 56 | ''+ 57 | ''+ 58 | ''+ 59 | ''+ 60 | ''+ 61 | ''+ 62 | ''+ 63 | ''+ 64 | ''+ 65 | ''+ 66 | ''+ 67 | ''+ 68 | ''+ 69 | ''+ 70 | ''+ 71 | ''+ 72 | ''+ 73 | ''+ 74 | ''+ 75 | ''+ 76 | ''+ 77 | ''+ 78 | ''+ 79 | ''+ 80 | ''+ 81 | ''+ 82 | ''+ 83 | ''+ 84 | ''+ 85 | ''+ 86 | ''+ 87 | ''+ 88 | ''+ 89 | ''+ 90 | ''+ 91 | ''+ 92 | ''+ 93 | ''+ 94 | ''+ 95 | ''+ 96 | ''+ 97 | ''+ 98 | ''+ 99 | ''+ 100 | ''+ 101 | ''+ 102 | ''+ 103 | ''+ 104 | ''+ 105 | ''+ 106 | ''+ 107 | ''+ 108 | ''+ 109 | ''+ 110 | ''+ 111 | ''+ 112 | ''+ 113 | ''+ 114 | ''+ 115 | ''+ 116 | ''+ 117 | ''+ 118 | ''+ 119 | ''+ 120 | ''+ 121 | '' 122 | var script = function() { 123 | var scripts = document.getElementsByTagName('script') 124 | return scripts[scripts.length - 1] 125 | }() 126 | var shouldInjectCss = script.getAttribute("data-injectcss") 127 | 128 | /** 129 | * document ready 130 | */ 131 | var ready = function(fn){ 132 | if(document.addEventListener){ 133 | document.addEventListener("DOMContentLoaded",function(){ 134 | document.removeEventListener("DOMContentLoaded",arguments.callee,false) 135 | fn() 136 | },false) 137 | }else if(document.attachEvent){ 138 | IEContentLoaded (window, fn) 139 | } 140 | 141 | function IEContentLoaded (w, fn) { 142 | var d = w.document, done = false, 143 | // only fire once 144 | init = function () { 145 | if (!done) { 146 | done = true 147 | fn() 148 | } 149 | } 150 | // polling for no errors 151 | ;(function () { 152 | try { 153 | // throws errors until after ondocumentready 154 | d.documentElement.doScroll('left') 155 | } catch (e) { 156 | setTimeout(arguments.callee, 50) 157 | return 158 | } 159 | // no errors, fire 160 | 161 | init() 162 | })() 163 | // trying to always fire before onload 164 | d.onreadystatechange = function() { 165 | if (d.readyState == 'complete') { 166 | d.onreadystatechange = null 167 | init() 168 | } 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Insert el before target 175 | * 176 | * @param {Element} el 177 | * @param {Element} target 178 | */ 179 | 180 | var before = function (el, target) { 181 | target.parentNode.insertBefore(el, target) 182 | } 183 | 184 | /** 185 | * Prepend el to target 186 | * 187 | * @param {Element} el 188 | * @param {Element} target 189 | */ 190 | 191 | var prepend = function (el, target) { 192 | if (target.firstChild) { 193 | before(el, target.firstChild) 194 | } else { 195 | target.appendChild(el) 196 | } 197 | } 198 | 199 | function appendSvg(){ 200 | var div,svg 201 | 202 | div = document.createElement('div') 203 | div.innerHTML = svgSprite 204 | svg = div.getElementsByTagName('svg')[0] 205 | if (svg) { 206 | svg.setAttribute('aria-hidden', 'true') 207 | svg.style.position = 'absolute' 208 | svg.style.width = 0 209 | svg.style.height = 0 210 | svg.style.overflow = 'hidden' 211 | prepend(svg,document.body) 212 | } 213 | } 214 | 215 | if(shouldInjectCss && !window.__iconfont__svg__cssinject__){ 216 | window.__iconfont__svg__cssinject__ = true 217 | try{ 218 | document.write(""); 219 | }catch(e){ 220 | console && console.log(e) 221 | } 222 | } 223 | 224 | ready(appendSvg) 225 | 226 | 227 | })(window) 228 | -------------------------------------------------------------------------------- /src/iconfont/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Sun Dec 4 17:32:51 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 41 | 44 | 46 | 50 | 53 | 69 | 72 | 75 | 78 | 81 | 85 | 90 | 96 | 103 | 107 | 112 | 116 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/src/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/src/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/less/common.less: -------------------------------------------------------------------------------- 1 | @import "./config"; 2 | 3 | /* 4 | 初始化页面布局 5 | */ 6 | 7 | #app { 8 | overflow: hidden; 9 | position: absolute; 10 | top: 0; 11 | right: 0; 12 | bottom: 0; 13 | left: 0; 14 | z-index: 2; 15 | max-width: 640px; 16 | margin: auto; 17 | font-size: 13px; 18 | background: #fff; 19 | -webkit-tap-highlight-color: transparent; 20 | } 21 | 22 | * { 23 | outline: none; 24 | } 25 | 26 | ul { 27 | padding: 0; 28 | margin: 0; 29 | } 30 | 31 | li { 32 | list-style: none; 33 | } 34 | 35 | a { 36 | text-decoration: none; 37 | color: @main; 38 | } 39 | 40 | .common-typeicon { 41 | position: absolute; 42 | top: 0; 43 | right: 0; 44 | z-index: 2; 45 | height: 80px; 46 | .icon { 47 | padding: 20px 5px; 48 | } 49 | .iconfont { 50 | display: block; 51 | font-size: 34px; 52 | transform: rotate(35deg); 53 | } 54 | .icon-topic-top { 55 | color: red; 56 | } 57 | .icon-topic-good { 58 | color: green; 59 | } 60 | } -------------------------------------------------------------------------------- /src/less/config.less: -------------------------------------------------------------------------------- 1 | @text: #222; 2 | @main: #80bd01; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'normalize.css' 2 | import 'flex.css' 3 | import './iconfont/iconfont.css' 4 | import 'github-markdown-css' 5 | import './css/common.css' 6 | import './less/common.less' 7 | 8 | import Vue from 'vue' 9 | 10 | import vuet from './vuet/' // 注意:Vuet要在所有组件初始化前执行,避免第三方插件无法使用 11 | import router from './router/' 12 | import * as filters from './filters/' 13 | import components from './components/' 14 | 15 | Object.keys(components).forEach((key) => { 16 | var name = key.replace(/(\w)/, (v) => v.toUpperCase()) // 首字母大写 17 | Vue.component(`v${name}`, components[key]) 18 | }) 19 | Object.keys(filters).forEach(k => Vue.filter(k, filters[k])) // 注册过滤器 20 | 21 | export default new Vue({ router, vuet }).$mount('#app') 22 | -------------------------------------------------------------------------------- /src/pages/about/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 37 | 47 | -------------------------------------------------------------------------------- /src/pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 91 | 229 | -------------------------------------------------------------------------------- /src/pages/self/home/headimg-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/src/pages/self/home/headimg-bg.jpg -------------------------------------------------------------------------------- /src/pages/self/home/index.vue: -------------------------------------------------------------------------------- 1 | 62 | 72 | 145 | -------------------------------------------------------------------------------- /src/pages/self/messages/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 48 | 108 | -------------------------------------------------------------------------------- /src/pages/signin/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 63 | 96 | -------------------------------------------------------------------------------- /src/pages/signout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 18 | 20 | -------------------------------------------------------------------------------- /src/pages/topic/create/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 50 | 94 | -------------------------------------------------------------------------------- /src/pages/topic/detail/index.vue: -------------------------------------------------------------------------------- 1 | 101 | 145 | 234 | -------------------------------------------------------------------------------- /src/pages/topic/detail/reply-box.vue: -------------------------------------------------------------------------------- 1 | 12 | 69 | 108 | -------------------------------------------------------------------------------- /src/pages/user/username/headimg-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzxb/vue-cnode/4881049ae063497718589b7d83fa8994f9a7fd7d/src/pages/user/username/headimg-bg.jpg -------------------------------------------------------------------------------- /src/pages/user/username/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 50 | 117 | -------------------------------------------------------------------------------- /src/pages/user/username/list.vue: -------------------------------------------------------------------------------- 1 | 22 | 33 | 81 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | 3 | export default router 4 | -------------------------------------------------------------------------------- /src/router/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import routes from './routes' 4 | 5 | Vue.use(Router) 6 | 7 | const router = new Router({ 8 | routes, 9 | mode: 'history', 10 | base: '/vue-cnode/' 11 | }) 12 | 13 | router.beforeEach(({ meta, path }, from, next) => { 14 | const { auth = true } = meta 15 | const isLogin = Boolean(localStorage.getItem('vue_cnode_accesstoken')) // true用户已登录, false用户未登录 16 | if (auth && !isLogin && path !== '/login') { 17 | let to = { path: '/login' } 18 | return next(to) 19 | } 20 | next() 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | import App from '../app' 2 | import Signin from '../pages/signin/' 3 | import Signout from '../pages/signout/' 4 | import About from '../pages/about/' 5 | import UserUsername from '../pages/user/username/' 6 | import SelfHome from '../pages/self/home/' 7 | import SelfMessages from '../pages/self/messages/' 8 | import TopicCreate from '../pages/topic/create/' 9 | import TopicDetail from '../pages/topic/detail/' 10 | import Index from '../pages/index/' 11 | /** 12 | * auth true登录才能访问,false不需要登录,默认true 13 | */ 14 | export default [ 15 | { 16 | path: '/', 17 | component: App, 18 | children: [ 19 | { 20 | path: '/signin', // 登录 21 | name: 'signin', 22 | meta: { auth: false }, 23 | component: Signin 24 | }, 25 | { 26 | path: '/signout', // 退出 27 | name: 'signout', 28 | component: Signout 29 | }, 30 | { 31 | path: '/about', // 关于 32 | name: 'about', 33 | meta: { auth: false }, 34 | component: About 35 | }, 36 | { 37 | path: '/user/:username', // 查看用户信息 38 | name: 'user-detail', 39 | meta: { auth: false }, 40 | component: UserUsername 41 | }, 42 | { 43 | path: '/self/home/', // 我的个人中心 44 | name: 'self-home', 45 | meta: { auth: false }, 46 | component: SelfHome 47 | }, 48 | { 49 | path: '/self/messages/', // 我的消息 50 | name: 'self-messages', 51 | meta: { auth: false }, 52 | component: SelfMessages 53 | }, 54 | { 55 | path: '/topic/create', // 创建帖子 56 | name: 'topic-create', 57 | meta: { auth: false }, 58 | component: TopicCreate 59 | }, 60 | { 61 | path: '/topic/:id', // 查看帖子信息 62 | name: 'topic-detail', 63 | meta: { auth: false }, 64 | component: TopicDetail 65 | }, 66 | { 67 | path: '/', // 首页 68 | name: 'index', 69 | meta: { auth: false }, 70 | component: Index 71 | }, 72 | { 73 | path: '*', // 其他页面 74 | redirect: '/signin' 75 | } 76 | ] 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /src/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <%= htmlWebpackPlugin.options.title %> 16 | 260 | 261 | 262 | 263 | 264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |

vue-cnode

277 |

中国最专业的 Node.js 开源技术社区

278 |
279 |
280 | 281 |
282 | 283 |
284 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * 消息消失框 4 | */ 5 | toast (msg = '', time = 1500) { 6 | var toast = document.createElement('div') 7 | toast.className = 'common-toast common-toast-show' 8 | toast.innerHTML = msg 9 | document.body.appendChild(toast) 10 | toast.style.display = 'block' 11 | toast.style.margin = `-${toast.offsetHeight / 2}px 0 0 -${toast.offsetWidth / 2}px` 12 | var timer = setTimeout(() => { 13 | toast.className = 'common-toast common-toast-hide' 14 | clearTimeout(timer) 15 | var timer2 = setTimeout(() => { 16 | document.body.removeChild(toast) 17 | clearTimeout(timer2) 18 | }, 200) 19 | }, time) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/is-seeing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测元素是否在可视区 3 | */ 4 | export default function (el, option) { 5 | const setting = Object.assign({ 6 | top: 0, // 元素在顶部伸出的距离 7 | right: 0, // 元素在右边伸出的距离才 8 | bottom: 0, // 元素在底部伸出的距离 9 | left: 0 // 元素在左边伸出的距离 10 | }, option) 11 | 12 | var bcr = el.getBoundingClientRect() // 取得元素在可视区的位置 13 | 14 | var mw = el.offsetWidth // 元素自身宽度 15 | var mh = el.offsetHeight // 元素自身的高度 16 | var w = window.innerWidth // 视窗的宽度 17 | var h = window.innerHeight // 视窗的高度 18 | var boolX = (!((bcr.right - setting.left) <= 0 && ((bcr.left + mw) - setting.left) <= 0) && !((bcr.left + setting.right) >= w && (bcr.right + setting.right) >= (mw + w))) // 上下符合条件 19 | var boolY = (!((bcr.bottom - setting.top) <= 0 && ((bcr.top + mh) - setting.top) <= 0) && !((bcr.top + setting.bottom) >= h && (bcr.bottom + setting.bottom) >= (mh + h))) // 上下符合条件 20 | 21 | return el.width !== 0 && el.height !== 0 && boolX && boolY 22 | } 23 | -------------------------------------------------------------------------------- /src/vuet/index.js: -------------------------------------------------------------------------------- 1 | import vuet from './vuet' 2 | 3 | export default vuet 4 | -------------------------------------------------------------------------------- /src/vuet/vuet.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuet from 'vuet' 3 | import utils from 'utils' 4 | import http from 'http' 5 | import VuetScroll from 'vuet-scroll' 6 | import VuetRoute from 'vuet-route' 7 | 8 | Vue 9 | .use(Vuet) 10 | .use(VuetScroll) 11 | 12 | Vuet 13 | .rule('route', VuetRoute) 14 | 15 | export default new Vuet({ 16 | pathJoin: '-', 17 | modules: { 18 | topic: { 19 | modules: { 20 | create: { 21 | data () { 22 | return { 23 | title: '', // 标题 24 | tab: '', // 发表的板块 25 | content: '' // 发表的内容 26 | } 27 | }, 28 | async create () { 29 | if (!this.title) { 30 | return utils.toast('标题不能为空') 31 | } else if (!this.tab) { 32 | return utils.toast('选项不能为空') 33 | } else if (!this.content) { 34 | return utils.toast('内容不能为空') 35 | } 36 | const res = await http.post(`/topics`, { 37 | ...this.state 38 | }) 39 | if (res.success) { 40 | this.reset() 41 | this.app.$router.push({ 42 | path: `/topic/${res.topic_id}` 43 | }) 44 | } else { 45 | utils.toast(res.error_msg) 46 | } 47 | return res 48 | } 49 | }, 50 | list: { 51 | data () { 52 | return { 53 | list: [], // 列表存储的数据 54 | loading: true, // 数据正在加载中 55 | done: false, // 数据是否已经全部加载完成 56 | page: 1 // 加载的页数 57 | } 58 | }, 59 | route: { 60 | once: true // 当前页面,只加载一次,这样我们就可以做上拉加载了 61 | }, 62 | async fetch () { 63 | const { tab = '' } = this.app.$route.query 64 | const query = { 65 | tab, 66 | mdrender: false, 67 | limit: 20, 68 | page: this.page 69 | } 70 | this.loading = true 71 | const res = await http.get('/topics', query) 72 | this.list = [...this.list, ...res.data] 73 | this.page++ 74 | this.loading = false 75 | this.done = res.data.length < 20 76 | } 77 | }, 78 | detail: { 79 | data () { 80 | return { 81 | topic: { 82 | id: null, 83 | author_id: null, 84 | tab: null, 85 | content: null, 86 | title: null, 87 | last_reply_at: null, 88 | good: false, 89 | top: false, 90 | reply_count: 0, 91 | visit_count: 0, 92 | create_at: null, 93 | author: { 94 | loginname: null, 95 | avatar_url: null 96 | }, 97 | replies: [], 98 | is_collect: false 99 | }, 100 | existence: true, 101 | loading: true, 102 | commentId: null 103 | } 104 | }, 105 | async fetch () { 106 | const route = this.app.$route 107 | const { data } = await http.get(`/topic/${route.params.id}`) 108 | if (data) { 109 | this.topic = data 110 | this.existence = true 111 | } else { 112 | this.existence = false 113 | } 114 | this.loading = false 115 | } 116 | } 117 | } 118 | }, 119 | user: { 120 | modules: { 121 | self: { 122 | data () { 123 | return JSON.parse(localStorage.getItem('vue_cnode_self')) || { 124 | avatar_url: null, 125 | id: null, 126 | loginname: null, 127 | success: false 128 | } 129 | }, 130 | async login (accesstoken) { 131 | const res = await http.post(`/accesstoken`, { accesstoken }) 132 | if (typeof res === 'object' && res.success) { 133 | this.state = res 134 | localStorage.setItem('vue_cnode_self', JSON.stringify(res)) 135 | localStorage.setItem('vue_cnode_accesstoken', accesstoken) 136 | } 137 | return res 138 | }, 139 | signout () { 140 | localStorage.removeItem('vue_cnode_self') 141 | localStorage.removeItem('vue_cnode_accesstoken') 142 | this.reset() 143 | this.app.$router.replace('/') 144 | } 145 | }, 146 | detail: { 147 | data () { 148 | return { 149 | user: { 150 | loginname: null, 151 | avatar_url: null, 152 | githubUsername: null, 153 | create_at: null, 154 | score: 0, 155 | recent_topics: [], 156 | recent_replies: [] 157 | }, 158 | existence: true, 159 | loading: true, 160 | tabIndex: 0 161 | } 162 | }, 163 | async fetch () { 164 | const route = this.app.$route 165 | const { data } = await http.get(`/user/${route.params.username}`) 166 | if (data) { 167 | this.user = data 168 | } else { 169 | this.existence = false 170 | } 171 | this.loading = false 172 | } 173 | }, 174 | messages: { 175 | data () { 176 | return { 177 | list: [], 178 | loading: true 179 | } 180 | }, 181 | async fetch () { 182 | if (!this.vuet.getState('user-self').id) return 183 | const { data } = await http.get(`/messages`, { mdrender: true }) 184 | this.list = [...data.has_read_messages, ...data.hasnot_read_messages] 185 | }, 186 | modules: { 187 | count: { 188 | data () { 189 | return 0 190 | }, 191 | async fetch () { 192 | if (!this.vuet.getState('user-self').id) return 193 | const res = await http.get('/message/count') 194 | if (!res.data) return 195 | this.state = res.data 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | }) 204 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const configs = require('./configs/') 3 | const webpack = require('webpack') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | const extract = new ExtractTextPlugin('css/[name].[hash].css') 8 | const autoprefixer = require('autoprefixer')({ browsers: ['iOS >= 7', 'Android >= 4.1'] }) 9 | const IS_ENV = process.env.NODE_ENV == 'production' 10 | const plugins = [] 11 | if (IS_ENV) { 12 | plugins.push(new webpack.DefinePlugin({ 13 | 'process.env': { 14 | NODE_ENV: JSON.stringify('production') 15 | } 16 | })) 17 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 18 | compress: { 19 | warnings: false 20 | }, 21 | sourceMap: true 22 | })) 23 | } 24 | 25 | module.exports = { 26 | target: 'web', 27 | entry: { 28 | main: ['babel-polyfill', 'whatwg-fetch', './src/main.js'] 29 | }, 30 | output: { 31 | filename: 'js/[name].[hash].js', 32 | path: path.resolve(__dirname, `${configs.dest}static`), 33 | publicPath: configs.publicPath 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.js$/, 39 | use: ['babel-loader', 'eslint-loader'], 40 | exclude: /node_modules/ 41 | }, 42 | { 43 | test: /\.vue$/, 44 | use: [ 45 | { 46 | loader: 'vue-loader', 47 | options: { 48 | loaders: { 49 | css: ExtractTextPlugin.extract({ 50 | use: ['css-loader'], 51 | fallback: 'vue-style-loader' 52 | }), 53 | less: ExtractTextPlugin.extract({ 54 | use: ['css-loader', 'less-loader'], 55 | fallback: 'vue-style-loader' 56 | }) 57 | }, 58 | postcss: [autoprefixer] 59 | } 60 | }, 61 | 'eslint-loader' 62 | ] 63 | }, 64 | { 65 | test: /\.css$/, 66 | use: extract.extract([ 67 | 'css-loader', 68 | { 69 | loader: 'postcss-loader', 70 | options: { 71 | plugins: [autoprefixer] 72 | } 73 | } 74 | ]) 75 | }, 76 | { 77 | test: /\.less$/, 78 | use: extract.extract([ 79 | 'css-loader', 80 | { 81 | loader: 'postcss-loader', 82 | options: { 83 | plugins: [autoprefixer] 84 | } 85 | }, 86 | 'less-loader' 87 | ]) 88 | }, 89 | { 90 | test: /\.(eot|woff|svg|ttf|woff2|)(\?|$)/, 91 | use: [ 92 | { 93 | loader: 'file-loader', 94 | options: { 95 | name: 'iconfont/[name].[hash].[ext]' 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | test: /\.(png|jpg|gif)$/, 102 | use: [ 103 | { 104 | loader: 'url-loader', 105 | options: { 106 | limit: 2000, 107 | name: 'images/[name].[hash].[ext]' 108 | } 109 | } 110 | ] 111 | } 112 | ] 113 | }, 114 | plugins: [ 115 | new HtmlWebpackPlugin({ 116 | template: path.resolve(__dirname, 'src/template/index.html'), 117 | filename: '../index.html', 118 | title: configs.title, 119 | hash: true, 120 | minify: { 121 | removeComments: true, 122 | collapseWhitespace: true, 123 | removeAttributeQuotes: true 124 | } 125 | }), 126 | extract 127 | ].concat(plugins), 128 | resolve: { 129 | alias: { 130 | 'vue$': 'vue/dist/vue.esm.js', 131 | 'utils$': path.resolve(__dirname, 'src/utils/index.js'), //常用工具方法 132 | 'is-seeing$': path.resolve(__dirname, 'src/utils/is-seeing.js'), 133 | 'http$': path.resolve(__dirname, 'src/http/index.js'), 134 | }, 135 | extensions: ['.js', '.vue', '.json'] 136 | }, 137 | devtool: false 138 | } --------------------------------------------------------------------------------