├── src
├── less
│ ├── config.less
│ └── common.less
├── vuet
│ ├── index.js
│ └── vuet.js
├── router
│ ├── index.js
│ ├── router.js
│ └── routes.js
├── iconfont
│ ├── iconfont.eot
│ ├── iconfont.ttf
│ ├── iconfont.woff
│ ├── iconfont.css
│ ├── demo_fontclass.html
│ ├── demo.css
│ ├── demo_unicode.html
│ ├── demo_symbol.html
│ ├── iconfont.svg
│ └── iconfont.js
├── pages
│ ├── self
│ │ ├── home
│ │ │ ├── headimg-bg.jpg
│ │ │ └── index.vue
│ │ └── messages
│ │ │ └── index.vue
│ ├── user
│ │ └── username
│ │ │ ├── headimg-bg.jpg
│ │ │ ├── list.vue
│ │ │ └── index.vue
│ ├── signout
│ │ └── index.vue
│ ├── about
│ │ └── index.vue
│ ├── topic
│ │ ├── create
│ │ │ └── index.vue
│ │ └── detail
│ │ │ ├── reply-box.vue
│ │ │ └── index.vue
│ ├── signin
│ │ └── index.vue
│ └── index
│ │ └── index.vue
├── components
│ ├── index.js
│ ├── content.vue
│ ├── data-null.vue
│ ├── header.vue
│ ├── loading.vue
│ └── footer.vue
├── utils
│ ├── index.js
│ └── is-seeing.js
├── main.js
├── app.vue
├── filters
│ └── index.js
├── css
│ └── common.css
├── http
│ └── index.js
└── template
│ └── index.html
├── .gitignore
├── configs
├── dev.js
├── index.js
└── base.js
├── shot
└── QR-code.png
├── .babelrc
├── .eslintrc.js
├── server.js
├── package.json
├── README.md
└── webpack.config.js
/src/less/config.less:
--------------------------------------------------------------------------------
1 | @text: #222;
2 | @main: #80bd01;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | npm-debug.log
3 | package-lock.json
4 | package-lock.json
--------------------------------------------------------------------------------
/src/vuet/index.js:
--------------------------------------------------------------------------------
1 | import vuet from './vuet'
2 |
3 | export default vuet
4 |
--------------------------------------------------------------------------------
/configs/dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | dest: './dist/' // 程序打包后导出的目录
3 | }
4 |
--------------------------------------------------------------------------------
/shot/QR-code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/shot/QR-code.png
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 |
3 | export default router
4 |
--------------------------------------------------------------------------------
/configs/index.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.assign({}, require('./base'), require('./dev'))
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0"
5 | ],
6 | "compact": true
7 | }
--------------------------------------------------------------------------------
/src/iconfont/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/src/iconfont/iconfont.eot
--------------------------------------------------------------------------------
/src/iconfont/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/src/iconfont/iconfont.ttf
--------------------------------------------------------------------------------
/src/iconfont/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/src/iconfont/iconfont.woff
--------------------------------------------------------------------------------
/src/pages/self/home/headimg-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/src/pages/self/home/headimg-bg.jpg
--------------------------------------------------------------------------------
/src/pages/user/username/headimg-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lzxb/vue-cnode/HEAD/src/pages/user/username/headimg-bg.jpg
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['standard'],
4 | env: {
5 | browser: true,
6 | },
7 | plugins: [
8 | 'html'
9 | ]
10 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/pages/signout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
20 |
--------------------------------------------------------------------------------
/src/components/content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
22 |
--------------------------------------------------------------------------------
/src/components/data-null.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ msg }}
3 |
4 |
15 |
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | })
--------------------------------------------------------------------------------
/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/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/app.vue:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
28 |
--------------------------------------------------------------------------------
/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/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/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/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/components/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
23 |
47 |
--------------------------------------------------------------------------------
/src/pages/about/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
31 |
32 |
33 |
34 |
37 |
47 |
--------------------------------------------------------------------------------
/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/components/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ done ? '没有了' : '' }}
5 |
6 |
7 |
8 |
35 |
69 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/pages/user/username/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
![]()
8 |
9 |
10 |
11 |
12 |
{{ item.author.loginname }}
13 |
14 |
15 | {{ item.title }}
16 |
17 |
18 |
19 |
没有记录
20 |
21 |
22 |
33 |
81 |
--------------------------------------------------------------------------------
/src/pages/topic/create/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
26 |
27 |
28 |
29 |
30 |
50 |
94 |
--------------------------------------------------------------------------------
/src/pages/signin/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
18 |
19 |
20 |
63 |
96 |
--------------------------------------------------------------------------------
/src/components/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
52 |
108 |
--------------------------------------------------------------------------------
/src/pages/topic/detail/reply-box.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
69 |
108 |
--------------------------------------------------------------------------------
/src/pages/user/username/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
![]()
16 |
17 |
18 |
{{ user.loginname }}
19 |
20 |
积分:{{ user.score }}
21 |
注册于:{{ user.create_at | formatDate }}
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
50 |
117 |
--------------------------------------------------------------------------------
/src/pages/self/messages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | -
8 |
9 |
10 |
11 |
12 |
{{ item.author.loginname }}
13 |
14 |
15 |
16 | 在话题
17 | {{ item.topic.title }} 中 @了你
18 |
19 |
20 |
21 | 回复你了的话题
22 | {{ item.topic.title }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
48 |
108 |
--------------------------------------------------------------------------------
/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 | [](http://lzxb.github.io/vue-cnode/)
110 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/pages/self/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ self.loginname }}
13 |
14 |
15 |
16 |
17 | 你还未登录,请先登录!
18 |
19 |
43 |
44 |
57 |
58 |
59 |
60 |
61 |
62 |
72 |
145 |
--------------------------------------------------------------------------------
/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 |
141 |
第二步:挑选相应图标并获取类名,应用于页面:
142 |
<i class="iconfont icon-xxx"></i>
143 |
144 | "iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/pages/index/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | -
14 |
15 |
16 |
17 |
18 |
{{ item.author.loginname }}
19 |
20 |
21 | #分享#
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{ item.title }}
34 |
35 |
36 |
37 |
{{ item.visit_count > 0 ? item.visit_count : '暂无阅读' }}
38 |
39 |
40 |
41 |
{{ item.reply_count > 0 ? item.reply_count : '暂无评论' }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
91 |
229 |
--------------------------------------------------------------------------------
/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_unicode.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IconFont
7 |
8 |
9 |
29 |
30 |
31 |
32 |
IconFont 图标
33 |
34 |
35 | -
36 |
37 |
首页
38 | 
39 |
40 |
41 | -
42 |
43 |
28 退出
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 | 
123 |
124 |
125 | -
126 |
127 |
赞赞
128 | 
129 |
130 |
131 | -
132 |
133 |
精华1
134 | 
135 |
136 |
137 | -
138 |
139 |
返回
140 | 
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">3</i>
178 |
179 |
180 | "iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。
181 |
182 |
183 |
184 |
185 |
186 |
187 |
--------------------------------------------------------------------------------
/src/pages/topic/detail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
{{ author.loginname }}
30 |
31 |
#楼主
32 |
33 |
34 |
35 |
36 |
37 | -
38 |
39 |
{{ topic.title }}
40 |
41 |
42 |
43 |
{{ topic.visit_count }}
44 |
45 |
46 |
47 |
{{ topic.reply_count }}
48 |
49 |
50 |
51 |
52 |
53 |
54 | -
55 | 共({{ replies.length }})条回复
56 |
57 |
58 | -
59 |
60 |
61 |
62 |
63 |
64 |
65 |
{{ item.author.loginname }}
66 |
67 |
#{{ $index + 1 }}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{ item.ups.length }}
77 |
78 |
79 |
80 |
81 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 你还未登录,请先
95 | 登录
96 |
97 |
98 |
99 |
100 |
101 |
145 |
234 |
--------------------------------------------------------------------------------
/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 |
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/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/iconfont/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
120 |
--------------------------------------------------------------------------------
/src/iconfont/iconfont.js:
--------------------------------------------------------------------------------
1 | ;(function(window) {
2 |
3 | var svgSprite = ''
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 |
--------------------------------------------------------------------------------