├── pages
├── star
│ ├── star.wxss
│ ├── star.json
│ ├── star.js
│ └── star.wxml
├── me
│ ├── me.json
│ ├── me.wxss
│ ├── me.js
│ └── me.wxml
├── detail
│ ├── detail.json
│ ├── detail.wxml
│ ├── detail.wxss
│ └── detail.js
├── recent-replies
│ ├── recent-replies.wxss
│ ├── recent-replies.json
│ ├── recent-replies.js
│ └── recent-replies.wxml
├── recent-topics
│ ├── recent-topics.wxss
│ ├── recent-topics.json
│ ├── recent-topics.js
│ └── recent-topics.wxml
├── source
│ ├── source.json
│ ├── source.wxml
│ ├── source.wxss
│ └── source.js
├── index
│ ├── index.json
│ ├── index.wxss
│ ├── index.wxml
│ └── index.js
└── message
│ ├── message.json
│ ├── message.wxml
│ ├── message.wxss
│ └── message.js
├── code-format.sh
├── assets
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── icon.png
└── logo.svg
├── icons
├── me.png
├── index.png
├── message.png
├── me-active.png
├── index-active.png
├── message-active.png
├── reply.svg
├── star-active.svg
├── error.svg
├── star.svg
├── thumb.svg
├── thumb-active.svg
└── new-message.svg
├── .prettierrc
├── app.wxss
├── bower.json
├── .gitignore
├── templates
├── topic.wxml
└── topic.wxss
├── README.md
├── LICENSE
├── app.json
├── app.js
└── utils.js
/pages/star/star.wxss:
--------------------------------------------------------------------------------
1 | @import '/templates/topic.wxss';
2 |
--------------------------------------------------------------------------------
/code-format.sh:
--------------------------------------------------------------------------------
1 | prettier --write 'pages/**/*.js' app.js utils.js
2 |
--------------------------------------------------------------------------------
/pages/me/me.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "个人中心"
3 | }
4 |
--------------------------------------------------------------------------------
/assets/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/assets/1.png
--------------------------------------------------------------------------------
/assets/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/assets/2.png
--------------------------------------------------------------------------------
/assets/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/assets/3.png
--------------------------------------------------------------------------------
/assets/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/assets/4.png
--------------------------------------------------------------------------------
/icons/me.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/me.png
--------------------------------------------------------------------------------
/pages/detail/detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "正在加载"
3 | }
4 |
--------------------------------------------------------------------------------
/pages/recent-replies/recent-replies.wxss:
--------------------------------------------------------------------------------
1 | @import '/templates/topic.wxss';
2 |
--------------------------------------------------------------------------------
/pages/recent-topics/recent-topics.wxss:
--------------------------------------------------------------------------------
1 | @import '/templates/topic.wxss';
2 |
--------------------------------------------------------------------------------
/pages/source/source.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "源代码"
3 | }
4 |
--------------------------------------------------------------------------------
/pages/star/star.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "收藏的话题"
3 | }
4 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/icons/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/index.png
--------------------------------------------------------------------------------
/icons/message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/message.png
--------------------------------------------------------------------------------
/icons/me-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/me-active.png
--------------------------------------------------------------------------------
/icons/index-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/index-active.png
--------------------------------------------------------------------------------
/pages/recent-replies/recent-replies.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "最近参与的话题"
3 | }
4 |
--------------------------------------------------------------------------------
/pages/recent-topics/recent-topics.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "最近创建的话题"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/icons/message-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pd4d10/cnode-weapp/HEAD/icons/message-active.png
--------------------------------------------------------------------------------
/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "社区",
3 | "enablePullDownRefresh": true
4 | }
5 |
--------------------------------------------------------------------------------
/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | @import '/templates/topic.wxss';
2 |
3 | .container {
4 | font-weight: 300;
5 | }
6 |
--------------------------------------------------------------------------------
/pages/message/message.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "消息",
3 | "enablePullDownRefresh": true
4 | }
5 |
--------------------------------------------------------------------------------
/pages/source/source.wxml:
--------------------------------------------------------------------------------
1 |
2 | https://github.com/pd4d10/cnode-weapp
3 |
4 |
--------------------------------------------------------------------------------
/pages/source/source.wxss:
--------------------------------------------------------------------------------
1 | .container {
2 | position: fixed;
3 | top: 40%;
4 | left: 0;
5 | width: 100%;
6 | text-align: center;
7 | font-size: 16px;
8 | color: #08c;
9 | }
10 |
--------------------------------------------------------------------------------
/pages/recent-replies/recent-replies.js:
--------------------------------------------------------------------------------
1 | Page({
2 | data: {},
3 | onLoad(options) {
4 | const app = getApp()
5 | this.setData({
6 | topics: app.globalData.recent_replies,
7 | })
8 | },
9 | })
10 |
--------------------------------------------------------------------------------
/pages/recent-topics/recent-topics.js:
--------------------------------------------------------------------------------
1 | Page({
2 | data: {},
3 | onLoad(options) {
4 | const app = getApp()
5 | this.setData({
6 | topics: app.globalData.recent_topics,
7 | })
8 | },
9 | })
10 |
--------------------------------------------------------------------------------
/pages/recent-replies/recent-replies.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pages/recent-topics/recent-topics.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/icons/reply.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/star-active.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/icons/error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/star.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pages/source/source.js:
--------------------------------------------------------------------------------
1 | // pages/source/source.js
2 | Page({
3 | data: {},
4 | onLoad: function(options) {
5 | // 页面初始化 options为页面跳转所带来的参数
6 | },
7 | onReady: function() {
8 | // 页面渲染完成
9 | },
10 | onShow: function() {
11 | // 页面显示
12 | },
13 | onHide: function() {
14 | // 页面隐藏
15 | },
16 | onUnload: function() {
17 | // 页面关闭
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/icons/thumb.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/icons/thumb-active.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/pages/star/star.js:
--------------------------------------------------------------------------------
1 | import { request } from '../../utils'
2 |
3 | Page({
4 | data: {},
5 | onLoad(options) {
6 | const app = getApp()
7 | const { loginname } = app.globalData
8 | request({
9 | url: `/topic_collect/${loginname}`,
10 | success: json => {
11 | this.setData({
12 | topics: json.data,
13 | })
14 | },
15 | })
16 | },
17 | })
18 |
--------------------------------------------------------------------------------
/pages/star/star.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/icons/new-message.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app.wxss:
--------------------------------------------------------------------------------
1 | @import '/bower_components/zanui-weapp/dist/index.wxss';
2 |
3 | .container {
4 | /*font-family: PingFang SC;*/
5 | }
6 |
7 | /*Tab*/
8 |
9 | .zan-tab__title {
10 | font-weight: 400;
11 | }
12 |
13 | .zan-tab__item--selected .zan-tab__title {
14 | color: #80bd01;
15 | border-bottom: 2px solid #80bd01;
16 | }
17 |
18 |
19 | /*.zan-cell {
20 | padding: 13px 15px;
21 | }
22 | .zan-cell--switch {
23 | padding-top: 6px;
24 | padding-bottom: 6px;
25 | }*/
26 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cnode-weapp",
3 | "homepage": "https://github.com/pd4d10/cnode-weapp",
4 | "authors": [
5 | "pd4d10 "
6 | ],
7 | "description": "",
8 | "main": "",
9 | "license": "MIT",
10 | "private": true,
11 | "ignore": [
12 | "**/.*",
13 | "node_modules",
14 | "bower_components",
15 | "test",
16 | "tests"
17 | ],
18 | "dependencies": {
19 | "zanui-weapp": "youzan/zanui-weapp#^2.1.0",
20 | "wxParse": "icindy/wxParse",
21 | "timeago.js": "^3.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | bower_components/
40 |
--------------------------------------------------------------------------------
/pages/message/message.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{item.author.loginname}}
7 | 回复了你的话题:
8 | 在话题中@了你:
9 | {{item.topic.title}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/templates/topic.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{topic.title}}
6 |
7 | {{topic.tag.text}}
8 | {{topic.tag.text}}
9 |
10 | {{topic.reply_count}}
11 | 回复 /
12 | {{topic.visit_count}}
13 | 浏览
14 |
15 | {{topic.time}}
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CNode weapp
2 |
3 | CNode weapp 是为 CNode 社区开发的微信小程序客户端。
4 |
5 | ## 安装
6 |
7 | 目前对于个人开发者,社区/论坛 类目的小程序无法通过审核,可以按照以下步骤安装体验:
8 |
9 | ### 在 PC 上体验
10 |
11 | 1. 克隆代码并安装依赖库:
12 |
13 | ```sh
14 | git clone https://github.com/pd4d10/cnode-weapp.git
15 | cd cnode-weapp
16 | bower install
17 | ```
18 |
19 | 2. [下载微信开发者工具](https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html),打开项目即可
20 |
21 | ### 在微信上体验
22 |
23 | 除了上述步骤之外,还需要以下额外步骤才能在微信上体验。
24 |
25 | 1. 访问[注册页面](https://mp.weixin.qq.com/wxopen/waregister?action=step1)注册一个小程序
26 | 2. 在管理后台找到 AppID(首次可能需要手动生成一个)
27 | 3. 在开发者工具中将此 AppID 与项目关联起来
28 | 4. 在开发者工具中,点击左侧“项目”,点击“预览”,代码上传成功后会弹出一个二维码,打开微信扫码即可
29 |
30 | ## 截图
31 |
32 |
33 |
34 |
35 |
36 |
37 | ## License
38 |
39 | MIT
40 |
--------------------------------------------------------------------------------
/pages/me/me.wxss:
--------------------------------------------------------------------------------
1 | .button-container {
2 | height: 100%;
3 | display: flex;
4 | align-items: center;
5 | position: fixed;
6 | width: 100%;
7 | }
8 |
9 | .button-container button {
10 | padding-left: 80px;
11 | padding-right: 80px;
12 | }
13 |
14 | page {
15 | background: #f9f9f9;
16 | }
17 |
18 | .user {
19 | padding: 12px;
20 | background: #fff;
21 | border-bottom: 1px solid #eee;
22 | margin-bottom: 20px;
23 | }
24 |
25 | .avatar {
26 | float: left;
27 | width: 56px;
28 | height: 56px;
29 | border-radius: 50%;
30 | margin-right: 10px;
31 | }
32 |
33 | .info {
34 | font-size: 18px;
35 | display: flex;
36 | flex-direction: column;
37 | justify-content: space-between;
38 | }
39 |
40 | .name {
41 | line-height: 32px;
42 | }
43 |
44 | .more {
45 | line-height: 24px;
46 | color: #555;
47 | font-size: 12px;
48 | display: flex;
49 | justify-content: space-between;
50 | }
51 |
52 | .exit {
53 | text-align: center;
54 | }
55 |
56 | .exit-active {
57 | background: #eee;
58 | }
59 |
--------------------------------------------------------------------------------
/pages/message/message.wxss:
--------------------------------------------------------------------------------
1 | .button-container {
2 | height: 100%;
3 | display: flex;
4 | align-items: center;
5 | position: fixed;
6 | width: 100%;
7 | }
8 |
9 | .button-container button {
10 | padding-left: 80px;
11 | padding-right: 80px;
12 | }
13 |
14 | .container {
15 | color: #333;
16 | font-size: 16px;
17 | }
18 |
19 | .link {
20 | border-bottom: 1px solid #eee;
21 | padding: 12px;
22 | font-size: 12px;
23 | font-weight: 300;
24 | color: #aaa;
25 | }
26 |
27 | .link-unread {
28 | background: #f4fcf0;
29 | }
30 |
31 | .avatar {
32 | width: 48px;
33 | height: 48px;
34 | border-radius: 50%;
35 | margin-right: 10px;
36 | float: left;
37 | }
38 |
39 | .content {
40 | height: 48px;
41 | }
42 |
43 | .author {
44 | color: #80bd01;
45 | font-weight: 400;
46 | font-size: 16px;
47 | }
48 |
49 | .title {
50 | font-weight: 400;
51 | font-size: 14px;
52 | color: #444;
53 | white-space: nowrap;
54 | text-overflow: ellipsis;
55 | overflow: hidden;
56 | margin-top: 4px;
57 | }
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Rongjian Zhang
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 |
--------------------------------------------------------------------------------
/templates/topic.wxss:
--------------------------------------------------------------------------------
1 | .link {
2 | padding: 12px;
3 | box-sizing: border-box;
4 | border-bottom: 1px solid #eee;
5 | }
6 |
7 | .avatar {
8 | width: 48px;
9 | height: 48px;
10 | border-radius: 50%;
11 | margin-right: 10px;
12 | float: left;
13 | }
14 |
15 | .content {
16 | height: 48px;
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: space-between;
20 | }
21 |
22 | .title {
23 | font-weight: 400;
24 | font-size: 16px;
25 | line-height: 24px;
26 | color: #333;
27 | white-space: nowrap;
28 | text-overflow: ellipsis;
29 | overflow: hidden;
30 | }
31 |
32 | .sub-content {
33 | display: flex;
34 | align-items: center;
35 | font-size: 12px;
36 | }
37 |
38 | .tag {
39 | background-color: #e5e5e5;
40 | color: #999;
41 | padding: 0 4px;
42 | border-radius: 2px;
43 | line-height: 20px;
44 | margin-right: 8px;
45 | }
46 |
47 | .tag-highlight {
48 | background: #80bd01;
49 | color: #fff;
50 | }
51 |
52 | .sub-sub-content {
53 | color: #b4b4b4;
54 | }
55 |
56 | .reply {
57 | color: #9e78c0;
58 | }
59 |
60 | .time {
61 | flex-grow: 1;
62 | text-align: right;
63 | color: #778087;
64 | }
65 |
--------------------------------------------------------------------------------
/pages/message/message.js:
--------------------------------------------------------------------------------
1 | import { getToken, request, showUpdateSuccessToast } from '../../utils'
2 |
3 | Page({
4 | data: {
5 | messages: [],
6 | verified: false,
7 | },
8 | onReady(options) {
9 | this.getMessage()
10 | },
11 | onPullDownRefresh() {
12 | this.getMessage(true)
13 | },
14 | getMessage(isRefresh = false) {
15 | getToken(token => {
16 | this.setData({
17 | verified: true,
18 | })
19 |
20 | request({
21 | url: `/messages?accesstoken=${token}`,
22 | success: json => {
23 | const { data } = json
24 | this.setData({
25 | messages: [...data.hasnot_read_messages, ...data.has_read_messages],
26 | })
27 | if (isRefresh) {
28 | showUpdateSuccessToast()
29 | }
30 |
31 | // Mask all messages as read
32 | request({
33 | url: '/message/mark_all',
34 | method: 'POST',
35 | data: {
36 | accesstoken: token,
37 | },
38 | })
39 | },
40 | complete() {
41 | wx.stopPullDownRefresh()
42 | },
43 | })
44 | })
45 | },
46 | })
47 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/index/index",
4 | "pages/detail/detail",
5 | "pages/message/message",
6 | "pages/me/me",
7 | "pages/recent-topics/recent-topics",
8 | "pages/recent-replies/recent-replies",
9 | "pages/source/source",
10 | "pages/star/star"
11 | ],
12 | "window": {
13 | "backgroundTextStyle": "dark",
14 | "navigationBarBackgroundColor": "#444",
15 | "navigationBarTitleText": "CNodeJS 社区",
16 | "navigationBarTextStyle": "#fff"
17 | },
18 | "tabBar": {
19 | "color": "#7a7e83",
20 | "backgroundColor": "#fff",
21 | "selectedColor": "#80bd01",
22 | "list": [
23 | {
24 | "pagePath": "pages/index/index",
25 | "text": "社区",
26 | "iconPath": "icons/index.png",
27 | "selectedIconPath": "icons/index-active.png"
28 | },
29 | {
30 | "pagePath": "pages/message/message",
31 | "text": "消息",
32 | "iconPath": "icons/message.png",
33 | "selectedIconPath": "icons/message-active.png"
34 | },
35 | {
36 | "pagePath": "pages/me/me",
37 | "text": "我",
38 | "iconPath": "icons/me.png",
39 | "selectedIconPath": "icons/me-active.png"
40 | }
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pages/me/me.js:
--------------------------------------------------------------------------------
1 | import Switch from '../../bower_components/zanui-weapp/dist/switch/index'
2 | import { formatTime, getToken, request } from '../../utils'
3 |
4 | Page(
5 | Object.assign({}, Switch, {
6 | data: {
7 | verified: false,
8 | },
9 | handleZanSwitchChange(e) {
10 | // For switch
11 | this.setData({
12 | [e.componentId]: e.checked,
13 | })
14 | getApp().globalData[e.componentId] = e.checked
15 | wx.setStorage({
16 | key: e.componentId,
17 | data: e.checked,
18 | })
19 | },
20 | onReady() {
21 | this.getData()
22 | },
23 | getData() {
24 | getToken(() => {
25 | const app = getApp()
26 | const { hasTail, messagePushEnabled } = app.globalData
27 | this.setData({ hasTail, messagePushEnabled, verified: true })
28 |
29 | const { loginname } = app.globalData
30 | request({
31 | url: `/user/${loginname}`,
32 | success: json => {
33 | const { data } = json
34 | this.setData({
35 | user: data,
36 | time: formatTime(data.create_at),
37 | })
38 |
39 | // Set recent topics and replies to global data
40 | app.globalData.recent_topics = data.recent_topics
41 | app.globalData.recent_replies = data.recent_replies
42 | },
43 | })
44 | })
45 | const app = getApp()
46 | },
47 | exit() {
48 | wx.showModal({
49 | title: '是否退出?',
50 | content: '退出登录后,如需再次登录请重新扫码',
51 | success: function(res) {
52 | if (res.confirm) {
53 | const app = getApp()
54 | delete app.globalData.token
55 | wx.removeStorage({
56 | key: 'token',
57 | complete: function() {
58 | wx.reLaunch({
59 | url: '/pages/index/index',
60 | })
61 | },
62 | })
63 | } else if (res.cancel) {
64 | }
65 | },
66 | })
67 | },
68 | })
69 | )
70 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pages/me/me.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{user.loginname}}
8 |
9 | 注册时间:{{time}}
10 | {{user.score}} 积分
11 |
12 |
13 |
14 |
15 |
16 |
17 | 最近参与的话题
18 |
19 |
20 |
21 | 最近创建的话题
22 |
23 |
24 |
25 | 收藏的话题
26 |
27 |
28 |
29 |
30 |
31 |
32 | 小尾巴
33 |
34 |
35 |
36 | 新消息提醒
37 |
38 |
39 |
40 |
41 |
42 |
43 | 源代码
44 |
45 |
46 |
47 |
48 |
49 |
50 | 退出登录
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import { request, showMessageToast } from './utils'
2 |
3 | // Storage keys
4 | // token - access token
5 | // hasTail - 发贴小尾巴
6 | // messagePushEnabled - 是否开启消息推送
7 |
8 | App({
9 | globalData: {},
10 | onLaunch() {
11 | this.checkToken()
12 | this.loadSettings()
13 |
14 | const fetchMessagePeriodically = () => {
15 | this.fetchMessage()
16 | setTimeout(fetchMessagePeriodically, 60 * 1000)
17 | }
18 | fetchMessagePeriodically()
19 | },
20 | loadSettings() {
21 | wx.getStorage({
22 | key: 'hasTail',
23 | success: res => {
24 | this.globalData.hasTail = res.data
25 | },
26 | fail: () => {
27 | this.globalData.hasTail = true
28 | wx.setStorage({
29 | key: 'hasTail',
30 | data: true,
31 | })
32 | },
33 | })
34 | wx.getStorage({
35 | key: 'messagePushEnabled',
36 | success: res => {
37 | this.globalData.messagePushEnabled = res.data
38 | },
39 | fail: () => {
40 | this.globalData.messagePushEnabled = true
41 | wx.setStorage({
42 | key: 'messagePushEnabled',
43 | data: true,
44 | })
45 | },
46 | })
47 | },
48 | checkToken() {
49 | // Try to get token from storage
50 | wx.getStorage({
51 | key: 'token',
52 | success: res => {
53 | const accesstoken = res.data
54 | // Check if token is valid
55 | request({
56 | url: '/accesstoken',
57 | method: 'POST',
58 | data: { accesstoken },
59 | success: json => {
60 | // Valid token, save user info
61 | this.globalData.token = accesstoken
62 | this.globalData.loginname = json.loginname
63 | },
64 | errorExtraHandle(res) {
65 | // Invalid token
66 | wx.removeStorage({
67 | key: 'token',
68 | })
69 | },
70 | })
71 | },
72 | fail: res => {
73 | // No token in storage, do nothing
74 | },
75 | })
76 | },
77 | fetchMessage(cb = () => {}) {
78 | // Send request only if token exsists and message push is enabled
79 | const { token, messagePushEnabled } = this.globalData
80 | if (!(token && messagePushEnabled)) {
81 | cb()
82 | return
83 | }
84 |
85 | request({
86 | url: `/message/count?accesstoken=${token}`,
87 | success: json => {
88 | if (json.data > 0) {
89 | showMessageToast()
90 | }
91 | cb()
92 | },
93 | })
94 | },
95 | })
96 |
--------------------------------------------------------------------------------
/pages/detail/detail.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{topic.title}}
6 |
7 |
8 |
9 | {{topic.author.loginname}}
10 | 创建于{{create_at}} {{topic.visit_count}} 次浏览
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 共 {{topic.reply_count}} 条回复
24 |
25 |
26 |
27 |
28 | {{item.author.loginname}}
29 | {{reply_create_at[index]}}
30 |
31 |
32 |
33 |
34 |
35 | {{item.ups.length}}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | import timeago from './bower_components/timeago.js/dist/timeago'
2 | const timeagoInstance = timeago()
3 | const URL_PREFIX = 'https://cnodejs.org/api/v1'
4 |
5 | // Simple wrapper for request
6 | export function request(options) {
7 | wx.request(
8 | Object.assign({}, options, {
9 | // Add prefix to URL
10 | url: `${URL_PREFIX}${options.url}`,
11 | success(res) {
12 | const json = res.data
13 | // If return success: false, show error message and exit
14 | if (!json.success) {
15 | showErrorToast(json.error_msg)
16 | // Add extra handler
17 | if (options.errorExtraHandle) {
18 | options.errorExtraHandle(res)
19 | }
20 | return
21 | }
22 | if (options.success) {
23 | options.success(json)
24 | }
25 | },
26 | fail(res) {
27 | showErrorToast(res.errMsg)
28 | if (options.fail) {
29 | options.fail(res)
30 | }
31 | },
32 | })
33 | )
34 | }
35 |
36 | export function formatTime(time) {
37 | return timeagoInstance.format(time, 'zh_CN')
38 | }
39 |
40 | // Toasts
41 | export function showLoadingToast(title = '加载中') {
42 | wx.showToast({
43 | title,
44 | icon: 'loading',
45 | mask: true,
46 | duration: 10000,
47 | })
48 | }
49 |
50 | export function showSuccessToast(title = '加载成功') {
51 | wx.showToast({ title })
52 | }
53 |
54 | export function showUpdateSuccessToast(title = '已更新') {
55 | wx.showToast({ title })
56 | }
57 |
58 | function showErrorToast(title = '操作失败') {
59 | wx.showToast({
60 | title,
61 | image: '/icons/error.svg',
62 | })
63 | }
64 |
65 | export function showMessageToast(title = '有新消息') {
66 | wx.showToast({
67 | title,
68 | image: '/icons/new-message.svg',
69 | })
70 | }
71 |
72 | export function onShareAppMessage() {
73 | return {
74 | title: 'CNodeJS社区',
75 | path: `/pages/index/index`,
76 | }
77 | }
78 |
79 | // Try to get access token
80 | // If token already exists, use it
81 | // If no token, call QRCode scan to get token
82 | export function getToken(cb) {
83 | const app = getApp()
84 |
85 | // Has token and already checked
86 | if (app.globalData.token) {
87 | cb(app.globalData.token)
88 | return
89 | }
90 |
91 | // No token, request user to scan QRCode
92 | wx.showModal({
93 | content:
94 | '请先在 PC 版 CNodeJS 社区登录,Access Token 的二维码位于“设置”页面左下角',
95 | showCancel: false,
96 | confirmText: '我知道了',
97 | complete() {
98 | wx.scanCode({
99 | success(res) {
100 | const accesstoken = res.result
101 | request({
102 | url: '/accesstoken',
103 | method: 'POST',
104 | data: { accesstoken },
105 | success(json) {
106 | // Valid token, save user info
107 | app.globalData.token = accesstoken
108 | app.globalData.loginname = json.loginname
109 | app.globalData.avatar_url = json.avatar_url
110 | cb(accesstoken)
111 | wx.setStorage({
112 | key: 'token',
113 | data: accesstoken,
114 | })
115 | },
116 | errorExtraHandle(res) {
117 | // Invalid token
118 | wx.removeStorage({
119 | key: 'token',
120 | })
121 | },
122 | })
123 | },
124 | fail(res) {
125 | showErrorToast('扫码失败')
126 | },
127 | })
128 | },
129 | })
130 | }
131 |
--------------------------------------------------------------------------------
/pages/detail/detail.wxss:
--------------------------------------------------------------------------------
1 | @import "/bower_components/wxParse/wxParse/wxParse.wxss";
2 |
3 | .container {
4 | padding: 12px 12px 0;
5 | font-weight: 300;
6 | }
7 |
8 | .title {
9 | font-size: 20px;
10 | font-weight: bold;
11 | margin-bottom: 12px;
12 | }
13 |
14 | .user {
15 | margin: 12px 0;
16 | position: relative;
17 | }
18 |
19 | .avatar {
20 | width: 48px;
21 | height: 48px;
22 | float: left;
23 | border-radius: 50%;
24 | margin-right: 8px;
25 | }
26 |
27 | .user-info {
28 | display: flex;
29 | justify-content: space-between;
30 | flex-direction: column;
31 | line-height: 24px;
32 | }
33 |
34 | .name {
35 | font-weight: 400;
36 | font-size: 16px;
37 | }
38 |
39 | .create-at {
40 | font-size: 12px;
41 | color: #555;
42 | }
43 |
44 | .operate-0 {
45 | position: absolute;
46 | top: 0;
47 | right: 0;
48 | display: flex;
49 | }
50 |
51 | .operate-0 > view {
52 | padding: 4px;
53 | }
54 |
55 | .star {
56 | /*padding: 6px 8px;
57 | line-height: 1;
58 | color: #fff;
59 | font-size: 12px;
60 | border-radius: 4px;
61 | background-color: #80bd01;
62 | letter-spacing: 1px;*/
63 | }
64 |
65 | .operate-0 image {
66 | width: 24px;
67 | height: 24px;
68 | }
69 | /*.star-active {
70 | background-color: #6ba44e;
71 | }*/
72 |
73 | .reply {
74 | padding: 8px 0;
75 | border-bottom: 1px solid #eee;
76 | position: relative;
77 | }
78 |
79 | .reply-title {
80 | background: #eee;
81 | padding: 6px;
82 | font-weight: 400;
83 | font-size: 16px;
84 | }
85 |
86 | .reply-avatar {
87 | width: 48px;
88 | height: 48px;
89 | float: left;
90 | border-radius: 50%;
91 | margin-right: 8px;
92 | }
93 |
94 | .reply-user-info {
95 | height: 48px;
96 | display: flex;
97 | flex-direction: column;
98 | justify-content: space-between;
99 | }
100 |
101 | .reply-name {
102 | font-weight: 400;
103 | font-size: 16px;
104 | line-height: 24px;
105 | }
106 |
107 | .reply-time {
108 | color: #555;
109 | font-size: 12px;
110 | line-height: 24px;
111 | }
112 |
113 | .operate {
114 | position: absolute;
115 | right: 0;
116 | top: 4px;
117 | font-size: 14px;
118 | line-height: 1;
119 | color: #08c;
120 | border-radius: 4px;
121 | display: flex;
122 | }
123 |
124 | .operate > view {
125 | padding: 4px;
126 | }
127 |
128 | .operate image {
129 | width: 20px;
130 | height: 20px;
131 | }
132 |
133 | .reply-add-active {
134 | /*color: #fff;
135 | background: #08c;*/
136 | }
137 |
138 | .reply-dialog {
139 | left: 0;
140 | padding: 12px;
141 | width: 100%;
142 | box-sizing: border-box;
143 | }
144 |
145 | .zan-dialog__container {
146 | transition: all 0s;
147 | top: 0;
148 | bottom: unset;
149 | transform:translateY(-150%);
150 | }
151 |
152 | .reply-dialog textarea {
153 | font-size: 14px;
154 | box-sizing: border-box;
155 | border-radius: 4px;
156 | width: 100%;
157 | height: 130px;
158 | border: 1px solid #eee;
159 | margin-bottom: 10px;
160 | padding: 12px;
161 | }
162 |
163 | /*markdown*/
164 |
165 | .wxParse-ul {
166 | margin: 8px 0 8px 12px;
167 | }
168 |
169 | .wxParse-li-circle {
170 | display: none;
171 | }
172 |
173 | .markdown-text {
174 | margin: 10px 0 6px;
175 | }
176 |
177 |
178 | /*Fix font size too large*/
179 |
180 | .wxParse-div {
181 | font-size: 16px;
182 | }
183 |
184 |
185 | /*Fix code*/
186 |
187 | .wxParse-pre {
188 | padding: 8px;
189 | overflow: auto;
190 | font-size: 85%;
191 | line-height: 1.45;
192 | background-color: #f7f7f7;
193 | border-radius: 2px;
194 | margin: 10px 0;
195 | }
196 |
197 |
198 | /*Fix scroll*/
199 |
200 | .wxParse-li-text {
201 | line-height: 24px;
202 | }
203 |
--------------------------------------------------------------------------------
/pages/index/index.js:
--------------------------------------------------------------------------------
1 | import Tab from '../../bower_components/zanui-weapp/dist/tab/index'
2 | import {
3 | formatTime,
4 | onShareAppMessage,
5 | request,
6 | showLoadingToast,
7 | showUpdateSuccessToast,
8 | } from '../../utils'
9 |
10 | Page(
11 | Object.assign({}, Tab, {
12 | data: {
13 | isLoading: false,
14 | page: 1,
15 | topics: [],
16 | tab: {
17 | list: [
18 | {
19 | id: 'all',
20 | title: '全部',
21 | },
22 | {
23 | id: 'good',
24 | title: '精华',
25 | },
26 | {
27 | id: 'share',
28 | title: '分享',
29 | },
30 | {
31 | id: 'ask',
32 | title: '问答',
33 | },
34 | {
35 | id: 'job',
36 | title: '招聘',
37 | },
38 | ],
39 | selectedId: 'all',
40 | scroll: false,
41 | },
42 | },
43 | onShareAppMessage,
44 | getTag(topic) {
45 | if (topic.top) {
46 | return { highlight: true, text: '置顶' }
47 | }
48 | if (topic.good) {
49 | return { highlight: true, text: '精华' }
50 | }
51 | const tagMap = {
52 | share: '分享',
53 | ask: '问答',
54 | job: '招聘',
55 | }
56 | return {
57 | highlight: false,
58 | text: tagMap[topic.tab],
59 | }
60 | },
61 | format(topics) {
62 | return topics.map(topic => {
63 | // Get display tag
64 | topic.tag = this.getTag(topic)
65 | // Format time
66 | topic.time = formatTime(topic.last_reply_at)
67 | return topic
68 | })
69 | },
70 | handleZanTabChange(e) {
71 | // For zanui tab
72 | var componentId = e.componentId
73 | var selectedId = e.selectedId
74 |
75 | if (selectedId === this.data.tab.selectedId) {
76 | return
77 | }
78 |
79 | this.setData({
80 | [`${componentId}.selectedId`]: selectedId,
81 | isLoading: true,
82 | })
83 | showLoadingToast()
84 | this.requestTopics({
85 | success: json => {
86 | this.setData({
87 | isLoading: false,
88 | page: 1,
89 | topics: this.format(json.data),
90 | })
91 | wx.hideToast()
92 | },
93 | })
94 | },
95 | requestTopics({ page = 1, success, fail, complete }) {
96 | const tab = this.data.tab.selectedId
97 | const queryTab = tab === 'all' ? '' : `&tab=${tab}`
98 | request({
99 | url: `/topics?limit=15&page=${page}${queryTab}`,
100 | success,
101 | fail,
102 | complete,
103 | })
104 | },
105 | onPullDownRefresh() {
106 | this.setData({
107 | isLoading: true,
108 | })
109 | this.requestTopics({
110 | page: 1,
111 | success: json => {
112 | showUpdateSuccessToast()
113 | this.setData({
114 | isLoading: false,
115 | topics: this.format(json.data),
116 | })
117 | },
118 | complete() {
119 | wx.stopPullDownRefresh()
120 | },
121 | })
122 | },
123 | onLoad() {
124 | this.setData({
125 | isLoading: true,
126 | })
127 |
128 | this.requestTopics({
129 | success: json => {
130 | this.setData({
131 | isLoading: false,
132 | topics: this.format(json.data),
133 | })
134 | },
135 | })
136 | },
137 | onReachBottom() {
138 | if (this.data.isLoading) {
139 | return
140 | }
141 |
142 | this.setData({
143 | isLoading: true,
144 | })
145 |
146 | this.requestTopics({
147 | page: this.data.page + 1,
148 | success: json => {
149 | this.setData({
150 | isLoading: false,
151 | page: this.data.page + 1,
152 | topics: [...this.data.topics, ...this.format(json.data)],
153 | })
154 | },
155 | })
156 | },
157 | })
158 | )
159 |
--------------------------------------------------------------------------------
/pages/detail/detail.js:
--------------------------------------------------------------------------------
1 | import { wxParse } from '../../bower_components/wxParse/wxParse/wxParse.js'
2 | import {
3 | formatTime,
4 | getToken,
5 | request,
6 | showSuccessToast,
7 | showFailToast,
8 | } from '../../utils'
9 |
10 | Page({
11 | data: {
12 | topic: undefined,
13 | end: true,
14 | isDialogVisible: false,
15 | replyId: undefined,
16 | replyContent: '',
17 | isSubmitting: false,
18 | },
19 | onLoad(options) {
20 | wx.showNavigationBarLoading()
21 | this.fetchTopic(options.id)
22 | },
23 | fetchTopic(id) {
24 | const app = getApp()
25 | const { token } = app.globalData
26 | const query = token ? `?accesstoken=${token}` : ''
27 |
28 | request({
29 | url: `/topic/${id}${query}`,
30 | success: json => {
31 | const { data } = json
32 | wx.setNavigationBarTitle({
33 | title: data.title,
34 | })
35 | this.setData({
36 | topic: data,
37 | create_at: formatTime(data.create_at),
38 | reply_create_at: data.replies.map(reply => {
39 | return formatTime(reply.create_at)
40 | }),
41 | replies: data.replies.slice(0, 10),
42 | end: data.replies.length <= 10,
43 | })
44 | // Render HTML
45 | wxParse('content', 'html', data.content, this, 20)
46 | this.data.replies.forEach((reply, i) => {
47 | wxParse(`replies_html[${i}]`, 'html', reply.content, this, 20)
48 | })
49 | wx.hideNavigationBarLoading()
50 | },
51 | })
52 | },
53 | onReachBottom() {
54 | const count = this.data.replies.length
55 | const moreReplies = this.data.topic.replies.slice(count, count + 10)
56 | this.setData({
57 | replies: [...this.data.replies, ...moreReplies],
58 | end: count + 10 >= this.data.topic.replies.length,
59 | })
60 | moreReplies.forEach((reply, i) => {
61 | wxParse(`replies_html[${i + count}]`, 'html', reply.content, this, 20)
62 | })
63 | },
64 | onShareAppMessage() {
65 | return {
66 | title: this.data.topic.title,
67 | path: `/pages/detail/detail?id=${this.data.topic.id}`,
68 | }
69 | },
70 | showDialog(e) {
71 | this.setData({
72 | isDialogVisible: true,
73 | // replyContent: `@${e.currentTarget.dataset.name} `,
74 | replyId: e.currentTarget.dataset.id,
75 | replyName: e.currentTarget.dataset.name,
76 | })
77 | },
78 | hideDialog() {
79 | this.setData({
80 | isDialogVisible: false,
81 | })
82 | },
83 | changeInput(e) {
84 | this.setData({
85 | replyContent: e.detail.value,
86 | })
87 | },
88 | submit() {
89 | this.setData({
90 | isSubmitting: true,
91 | })
92 |
93 | getToken(token => {
94 | const app = getApp()
95 | const tail = app.globalData.hasTail
96 | ? '\n\n来自 [CNode weapp](https://github.com/pd4d10/cnode-weapp)'
97 | : ''
98 | const replyName = this.data.replyName ? `@${this.data.replyName} ` : ''
99 | const content = `${replyName}${this.data.replyContent}${tail}`
100 |
101 | request({
102 | url: `/topic/${this.data.topic.id}/replies`,
103 | method: 'POST',
104 | data: {
105 | accesstoken: token,
106 | content,
107 | reply_id: this.data.replyId,
108 | },
109 | success: res => {
110 | this.setData({
111 | isSubmitting: false,
112 | isDialogVisible: false,
113 | })
114 | showSuccessToast('回复成功')
115 | if (this.data.end) {
116 | setTimeout(() => {
117 | wx.redirectTo({
118 | url: `/pages/detail/detail?id=${this.data.topic.id}`,
119 | })
120 | }, 500)
121 | }
122 | },
123 | })
124 | })
125 | },
126 | thumb(e) {
127 | const { id, index } = e.currentTarget.dataset
128 | getToken(token => {
129 | request({
130 | url: `/reply/${id}/ups`,
131 | method: 'POST',
132 | data: {
133 | accesstoken: token,
134 | },
135 | success: json => {
136 | const isUp = json.action === 'up'
137 | const ups = this.data.replies[index].ups.slice()
138 | // Can't get reply id
139 | if (isUp) {
140 | ups.push('me')
141 | } else {
142 | ups.pop()
143 | }
144 | this.setData({
145 | [`replies[${index}].is_uped`]: isUp,
146 | [`replies[${index}].ups`]: ups,
147 | })
148 | },
149 | })
150 | })
151 | },
152 | star(e) {
153 | const { id } = e.currentTarget.dataset
154 | const { is_collect } = this.data.topic
155 | const url = `/topic_collect/${is_collect ? 'de_collect' : 'collect'}`
156 | getToken(token => {
157 | request({
158 | url,
159 | method: 'POST',
160 | data: {
161 | accesstoken: token,
162 | topic_id: id,
163 | },
164 | success: () => {
165 | this.setData({
166 | 'topic.is_collect': !is_collect,
167 | })
168 | showSuccessToast(is_collect ? '已取消收藏' : '已收藏')
169 | },
170 | })
171 | })
172 | },
173 | })
174 |
--------------------------------------------------------------------------------