├── .browserslistrc
├── src
├── styles
│ ├── icon.less
│ ├── mixins.less
│ ├── reset.less
│ ├── index.less
│ ├── variables.less
│ └── base.less
├── utils
│ ├── global-bus.js
│ ├── sleep.js
│ ├── storage.js
│ ├── date-time.js
│ └── request.js
├── assets
│ └── logo.png
├── views
│ ├── home
│ │ ├── logo.png
│ │ ├── components
│ │ │ ├── article-list.vue
│ │ │ └── channel-edit.vue
│ │ └── index.vue
│ ├── my
│ │ ├── banner.png
│ │ ├── mobile.png
│ │ └── index.vue
│ ├── user-notify
│ │ └── index.vue
│ ├── qa
│ │ └── index.vue
│ ├── video
│ │ └── index.vue
│ ├── user-profile
│ │ ├── components
│ │ │ ├── update-birthday.vue
│ │ │ ├── update-name.vue
│ │ │ └── update-avatar.vue
│ │ └── index.vue
│ ├── search
│ │ ├── components
│ │ │ ├── search-suggestion.vue
│ │ │ ├── search-history.vue
│ │ │ └── search-result.vue
│ │ └── index.vue
│ ├── user-follow
│ │ ├── index.vue
│ │ └── components
│ │ │ └── follow-list.vue
│ ├── my-article
│ │ ├── index.vue
│ │ └── components
│ │ │ └── article-item.vue
│ ├── tab-bar
│ │ └── index.vue
│ ├── user
│ │ ├── index.vue
│ │ └── components
│ │ │ ├── article-item.vue
│ │ │ ├── user-info.vue
│ │ │ └── article-list.vue
│ ├── article
│ │ ├── components
│ │ │ ├── comment-list.vue
│ │ │ ├── comment-item.vue
│ │ │ ├── post-comment.vue
│ │ │ ├── article-footer.vue
│ │ │ └── comment-reply.vue
│ │ ├── index.vue
│ │ └── github-markdown.css
│ ├── user-avatar
│ │ └── index.vue
│ ├── user-chat
│ │ └── index.vue
│ └── login
│ │ └── index.vue
├── components
│ ├── img-cropper
│ │ ├── cat.jpeg
│ │ └── index.vue
│ ├── error-page
│ │ ├── no-network.png
│ │ └── index.vue
│ ├── loading-page
│ │ └── index.vue
│ ├── follow-user
│ │ └── index.vue
│ ├── loading-list
│ │ └── index.vue
│ ├── article-auth
│ │ └── index.vue
│ └── article-item
│ │ └── index.vue
├── api
│ ├── search.js
│ ├── channel.js
│ ├── comment.js
│ ├── article.js
│ └── user.js
├── App.vue
├── notify-test.js
├── main.js
├── store
│ └── index.js
└── router
│ └── index.js
├── public
├── favicon.ico
├── qrcode_toutiao.m.lipengzhou.com.png
├── index.html
└── manifest.json
├── babel.config.js
├── vercel.json
├── .editorconfig
├── .gitignore
├── postcss.config.js
├── api
└── proxy.js
├── .eslintrc.js
├── vue.config.js
├── LICENSE
├── README.md
└── package.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/src/styles/icon.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 字体图标样式
3 | */
4 |
--------------------------------------------------------------------------------
/src/styles/mixins.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 公共的混入
3 | */
4 |
--------------------------------------------------------------------------------
/src/styles/reset.less:
--------------------------------------------------------------------------------
1 | /**
2 | * reset 初始化
3 | */
4 |
--------------------------------------------------------------------------------
/src/utils/global-bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export default new Vue()
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/src/views/home/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/views/home/logo.png
--------------------------------------------------------------------------------
/src/views/my/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/views/my/banner.png
--------------------------------------------------------------------------------
/src/views/my/mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/views/my/mobile.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/sleep.js:
--------------------------------------------------------------------------------
1 | export default time => {
2 | return new Promise(resolve => setTimeout(resolve, time))
3 | }
4 |
--------------------------------------------------------------------------------
/src/styles/index.less:
--------------------------------------------------------------------------------
1 | // 把全局公共样式写到这里
2 | @import "./reset.less";
3 | @import "./base.less";
4 | @import "./icon.less";
5 |
--------------------------------------------------------------------------------
/src/components/img-cropper/cat.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/components/img-cropper/cat.jpeg
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "src": "/api/(.*)",
5 | "dest": "/api/proxy"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/error-page/no-network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/src/components/error-page/no-network.png
--------------------------------------------------------------------------------
/public/qrcode_toutiao.m.lipengzhou.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lipengzhou/toutiao-m/HEAD/public/qrcode_toutiao.m.lipengzhou.com.png
--------------------------------------------------------------------------------
/src/styles/variables.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 公共变量
3 | */
4 |
5 | // 颜色定义规范
6 | @color-background: #f5f7f9;
7 | @color-text: #666;
8 | @color-primary: #3296fa;
9 |
10 | // 页面内边距
11 | @page-padding: 20px;
12 |
13 | // 字体定义规范
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
7 | [*.{js,jsx,ts,tsx,vue}]
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-pxtorem': {
4 | // 转换的基准值,以设计稿为准
5 | // 375: 37.5
6 | // 750: 75
7 | // Vant 组件的样式是以 375 设计稿开发的
8 | // 我们的设计稿
9 | // 375,量多少,写多少
10 | // 750,量出来的尺寸 ÷ 2
11 | rootValue: 37.5,
12 | propList: ['*']
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/views/user-notify/index.vue:
--------------------------------------------------------------------------------
1 |
2 | 用户通知
3 |
4 |
5 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/api/proxy.js:
--------------------------------------------------------------------------------
1 | const { createProxyMiddleware } = require('http-proxy-middleware')
2 |
3 | module.exports = (req, res) => {
4 | // 创建代理对象并转发请求
5 | createProxyMiddleware({
6 | target: 'http://ttapi.research.itcast.cn/',
7 | changeOrigin: true,
8 | pathRewrite: {
9 | // 通过路径重写,去除请求路径中的 /api
10 | // 例如 /api/xxx 将被转发到 http://ttapi.research.itcast.cn/xxx
11 | '^/api/': ''
12 | }
13 | })(req, res)
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | '@vue/standard'
9 | ],
10 | rules: {
11 | 'no-console': 'off',
12 | // 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
14 | },
15 | parserOptions: {
16 | parser: 'babel-eslint'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | export const setItem = (name, value) => {
2 | if (typeof value === 'object') {
3 | value = JSON.stringify(value)
4 | }
5 | window.localStorage.setItem(name, value)
6 | }
7 |
8 | export const getItem = name => {
9 | const data = window.localStorage.getItem(name)
10 | try {
11 | return JSON.parse(data)
12 | } catch (err) {
13 | return data
14 | }
15 | }
16 |
17 | export const removeItem = name => {
18 | window.localStorage.removeItem(name)
19 | }
20 |
--------------------------------------------------------------------------------
/src/api/search.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 搜索相关接口模块
3 | */
4 | import request from '@/utils/request'
5 |
6 | /**
7 | * 用户登录
8 | */
9 | export function getSuggestions (q) {
10 | return request({
11 | method: 'GET',
12 | url: '/app/v1_0/suggestion',
13 | params: {
14 | q
15 | }
16 | })
17 | }
18 |
19 | /**
20 | * 获取搜索结果
21 | */
22 | export function getSearch (params) {
23 | return request({
24 | method: 'GET',
25 | url: '/app/v1_0/search',
26 | params
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | publicPath: '/',
3 | devServer: {
4 | proxy: {
5 | '/api': {
6 | // target: 'http://api-toutiao-web.itheima.net',
7 | target: 'http://ttapi.research.itcast.cn/',
8 | changeOrigin: true,
9 | ws: true,
10 | pathRewrite: {
11 | '^/api/': ''
12 | }
13 | }
14 | }
15 | },
16 | css: {
17 | loaderOptions: {
18 | less: {
19 | modifyVars: {
20 | blue: '#3296FA',
21 | 'text-color': '#333'
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/loading-page/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | 加载中...
9 |
10 |
11 |
12 |
28 |
29 |
35 |
--------------------------------------------------------------------------------
/src/api/channel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 频道相关接口
3 | */
4 | import request from '@/utils/request'
5 |
6 | /**
7 | * 获取所有频道列表
8 | */
9 | export function getAllChannels () {
10 | return request({
11 | method: 'GET',
12 | url: '/app/v1_0/channels'
13 | })
14 | }
15 |
16 | export const addChannel = channel => {
17 | return request({
18 | method: 'PATCH',
19 | url: '/app/v1_0/user/channels',
20 | data: {
21 | channels: [channel]
22 | }
23 | })
24 | }
25 |
26 | export const deleteChannel = channelId => {
27 | return request({
28 | method: 'DELETE',
29 | url: `/app/v1_0/user/channels/${channelId}`
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/date-time.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装自定义 dayjs 日期处理模块
3 | */
4 | import dayjs from 'dayjs'
5 | import rTime from 'dayjs/plugin/relativeTime'
6 | import 'dayjs/locale/zh-cn'
7 |
8 | // 全局使用中文
9 | dayjs.locale('zh-cn')
10 |
11 | // dayjs 本身只处理日期格式化之类的核心功能
12 | // 其它例如相对时间,需要单独配置它自己的插件才可以使用
13 | dayjs.extend(rTime)
14 |
15 | export const relativeTime = value => {
16 | return dayjs().to(dayjs(value))
17 | }
18 |
19 | export const formatTime = (value, format = 'YYYY-MM-DD hh:mm:ss') => {
20 | return dayjs(value).format(format)
21 | }
22 |
23 | export default {
24 | install (Vue) {
25 | Vue.filter('relativeTime', relativeTime)
26 | Vue.filter('formatTime', formatTime)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/views/qa/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
开发中...
9 |
10 |
11 |
12 |
29 |
30 |
39 |
--------------------------------------------------------------------------------
/src/views/video/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
开发中...
9 |
10 |
11 |
12 |
29 |
30 |
39 |
--------------------------------------------------------------------------------
/src/styles/base.less:
--------------------------------------------------------------------------------
1 | /**
2 | * 基础样式
3 | */
4 | body {
5 | font-family: -apple-system, BlinkMacSystemFont, "PingFang SC","Helvetica Neue",STHeiti,"Microsoft Yahei",Tahoma,Simsun,sans-serif;
6 | color: #333;
7 | background-color: #ededed;
8 | }
9 |
10 | * {
11 | box-sizing: border-box;
12 | }
13 |
14 | .page-navbar {
15 | background-color: #3196fa;
16 | .van-nav-bar__title, .van-icon, .van-nav-bar__text {
17 | color: #fff;
18 | }
19 | }
20 |
21 | .page-container {
22 | padding: 46px 0 50px;
23 | font-size: 14px;
24 | }
25 |
26 | .fixed-tabs {
27 | padding-top: 44px;
28 | .van-tabs__wrap {
29 | position: fixed;
30 | top: 46px;
31 | right: 0;
32 | left: 0;
33 | z-index: 2;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
32 |
33 |
40 |
--------------------------------------------------------------------------------
/src/notify-test.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 | import store from '@/store'
3 | const socket = io('http://ttapi.research.itcast.cn', {
4 | query: {
5 | token: store.state.user.token
6 | }
7 | })
8 |
9 | socket.on('connect', () => {
10 | console.log('connect')
11 | })
12 |
13 | socket.on('connection', () => {
14 | console.log('connection')
15 | })
16 |
17 | socket.on('message', data => {
18 | console.log('message => ', data)
19 | })
20 |
21 | socket.on('following notify', data => {
22 | console.log('following notify =>', data)
23 | })
24 |
25 | socket.on('liking notify', data => {
26 | console.log('liking notify =>', data)
27 | })
28 |
29 | socket.on('comment notify', data => {
30 | console.log('comment notify =>', data)
31 | })
32 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 黑马头条-预览版
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/error-page/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
亲,网络不给力哦~
5 |
点击重试
11 |
12 |
13 |
14 |
29 |
30 |
42 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | import store from './store'
5 | import dateTime from './utils/date-time'
6 | import Vant, { Lazyload } from 'vant'
7 | import 'vant/lib/index.css'
8 | import 'amfe-flexible'
9 | import './styles/index.less'
10 |
11 | if (process.env.NODE_ENV === 'production') {
12 | const Sentry = require('@sentry/browser')
13 | const Integrations = require('@sentry/integrations')
14 |
15 | Sentry.init({
16 | dsn: 'https://34abf2d89d6e40e2ac3bad5fba752daf@sentry.itheima.net/55',
17 | integrations: [new Integrations.Vue({ Vue, attachProps: true })]
18 | })
19 | }
20 |
21 | Vue.use(Vant)
22 | Vue.use(Lazyload)
23 | Vue.use(dateTime)
24 |
25 | Vue.config.productionTip = false
26 |
27 | new Vue({
28 | router,
29 | store,
30 | render: h => h(App)
31 | }).$mount('#app')
32 |
--------------------------------------------------------------------------------
/src/views/user-profile/components/update-birthday.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/views/user-profile/components/update-name.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
21 |
22 |
23 |
24 |
25 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 lipengzhou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import { setItem, getItem } from '@/utils/storage'
4 | import decodeJwt from 'jwt-decode'
5 |
6 | Vue.use(Vuex)
7 |
8 | export default new Vuex.Store({
9 | state: {
10 | // 登录用户,一个对象,包含 token 信息
11 | user: getItem('user'),
12 | cachedPages: ['TabBar']
13 | },
14 |
15 | mutations: {
16 | setUser (state, data) {
17 | // 解析 JWT 中的数据(需要使用用户ID)
18 | if (data && data.token) {
19 | const user = decodeJwt(data.token)
20 | data.user_id = user.user_id
21 | }
22 |
23 | state.user = data
24 |
25 | // 为了防止刷新丢失 state 中的 user 状态,我们把它放到本地存储
26 | setItem('user', state.user)
27 | },
28 |
29 | setKeepAlive (state, data) {
30 | state.keepAlive = data
31 | },
32 |
33 | removeCachePage (state, pageName) {
34 | const index = state.cachedPages.indexOf(pageName)
35 | if (index !== -1) {
36 | state.cachedPages.splice(index, 1)
37 | }
38 | },
39 |
40 | addCachePage (state, pageName) {
41 | state.cachedPages.push(pageName)
42 | }
43 | },
44 | actions: {}
45 | })
46 |
--------------------------------------------------------------------------------
/src/views/search/components/search-suggestion.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/follow-user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | {{ value ? '已关注' : '关注' }}
12 |
13 |
14 |
15 |
16 |
64 |
65 |
71 |
--------------------------------------------------------------------------------
/src/views/search/components/search-history.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 全部删除
6 |
7 | 完成
8 |
9 |
10 |
11 |
17 |
21 |
22 |
23 |
24 |
25 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/components/loading-list/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | {{ item }}
10 |
11 |
12 |
13 |
14 |
70 |
71 |
76 |
--------------------------------------------------------------------------------
/src/views/search/components/search-result.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
19 |
20 |
21 |
22 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue.js 移动端项目——黑马头条
2 |
3 | ## 扫码体验
4 |
5 |
6 |
7 | ## 相关资源
8 |
9 | - GitHub 仓库:https://github.com/lipengzhou/topline-m
10 | - 学习笔记:https://www.yuque.com/lipengzhou/toutiao-mobile-vue/
11 | - 接口文档:http://toutiao.m.lipengzhou.com/api.html
12 | - 在线预览:http://toutiao.m.lipengzhou.com/
13 |
14 | ## 功能列表
15 |
16 | - [x] 登录注册
17 |
18 | - [x] 首页
19 | + [x] 频道列表
20 | + [x] 文章列表
21 | + [x] 频道编辑
22 | - [x] 搜索
23 | - [x] 联想建议
24 | - [x] 搜索历史记录
25 | - [x] 搜索结果
26 |
27 | - [x] 文章详情
28 | + [x] 关注用户
29 | + [x] 文章收藏
30 | + [x] 文章点赞
31 | + [x] 文章分享
32 | - [x] 文章评论
33 | - [x] 文章评论
34 | - [x] 评论回复
35 | - [x] 发布文章评论
36 | - [x] 发布评论回复
37 | - [x] 评论点赞
38 | - [x] 我的
39 | + [x] 我的收藏
40 | + [x] 我的历史
41 | + [x] 我的作品
42 | - [x] 个人中心
43 | - [x] 展示当前登录用户信息
44 | - [x] 退出登录
45 | - [x] 用户页面
46 | - [x] 展示用户信息
47 | - [x] 关注用户
48 | - [x] 用户文章列表
49 | - [ ] 用户关注/粉丝
50 | - [ ] 用户关注列表
51 | - [ ] 用户粉丝列表
52 | - [ ] 关注/取消关注用户/粉丝
53 | - [x] 小智同学
54 | - [x] 展示聊天消息列表
55 | - [x] 发送/接收消息
56 | - [ ] 消息通知
57 | - [ ] 点赞通知
58 | - [ ] 评论通知
59 | - [ ] 关注通知
60 |
61 | ## 一些计划
62 |
63 | - [ ] 动画交互
64 | - [ ] 黑暗模式
65 | - [ ] 发布 Android App
66 | - [ ] 发布 iOS App
67 | - [ ] 小程序 App
68 | - [ ] Flutter App
69 | - [ ] 重写后端接口
70 |
71 | ## 本地开发
72 |
73 | ```sh
74 | # 下载源码
75 | git clone https://github.com/lipengzhou/topline-m.git
76 |
77 | # 安装依赖
78 | # 或者 npm install
79 | yarn install
80 |
81 | # 启动开发服务
82 | # 或者 npm run serve
83 | yarn serve
84 |
85 | # 打包构建
86 | # 或者 npm run build
87 | yarn build
88 |
89 | # 代码格式校验
90 | # 或者 npm run lint
91 | yarn lint
92 | ```
93 |
--------------------------------------------------------------------------------
/src/views/user-profile/components/update-avatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
14 |
15 |
16 |
17 |
18 |
23 |
28 |
29 | 取消
30 | 完成
31 |
32 |
33 |
34 |
35 |
36 |
37 |
52 |
53 |
70 |
--------------------------------------------------------------------------------
/src/views/user-follow/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
54 |
55 |
72 |
--------------------------------------------------------------------------------
/src/api/comment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 评论接口模块
3 | */
4 | import request from '@/utils/request'
5 |
6 | /**
7 | * 获取文章评论或评论回复列表
8 | */
9 | export function getComments (params) {
10 | return request({
11 | method: 'GET',
12 | url: '/app/v1_0/comments',
13 | params
14 | })
15 | }
16 |
17 | /**
18 | * 获取文章评论
19 | */
20 | export function getArticleComments (articleId, {
21 | page = 1,
22 | perPage = 10
23 | } = {}) {
24 | return request({
25 | method: 'GET',
26 | url: '/app/v1_0/comments',
27 | params: {
28 | type: 'a', // a或c 评论类型,a-对文章(article)的评论,c-对评论(comment)的回复
29 | source: articleId, // 文章id
30 | offset: page, // 偏移量,相当于页码
31 | limit: perPage // 每页大小
32 | }
33 | })
34 | }
35 |
36 | /**
37 | * 获取评论回复
38 | */
39 | export function getCommentReplies (commentId, {
40 | page = 1,
41 | perPage = 10
42 | } = {}) {
43 | return request({
44 | method: 'GET',
45 | url: '/app/v1_0/comments',
46 | params: {
47 | type: 'c', // a或c 评论类型,a-对文章(article)的评论,c-对评论(comment)的回复
48 | source: commentId.toString(), // 评论id
49 | offset: page, // 偏移量,相当于页码
50 | limit: perPage // 每页大小
51 | }
52 | })
53 | }
54 |
55 | /**
56 | * 添加评论或评论回复
57 | */
58 | export function addComment (data) {
59 | return request({
60 | method: 'POST',
61 | url: '/app/v1_0/comments',
62 | data
63 | })
64 | }
65 |
66 | /**
67 | * 对评论或评论回复点赞
68 | */
69 | export function addCommentLike (commentId) {
70 | return request({
71 | method: 'POST',
72 | url: '/app/v1_0/comment/likings',
73 | data: {
74 | target: commentId
75 | }
76 | })
77 | }
78 |
79 | /**
80 | * 取消对评论或评论回复点赞
81 | */
82 | export function deleteCommentLike (commentId) {
83 | return request({
84 | method: 'DELETE',
85 | url: `/app/v1_0/comment/likings/${commentId}`
86 | })
87 | }
88 |
--------------------------------------------------------------------------------
/src/views/my-article/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/components/article-auth/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
{{ article.aut_name }}
18 |
{{ article.pubdate | relativeTime }}
19 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
58 |
59 |
85 |
--------------------------------------------------------------------------------
/src/views/tab-bar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 | 首页
13 |
18 |
19 |
20 | 问答
21 |
26 |
27 |
28 | 视频
29 |
34 |
35 |
39 | {{ $store.state.user ? '我的' : '未登录' }}
40 |
45 |
46 |
47 |
48 |
49 |
50 |
67 |
68 |
73 |
--------------------------------------------------------------------------------
/src/components/img-cropper/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
64 |
65 |
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "heimatoutiao-mobile",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@sentry/browser": "^5.13.2",
12 | "@sentry/integrations": "^5.13.2",
13 | "amfe-flexible": "^2.2.1",
14 | "axios": "^0.21.1",
15 | "core-js": "^3.4.3",
16 | "cropperjs": "^1.5.6",
17 | "dayjs": "^1.8.17",
18 | "json-bigint": "^1.0.0",
19 | "jwt-decode": "^2.2.0",
20 | "less": "^3.10.3",
21 | "less-loader": "^5.0.0",
22 | "lodash": "^4.17.15",
23 | "socket.io-client": "^2.3.0",
24 | "vant": "^2.6.1",
25 | "vee-validate": "^3.2.1",
26 | "vue": "^2.6.10",
27 | "vue-router": "^3.1.3",
28 | "vuex": "^3.1.2"
29 | },
30 | "devDependencies": {
31 | "@commitlint/cli": "^8.2.0",
32 | "@commitlint/config-conventional": "^8.2.0",
33 | "@vue/cli-plugin-babel": "^4.5.9",
34 | "@vue/cli-plugin-eslint": "^4.5.9",
35 | "@vue/cli-plugin-router": "^4.5.9",
36 | "@vue/cli-plugin-vuex": "^4.5.9",
37 | "@vue/cli-service": "^4.1.0",
38 | "@vue/eslint-config-standard": "^5.1.2",
39 | "babel-eslint": "^10.1.0",
40 | "babel-plugin-transform-remove-console": "^6.9.4",
41 | "eslint": "^6.7.2",
42 | "eslint-plugin-import": "^2.20.2",
43 | "eslint-plugin-node": "^11.1.0",
44 | "eslint-plugin-promise": "^4.2.1",
45 | "eslint-plugin-standard": "^4.0.0",
46 | "eslint-plugin-vue": "^6.2.2",
47 | "http-proxy-middleware": "^1.0.6",
48 | "lint-staged": "^9.4.3",
49 | "postcss-pxtorem": "^4.0.1",
50 | "vue-template-compiler": "^2.6.10"
51 | },
52 | "commitlint": {
53 | "extends": [
54 | "@commitlint/config-conventional"
55 | ]
56 | },
57 | "gitHooks": {
58 | "pre-commit": "lint-staged",
59 | "commit-msg": "commitlint -E GIT_PARAMS"
60 | },
61 | "lint-staged": {
62 | "*.{js,vue}": [
63 | "vue-cli-service lint",
64 | "git add"
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/views/user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
80 |
81 |
85 |
--------------------------------------------------------------------------------
/src/views/article/components/comment-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
16 |
17 |
18 |
19 |
20 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/views/article/components/comment-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
84 |
85 |
99 |
--------------------------------------------------------------------------------
/src/views/search/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
89 |
90 |
106 |
--------------------------------------------------------------------------------
/src/views/user-avatar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
32 |
33 |
34 |
39 |
44 |
45 | 取消
46 | 完成
47 |
48 |
49 |
50 |
51 |
52 |
53 |
92 |
93 |
120 |
--------------------------------------------------------------------------------
/src/views/home/components/article-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
99 |
100 |
105 |
--------------------------------------------------------------------------------
/src/api/article.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 文章相关的数据接口
3 | */
4 | import request from '@/utils/request'
5 |
6 | /**
7 | * 获取文章列表
8 | */
9 | export function getArticles (params) {
10 | return request({
11 | method: 'GET',
12 | url: '/app/v1_1/articles',
13 | params
14 | })
15 | }
16 |
17 | /**
18 | * 获取文章详情
19 | */
20 | export function getArticle (articleId) {
21 | return request({
22 | method: 'GET',
23 | url: `/app/v1_0/articles/${articleId}`
24 | })
25 | }
26 |
27 | /**
28 | * 对文章点赞
29 | */
30 | export function addLike (articleId) {
31 | return request({
32 | method: 'POST',
33 | url: '/app/v1_0/article/likings',
34 | data: {
35 | target: articleId
36 | }
37 | })
38 | }
39 |
40 | /**
41 | * 取消文章点赞
42 | */
43 | export function deleteLike (articleId) {
44 | return request({
45 | method: 'DELETE',
46 | url: `/app/v1_0/article/likings/${articleId}`
47 | })
48 | }
49 |
50 | /**
51 | * 对文章不喜欢
52 | */
53 | export function addDislike (articleId) {
54 | return request({
55 | method: 'POST',
56 | url: '/app/v1_0/article/dislikes',
57 | data: {
58 | target: articleId
59 | }
60 | })
61 | }
62 |
63 | /**
64 | * 取消对文章不喜欢
65 | */
66 | export function deleteDislike (articleId) {
67 | return request({
68 | method: 'DELETE',
69 | url: `/app/v1_0/article/dislikes/${articleId}`
70 | })
71 | }
72 |
73 | /**
74 | * 获取指定用户文章列表
75 | */
76 | export function getArticlesByUser (userId, params) {
77 | return request({
78 | method: 'GET',
79 | url: `/app/v1_0/users/${userId}/articles`,
80 | params
81 | })
82 | }
83 |
84 | /**
85 | * 获取当前用户文章列表
86 | */
87 | export function getUserArticles (params) {
88 | return request({
89 | method: 'GET',
90 | url: '/app/v1_0/user/articles',
91 | params
92 | })
93 | }
94 |
95 | /**
96 | * 获取当前用户收藏文章列表
97 | */
98 | export function getUserCollectArticles (params) {
99 | return request({
100 | method: 'GET',
101 | url: '/app/v1_0/article/collections',
102 | params
103 | })
104 | }
105 |
106 | /**
107 | * 获取当前用户阅读历史文章列表
108 | */
109 | export function getUserHistoryArticles (params) {
110 | return request({
111 | method: 'GET',
112 | url: '/app/v1_0/user/histories',
113 | params
114 | })
115 | }
116 |
117 | /**
118 | * 收藏文章
119 | */
120 | export function addCollect (articleId) {
121 | return request({
122 | method: 'POST',
123 | url: '/app/v1_0/article/collections',
124 | data: {
125 | target: articleId // 收藏的目标文章id
126 | }
127 | })
128 | }
129 |
130 | /**
131 | * 取消收藏
132 | */
133 | export function deleteCollect (articleId) {
134 | return request({
135 | method: 'DELETE',
136 | url: `/app/v1_0/article/collections/${articleId}`
137 | })
138 | }
139 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 用户相关的请求模块
3 | */
4 | import request from '@/utils/request'
5 |
6 | /**
7 | * 用户登录
8 | */
9 | export function login (data) {
10 | return request({
11 | method: 'POST',
12 | url: '/app/v1_0/authorizations',
13 | data
14 | })
15 | }
16 |
17 | /**
18 | * 获取用户频道列表
19 | * 如果登录了:获取用户频道列表
20 | * 没有登录:获取默认推荐的频道列表
21 | */
22 | export function getUserChannels () {
23 | return request({
24 | method: 'GET',
25 | url: '/app/v1_0/user/channels'
26 | })
27 | }
28 |
29 | /**
30 | * 关注用户
31 | */
32 | export function followUser (userId) {
33 | return request({
34 | method: 'POST',
35 | url: '/app/v1_0/user/followings',
36 | data: {
37 | target: userId
38 | }
39 | })
40 | }
41 |
42 | /**
43 | * 取消关注用户
44 | */
45 | export function unFollowUser (userId) {
46 | return request({
47 | method: 'DELETE',
48 | url: `/app/v1_0/user/followings/${userId}`
49 | })
50 | }
51 |
52 | /**
53 | * 获取当前登录用户自己信息
54 | */
55 | export function getSelf () {
56 | return request({
57 | method: 'GET',
58 | url: '/app/v1_0/user'
59 | })
60 | }
61 |
62 | /**
63 | * 获取用户个人资料
64 | */
65 | export function getProfile (userId) {
66 | return request({
67 | method: 'GET',
68 | url: '/app/v1_0/user/profile'
69 | })
70 | }
71 |
72 | /**
73 | * 更新用户照片资料
74 | */
75 | export function updateUserPhoto (formData) {
76 | return request({
77 | method: 'PATCH',
78 | url: '/app/v1_0/user/photo',
79 | // Content-Type multipart/form-data
80 | // 必须传递 FormData 对象
81 | data: formData
82 | })
83 | }
84 |
85 | /**
86 | * 更新用户照片资料
87 | */
88 | export function updateUserProfile (data) {
89 | return request({
90 | method: 'PATCH',
91 | url: '/app/v1_0/user/profile',
92 | data
93 | })
94 | }
95 |
96 | /**
97 | * 获取验证码
98 | */
99 | export function sendSmsCode (mobile) {
100 | return request({
101 | method: 'GET',
102 | url: `/app/v1_0/sms/codes/${mobile}`
103 | })
104 | }
105 |
106 | /**
107 | * 获取指定用户信息
108 | */
109 | export function getUserById (userId) {
110 | return request({
111 | method: 'GET',
112 | url: `/app/v1_0/users/${userId}`
113 | })
114 | }
115 |
116 | /**
117 | * 获取用户的关注列表
118 | */
119 | export function getFollowingsByUser (userId, params) {
120 | return request({
121 | method: 'GET',
122 | url: '/app/v1_0/user/followings',
123 | params,
124 | data: {
125 | target: userId.toString()
126 | }
127 | })
128 | }
129 |
130 | /**
131 | * 获取用户的粉丝列表
132 | */
133 | export function getFollowersByUser (userId, params) {
134 | return request({
135 | method: 'GET',
136 | url: '/app/v1_0/user/followers',
137 | params,
138 | data: {
139 | target: userId.toString()
140 | }
141 | })
142 | }
143 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 | import { Dialog } from 'vant'
4 | import store from '@/store'
5 |
6 | Vue.use(VueRouter)
7 |
8 | const routes = [
9 | { // 登录
10 | name: 'login',
11 | path: '/login',
12 | component: () => import('@/views/login')
13 | },
14 | { // 底部标签栏
15 | path: '/',
16 | component: () => import('@/views/tab-bar'),
17 | children: [
18 | { // 首页
19 | name: 'home',
20 | path: '', // 默认子路由
21 | component: () => import('@/views/home')
22 | },
23 | { // 我的
24 | name: 'my',
25 | path: '/my',
26 | component: () => import('@/views/my')
27 | },
28 | { // 问答
29 | name: 'qa',
30 | path: '/qa',
31 | component: () => import('@/views/qa')
32 | },
33 | { // 视频
34 | name: 'video',
35 | path: '/video',
36 | component: () => import('@/views/video')
37 | }
38 | ]
39 | },
40 | { // 搜索
41 | name: 'search',
42 | path: '/search',
43 | component: () => import('@/views/search')
44 | },
45 | { // 文章详情
46 | name: 'article',
47 | path: '/article/:articleId',
48 | component: () => import('@/views/article'),
49 | props: true
50 | },
51 | { // 用户资料
52 | name: 'user-profile',
53 | path: '/user/profile',
54 | component: () => import('@/views/user-profile')
55 | },
56 | {
57 | path: '/user/avatar',
58 | name: 'user-avatar',
59 | component: () => import('@/views/user-avatar')
60 | },
61 | { // 小智同学
62 | name: 'user-chat',
63 | path: '/user/chat',
64 | component: () => import('@/views/user-chat'),
65 | meta: { requiresAuth: true }
66 | },
67 | { // 用户关注/粉丝
68 | path: '/user/:userId/follow',
69 | component: () => import('@/views/user-follow'),
70 | props: true,
71 | meta: { requiresAuth: true }
72 | },
73 | { // 用户主页
74 | name: 'user',
75 | path: '/user/:userId',
76 | component: () => import('@/views/user'),
77 | props: true
78 | },
79 | { // 我的作品、收藏、历史
80 | name: 'my-article',
81 | path: '/my-article/:type?',
82 | component: () => import('@/views/my-article'),
83 | props: true,
84 | meta: { requiresAuth: true }
85 | }
86 | ]
87 |
88 | const router = new VueRouter({
89 | routes
90 | })
91 |
92 | router.beforeEach((to, from, next) => {
93 | if (to.name === 'login' || !to.meta.requiresAuth) {
94 | return next()
95 | }
96 |
97 | if (store.state.user) {
98 | return next()
99 | }
100 |
101 | Dialog.confirm({
102 | title: '该功能需要登录,确认登录吗?'
103 | }).then(() => {
104 | next({
105 | name: 'login',
106 | query: {
107 | redirect: from.fullPath
108 | }
109 | })
110 | }).catch(() => {
111 | // on cancel
112 | })
113 | })
114 |
115 | export default router
116 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 封装 axios 请求模块
3 | */
4 | import axios from 'axios'
5 | import jsonBig from 'json-bigint'
6 | import store from '@/store'
7 | import router from '@/router'
8 | import { Notify } from 'vant'
9 |
10 | // axios.create 方法:复制一个 axios
11 | const request = axios.create({
12 | baseURL: '/api'
13 | // baseURL: 'http://ttapi.research.itcast.cn/'
14 | // baseURL: process.env.NODE_ENV === 'production'
15 | // ? 'http://api-toutiao-web.itheima.net'
16 | // : 'http://ttapi.research.itcast.cn/'
17 | })
18 |
19 | /**
20 | * 配置处理后端返回数据中超出 js 安全整数范围问题
21 | */
22 | request.defaults.transformResponse = [function (data) {
23 | try {
24 | return jsonBig.parse(data)
25 | } catch (err) {
26 | return {}
27 | }
28 | }]
29 |
30 | // 请求拦截器
31 | request.interceptors.request.use(
32 | function (config) {
33 | const user = store.state.user
34 | if (user) {
35 | config.headers.Authorization = `Bearer ${user.token}`
36 | }
37 | // Do something before request is sent
38 | return config
39 | },
40 | function (error) {
41 | // Do something with request error
42 | return Promise.reject(error)
43 | }
44 | )
45 |
46 | // 响应拦截器
47 | request.interceptors.response.use(
48 | response => {
49 | return response
50 | },
51 | async error => {
52 | if (error.response && error.response.status === 401) {
53 | // 校验是否有 refresh_token
54 | const user = store.state.user
55 |
56 | if (!user || !user.refresh_token) {
57 | // router.push('/login')
58 | redirectLogin()
59 |
60 | // 代码不要往后执行了
61 | return
62 | }
63 |
64 | // 如果有refresh_token,则请求获取新的 token
65 | try {
66 | const res = await axios({
67 | method: 'PUT',
68 | url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
69 | headers: {
70 | Authorization: `Bearer ${user.refresh_token}`
71 | }
72 | })
73 |
74 | // 如果获取成功,则把新的 token 更新到容器中
75 | store.commit('setUser', {
76 | ...user,
77 | token: res.data.data.token // 最新获取的可用 token
78 | })
79 |
80 | // 把之前失败的用户请求继续发出去
81 | // config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有
82 | // return 把 request 的请求结果继续返回给发请求的具体位置
83 | return request(error.config)
84 | } catch (err) {
85 | // 如果获取失败,直接跳转 登录页
86 | console.log('请求刷线 token 失败', err)
87 | // router.push('/login')
88 | redirectLogin()
89 | }
90 | } else if (error.response.status === 500) {
91 | Notify('服务端异常,请稍后重试')
92 | }
93 | return Promise.reject(error)
94 | }
95 | )
96 |
97 | function redirectLogin () {
98 | router.push({
99 | name: 'login',
100 | query: {
101 | redirect: router.currentRoute.fullPath
102 | }
103 | })
104 | }
105 |
106 | export default request
107 |
--------------------------------------------------------------------------------
/src/views/article/components/post-comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
104 |
105 |
119 |
--------------------------------------------------------------------------------
/src/views/article/components/article-footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
30 |
93 |
94 |
121 |
--------------------------------------------------------------------------------
/src/views/user/components/article-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
{{ article.aut_name }}
20 |
{{ article.pubdate | relativeTime }}
21 |
22 |
23 |
24 |
{{ article.title }}
25 |
30 |
31 |
{{ article.title }}
32 |
33 |
34 |
35 | {{ article.comm_count }}
36 |
37 |
38 |
39 | {{ article.like_count }}
40 |
41 |
42 |
43 | {{ article.collect_count }}
44 |
45 |
46 |
47 |
48 |
49 |
69 |
70 |
131 |
--------------------------------------------------------------------------------
/src/views/my-article/components/article-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
{{ article.aut_name }}
20 |
{{ article.pubdate | relativeTime }}
21 |
22 |
23 |
24 |
{{ article.title }}
25 |
30 |
31 |
{{ article.title }}
32 |
33 |
34 |
35 | {{ article.comm_count }}
36 |
37 |
38 |
39 | {{ article.like_count }}
40 |
41 |
42 |
43 | {{ article.collect_count }}
44 |
45 |
46 |
47 |
48 |
49 |
69 |
70 |
131 |
--------------------------------------------------------------------------------
/src/views/user-follow/components/follow-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
23 | {{ item.name.trim() || '黑马头条号' }}
24 | 粉丝数:{{ item.fans_count }}
25 |
30 |
31 | {{ item.mutual_follow ? '互相关注' : '已关注' }}
32 |
33 | 关注
34 |
35 |
40 | {{ item.mutual_follow ? '互相关注' : '关注' }}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
111 |
112 |
143 |
--------------------------------------------------------------------------------
/src/views/user/components/user-info.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
{{ user.art_count }}
15 |
发布
16 |
17 |
18 |
{{ user.follow_count }}
19 |
关注
20 |
21 |
22 |
{{ user.fans_count }}
23 |
粉丝
24 |
25 |
26 |
{{ user.like_count }}
27 |
获赞
28 |
29 |
30 |
31 |
34 |
35 | 私信
40 |
44 |
45 |
46 | 编辑资料
52 |
53 |
54 |
55 |
56 |
57 | 认证:
58 | {{ user.certi }}
59 |
60 |
61 | 简介:
62 | {{ user.intro }}
63 |
64 |
65 |
66 |
67 |
68 |
98 |
99 |
142 |
--------------------------------------------------------------------------------
/src/components/article-item/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
{{ article.title }}
9 |
10 | {{ article.aut_name }}
11 | {{ article.comm_count }}评论
12 | {{ article.pubdate | relativeTime }}
13 |
14 |
15 |
16 |
17 |
18 |
23 |
{{ article.title }}
24 |
25 |
33 |
34 |
35 | {{ article.aut_name }}
36 | {{ article.comm_count }}评论
37 | {{ article.pubdate | relativeTime }}
38 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
{{ article.title }}
50 |
51 | {{ article.aut_name }}
52 | {{ article.comm_count }}评论
53 | {{ article.pubdate | relativeTime }}
54 |
55 |
56 |
62 |
63 |
64 |
65 |
66 |
95 |
96 |
165 |
--------------------------------------------------------------------------------
/src/views/article/components/comment-reply.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
46 |
47 |
48 |
49 |
50 |
137 |
138 |
172 |
--------------------------------------------------------------------------------
/src/views/user/components/article-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
20 |
21 |
27 |
28 |
{{ user.name }}
29 |
{{ article.pubdate | relativeTime }}
30 |
31 |
32 |
33 |
{{ article.title }}
34 |
39 |
40 |
{{ article.title }}
41 |
42 |
43 |
44 | {{ article.comm_count }}
45 |
46 |
47 |
48 | {{ article.like_count }}
49 |
50 |
51 |
52 | {{ article.collect_count }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
110 |
111 |
174 |
--------------------------------------------------------------------------------
/src/views/user-chat/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
32 |
33 |
34 |
35 |
36 |
41 | 发送
47 |
48 |
49 |
50 |
51 |
52 |
53 |
134 |
135 |
193 |
--------------------------------------------------------------------------------
/src/views/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
57 |
58 |
59 |
60 |
61 |
132 |
133 |
182 |
--------------------------------------------------------------------------------
/src/views/home/components/channel-edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 我的频道
7 | 点击进入频道
8 |
9 | {{ isEdit ? '完成' : '编辑' }}
17 |
18 |
19 |
23 |
29 | {{ channel.name }}
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 推荐频道
43 | 点击添加频道
44 |
45 |
46 |
47 |
53 | {{ channel.name }}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
159 |
160 |
191 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
33 |
34 |
41 |
49 | 获取验证码
50 |
51 |
52 |
53 |
54 |
55 |
56 | 登录
57 |
58 |
59 |
60 |
账号:13611111111 密码:246810
61 |
如果收不到验证码,请使用万能验证码:246810
62 |
63 |
64 |
65 |
193 |
194 |
207 |
--------------------------------------------------------------------------------
/src/views/article/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
{{ article.title }}
21 |
22 |
27 |
正文结束
28 |
29 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
79 |
80 |
81 |
82 |
83 |
84 |
185 |
186 |
202 |
--------------------------------------------------------------------------------
/src/views/my/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
{{ user.name }}
19 |
20 |
编辑资料
25 |
26 |
27 |
28 | {{ user.art_count }}
29 | 头条
30 |
31 |
32 | {{ user.follow_count }}
33 | 关注
34 |
35 |
36 | {{ user.fans_count }}
37 | 粉丝
38 |
39 |
40 | {{ user.like_count }}
41 | 获赞
42 |
43 |
44 |
45 |
46 |
47 |
48 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
86 |
87 |
88 |
89 |
139 |
140 |
200 |
--------------------------------------------------------------------------------
/src/views/user-profile/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
17 |
18 |
25 |
31 |
37 |
43 |
44 |
45 |
46 |
76 |
77 |
78 |
79 |
84 |
93 |
94 |
100 |
101 |
102 |
103 |
107 |
115 |
116 |
117 |
118 |
119 |
124 |
129 |
130 | 取消
131 | 完成
132 |
133 |
134 |
135 |
136 |
137 |
148 |
149 |
150 |
151 |
152 |
357 |
358 |
397 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "@platforms" : [ "android", "iPhone", "iPad" ],
3 | "id" : "H5A00F9D9", /*应用的标识*/
4 | "name" : "你好世界", /*应用名称,程序桌面图标名称*/
5 | "version" : {
6 | "name" : "1.0", /*应用版本名称*/
7 | "code" : ""
8 | },
9 | "description" : "", /*应用描述信息*/
10 | "icons" : {
11 | "72" : "icon.png"
12 | },
13 | "launch_path" : "http://192.168.31.171:8080/", /*应用的入口页面,默认为根目录下的index.html;支持网络地址,必须以http://或https://开头*/
14 | "developer" : {
15 | "name" : "", /*开发者名称*/
16 | "email" : "", /*开发者邮箱地址*/
17 | "url" : "" /*开发者个人主页地址*/
18 | },
19 | "permissions" : {
20 | "Accelerometer" : {
21 | "description" : "访问加速度感应器"
22 | },
23 | "Audio" : {
24 | "description" : "访问麦克风"
25 | },
26 | "Messaging" : {
27 | "description" : "短彩邮件插件"
28 | },
29 | "Cache" : {
30 | "description" : "管理应用缓存"
31 | },
32 | "Camera" : {
33 | "description" : "访问摄像头"
34 | },
35 | "Console" : {
36 | "description" : "跟踪调试输出日志"
37 | },
38 | "Contacts" : {
39 | "description" : "访问系统联系人信息"
40 | },
41 | "Device" : {
42 | "description" : "访问设备信息"
43 | },
44 | "Downloader" : {
45 | "description" : "文件下载管理"
46 | },
47 | "Events" : {
48 | "description" : "应用扩展事件"
49 | },
50 | "File" : {
51 | "description" : "访问本地文件系统"
52 | },
53 | "Gallery" : {
54 | "description" : "访问系统相册"
55 | },
56 | "Geolocation" : {
57 | "description" : "访问位置信息"
58 | },
59 | "Invocation" : {
60 | "description" : "使用Native.js能力"
61 | },
62 | "Orientation" : {
63 | "description" : "访问方向感应器"
64 | },
65 | "Proximity" : {
66 | "description" : "访问距离感应器"
67 | },
68 | "Storage" : {
69 | "description" : "管理应用本地数据"
70 | },
71 | "Uploader" : {
72 | "description" : "管理文件上传任务"
73 | },
74 | "Runtime" : {
75 | "description" : "访问运行期环境"
76 | },
77 | "XMLHttpRequest" : {
78 | "description" : "跨域网络访问"
79 | },
80 | "Zip" : {
81 | "description" : "文件压缩与解压缩"
82 | },
83 | "Barcode" : {
84 | "description" : "管理二维码扫描插件"
85 | },
86 | "Maps" : {
87 | "description" : "管理地图插件"
88 | },
89 | "Speech" : {
90 | "description" : "管理语音识别插件"
91 | },
92 | "Webview" : {
93 | "description" : "窗口管理"
94 | },
95 | "NativeUI" : {
96 | "description" : "原生UI控件"
97 | },
98 | "Navigator" : {
99 | "description" : "浏览器信息"
100 | },
101 | "NativeObj" : {
102 | "description" : "原生对象"
103 | }
104 | },
105 | "plus" : {
106 | "splashscreen" : {
107 | "autoclose" : true, /*是否自动关闭程序启动界面,true表示应用加载应用入口页面后自动关闭;false则需调plus.navigator.closeSplashscreen()关闭*/
108 | "waiting" : true /*是否在程序启动界面显示等待雪花,true表示显示,false表示不显示。*/
109 | },
110 | "popGesture" : "close", /*设置应用默认侧滑返回关闭Webview窗口,"none"为无侧滑返回功能,"hide"为侧滑隐藏Webview窗口。参考http://ask.dcloud.net.cn/article/102*/
111 | "runmode" : "normal", /*应用的首次启动运行模式,可取liberate或normal,liberate模式在第一次启动时将解压应用资源(Android平台File API才可正常访问_www目录)*/
112 | "signature" : "Sk9JTiBVUyBtYWlsdG86aHIyMDEzQGRjbG91ZC5pbw==", /*可选,保留给应用签名,暂不使用*/
113 | "distribute" : {
114 | "apple" : {
115 | "appid" : "", /*iOS应用标识,苹果开发网站申请的appid,如io.dcloud.HelloH5*/
116 | "mobileprovision" : "", /*iOS应用打包配置文件*/
117 | "password" : "", /*iOS应用打包个人证书导入密码*/
118 | "p12" : "", /*iOS应用打包个人证书,打包配置文件关联的个人证书*/
119 | "devices" : "universal", /*iOS应用支持的设备类型,可取值iphone/ipad/universal*/
120 | "frameworks" : [] /*调用Native.js调用原生Objective-c API需要引用的FrameWork,如需调用GameCenter,则添加"GameKit.framework"*/
121 | },
122 | "google" : {
123 | "packagename" : "", /*Android应用包名,如io.dcloud.HelloH5*/
124 | "keystore" : "", /*Android应用打包使用的密钥库文件*/
125 | "password" : "", /*Android应用打包使用密钥库中证书的密码*/
126 | "aliasname" : "", /*Android应用打包使用密钥库中证书的别名*/
127 | "permissions" : [
128 | "",
129 | "",
130 | "",
131 | "",
132 | "",
133 | "",
134 | "",
135 | "",
136 | "",
137 | "",
138 | "",
139 | "",
140 | "",
141 | "",
142 | "",
143 | "",
144 | "",
145 | "",
146 | "",
147 | "",
148 | "",
149 | ""
150 | ]
151 | },
152 | /*使用Native.js调用原生安卓API需要使用到的系统权限*/
153 | "orientation" : [ "portrait-primary" ], /*应用支持的方向,portrait-primary:竖屏正方向;portrait-secondary:竖屏反方向;landscape-primary:横屏正方向;landscape-secondary:横屏反方向*/
154 | "icons" : {
155 | "ios" : {
156 | "prerendered" : true, /*应用图标是否已经高亮处理,在iOS6及以下设备上有效*/
157 | "auto" : "", /*应用图标,分辨率:512x512,用于自动生成各种尺寸程序图标*/
158 | "iphone" : {
159 | "normal" : "", /*iPhone3/3GS程序图标,分辨率:57x57*/
160 | "retina" : "", /*iPhone4程序图标,分辨率:114x114*/
161 | "retina7" : "", /*iPhone4S/5/6程序图标,分辨率:120x120*/
162 | "retina8" : "", /*iPhone6 Plus程序图标,分辨率:180x180*/
163 | "spotlight-normal" : "", /*iPhone3/3GS Spotlight搜索程序图标,分辨率:29x29*/
164 | "spotlight-retina" : "", /*iPhone4 Spotlight搜索程序图标,分辨率:58x58*/
165 | "spotlight-retina7" : "", /*iPhone4S/5/6 Spotlight搜索程序图标,分辨率:80x80*/
166 | "settings-normal" : "", /*iPhone4设置页面程序图标,分辨率:29x29*/
167 | "settings-retina" : "", /*iPhone4S/5/6设置页面程序图标,分辨率:58x58*/
168 | "settings-retina8" : "" /*iPhone6Plus设置页面程序图标,分辨率:87x87*/
169 | },
170 | "ipad" : {
171 | "normal" : "", /*iPad普通屏幕程序图标,分辨率:72x72*/
172 | "retina" : "", /*iPad高分屏程序图标,分辨率:144x144*/
173 | "normal7" : "", /*iPad iOS7程序图标,分辨率:76x76*/
174 | "retina7" : "", /*iPad iOS7高分屏程序图标,分辨率:152x152*/
175 | "spotlight-normal" : "", /*iPad Spotlight搜索程序图标,分辨率:50x50*/
176 | "spotlight-retina" : "", /*iPad高分屏Spotlight搜索程序图标,分辨率:100x100*/
177 | "spotlight-normal7" : "", /*iPad iOS7 Spotlight搜索程序图标,分辨率:40x40*/
178 | "spotlight-retina7" : "", /*iPad iOS7高分屏Spotlight搜索程序图标,分辨率:80x80*/
179 | "settings-normal" : "", /*iPad设置页面程序图标,分辨率:29x29*/
180 | "settings-retina" : "" /*iPad高分屏设置页面程序图标,分辨率:58x58*/
181 | }
182 | },
183 | "android" : {
184 | "mdpi" : "", /*普通屏程序图标,分辨率:48x48*/
185 | "ldpi" : "", /*大屏程序图标,分辨率:48x48*/
186 | "hdpi" : "", /*高分屏程序图标,分辨率:72x72*/
187 | "xhdpi" : "", /*720P高分屏程序图标,分辨率:96x96*/
188 | "xxhdpi" : "" /*1080P 高分屏程序图标,分辨率:144x144*/
189 | }
190 | },
191 | "splashscreen" : {
192 | "ios" : {
193 | "iphone" : {
194 | "default" : "", /*iPhone3启动图片选,分辨率:320x480*/
195 | "retina35" : "", /*3.5英寸设备(iPhone4)启动图片,分辨率:640x960*/
196 | "retina40" : "", /*4.0 英寸设备(iPhone5/iPhone5s)启动图片,分辨率:640x1136*/
197 | "retina47" : "", /*4.7 英寸设备(iPhone6)启动图片,分辨率:750x1334*/
198 | "retina55" : "", /*5.5 英寸设备(iPhone6 Plus)启动图片,分辨率:1242x2208*/
199 | "retina55l" : "" /*5.5 英寸设备(iPhone6 Plus)横屏启动图片,分辨率:2208x1242*/
200 | },
201 | "ipad" : {
202 | "portrait" : "", /*iPad竖屏启动图片,分辨率:768x1004*/
203 | "portrait-retina" : "", /*iPad高分屏竖屏图片,分辨率:1536x2008*/
204 | "landscape" : "", /*iPad横屏启动图片,分辨率:1024x748*/
205 | "landscape-retina" : "", /*iPad高分屏横屏启动图片,分辨率:2048x1496*/
206 | "portrait7" : "", /*iPad iOS7竖屏启动图片,分辨率:768x1024*/
207 | "portrait-retina7" : "", /*iPad iOS7高分屏竖屏图片,分辨率:1536x2048*/
208 | "landscape7" : "", /*iPad iOS7横屏启动图片,分辨率:1024x768*/
209 | "landscape-retina7" : "" /*iPad iOS7高分屏横屏启动图片,分辨率:2048x1536*/
210 | }
211 | },
212 | "android" : {
213 | "mdpi" : "", /*普通屏启动图片,分辨率:240x282*/
214 | "ldpi" : "", /*大屏启动图片,分辨率:320x442*/
215 | "hdpi" : "", /*高分屏启动图片,分辨率:480x762*/
216 | "xhdpi" : "", /*720P高分屏启动图片,分辨率:720x1242*/
217 | "xxhdpi" : "" /*1080P高分屏启动图片,分辨率:1080x1882*/
218 | }
219 | },
220 | "plugins" : {
221 | "speech" : {
222 | "ifly" : {}
223 | }
224 | }
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/views/article/github-markdown.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: octicons-link;
3 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
4 | }
5 |
6 | .markdown-body .octicon {
7 | display: inline-block;
8 | fill: currentColor;
9 | vertical-align: text-bottom;
10 | }
11 |
12 | .markdown-body .anchor {
13 | float: left;
14 | line-height: 1;
15 | margin-left: -20px;
16 | padding-right: 4px;
17 | }
18 |
19 | .markdown-body .anchor:focus {
20 | outline: none;
21 | }
22 |
23 | .markdown-body h1 .octicon-link,
24 | .markdown-body h2 .octicon-link,
25 | .markdown-body h3 .octicon-link,
26 | .markdown-body h4 .octicon-link,
27 | .markdown-body h5 .octicon-link,
28 | .markdown-body h6 .octicon-link {
29 | color: #1b1f23;
30 | vertical-align: middle;
31 | visibility: hidden;
32 | }
33 |
34 | .markdown-body h1:hover .anchor,
35 | .markdown-body h2:hover .anchor,
36 | .markdown-body h3:hover .anchor,
37 | .markdown-body h4:hover .anchor,
38 | .markdown-body h5:hover .anchor,
39 | .markdown-body h6:hover .anchor {
40 | text-decoration: none;
41 | }
42 |
43 | .markdown-body h1:hover .anchor .octicon-link,
44 | .markdown-body h2:hover .anchor .octicon-link,
45 | .markdown-body h3:hover .anchor .octicon-link,
46 | .markdown-body h4:hover .anchor .octicon-link,
47 | .markdown-body h5:hover .anchor .octicon-link,
48 | .markdown-body h6:hover .anchor .octicon-link {
49 | visibility: visible;
50 | }
51 |
52 | .markdown-body {
53 | -ms-text-size-adjust: 100%;
54 | -webkit-text-size-adjust: 100%;
55 | color: #24292e;
56 | line-height: 1.5;
57 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
58 | font-size: 16px;
59 | line-height: 1.5;
60 | word-wrap: break-word;
61 | }
62 |
63 | .markdown-body .pl-c {
64 | color: #6a737d;
65 | }
66 |
67 | .markdown-body .pl-c1,
68 | .markdown-body .pl-s .pl-v {
69 | color: #005cc5;
70 | }
71 |
72 | .markdown-body .pl-e,
73 | .markdown-body .pl-en {
74 | color: #6f42c1;
75 | }
76 |
77 | .markdown-body .pl-s .pl-s1,
78 | .markdown-body .pl-smi {
79 | color: #24292e;
80 | }
81 |
82 | .markdown-body .pl-ent {
83 | color: #22863a;
84 | }
85 |
86 | .markdown-body .pl-k {
87 | color: #d73a49;
88 | }
89 |
90 | .markdown-body .pl-pds,
91 | .markdown-body .pl-s,
92 | .markdown-body .pl-s .pl-pse .pl-s1,
93 | .markdown-body .pl-sr,
94 | .markdown-body .pl-sr .pl-cce,
95 | .markdown-body .pl-sr .pl-sra,
96 | .markdown-body .pl-sr .pl-sre {
97 | color: #032f62;
98 | }
99 |
100 | .markdown-body .pl-smw,
101 | .markdown-body .pl-v {
102 | color: #e36209;
103 | }
104 |
105 | .markdown-body .pl-bu {
106 | color: #b31d28;
107 | }
108 |
109 | .markdown-body .pl-ii {
110 | background-color: #b31d28;
111 | color: #fafbfc;
112 | }
113 |
114 | .markdown-body .pl-c2 {
115 | background-color: #d73a49;
116 | color: #fafbfc;
117 | }
118 |
119 | .markdown-body .pl-c2:before {
120 | content: "^M";
121 | }
122 |
123 | .markdown-body .pl-sr .pl-cce {
124 | color: #22863a;
125 | font-weight: 700;
126 | }
127 |
128 | .markdown-body .pl-ml {
129 | color: #735c0f;
130 | }
131 |
132 | .markdown-body .pl-mh,
133 | .markdown-body .pl-mh .pl-en,
134 | .markdown-body .pl-ms {
135 | color: #005cc5;
136 | font-weight: 700;
137 | }
138 |
139 | .markdown-body .pl-mi {
140 | color: #24292e;
141 | font-style: italic;
142 | }
143 |
144 | .markdown-body .pl-mb {
145 | color: #24292e;
146 | font-weight: 700;
147 | }
148 |
149 | .markdown-body .pl-md {
150 | background-color: #ffeef0;
151 | color: #b31d28;
152 | }
153 |
154 | .markdown-body .pl-mi1 {
155 | background-color: #f0fff4;
156 | color: #22863a;
157 | }
158 |
159 | .markdown-body .pl-mc {
160 | background-color: #ffebda;
161 | color: #e36209;
162 | }
163 |
164 | .markdown-body .pl-mi2 {
165 | background-color: #005cc5;
166 | color: #f6f8fa;
167 | }
168 |
169 | .markdown-body .pl-mdr {
170 | color: #6f42c1;
171 | font-weight: 700;
172 | }
173 |
174 | .markdown-body .pl-ba {
175 | color: #586069;
176 | }
177 |
178 | .markdown-body .pl-sg {
179 | color: #959da5;
180 | }
181 |
182 | .markdown-body .pl-corl {
183 | color: #032f62;
184 | text-decoration: underline;
185 | }
186 |
187 | .markdown-body details {
188 | display: block;
189 | }
190 |
191 | .markdown-body summary {
192 | display: list-item;
193 | }
194 |
195 | .markdown-body a {
196 | background-color: transparent;
197 | }
198 |
199 | .markdown-body a:active,
200 | .markdown-body a:hover {
201 | outline-width: 0;
202 | }
203 |
204 | .markdown-body strong {
205 | font-weight: inherit;
206 | font-weight: bolder;
207 | }
208 |
209 | .markdown-body h1 {
210 | font-size: 2em;
211 | margin: .67em 0;
212 | }
213 |
214 | .markdown-body img {
215 | border-style: none;
216 | }
217 |
218 | .markdown-body code,
219 | .markdown-body kbd,
220 | .markdown-body pre {
221 | font-family: monospace,monospace;
222 | font-size: 1em;
223 | }
224 |
225 | .markdown-body hr {
226 | box-sizing: content-box;
227 | height: 0;
228 | overflow: visible;
229 | }
230 |
231 | .markdown-body input {
232 | font: inherit;
233 | margin: 0;
234 | }
235 |
236 | .markdown-body input {
237 | overflow: visible;
238 | }
239 |
240 | .markdown-body [type=checkbox] {
241 | box-sizing: border-box;
242 | padding: 0;
243 | }
244 |
245 | .markdown-body * {
246 | box-sizing: border-box;
247 | }
248 |
249 | .markdown-body input {
250 | font-family: inherit;
251 | font-size: inherit;
252 | line-height: inherit;
253 | }
254 |
255 | .markdown-body a {
256 | color: #0366d6;
257 | text-decoration: none;
258 | }
259 |
260 | .markdown-body a:hover {
261 | text-decoration: underline;
262 | }
263 |
264 | .markdown-body strong {
265 | font-weight: 600;
266 | }
267 |
268 | .markdown-body hr {
269 | background: transparent;
270 | border: 0;
271 | border-bottom: 1px solid #dfe2e5;
272 | height: 0;
273 | margin: 15px 0;
274 | overflow: hidden;
275 | }
276 |
277 | .markdown-body hr:before {
278 | content: "";
279 | display: table;
280 | }
281 |
282 | .markdown-body hr:after {
283 | clear: both;
284 | content: "";
285 | display: table;
286 | }
287 |
288 | .markdown-body table {
289 | border-collapse: collapse;
290 | border-spacing: 0;
291 | }
292 |
293 | .markdown-body td,
294 | .markdown-body th {
295 | padding: 0;
296 | }
297 |
298 | .markdown-body details summary {
299 | cursor: pointer;
300 | }
301 |
302 | .markdown-body h1,
303 | .markdown-body h2,
304 | .markdown-body h3,
305 | .markdown-body h4,
306 | .markdown-body h5,
307 | .markdown-body h6 {
308 | margin-bottom: 0;
309 | margin-top: 0;
310 | }
311 |
312 | .markdown-body h1 {
313 | font-size: 32px;
314 | }
315 |
316 | .markdown-body h1,
317 | .markdown-body h2 {
318 | font-weight: 600;
319 | }
320 |
321 | .markdown-body h2 {
322 | font-size: 24px;
323 | }
324 |
325 | .markdown-body h3 {
326 | font-size: 20px;
327 | }
328 |
329 | .markdown-body h3,
330 | .markdown-body h4 {
331 | font-weight: 600;
332 | }
333 |
334 | .markdown-body h4 {
335 | font-size: 16px;
336 | }
337 |
338 | .markdown-body h5 {
339 | font-size: 14px;
340 | }
341 |
342 | .markdown-body h5,
343 | .markdown-body h6 {
344 | font-weight: 600;
345 | }
346 |
347 | .markdown-body h6 {
348 | font-size: 12px;
349 | }
350 |
351 | .markdown-body p {
352 | margin-bottom: 10px;
353 | margin-top: 0;
354 | }
355 |
356 | .markdown-body blockquote {
357 | margin: 0;
358 | }
359 |
360 | .markdown-body ol,
361 | .markdown-body ul {
362 | margin-bottom: 0;
363 | margin-top: 0;
364 | padding-left: 0;
365 | }
366 |
367 | .markdown-body ol ol,
368 | .markdown-body ul ol {
369 | list-style-type: lower-roman;
370 | }
371 |
372 | .markdown-body ol ol ol,
373 | .markdown-body ol ul ol,
374 | .markdown-body ul ol ol,
375 | .markdown-body ul ul ol {
376 | list-style-type: lower-alpha;
377 | }
378 |
379 | .markdown-body dd {
380 | margin-left: 0;
381 | }
382 |
383 | .markdown-body code,
384 | .markdown-body pre {
385 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
386 | font-size: 12px;
387 | }
388 |
389 | .markdown-body pre {
390 | margin-bottom: 0;
391 | margin-top: 0;
392 | }
393 |
394 | .markdown-body input::-webkit-inner-spin-button,
395 | .markdown-body input::-webkit-outer-spin-button {
396 | -webkit-appearance: none;
397 | appearance: none;
398 | margin: 0;
399 | }
400 |
401 | .markdown-body .border {
402 | border: 1px solid #e1e4e8!important;
403 | }
404 |
405 | .markdown-body .border-0 {
406 | border: 0!important;
407 | }
408 |
409 | .markdown-body .border-bottom {
410 | border-bottom: 1px solid #e1e4e8!important;
411 | }
412 |
413 | .markdown-body .rounded-1 {
414 | border-radius: 3px!important;
415 | }
416 |
417 | .markdown-body .bg-white {
418 | background-color: #fff!important;
419 | }
420 |
421 | .markdown-body .bg-gray-light {
422 | background-color: #fafbfc!important;
423 | }
424 |
425 | .markdown-body .text-gray-light {
426 | color: #6a737d!important;
427 | }
428 |
429 | .markdown-body .mb-0 {
430 | margin-bottom: 0!important;
431 | }
432 |
433 | .markdown-body .my-2 {
434 | margin-bottom: 8px!important;
435 | margin-top: 8px!important;
436 | }
437 |
438 | .markdown-body .pl-0 {
439 | padding-left: 0!important;
440 | }
441 |
442 | .markdown-body .py-0 {
443 | padding-bottom: 0!important;
444 | padding-top: 0!important;
445 | }
446 |
447 | .markdown-body .pl-1 {
448 | padding-left: 4px!important;
449 | }
450 |
451 | .markdown-body .pl-2 {
452 | padding-left: 8px!important;
453 | }
454 |
455 | .markdown-body .py-2 {
456 | padding-bottom: 8px!important;
457 | padding-top: 8px!important;
458 | }
459 |
460 | .markdown-body .pl-3,
461 | .markdown-body .px-3 {
462 | padding-left: 16px!important;
463 | }
464 |
465 | .markdown-body .px-3 {
466 | padding-right: 16px!important;
467 | }
468 |
469 | .markdown-body .pl-4 {
470 | padding-left: 24px!important;
471 | }
472 |
473 | .markdown-body .pl-5 {
474 | padding-left: 32px!important;
475 | }
476 |
477 | .markdown-body .pl-6 {
478 | padding-left: 40px!important;
479 | }
480 |
481 | .markdown-body .f6 {
482 | font-size: 12px!important;
483 | }
484 |
485 | .markdown-body .lh-condensed {
486 | line-height: 1.25!important;
487 | }
488 |
489 | .markdown-body .text-bold {
490 | font-weight: 600!important;
491 | }
492 |
493 | .markdown-body:before {
494 | content: "";
495 | display: table;
496 | }
497 |
498 | .markdown-body:after {
499 | clear: both;
500 | content: "";
501 | display: table;
502 | }
503 |
504 | .markdown-body>:first-child {
505 | margin-top: 0!important;
506 | }
507 |
508 | .markdown-body>:last-child {
509 | margin-bottom: 0!important;
510 | }
511 |
512 | .markdown-body a:not([href]) {
513 | color: inherit;
514 | text-decoration: none;
515 | }
516 |
517 | .markdown-body blockquote,
518 | .markdown-body dl,
519 | .markdown-body ol,
520 | .markdown-body p,
521 | .markdown-body pre,
522 | .markdown-body table,
523 | .markdown-body ul {
524 | margin-bottom: 16px;
525 | margin-top: 0;
526 | }
527 |
528 | .markdown-body hr {
529 | background-color: #e1e4e8;
530 | border: 0;
531 | height: .25em;
532 | margin: 24px 0;
533 | padding: 0;
534 | }
535 |
536 | .markdown-body blockquote {
537 | border-left: .25em solid #dfe2e5;
538 | color: #6a737d;
539 | padding: 0 1em;
540 | }
541 |
542 | .markdown-body blockquote>:first-child {
543 | margin-top: 0;
544 | }
545 |
546 | .markdown-body blockquote>:last-child {
547 | margin-bottom: 0;
548 | }
549 |
550 | .markdown-body kbd {
551 | background-color: #fafbfc;
552 | border: 1px solid #c6cbd1;
553 | border-bottom-color: #959da5;
554 | border-radius: 3px;
555 | box-shadow: inset 0 -1px 0 #959da5;
556 | color: #444d56;
557 | display: inline-block;
558 | font-size: 11px;
559 | line-height: 10px;
560 | padding: 3px 5px;
561 | vertical-align: middle;
562 | }
563 |
564 | .markdown-body h1,
565 | .markdown-body h2,
566 | .markdown-body h3,
567 | .markdown-body h4,
568 | .markdown-body h5,
569 | .markdown-body h6 {
570 | font-weight: 600;
571 | line-height: 1.25;
572 | margin-bottom: 16px;
573 | margin-top: 24px;
574 | }
575 |
576 | .markdown-body h1 {
577 | font-size: 1.2em;
578 | }
579 |
580 | .markdown-body h1,
581 | .markdown-body h2 {
582 | border-bottom: 1px solid #eaecef;
583 | padding-bottom: .3em;
584 | }
585 |
586 | .markdown-body h2 {
587 | font-size: 1.1em;
588 | }
589 |
590 | .markdown-body h3 {
591 | font-size: 1em;
592 | }
593 |
594 | .markdown-body h4 {
595 | font-size: .8em;
596 | }
597 |
598 | .markdown-body h5 {
599 | font-size: .775em;
600 | }
601 |
602 | .markdown-body h6 {
603 | color: #6a737d;
604 | font-size: .75em;
605 | }
606 |
607 | .markdown-body ol,
608 | .markdown-body ul {
609 | padding-left: 2em;
610 | }
611 |
612 | .markdown-body ol ol,
613 | .markdown-body ol ul,
614 | .markdown-body ul ol,
615 | .markdown-body ul ul {
616 | margin-bottom: 0;
617 | margin-top: 0;
618 | }
619 |
620 | .markdown-body li {
621 | word-wrap: break-all;
622 | }
623 |
624 | .markdown-body li>p {
625 | margin-top: 16px;
626 | }
627 |
628 | .markdown-body li+li {
629 | margin-top: .25em;
630 | }
631 |
632 | .markdown-body dl {
633 | padding: 0;
634 | }
635 |
636 | .markdown-body dl dt {
637 | font-size: 1em;
638 | font-style: italic;
639 | font-weight: 600;
640 | margin-top: 16px;
641 | padding: 0;
642 | }
643 |
644 | .markdown-body dl dd {
645 | margin-bottom: 16px;
646 | padding: 0 16px;
647 | }
648 |
649 | .markdown-body table {
650 | display: block;
651 | overflow: auto;
652 | width: 100%;
653 | }
654 |
655 | .markdown-body table th {
656 | font-weight: 600;
657 | }
658 |
659 | .markdown-body table td,
660 | .markdown-body table th {
661 | border: 1px solid #dfe2e5;
662 | padding: 6px 13px;
663 | }
664 |
665 | .markdown-body table tr {
666 | background-color: #fff;
667 | border-top: 1px solid #c6cbd1;
668 | }
669 |
670 | .markdown-body table tr:nth-child(2n) {
671 | background-color: #f6f8fa;
672 | }
673 |
674 | .markdown-body img {
675 | background-color: #fff;
676 | box-sizing: content-box;
677 | max-width: 100%;
678 | }
679 |
680 | .markdown-body img[align=right] {
681 | padding-left: 20px;
682 | }
683 |
684 | .markdown-body img[align=left] {
685 | padding-right: 20px;
686 | }
687 |
688 | .markdown-body code {
689 | background-color: rgba(27,31,35,.05);
690 | border-radius: 3px;
691 | font-size: 85%;
692 | margin: 0;
693 | padding: .2em .4em;
694 | }
695 |
696 | .markdown-body pre {
697 | word-wrap: normal;
698 | }
699 |
700 | .markdown-body pre>code {
701 | background: transparent;
702 | border: 0;
703 | font-size: 100%;
704 | margin: 0;
705 | padding: 0;
706 | white-space: pre;
707 | word-break: normal;
708 | }
709 |
710 | .markdown-body .highlight {
711 | margin-bottom: 16px;
712 | }
713 |
714 | .markdown-body .highlight pre {
715 | margin-bottom: 0;
716 | word-break: normal;
717 | }
718 |
719 | .markdown-body .highlight pre,
720 | .markdown-body pre {
721 | background-color: #f6f8fa;
722 | border-radius: 3px;
723 | font-size: 85%;
724 | line-height: 1.45;
725 | overflow: auto;
726 | padding: 16px;
727 | }
728 |
729 | .markdown-body pre code {
730 | background-color: transparent;
731 | border: 0;
732 | display: inline;
733 | line-height: inherit;
734 | margin: 0;
735 | max-width: auto;
736 | overflow: visible;
737 | padding: 0;
738 | word-wrap: normal;
739 | }
740 |
741 | .markdown-body .commit-tease-sha {
742 | color: #444d56;
743 | display: inline-block;
744 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
745 | font-size: 90%;
746 | }
747 |
748 | .markdown-body .blob-wrapper {
749 | border-bottom-left-radius: 3px;
750 | border-bottom-right-radius: 3px;
751 | overflow-x: auto;
752 | overflow-y: hidden;
753 | }
754 |
755 | .markdown-body .blob-wrapper-embedded {
756 | max-height: 240px;
757 | overflow-y: auto;
758 | }
759 |
760 | .markdown-body .blob-num {
761 | -moz-user-select: none;
762 | -ms-user-select: none;
763 | -webkit-user-select: none;
764 | color: rgba(27,31,35,.3);
765 | cursor: pointer;
766 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
767 | font-size: 12px;
768 | line-height: 20px;
769 | min-width: 50px;
770 | padding-left: 10px;
771 | padding-right: 10px;
772 | text-align: right;
773 | user-select: none;
774 | vertical-align: top;
775 | white-space: nowrap;
776 | width: 1%;
777 | }
778 |
779 | .markdown-body .blob-num:hover {
780 | color: rgba(27,31,35,.6);
781 | }
782 |
783 | .markdown-body .blob-num:before {
784 | content: attr(data-line-number);
785 | }
786 |
787 | .markdown-body .blob-code {
788 | line-height: 20px;
789 | padding-left: 10px;
790 | padding-right: 10px;
791 | position: relative;
792 | vertical-align: top;
793 | }
794 |
795 | .markdown-body .blob-code-inner {
796 | color: #24292e;
797 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
798 | font-size: 12px;
799 | overflow: visible;
800 | white-space: pre;
801 | word-wrap: normal;
802 | }
803 |
804 | .markdown-body .pl-token.active,
805 | .markdown-body .pl-token:hover {
806 | background: #ffea7f;
807 | cursor: pointer;
808 | }
809 |
810 | .markdown-body kbd {
811 | background-color: #fafbfc;
812 | border: 1px solid #d1d5da;
813 | border-bottom-color: #c6cbd1;
814 | border-radius: 3px;
815 | box-shadow: inset 0 -1px 0 #c6cbd1;
816 | color: #444d56;
817 | display: inline-block;
818 | font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
819 | line-height: 10px;
820 | padding: 3px 5px;
821 | vertical-align: middle;
822 | }
823 |
824 | .markdown-body :checked+.radio-label {
825 | border-color: #0366d6;
826 | position: relative;
827 | z-index: 1;
828 | }
829 |
830 | .markdown-body .tab-size[data-tab-size="1"] {
831 | -moz-tab-size: 1;
832 | tab-size: 1;
833 | }
834 |
835 | .markdown-body .tab-size[data-tab-size="2"] {
836 | -moz-tab-size: 2;
837 | tab-size: 2;
838 | }
839 |
840 | .markdown-body .tab-size[data-tab-size="3"] {
841 | -moz-tab-size: 3;
842 | tab-size: 3;
843 | }
844 |
845 | .markdown-body .tab-size[data-tab-size="4"] {
846 | -moz-tab-size: 4;
847 | tab-size: 4;
848 | }
849 |
850 | .markdown-body .tab-size[data-tab-size="5"] {
851 | -moz-tab-size: 5;
852 | tab-size: 5;
853 | }
854 |
855 | .markdown-body .tab-size[data-tab-size="6"] {
856 | -moz-tab-size: 6;
857 | tab-size: 6;
858 | }
859 |
860 | .markdown-body .tab-size[data-tab-size="7"] {
861 | -moz-tab-size: 7;
862 | tab-size: 7;
863 | }
864 |
865 | .markdown-body .tab-size[data-tab-size="8"] {
866 | -moz-tab-size: 8;
867 | tab-size: 8;
868 | }
869 |
870 | .markdown-body .tab-size[data-tab-size="9"] {
871 | -moz-tab-size: 9;
872 | tab-size: 9;
873 | }
874 |
875 | .markdown-body .tab-size[data-tab-size="10"] {
876 | -moz-tab-size: 10;
877 | tab-size: 10;
878 | }
879 |
880 | .markdown-body .tab-size[data-tab-size="11"] {
881 | -moz-tab-size: 11;
882 | tab-size: 11;
883 | }
884 |
885 | .markdown-body .tab-size[data-tab-size="12"] {
886 | -moz-tab-size: 12;
887 | tab-size: 12;
888 | }
889 |
890 | .markdown-body .task-list-item {
891 | list-style-type: none;
892 | }
893 |
894 | .markdown-body .task-list-item+.task-list-item {
895 | margin-top: 3px;
896 | }
897 |
898 | .markdown-body .task-list-item input {
899 | margin: 0 .2em .25em -1.6em;
900 | vertical-align: middle;
901 | }
902 |
903 | .markdown-body hr {
904 | border-bottom-color: #eee;
905 | }
906 |
907 | .markdown-body .pl-0 {
908 | padding-left: 0!important;
909 | }
910 |
911 | .markdown-body .pl-1 {
912 | padding-left: 4px!important;
913 | }
914 |
915 | .markdown-body .pl-2 {
916 | padding-left: 8px!important;
917 | }
918 |
919 | .markdown-body .pl-3 {
920 | padding-left: 16px!important;
921 | }
922 |
923 | .markdown-body .pl-4 {
924 | padding-left: 24px!important;
925 | }
926 |
927 | .markdown-body .pl-5 {
928 | padding-left: 32px!important;
929 | }
930 |
931 | .markdown-body .pl-6 {
932 | padding-left: 40px!important;
933 | }
934 |
935 | .markdown-body .pl-7 {
936 | padding-left: 48px!important;
937 | }
938 |
939 | .markdown-body .pl-8 {
940 | padding-left: 64px!important;
941 | }
942 |
943 | .markdown-body .pl-9 {
944 | padding-left: 80px!important;
945 | }
946 |
947 | .markdown-body .pl-10 {
948 | padding-left: 96px!important;
949 | }
950 |
951 | .markdown-body .pl-11 {
952 | padding-left: 112px!important;
953 | }
954 |
955 | .markdown-body .pl-12 {
956 | padding-left: 128px!important;
957 | }
958 |
--------------------------------------------------------------------------------