├── .gitignore
├── App
├── app.js
├── app.json
├── app.wxss
├── components
│ ├── hot
│ │ ├── hot.wxml
│ │ └── hot.wxss
│ ├── live
│ │ ├── live.wxml
│ │ ├── live.wxss
│ │ ├── live_middle.wxml
│ │ ├── live_middle.wxss
│ │ ├── live_small.wxml
│ │ └── live_small.wxss
│ ├── user
│ │ ├── user.wxml
│ │ ├── user.wxss
│ │ ├── user_small.wxml
│ │ └── user_small.wxss
│ └── widget
│ │ ├── rating.wxml
│ │ └── rating.wxss
├── images
│ ├── explore_normal.png
│ ├── explore_pressed.png
│ ├── hot_normal.png
│ ├── hot_pressed.png
│ ├── rating
│ │ ├── semistar.png
│ │ ├── semistar_s.png
│ │ ├── star.png
│ │ ├── star_s.png
│ │ ├── unstar.png
│ │ └── unstar_s.png
│ ├── search-off.png
│ ├── search-on.png
│ ├── search@1x.png
│ └── search@2x.png
├── pages
│ ├── explore
│ │ ├── explore.js
│ │ ├── explore.json
│ │ ├── explore.wxml
│ │ └── explore.wxss
│ ├── hot
│ │ ├── lib.js
│ │ ├── monthly.js
│ │ ├── monthly.json
│ │ ├── monthly.wxml
│ │ ├── monthly.wxss
│ │ ├── weekly.js
│ │ ├── weekly.json
│ │ ├── weekly.wxml
│ │ └── weekly.wxss
│ ├── live
│ │ ├── live.js
│ │ ├── live.json
│ │ ├── live.wxml
│ │ └── live.wxss
│ ├── search
│ │ ├── search.js
│ │ ├── search.json
│ │ ├── search.wxml
│ │ └── search.wxss
│ ├── topic
│ │ ├── hot_topics.js
│ │ ├── hot_topics.json
│ │ ├── hot_topics.wxml
│ │ ├── hot_topics.wxss
│ │ ├── topic.js
│ │ ├── topic.json
│ │ ├── topic.wxml
│ │ └── topic.wxss
│ └── users
│ │ ├── user.js
│ │ ├── user.json
│ │ ├── user.wxml
│ │ ├── user.wxss
│ │ ├── users.js
│ │ ├── users.json
│ │ ├── users.wxml
│ │ └── users.wxss
└── utils
│ ├── api.js
│ └── util.js
├── LICENSE
├── README.md
├── Server
├── LogGraph.ipynb
├── app.py
├── client.py
├── config.py
├── crawl.py
├── exception.py
├── models
│ ├── __init__.py
│ ├── live.py
│ ├── speaker.py
│ ├── topic.py
│ └── utils.py
├── requirements.txt
├── static
│ └── images
│ │ ├── default-cover.png
│ │ ├── default-topic.png
│ │ ├── monthly.svg
│ │ └── weekly.svg
├── stopwords-utf8.txt
├── test_es.py
├── utils.py
└── views
│ ├── __init__.py
│ ├── api.py
│ ├── protocol.py
│ ├── schemas.py
│ └── utils.py
└── screenshot
├── zhihulive.gif
└── zhihulive.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
--------------------------------------------------------------------------------
/App/app.js:
--------------------------------------------------------------------------------
1 | //app.js
2 | App({
3 | onLaunch: function () {
4 | console.log('app Launching ...');
5 | var that = this
6 | wx.getSystemInfo({
7 | success(res) {
8 | that.systemInfo = res;
9 | },
10 | });
11 | },
12 | getUserInfo: function (cb) {
13 | var that = this
14 | if (this.globalData.userInfo) {
15 | typeof cb == "function" && cb(this.globalData.userInfo)
16 | } else {
17 | wx.login({
18 | success: function () {
19 | wx.getUserInfo({
20 | success: function (res) {
21 | that.globalData.userInfo = res.userInfo
22 | typeof cb == "function" && cb(that.globalData.userInfo)
23 | }
24 | })
25 | }
26 | })
27 | }
28 | },
29 | globalData: {
30 | userInfo: null
31 | },
32 | systemInfo: null
33 | })
--------------------------------------------------------------------------------
/App/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/explore/explore",
4 | "pages/search/search",
5 | "pages/users/users",
6 | "pages/users/user",
7 | "pages/hot/weekly",
8 | "pages/topic/hot_topics",
9 | "pages/live/live",
10 | "pages/topic/topic",
11 | "pages/hot/monthly"
12 | ],
13 | "window": {
14 | "backgroundTextStyle": "dark",
15 | "navigationBarBackgroundColor": "#4abdcc",
16 | "navigationBarTitleText": "知乎Live",
17 | "navigationBarTextStyle": "white",
18 | "enablePullDownRefresh": true
19 | },
20 | "tabBar": {
21 | "color": "#b0b0b0",
22 | "selectedColor": "#4abdcc",
23 | "borderStyle": "white",
24 | "backgroundColor": "#fff",
25 | "list": [
26 | {
27 | "pagePath": "pages/explore/explore",
28 | "iconPath": "images/explore_normal.png",
29 | "selectedIconPath": "images/explore_pressed.png",
30 | "text": "发现"
31 | },
32 | {
33 | "pagePath": "pages/hot/weekly",
34 | "iconPath": "images/explore_normal.png",
35 | "selectedIconPath": "images/explore_pressed.png",
36 | "text": "热门"
37 | },
38 | {
39 | "pagePath": "pages/search/search",
40 | "iconPath": "images/search-off.png",
41 | "selectedIconPath": "images/search-on.png",
42 | "text": "搜索"
43 | }
44 | ]
45 | },
46 | "debug": true
47 | }
--------------------------------------------------------------------------------
/App/app.wxss:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | justify-content: space-between;
7 | padding: 200rpx 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | page {
12 | font: 14px "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, STHeiti;
13 | line-height: 1.5;
14 | }
15 |
16 | .clearfix:after {
17 | content: '';
18 | clear: both;
19 | overflow: hidden;
20 | height: 0;
21 | display: block;
22 | }
23 |
--------------------------------------------------------------------------------
/App/components/hot/hot.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/App/components/hot/hot.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/live/live_middle.wxss";
2 |
3 | page {
4 | background-color: #fff;
5 | }
6 |
7 | .header {
8 | margin-top: 20rpx;
9 | border-radius: 8rpx 8rpx 0 0;
10 | position: relative;
11 | height: 96rpx;
12 | display: flex;
13 | -webkit-box-pack: center;
14 | -ms-flex-pack: center;
15 | justify-content: center;
16 | -webkit-box-align: center;
17 | -ms-flex-align: center;
18 | align-items: center;
19 | z-index: 1;
20 | border-bottom: 2rpx solid #eee;
21 | border-image-slice: 0 0 1;
22 | }
23 |
24 | .nav {
25 | display: inline-block;
26 | border: 2rpx solid #4abdcc;
27 | border-radius: 8rpx;
28 | }
29 |
30 | .nav .selected {
31 | background: #4abdcc;
32 | color: #fff;
33 | }
34 |
35 | .nav view {
36 | text-align: center;
37 | min-width: 184rpx;
38 | display: inline-block;
39 | padding: 19rpx 32rpx;
40 | box-sizing: border-box;
41 | line-height: 1;
42 | font-weight: 500;
43 | font-size: 26rpx;
44 | color: #4abdcc;
45 | }
46 |
47 | .banner {
48 | width: 100%;
49 | height: 100px;
50 | background-color: rgb(255, 255, 255);
51 | background-size: cover;
52 | background-position: center center;
53 | background-repeat: no-repeat;
54 | background-image: url(http://localhost:8300/static/images/weekly.svg);
55 | }
56 |
--------------------------------------------------------------------------------
/App/components/live/live.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ live.subject }}
7 |
8 |
9 |
10 |
11 | by {{ live.speaker.name }}
12 |
13 |
14 |
15 |
16 | 开始时间{{ live.starts_at }}
17 |
18 |
19 | {{ live.seats_taken }}参与 / {{ live.liked_num }}喜欢 / {{ live.speaker_message_count }}个回答
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/App/components/live/live.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/widget/rating.wxss";
2 |
3 | .m-live {
4 | overflow: hidden;
5 | position: relative;
6 | border-radius: 8rpx;
7 | margin-bottom: 20rpx;
8 | }
9 |
10 | .m-live .cover {
11 | width: 100%;
12 | height: 352rpx;
13 | vertical-align: bottom;
14 | }
15 |
16 | .m-live::before {
17 | content: '';
18 | display: block;
19 | position: absolute;
20 | left: 0;
21 | top: 0;
22 | width: 560rpx;
23 | height: 100%;
24 | border-top-left-radius: 8rpx;
25 | border-bottom-left-radius: 8rpx;
26 | background: linear-gradient(to right, rgba(0, 0, 0, 0.6) 0, rgba(0, 0, 0, 0) 100%);
27 | }
28 |
29 | .m-live .info {
30 | color: #fff;
31 | padding: 0 30rpx;
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | height: 100%;
36 | width: 100%;
37 | z-index: 1;
38 | box-sizing: border-box;
39 | }
40 |
41 | .m-live .info .h2 {
42 | font-size: 36rpx;
43 | font-weight: 600;
44 | line-height: 1.22;
45 | text-shadow: 2rpx 2rpx rgba(0, 0, 0, 0.4);
46 | overflow: hidden;
47 | text-overflow: ellipsis;
48 | -webkit-line-clamp: 2;
49 | display: -webkit-box;
50 | -webkit-box-orient: vertical;
51 | margin-top: 30rpx;
52 | }
53 |
54 | .m-live .detail {
55 | margin-top: 36rpx;
56 | padding-left: 18rpx;
57 | position: relative;
58 | font-size: 18rpx;
59 | line-height: 1;
60 | font-weight: bold;
61 | text-shadow: 2rpx 4rpx 0px rgba(0, 0, 0, 0.20);
62 | }
63 |
64 | .m-live .detail view {
65 | margin-bottom: 6rpx;
66 | }
67 |
68 | .m-live .detail text {
69 | margin-right: 20rpx;
70 | }
71 |
72 | .m-live .detail .place {
73 | font-weight: normal;
74 | font-size: 16rpx;
75 | }
76 |
77 | .m-live .detail::before {
78 | content: '';
79 | width: 6rpx;
80 | height: 100%;
81 | border-right: 1rpx;
82 | background-color: #4abdcc;
83 | position: absolute;
84 | left: 4rpx;
85 | top: 2rpx;
86 | }
87 |
88 | .m-live .user {
89 | position: absolute;
90 | width: 100%;
91 | top: 75rpx;
92 | left: 300rpx;
93 | line-height: 30rpx;
94 | font-size: 18rpx;
95 | }
96 |
97 | .m-live .avatar {
98 | width: 30rpx;
99 | height: 30rpx;
100 | float: left;
101 | margin-right: 15rpx;
102 | border-radius: 30rpx;
103 | vertical-align: bottom;
104 | }
105 |
106 | .m-live .bottom {
107 | position: absolute;
108 | bottom: 24rpx;
109 | font-size: 20rpx;
110 | }
111 |
112 | .m-live-s {
113 | color: #fff;
114 | overflow: hidden;
115 | position: relative;
116 | border-radius: 8rpx;
117 | margin-bottom: 20rpx;
118 | }
119 |
120 | .m-live-s .mask {
121 | position: absolute;
122 | left: 0;
123 | top: 0;
124 | width: 222rpx;
125 | height: 240rpx;
126 | background-color: rgba(255, 255, 255, 0);
127 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
128 | border-top-left-radius: 8rpx;
129 | border-bottom-left-radius: 8rpx;
130 | }
131 |
132 | .m-live-s .cover {
133 | width: 100%;
134 | height: 240rpx;
135 | vertical-align: bottom;
136 | background-position: center center;
137 | }
138 |
139 | .m-live-s .info {
140 | position: absolute;
141 | left: 0;
142 | top: 0;
143 | margin: 20rpx 28rpx;
144 | text-shadow: 2rpx 3rpx rgba(0, 0, 0, 0.2);
145 | }
146 |
147 | .m-live-s .title {
148 | font-size: 36rpx;
149 | font-weight: bold;
150 | margin-left: -4rpx;
151 | line-height: 50rpx;
152 | }
153 |
154 | .m-live-s .detail {
155 | font-size: 20rpx;
156 | position: relative;
157 | height: 16rpx;
158 | line-height: 16rpx;
159 | padding-left: 16rpx;
160 | margin-top: 4rpx;
161 | }
162 |
163 | .m-live-s .detail::before {
164 | content: '';
165 | position: absolute;
166 | left: 0;
167 | top: 0;
168 | width: 6rpx;
169 | height: 16rpx;
170 | background-color: #4abdcc;
171 | }
172 |
173 | .m-live-s .detail view {
174 | display: inline-block;
175 | margin-right: 16rpx;
176 | }
177 |
178 | .m-live-s .detail text {
179 | font-weight: bold;
180 | }
181 |
182 | .m-live-s .stat {
183 | position: absolute;
184 | width: 100%;
185 | left: 0;
186 | bottom: 24rpx;
187 | height: 32rpx;
188 | line-height: 31rpx;
189 | font-size: 20rpx;
190 | padding-left: 28rpx;
191 | background-color: rgba(0, 0, 0, 0.4);
192 | z-index: 2;
193 | }
194 |
195 | .m-live-s .stat::before {
196 | content: '';
197 | width: 100%;
198 | position: absolute;
199 | height: 40rpx;
200 | left: 0;
201 | bottom: -4rpx;
202 | background-color: rgba(0, 0, 0, 0.4);
203 | z-index: -1;
204 | }
205 |
206 | .m-live-s .stat view {
207 | position: relative;
208 | display: inline-block;
209 | margin-right: 30rpx;
210 | padding-left: 16rpx;
211 | }
212 |
213 | .m-live-s .stat view::before {
214 | content: '';
215 | width: 8rpx;
216 | height: 8rpx;
217 | position: absolute;
218 | left: 0;
219 | top: 12rpx;
220 | background-color: #4abdcc;
221 | border-radius: 8rpx;
222 | }
223 |
224 | .m-live-s .stat text {
225 | font-weight: bold;
226 | }
227 |
--------------------------------------------------------------------------------
/App/components/live/live_middle.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ live.subject }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ live.speaker.name }}
15 |
16 |
17 |
18 | {{ live.outline || live.description }}
19 |
20 |
21 | {{ live.liked_num }} 喜欢 / {{ live.seats_taken }} 参加
22 |
23 |
24 |
--------------------------------------------------------------------------------
/App/components/live/live_middle.wxss:
--------------------------------------------------------------------------------
1 | .m-live {
2 | position: relative;
3 | margin: 20rpx;
4 | border-bottom: 1px solid #eaeaea;
5 | padding-bottom: 20rpx;
6 | }
7 |
8 | .m-live .cover {
9 | width: 220rpx;
10 | height: 220rpx;
11 | border-radius: 8rpx;
12 | float: left;
13 | margin-right: 20rpx;
14 | background-color: #e7ddc7;
15 | }
16 |
17 | .m-live .label {
18 | position: absolute;
19 | left: -2rpx;
20 | top: 20rpx;
21 | width: 57rpx;
22 | height: 37rpx;
23 | }
24 |
25 | .m-live .info {
26 | height: 220rpx;
27 | overflow: hidden;
28 | position: relative;
29 | }
30 |
31 | .m-live .title {
32 | font-size: 28rpx;
33 | line-height: 28rpx;
34 | font-weight: bold;
35 | }
36 |
37 | .m-live .rating {
38 | color: #999;
39 | font-size: 22rpx;
40 | position: absolute;
41 | top: 62rpx;
42 | }
43 |
44 | .m-live .rating image {
45 | width: 26rpx;
46 | height: 26rpx;
47 | margin-right: 7rpx;
48 | }
49 |
50 | .m-live .m-rating {
51 | display: inline-block;
52 | vertical-align: middle;
53 | }
54 |
55 | .m-live .rating .count {
56 | margin-left: 10rpx;
57 | }
58 |
59 | .m-live .desc {
60 | color: #5c5c5c;
61 | font-size: 24rpx;
62 | line-height: 34rpx;
63 | overflow: hidden;
64 | text-overflow: ellipsis;
65 | display: -webkit-box;
66 | -webkit-line-clamp: 2;
67 | -webkit-box-orient: vertical;
68 | position: absolute;
69 | bottom: 46rpx;
70 | }
71 |
72 | .m-live .detail {
73 | color: #999;
74 | position: absolute;
75 | bottom: 0;
76 | left: 0;
77 | font-size: 22rpx;
78 | }
79 |
80 | .m-live .avatar {
81 | width: 36rpx;
82 | height: 36rpx;
83 | float: left;
84 | margin-right: 8rpx;
85 | border-radius: 36rpx;
86 | vertical-align: bottom;
87 | }
88 |
89 | .m-live .user {
90 | position: absolute;
91 | width: 100%;
92 | top: 62rpx;
93 | left: 200rpx;
94 | line-height: 36rpx;
95 | font-size: 20rpx;
96 | }
97 |
--------------------------------------------------------------------------------
/App/components/live/live_small.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ live.subject }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ live.speaker.name }}
15 |
16 |
17 |
18 | {{ live.outline || live.description }}
19 |
20 |
21 | {{ live.liked_num }} 喜欢 / {{ live.seats_taken }} 参加
22 |
23 |
24 |
--------------------------------------------------------------------------------
/App/components/live/live_small.wxss:
--------------------------------------------------------------------------------
1 | .m-live {
2 | position: relative;
3 | margin: 20rpx;
4 | border-bottom: 1px solid #eaeaea;
5 | padding-bottom: 20rpx;
6 | }
7 |
8 | .m-live .cover {
9 | width: 190rpx;
10 | height: 190rpx;
11 | border-radius: 8rpx;
12 | float: left;
13 | margin-right: 20rpx;
14 | background-color: #e7ddc7;
15 | }
16 |
17 | .m-live .label {
18 | position: absolute;
19 | left: -2rpx;
20 | top: 20rpx;
21 | width: 57rpx;
22 | height: 37rpx;
23 | }
24 |
25 | .m-live .info {
26 | height: 190rpx;
27 | overflow: hidden;
28 | position: relative;
29 | }
30 |
31 | .m-live .title {
32 | font-size: 30rpx;
33 | line-height: 30rpx;
34 | font-weight: bold;
35 | }
36 |
37 | .m-live .rating {
38 | color: #999;
39 | font-size: 22rpx;
40 | margin-top: 8rpx;
41 | }
42 |
43 | .m-live .rating image {
44 | width: 26rpx;
45 | height: 26rpx;
46 | margin-right: 7rpx;
47 | }
48 |
49 | .m-live .m-rating {
50 | display: inline-block;
51 | vertical-align: middle;
52 | }
53 |
54 | .m-live .rating .count {
55 | margin-left: 10rpx;
56 | }
57 |
58 | .m-live .desc {
59 | color: #5c5c5c;
60 | font-size: 24rpx;
61 | margin-top: 5rpx;
62 | line-height: 34rpx;
63 | overflow: hidden;
64 | text-overflow: ellipsis;
65 | display: -webkit-box;
66 | -webkit-line-clamp: 2;
67 | -webkit-box-orient: vertical;
68 | }
69 |
70 | .m-live .detail {
71 | color: #999;
72 | position: absolute;
73 | bottom: 0;
74 | left: 0;
75 | font-size: 22rpx;
76 | }
77 |
78 | .m-live .avatar {
79 | width: 36rpx;
80 | height: 36rpx;
81 | float: left;
82 | margin-right: 8rpx;
83 | border-radius: 36rpx;
84 | vertical-align: bottom;
85 | }
86 |
87 | .m-live .user {
88 | position: absolute;
89 | width: 100%;
90 | top: 38rpx;
91 | left: 200rpx;
92 | line-height: 36rpx;
93 | font-size: 18rpx;
94 | }
95 |
--------------------------------------------------------------------------------
/App/components/user/user.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ live.subject }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ live.speaker.name }}
15 |
16 |
17 |
18 | {{ live.outline || live.description }}
19 |
20 |
21 | {{ live.liked_num }} 喜欢 / {{ live.seats_taken }} 参加
22 |
23 |
24 |
--------------------------------------------------------------------------------
/App/components/user/user.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/widget/rating.wxss";
2 |
3 | .m-live {
4 | overflow: hidden;
5 | position: relative;
6 | border-radius: 8rpx;
7 | margin-bottom: 20rpx;
8 | }
9 |
10 | .m-live .cover {
11 | width: 100%;
12 | height: 352rpx;
13 | vertical-align: bottom;
14 | }
15 |
16 | .m-live::before {
17 | content: '';
18 | display: block;
19 | position: absolute;
20 | left: 0;
21 | top: 0;
22 | width: 560rpx;
23 | height: 100%;
24 | border-top-left-radius: 8rpx;
25 | border-bottom-left-radius: 8rpx;
26 | background: linear-gradient(to right, rgba(0, 0, 0, 0.6) 0, rgba(0, 0, 0, 0) 100%);
27 | }
28 |
29 | .m-live .info {
30 | color: #fff;
31 | padding: 0 30rpx;
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | height: 100%;
36 | width: 100%;
37 | z-index: 1;
38 | box-sizing: border-box;
39 | }
40 |
41 | .m-live .info .h2 {
42 | font-size: 36rpx;
43 | font-weight: 600;
44 | line-height: 1.22;
45 | text-shadow: 2rpx 2rpx rgba(0, 0, 0, 0.4);
46 | overflow: hidden;
47 | text-overflow: ellipsis;
48 | -webkit-line-clamp: 2;
49 | display: -webkit-box;
50 | -webkit-box-orient: vertical;
51 | margin-top: 30rpx;
52 | }
53 |
54 | .m-live .detail {
55 | margin-top: 36rpx;
56 | padding-left: 18rpx;
57 | position: relative;
58 | font-size: 18rpx;
59 | line-height: 1;
60 | font-weight: bold;
61 | text-shadow: 2rpx 4rpx 0px rgba(0, 0, 0, 0.20);
62 | }
63 |
64 | .m-live .detail view {
65 | margin-bottom: 6rpx;
66 | }
67 |
68 | .m-live .detail text {
69 | margin-right: 20rpx;
70 | }
71 |
72 | .m-live .detail .place {
73 | font-weight: normal;
74 | font-size: 16rpx;
75 | }
76 |
77 | .m-live .detail::before {
78 | content: '';
79 | width: 6rpx;
80 | height: 100%;
81 | border-right: 1rpx;
82 | background-color: #4abdcc;
83 | position: absolute;
84 | left: 4rpx;
85 | top: 2rpx;
86 | }
87 |
88 | .m-live .user {
89 | position: absolute;
90 | width: 100%;
91 | top: 75rpx;
92 | left: 300rpx;
93 | line-height: 30rpx;
94 | font-size: 18rpx;
95 | }
96 |
97 | .m-live .avatar {
98 | width: 30rpx;
99 | height: 30rpx;
100 | float: left;
101 | margin-right: 15rpx;
102 | border-radius: 30rpx;
103 | vertical-align: bottom;
104 | }
105 |
106 | .m-live .bottom {
107 | position: absolute;
108 | bottom: 24rpx;
109 | font-size: 20rpx;
110 | }
111 |
112 | .m-live-s {
113 | color: #fff;
114 | overflow: hidden;
115 | position: relative;
116 | border-radius: 8rpx;
117 | margin-bottom: 20rpx;
118 | }
119 |
120 | .m-live-s .mask {
121 | position: absolute;
122 | left: 0;
123 | top: 0;
124 | width: 222rpx;
125 | height: 240rpx;
126 | background-color: rgba(255, 255, 255, 0);
127 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
128 | border-top-left-radius: 8rpx;
129 | border-bottom-left-radius: 8rpx;
130 | }
131 |
132 | .m-live-s .cover {
133 | width: 100%;
134 | height: 240rpx;
135 | vertical-align: bottom;
136 | background-position: center center;
137 | }
138 |
139 | .m-live-s .info {
140 | position: absolute;
141 | left: 0;
142 | top: 0;
143 | margin: 20rpx 28rpx;
144 | text-shadow: 2rpx 3rpx rgba(0, 0, 0, 0.2);
145 | }
146 |
147 | .m-live-s .title {
148 | font-size: 36rpx;
149 | font-weight: bold;
150 | margin-left: -4rpx;
151 | line-height: 50rpx;
152 | }
153 |
154 | .m-live-s .detail {
155 | font-size: 20rpx;
156 | position: relative;
157 | height: 16rpx;
158 | line-height: 16rpx;
159 | padding-left: 16rpx;
160 | margin-top: 4rpx;
161 | }
162 |
163 | .m-live-s .detail::before {
164 | content: '';
165 | position: absolute;
166 | left: 0;
167 | top: 0;
168 | width: 6rpx;
169 | height: 16rpx;
170 | background-color: #4abdcc;
171 | }
172 |
173 | .m-live-s .detail view {
174 | display: inline-block;
175 | margin-right: 16rpx;
176 | }
177 |
178 | .m-live-s .detail text {
179 | font-weight: bold;
180 | }
181 |
182 | .m-live-s .stat {
183 | position: absolute;
184 | width: 100%;
185 | left: 0;
186 | bottom: 24rpx;
187 | height: 32rpx;
188 | line-height: 31rpx;
189 | font-size: 20rpx;
190 | padding-left: 28rpx;
191 | background-color: rgba(0, 0, 0, 0.4);
192 | z-index: 2;
193 | }
194 |
195 | .m-live-s .stat::before {
196 | content: '';
197 | width: 100%;
198 | position: absolute;
199 | height: 40rpx;
200 | left: 0;
201 | bottom: -4rpx;
202 | background-color: rgba(0, 0, 0, 0.4);
203 | z-index: -1;
204 | }
205 |
206 | .m-live-s .stat view {
207 | position: relative;
208 | display: inline-block;
209 | margin-right: 30rpx;
210 | padding-left: 16rpx;
211 | }
212 |
213 | .m-live-s .stat view::before {
214 | content: '';
215 | width: 8rpx;
216 | height: 8rpx;
217 | position: absolute;
218 | left: 0;
219 | top: 12rpx;
220 | background-color: #4abdcc;
221 | border-radius: 8rpx;
222 | }
223 |
224 | .m-live-s .stat text {
225 | font-weight: bold;
226 | }
227 |
--------------------------------------------------------------------------------
/App/components/user/user_small.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ user.name }}
8 |
9 |
10 |
11 | {{ user.bio || user.headline || description }}
12 |
13 |
14 | 举办过 {{ user.live_count }} 场Live ⚡️
15 |
16 |
17 |
--------------------------------------------------------------------------------
/App/components/user/user_small.wxss:
--------------------------------------------------------------------------------
1 | .m-user {
2 | position: relative;
3 | margin: 20rpx;
4 | border-bottom: 1px solid #eaeaea;
5 | padding-bottom: 20rpx;
6 | }
7 |
8 | .m-user .cover {
9 | width: 190rpx;
10 | height: 190rpx;
11 | border-radius: 8rpx;
12 | float: left;
13 | margin-right: 20rpx;
14 | background-color: #e7ddc7;
15 | }
16 |
17 | .m-user .label {
18 | position: absolute;
19 | left: -2rpx;
20 | top: 20rpx;
21 | width: 57rpx;
22 | height: 37rpx;
23 | }
24 |
25 | .m-user .info {
26 | height: 190rpx;
27 | overflow: hidden;
28 | position: relative;
29 | }
30 |
31 | .m-user .title {
32 | font-size: 30rpx;
33 | line-height: 30rpx;
34 | font-weight: bold;
35 | }
36 |
37 | .m-user .desc {
38 | color: #5c5c5c;
39 | font-size: 24rpx;
40 | margin-top: 30rpx;
41 | line-height: 34rpx;
42 | overflow: hidden;
43 | text-overflow: ellipsis;
44 | display: -webkit-box;
45 | -webkit-line-clamp: 2;
46 | -webkit-box-orient: vertical;
47 | }
48 |
49 | .m-user .detail {
50 | color: #999;
51 | position: absolute;
52 | bottom: 0;
53 | left: 0;
54 | font-size: 22rpx;
55 | }
56 |
57 | .m-user .avatar {
58 | width: 36rpx;
59 | height: 36rpx;
60 | float: left;
61 | margin-right: 8rpx;
62 | border-radius: 36rpx;
63 | vertical-align: bottom;
64 | }
65 |
--------------------------------------------------------------------------------
/App/components/widget/rating.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/App/components/widget/rating.wxss:
--------------------------------------------------------------------------------
1 | .m-rating {
2 | float: left;
3 | }
4 |
5 | .m-rating image {
6 | width: 16rpx;
7 | height: 16rpx;
8 | float: left;
9 | position: relative;
10 | top: 6rpx;
11 | margin-right: 8rpx;
12 | }
13 |
14 | .rating .count {
15 | float: left;
16 | margin-left: 10rpx;
17 | font-size: 18rpx;
18 | color: rgba(255, 255, 255, 0.6);
19 | text-shadow: 2rpx 4rpx rgba(0, 0, 0, 0.12);
20 | }
21 |
--------------------------------------------------------------------------------
/App/images/explore_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/explore_normal.png
--------------------------------------------------------------------------------
/App/images/explore_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/explore_pressed.png
--------------------------------------------------------------------------------
/App/images/hot_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/hot_normal.png
--------------------------------------------------------------------------------
/App/images/hot_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/hot_pressed.png
--------------------------------------------------------------------------------
/App/images/rating/semistar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/semistar.png
--------------------------------------------------------------------------------
/App/images/rating/semistar_s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/semistar_s.png
--------------------------------------------------------------------------------
/App/images/rating/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/star.png
--------------------------------------------------------------------------------
/App/images/rating/star_s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/star_s.png
--------------------------------------------------------------------------------
/App/images/rating/unstar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/unstar.png
--------------------------------------------------------------------------------
/App/images/rating/unstar_s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/rating/unstar_s.png
--------------------------------------------------------------------------------
/App/images/search-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/search-off.png
--------------------------------------------------------------------------------
/App/images/search-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/search-on.png
--------------------------------------------------------------------------------
/App/images/search@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/search@1x.png
--------------------------------------------------------------------------------
/App/images/search@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/App/images/search@2x.png
--------------------------------------------------------------------------------
/App/pages/explore/explore.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 | const util = require('../../utils/util.js');
4 |
5 | const formatTime = util.formatTime;
6 |
7 | Page({
8 | data: {
9 | lives: [],
10 | start: 0,
11 | limit: 20,
12 | loading: false,
13 | windowWidth: App.systemInfo.windowWidth,
14 | windowHeight: App.systemInfo.windowHeight,
15 | },
16 | onLoad() {
17 | this.loadMore();
18 | },
19 | onPullDownRefresh() {
20 | this.loadMore(null, true);
21 | },
22 | loadMore(e, needRefresh) {
23 | const self = this;
24 | const loading = self.data.loading;
25 | const data = {
26 | start: self.data.start,
27 | };
28 | if (loading) {
29 | return;
30 | }
31 | self.setData({
32 | loading: true,
33 | });
34 | api.explore({
35 | data,
36 | success: (res) => {
37 | let lives = res.data.rs;
38 | lives.map((live) => {
39 | const item = live;
40 | item.starts_at = formatTime(new Date(item.starts_at * 1000), 1);
41 | return item;
42 | });
43 | if (needRefresh) {
44 | wx.stopPullDownRefresh();
45 | } else {
46 | lives = self.data.lives.concat(lives);
47 | }
48 | self.setData({
49 | lives: lives,
50 | start: self.data.start + self.data.limit,
51 | loading: false,
52 | });
53 | },
54 | });
55 | },
56 | onViewTap(e) {
57 | const ds = e.currentTarget.dataset;
58 | const t = ds['type'] === 'live' ? 'live/live' : 'users/user'
59 | wx.navigateTo({
60 | url: `../${t}?id=${ds.id}`,
61 | });
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/App/pages/explore/explore.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "Live发现"
3 | }
--------------------------------------------------------------------------------
/App/pages/explore/explore.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 正在加载...
8 |
9 |
--------------------------------------------------------------------------------
/App/pages/explore/explore.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/live/live.wxss";
2 |
3 | page {
4 | background-color: #f6f6f6;
5 | }
6 |
7 | .list {
8 | }
9 |
10 | .m-live {
11 | margin: 20rpx;
12 | }
13 |
14 | .live-title {
15 | margin-bottom: 10rpx;
16 | }
17 |
18 | .loading {
19 | color: #b0b0b0;
20 | text-align: center;
21 | margin: 20rpx 0;
22 | }
23 |
--------------------------------------------------------------------------------
/App/pages/hot/lib.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 | const util = require('../../utils/util.js');
4 | const formatTime = util.formatTime;
5 |
6 | export function gen_page(type) {
7 | return {
8 | data: {
9 | lives: [],
10 | windowWidth: App.systemInfo.windowWidth,
11 | windowHeight: App.systemInfo.windowHeight,
12 | },
13 | onLoad() {
14 | const self = this;
15 | wx.showToast({
16 | title: '正在加载',
17 | icon: 'loading',
18 | duration: 10000,
19 | });
20 | api[`getHotBy${type}ly`]({
21 | success: (res) => {
22 | let lives = res.data.rs;
23 | lives.map((live) => {
24 | const item = live;
25 | item.starts_at = formatTime(new Date(item.starts_at * 1000), 1);
26 | return item;
27 | });
28 |
29 | self.setData({ lives, type });
30 | wx.hideToast();
31 | }
32 | });
33 | },
34 | onViewTap(e) {
35 | const ds = e.currentTarget.dataset;
36 | wx.navigateTo({
37 | url: `../live/live?id=${ds.id}`,
38 | });
39 | },
40 | onChangeTab(e) {
41 | const ds = e.currentTarget.dataset;
42 | if (type == 'Month' && ds.type == 'Week') {
43 | wx.navigateBack('../hot/weekly');
44 | } else {
45 | wx.navigateTo({
46 | url: `../hot/${ds.type.toLowerCase()}ly`,
47 | });
48 | }
49 | },
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/App/pages/hot/monthly.js:
--------------------------------------------------------------------------------
1 | const lib = require('./lib.js');
2 |
3 | Page(lib.gen_page('Month'))
--------------------------------------------------------------------------------
/App/pages/hot/monthly.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "30 天最受欢迎 Live"
3 | }
--------------------------------------------------------------------------------
/App/pages/hot/monthly.wxml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/App/pages/hot/monthly.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/hot/hot.wxss";
2 |
3 | .banner {
4 | background-image: url(http://localhost:8300/static/images/monthly.svg);
5 | }
6 |
--------------------------------------------------------------------------------
/App/pages/hot/weekly.js:
--------------------------------------------------------------------------------
1 | const lib = require('./lib.js');
2 |
3 | Page(lib.gen_page('Week'))
--------------------------------------------------------------------------------
/App/pages/hot/weekly.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "7 天最受欢迎 Live"
3 | }
--------------------------------------------------------------------------------
/App/pages/hot/weekly.wxml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/App/pages/hot/weekly.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/hot/hot.wxss";
2 |
--------------------------------------------------------------------------------
/App/pages/live/live.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 | const util = require('../../utils/util.js');
4 |
5 | const formatTime = util.formatTime;
6 |
7 | Page({
8 | data: {
9 | 'live': {},
10 | windowWidth: App.systemInfo.windowWidth,
11 | windowHeight: App.systemInfo.windowHeight,
12 | },
13 | onReady() {
14 | const self = this;
15 | wx.setNavigationBarTitle({
16 | title: self.data.live.subject,
17 | });
18 | },
19 | onLoad(options) {
20 | const {id} = options;
21 | const data = { id };
22 | const self = this;
23 | wx.showToast({
24 | title: '正在加载',
25 | icon: 'loading',
26 | duration: 10000,
27 | });
28 | api.getLiveInfoById({
29 | data,
30 | success: (res) => {
31 | let live = res.data.rs;
32 | live.starts_at = formatTime(new Date(live.starts_at * 1000), 1);
33 | live.description = live.description.split('\r\n');
34 | self.setData({ live });
35 | wx.hideToast();
36 | },
37 | });
38 | },
39 | onViewTap(e) {
40 | const ds = e.currentTarget.dataset;
41 | wx.navigateTo({
42 | url: `../users/user?id=${ds.id}`,
43 | });
44 | },
45 | });
--------------------------------------------------------------------------------
/App/pages/live/live.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/App/pages/live/live.wxml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 | 主讲人: {{ live.speaker.name }}
16 |
17 | {{ live.seats_taken }} 参与 / {{ live.liked_num }} 喜欢 / {{ live.speaker_message_count }} 个回答
18 |
19 |
20 |
21 |
22 | 开始时间 {{ live.starts_at }}
23 |
24 |
25 |
26 |
27 |
28 | Live 简介
29 |
30 |
31 |
32 | {{item}}"
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/App/pages/live/live.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/widget/rating.wxss";
2 |
3 | page {
4 | background-color: #efeff4;
5 | }
6 |
7 | .m-live-header {
8 | overflow: hidden;
9 | position: relative;
10 | border-radius: 8rpx;
11 | margin-bottom: 20rpx;
12 | }
13 |
14 | .m-live-header .cover {
15 | width: 100%;
16 | height: 352rpx;
17 | vertical-align: bottom;
18 | }
19 |
20 | .m-live-header::before {
21 | content: '';
22 | display: block;
23 | position: absolute;
24 | left: 0;
25 | top: 0;
26 | width: 100%;
27 | height: 100%;
28 | border-top-left-radius: 8rpx;
29 | border-bottom-left-radius: 8rpx;
30 | background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0, rgba(0, 0, 0, 0) 100%);
31 | }
32 |
33 | .m-live-header .info {
34 | color: #fff;
35 | padding: 0 30rpx;
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | height: 100%;
40 | width: 100%;
41 | z-index: 1;
42 | box-sizing: border-box;
43 | }
44 |
45 | .m-live-header .info .h2 {
46 | font-size: 40rpx;
47 | font-weight: 600;
48 | line-height: 1.22;
49 | text-shadow: 2rpx 2rpx rgba(0, 0, 0, 0.4);
50 | overflow: hidden;
51 | text-overflow: ellipsis;
52 | -webkit-line-clamp: 2;
53 | display: -webkit-box;
54 | -webkit-box-orient: vertical;
55 | margin-top: 218rpx;
56 | }
57 |
58 | .m-live-info {
59 | position: relative;
60 | background-color: #fff;
61 | margin-bottom: 20rpx;
62 | padding: 20rpx;
63 | }
64 |
65 | .m-live-info .detail {
66 | margin-top: 20rpx;
67 | line-height: 1;
68 | display: inline-block;
69 | font-size: 28rpx;
70 | line-height: 1;
71 | color: #646464;
72 | }
73 |
74 | .m-live-info .desc {
75 | display: inline-block;
76 | font-size: 24rpx;
77 | line-height: 1;
78 | color: #999;
79 | }
80 |
81 | .m-live-info .sep {
82 | border-top: 2rpx solid #eee;
83 | height: 20rpx;
84 | margin-top: 20rpx;
85 | }
86 |
87 | .m-live-info .user {
88 | position: relative;
89 | display: flex;
90 | -webkit-box-align: center;
91 | -ms-flex-align: center;
92 | align-items: center;
93 | margin-bottom: 20rpx;
94 | }
95 |
96 | .m-live-info .avatar {
97 | width: 50px;
98 | height: 50px;
99 | vertical-align: top;
100 | margin-right: 12px;
101 | display: inline-block;
102 | overflow: hidden;
103 | border-radius: 10%;
104 | }
105 |
106 | .m-live-info .name {
107 | overflow: hidden;
108 | flex: 1;
109 | align-items: center;
110 | color: #646464;
111 | font-size: 16px;
112 | line-height: 1.5;
113 | }
114 |
115 | .m-live-header .rating {
116 | color: #999;
117 | font-size: 22rpx;
118 | margin-top: 8rpx;
119 | }
120 |
121 | .m-live-header .rating image {
122 | width: 26rpx;
123 | height: 26rpx;
124 | margin-right: 7rpx;
125 | }
126 |
127 | .m-live-header .m-rating {
128 | display: inline-block;
129 | vertical-align: middle;
130 | }
131 |
132 | .button-hover {
133 | background-color: #0097a7;
134 | }
135 |
136 | button {
137 | background-color: #4abdcc;
138 | border-color: #4abdcc;
139 | font-weight: 500;
140 | color: #fff;
141 | }
142 |
143 | .m-live-intro {
144 | padding: 15px;
145 | background-color: #fff;
146 | }
147 |
148 | .m-live-intro .content {
149 | color: #646464;
150 | font-weight: 400;
151 | font-size: 15px;
152 | line-height: 1.5;
153 | white-space: pre-wrap;
154 | }
155 |
--------------------------------------------------------------------------------
/App/pages/search/search.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 | const util = require('../../utils/util.js');
4 |
5 | const lengthStr = util.lengthStr;
6 |
7 | Page({
8 | data: {
9 | title: '',
10 | q: '',
11 | suggest: false,
12 | status: 'all',
13 | lives: [],
14 | users: [],
15 | start: 0,
16 | loading: false,
17 | limit: 20,
18 | hasMore: true,
19 | windowWidth: App.systemInfo.windowWidth,
20 | windowHeight: App.systemInfo.windowHeight,
21 | pixelRatio: App.systemInfo.pixelRatio,
22 | },
23 | onPullDownRefresh() {
24 | this.loadMore(null, true);
25 | },
26 | loadMore(e, needRefresh) {
27 | const self = this;
28 | if (!self.data.hasMore) {
29 | return;
30 | }
31 | const loading = self.data.loading;
32 | const data = {
33 | start: self.data.start,
34 | q: self.data.q,
35 | status: self.data.status,
36 | limit: self.data.limit
37 | };
38 | if (loading
39 | ) {
40 | return;
41 | }
42 | self.setData({
43 | loading: true
44 | });
45 | wx.showToast({
46 | title: '正在加载',
47 | icon: 'loading',
48 | duration: 10000,
49 | });
50 | api.search({
51 | data,
52 | success: (res) => {
53 | let rs = res.data.rs;
54 | let lives = [], users = [];
55 | rs.map((item) => {
56 | if (item.type === 'live') {
57 | item.pic_url = item.cover;
58 | lives.push(item);
59 | } else {
60 | item.pic_url = item.avatar_url;
61 | users.push(item);
62 | }
63 | return item;
64 | });
65 | if (needRefresh) {
66 | wx.stopPullDownRefresh();
67 | } else {
68 | lives = self.data.lives.concat(lives);
69 | users = self.data.users.concat(users);
70 | }
71 | if (self.data.status !== 'all') {
72 | users = [];
73 | }
74 | self.setData({
75 | lives, users,
76 | start: self.data.start + self.data.limit,
77 | loading: false,
78 | hasMore: lives.length === self.data.limit
79 | });
80 | wx.hideToast();
81 | },
82 | });
83 | },
84 | bindKeyInput(e) {
85 | const self = this;
86 | const q = e.detail.value.replace(/'/g, '').trim();
87 | const data = { q };
88 | this.setData({ q, suggest: true, users: [] });
89 | if (lengthStr(q) > 4) {
90 | api.suggest({
91 | data,
92 | success: (res) => {
93 | let lives = res.data.rs;
94 | self.setData({ lives });
95 | },
96 | });
97 | }
98 | },
99 | onSearch(e) {
100 | const self = this;
101 | self.setData({ suggest: false });
102 | self.loadData(true);
103 | },
104 | onChangeTab(e) {
105 | const self = this;
106 | const status = e.currentTarget.dataset.status;
107 | self.setData({ status });
108 | self.loadData(true);
109 | },
110 | loadData(refresh) {
111 | if (refresh) {
112 | this.setData({
113 | start: 0, lives: [], users: [], hasMore: true
114 | });
115 | }
116 | this.loadMore(null, !refresh);
117 | },
118 | viewHotTopics() {
119 | wx.navigateTo({
120 | url: `../topic/hot_topics`,
121 | });
122 | },
123 | onViewTap(e) {
124 | const ds = e.currentTarget.dataset;
125 | const t = ds['type'] === 'live' ? 'live/live' : 'users/user'
126 | wx.navigateTo({
127 | url: `../${t}?id=${ds.id}`,
128 | });
129 | },
130 | });
--------------------------------------------------------------------------------
/App/pages/search/search.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "Live搜索"
3 | }
--------------------------------------------------------------------------------
/App/pages/search/search.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ live.subject }}
10 |
11 |
12 |
13 |
14 | {{ live.liked_num }} 喜欢 / {{ live.seats_taken }} 参加
15 |
16 |
17 |
18 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/App/pages/search/search.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/live/live_small.wxss";
2 | @import "../../components/user/user_small.wxss";
3 |
4 | page {
5 | margin-top: 90rpx;
6 | }
7 |
8 | .header {
9 | position: fixed;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 70rpx;
14 | background-color: #fff;
15 | box-sizing: border-box;
16 | z-index: 100;
17 | }
18 |
19 | .nav {
20 | display: inline-block;
21 | margin: 10rpx 20rpx;
22 | white-space: nowrap;
23 | overflow: hidden;
24 | }
25 |
26 | .nav view {
27 | color: #b0b0b0;
28 | display: inline-block;
29 | height: 50rpx;
30 | line-height: 50rpx;
31 | padding: 0 30rpx;
32 | }
33 |
34 | .nav .selected {
35 | color: #fff;
36 | border-radius: 50rpx;
37 | background-image: linear-gradient(to top, #4abdcc, #51d7df);
38 | }
39 |
40 | .list {
41 | position: absolute;
42 | top: 270rpx;
43 | }
44 |
45 | .loading {
46 | color: #999;
47 | text-align: center;
48 | margin: 20rpx 0;
49 | }
50 |
51 | .search {
52 | padding-top: 70px;
53 | }
54 |
55 | .search-icon {
56 | display: inline-block;
57 | vertical-align: middle;
58 | }
59 |
60 | .search-icon:before {
61 | content: "";
62 | display: inline-block;
63 | width: 20px;
64 | height: 20px;
65 | background-image: url(../../images/search@1x.png);
66 | background-repeat: no-repeat;
67 | margin: 10px;
68 | cursor: pointer;
69 | }
70 |
71 | .search-input {
72 | display: inline-block;
73 | margin-left: 100rpx;
74 | padding-left: 20rpx;
75 | overflow: unset;
76 | width: 250px;
77 | height: 40px;
78 | line-height: 40px;
79 | background-color: #fff;
80 | border: 1px solid #4abdcc;
81 | }
82 |
83 | .sep {
84 | height: 20rpx;
85 | }
86 |
87 | .text {
88 | color: #fff;
89 | height: 176px;
90 | background: linear-gradient(270deg, #28e0b0, #c328e0, #28a1e0, #e0286b) center / cover;
91 | background-size: 800% 800%;
92 | -webkit-animation: AnimationName 10s ease infinite;
93 | -moz-animation: AnimationName 10s ease infinite;
94 | animation: AnimationName 10s ease infinite;
95 | }
96 |
97 | @keyframes AnimationName {
98 | 0% {
99 | background-position: 0% 50%;
100 | }
101 |
102 | 50% {
103 | background-position: 100% 50%;
104 | }
105 |
106 | 100% {
107 | background-position: 0% 50%;
108 | }
109 | }
110 |
111 | .suggest {
112 | width: 260px;
113 | margin-top: -6rpx;
114 | border: 0 none rgba(0, 0, 0, 0);
115 | border-radius: 10rpx;
116 | box-shadow: 0 2px 8rpx rgba(0, 0, 0, 0.5);
117 | margin-left: 100rpx;
118 | }
119 |
120 | .m-live-s {
121 | width: 250px;
122 | position: relative;
123 | border-bottom: 2rpx solid #eaeaea;
124 | background: #fff;
125 | cursor: pointer;
126 | zoom: 1;
127 | clear: both;
128 | padding: 10rpx;
129 | }
130 |
131 | .m-live-s .cover {
132 | width: 120rpx;
133 | height: 120rpx;
134 | border-radius: 8rpx;
135 | float: left;
136 | margin-right: 20rpx;
137 | background-color: #e7ddc7;
138 | }
139 |
140 | .m-live-s .info {
141 | height: 120rpx;
142 | overflow: hidden;
143 | position: relative;
144 | }
145 |
146 | .m-live-s .title {
147 | font-size: 26rpx;
148 | line-height: 26rpx;
149 | font-weight: bold;
150 | }
151 |
152 | .m-live-s .rating {
153 | color: #999;
154 | font-size: 22rpx;
155 | margin-top: 8rpx;
156 | }
157 |
158 | .m-live-s .rating image {
159 | width: 26rpx;
160 | height: 26rpx;
161 | margin-right: 7rpx;
162 | }
163 |
164 | .m-live-s .m-rating {
165 | display: inline-block;
166 | vertical-align: middle;
167 | }
168 |
169 | .m-live-s .rating .count {
170 | margin-left: 10rpx;
171 | }
172 |
173 | .m-live-s .detail {
174 | color: #999;
175 | position: absolute;
176 | bottom: 0;
177 | left: 0;
178 | font-size: 22rpx;
179 | }
180 |
--------------------------------------------------------------------------------
/App/pages/topic/hot_topics.js:
--------------------------------------------------------------------------------
1 | const api = require('../../utils/api.js');
2 |
3 | const App = getApp();
4 | Page({
5 | data: {
6 | topics: [],
7 | windowWidth: App.systemInfo.windowWidth,
8 | },
9 | onLoad() {
10 | const self = this;
11 | wx.showToast({
12 | title: '正在加载',
13 | icon: 'loading',
14 | duration: 10000,
15 | });
16 | api.getHotTopics({
17 | success: (res) => {
18 | const topics = res.data.rs;
19 | self.setData({
20 | topics: topics,
21 | });
22 | wx.hideToast();
23 | },
24 | });
25 | },
26 | onViewTap(e) {
27 | const data = e.currentTarget.dataset;
28 | console.log();
29 | wx.navigateTo({
30 | url: `../topic/topic?id=${data.id}&followers_count=${data.followers_count}&best_answerers_count=${data.best_answerers_count}&best_answers_count=${data.best_answers_count}&name=${data.name}`,
31 | });
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/App/pages/topic/hot_topics.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/App/pages/topic/hot_topics.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ topic.name }}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/App/pages/topic/hot_topics.wxss:
--------------------------------------------------------------------------------
1 | page {
2 | background-color: #f6f6f6;
3 | }
4 |
5 | .title {
6 | color: #5c5c5c;
7 | font-size: 30rpx;
8 | margin: 20rpx;
9 | }
10 |
11 | .list {
12 | display: flex;
13 | flex-direction: row;
14 | flex-wrap: wrap;
15 | justify-content: space-between;
16 | align-content: flex-start;
17 | align-items: center;
18 | padding: 0 20rpx;
19 | margin-bottom: 50rpx;
20 | }
21 |
22 | .topic {
23 | position: relative;
24 | margin-bottom: 12rpx;
25 | }
26 |
27 | .topic image {
28 | width: 349rpx;
29 | height: 349rpx;
30 | border-radius: 8rpx;
31 | vertical-align: bottom;
32 | }
33 |
34 | .topic .mask {
35 | width: 100%;
36 | height: 80rpx;
37 | position: absolute;
38 | bottom: 0;
39 | left: 0;
40 | background-image: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
41 | border-bottom-left-radius: 8rpx;
42 | border-bottom-right-radius: 8rpx;
43 | }
44 |
45 | .topic .name {
46 | position: absolute;
47 | left: 28rpx;
48 | bottom: 24rpx;
49 | color: #fff;
50 | font-size: 40rpx;
51 | font-weight: bold;
52 | }
53 |
--------------------------------------------------------------------------------
/App/pages/topic/topic.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 | const util = require('../../utils/util.js');
4 |
5 | const formatTime = util.formatTime;
6 |
7 | Page({
8 | data: {
9 | best_answerers_count: '',
10 | best_answers_count: '',
11 | name: '',
12 | followers_count: '',
13 | lives: [],
14 | start: 0,
15 | limit: 20,
16 | loading: false,
17 | windowWidth: App.systemInfo.windowWidth,
18 | windowHeight: App.systemInfo.windowHeight,
19 | },
20 | onReady() {
21 | const self = this;
22 | wx.setNavigationBarTitle({
23 | title: self.data.name,
24 | });
25 | },
26 | onLoad(options) {
27 | const {id, name, best_answerers_count, best_answers_count, followers_count} = options;
28 |
29 | wx.showToast({
30 | title: '正在加载',
31 | icon: 'loading',
32 | duration: 10000,
33 | });
34 | this.setData({
35 | name: name,
36 | best_answerers_count: best_answerers_count,
37 | best_answers_count: best_answers_count,
38 | followers_count: followers_count
39 | });
40 | this.loadMore();
41 | },
42 | onPullDownRefresh() {
43 | this.loadMore(null, true);
44 | },
45 | loadMore(e, needRefresh) {
46 | const self = this;
47 | const loading = self.data.loading;
48 | const data = {
49 | start: self.data.start,
50 | topic: self.data.name,
51 | };
52 | if (loading) {
53 | return;
54 | }
55 | self.setData({
56 | loading: true,
57 | });
58 | api.getTopicByName({
59 | data,
60 | success: (res) => {
61 | let lives = res.data.rs;
62 | lives.map((live) => {
63 | const item = live;
64 | item.starts_at = formatTime(new Date(item.starts_at * 1000), 1);
65 | return item;
66 | });
67 | if (needRefresh) {
68 | wx.stopPullDownRefresh();
69 | } else {
70 | lives = self.data.lives.concat(lives);
71 | }
72 | self.setData({
73 | lives: lives,
74 | start: self.data.start + self.data.limit,
75 | loading: false,
76 | });
77 | wx.hideToast();
78 | },
79 | });
80 | },
81 | onViewTap(e) {
82 | const ds = e.currentTarget.dataset;
83 | const t = ds['type'] === 'live' ? 'live/live' : 'users/user'
84 | wx.navigateTo({
85 | url: `../${t}?id=${ds.id}`,
86 | });
87 | },
88 | });
--------------------------------------------------------------------------------
/App/pages/topic/topic.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/App/pages/topic/topic.wxml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/App/pages/topic/topic.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/live/live.wxss";
2 |
3 | page {
4 | background-color: #fbf7ed;
5 | }
6 |
7 | .header {
8 | color: #fff;
9 | position: relative;
10 | margin-bottom: 20rpx;
11 | }
12 |
13 | .header image {
14 | height: 400rpx;
15 | vertical-align: bottom;
16 | }
17 |
18 | .header .title {
19 | position: absolute;
20 | z-index: 5;
21 | left: 30rpx;
22 | bottom: 50rpx;
23 | }
24 |
25 | .header .h2 {
26 | font-size: 60rpx;
27 | font-weight: bold;
28 | margin-bottom: 20rpx;
29 | }
30 |
31 | .header .detail {
32 | color: rgba(255, 255, 255, 0.8);
33 | font-size: 22rpx;
34 | }
35 |
36 | .header .mask {
37 | position: absolute;
38 | z-index: 1;
39 | left: 0;
40 | bottom: 0;
41 | width: 100%;
42 | height: 120rpx;
43 | background-image: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
44 | }
45 |
46 | m-live::before {
47 | border-radius: 8rpx;
48 | }
49 |
--------------------------------------------------------------------------------
/App/pages/users/user.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 |
4 | Page({
5 | data: {
6 | user: {},
7 | lives: [],
8 | windowWidth: App.systemInfo.windowWidth,
9 | windowHeight: App.systemInfo.windowHeight,
10 | },
11 | onReady() {
12 | const self = this;
13 | wx.setNavigationBarTitle({
14 | title: self.data.user.name,
15 | });
16 | },
17 | onLoad(options) {
18 | const {id} = options;
19 | const data = { userId: id };
20 | const self = this;
21 | wx.showToast({
22 | title: '正在加载',
23 | icon: 'loading',
24 | duration: 10000,
25 | });
26 | api.getUserInfoById({
27 | data,
28 | success: (res) => {
29 | let rs = res.data.rs;
30 | let lives = [], user = [];
31 | rs.map((item) => {
32 | if (item.type === 'live') {
33 | item.pic_url = item.cover;
34 | item.hiddenUser = true;
35 | lives.push(item);
36 | } else {
37 | item.pic_url = item.avatar_url;
38 | let gender = "Ta";
39 | if (item.gender === 0) {
40 | gender = "她";
41 | } else if (item.gedner === 1) {
42 | gender = "他";
43 | }
44 | item.gender = gender;
45 | user = item;
46 | }
47 | return item;
48 | });
49 | self.setData({ user, lives })
50 | wx.hideToast();
51 | },
52 | });
53 | },
54 | onViewTap(e) {
55 | const ds = e.currentTarget.dataset;
56 | const t = ds['type'] === 'live' ? `live/live?id=${ds.id}` : 'users/users'
57 | wx.navigateTo({
58 | url: `../${t}`,
59 | });
60 | },
61 | viewUserList() {
62 | const self = this;
63 | wx.navigateTo({
64 | url: '../users/users',
65 | });
66 | },
67 | });
--------------------------------------------------------------------------------
/App/pages/users/user.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/App/pages/users/user.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ user.name }}
14 | {{ user.headline || user.bio }}
15 |
16 |
17 | 个人简介
18 | {{ user.description }}
19 |
20 |
21 |
22 |
23 |
24 | {{user.gender}}举办的Live({{ user.live_count }})
25 |
26 |
27 |
28 |
29 |
30 | 全部Live主讲人
31 |
--------------------------------------------------------------------------------
/App/pages/users/user.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/live/live_small.wxss";
2 |
3 | .sep {
4 | height: 20rpx;
5 | }
6 |
7 | .cover image {
8 | background-color: #4abdcc;
9 | width: 100%;
10 | height: 100rpx;
11 | background-size: "cover";
12 | }
13 |
14 | .main {
15 | position: relative;
16 | width: 100%;
17 | background: #fff;
18 | box-sizing: border-box;
19 | margin-bottom: 60rpx;
20 | }
21 |
22 | .m-user .info {
23 | margin: 0 24rpx 24rpx;
24 | }
25 |
26 | .m-user .info .avatar {
27 | display: inline-block;
28 | overflow: hidden;
29 | border: 4rpx solid #fff;
30 | border-radius: 8rpx;
31 | vertical-align: top;
32 | position: absolute;
33 | top: -30rpx;
34 | left: 24rpx;
35 | z-index: 4;
36 | }
37 |
38 | .m-user .info .avatar image {
39 | border-radius: 4px;
40 | width: 160rpx;
41 | height: 160rpx;
42 | }
43 |
44 | .content {
45 | padding-left: 32rpx;
46 | border-left: 164rpx solid transparent;
47 | flex: 1;
48 | overflow: hidden;
49 | text-overflow: ellipsis;
50 | white-space: nowrap;
51 | }
52 |
53 | .content .username {
54 | font-size: 34rpx;
55 | font-weight: 500;
56 | line-height: 60rpx;
57 | }
58 |
59 | .content .headline {
60 | font-size: 28rpx;
61 | white-space: nowrap;
62 | word-break: break-word;
63 | }
64 |
65 | .desc {
66 | width: 100%;
67 | font-size: 14px;
68 | line-height: 1.8;
69 | color: #404040;
70 | display: flex;
71 | margin-top: 60rpx;
72 | }
73 |
74 | .desc .label {
75 | width: 60px;
76 | margin-right: 37px;
77 | font-weight: 500;
78 | }
79 |
80 | .desc .value {
81 | flex: 1;
82 | overflow: hidden;
83 | font-size: 24rpx;
84 | }
85 |
86 | .m-live-count {
87 | background-color: #4abdcc;
88 | height: 24rpx;
89 | color: #fff;
90 | padding: 24rpx;
91 | line-height: 1;
92 | }
93 |
94 | .all {
95 | background-color: #4abdcc;
96 | color: #fff;
97 | font-size: 30rpx;
98 | border-radius: 80rpx;
99 | height: 80rpx;
100 | line-height: 80rpx;
101 | text-align: center;
102 | margin: 0 20rpx 40rpx;
103 | }
104 |
--------------------------------------------------------------------------------
/App/pages/users/users.js:
--------------------------------------------------------------------------------
1 | const App = getApp();
2 | const api = require('../../utils/api.js');
3 |
4 | const sortArray = [
5 | {
6 | type: "live_count",
7 | name: "举办次数"
8 | },
9 | {
10 | type: "updated_time",
11 | name: "最近更新"
12 | },
13 | {
14 | type: "id",
15 | name: "加入时间"
16 | }
17 | ];
18 |
19 | Page({
20 | data: {
21 | orderBy: "live_count",
22 | users: [],
23 | sortArray: sortArray,
24 | sortMap: sortArray.reduce(function (map, obj) {
25 | map[obj.type] = obj.name;
26 | return map;
27 | }, {}),
28 | start: 0,
29 | desc: 1,
30 | loading: false,
31 | limit: 20,
32 | hasMore: true,
33 | windowWidth: App.systemInfo.windowWidth,
34 | windowHeight: App.systemInfo.windowHeight,
35 | pixelRatio: App.systemInfo.pixelRatio,
36 | },
37 | onLoad() {
38 | this.loadMore();
39 | },
40 | onPullDownRefresh() {
41 | this.loadMore(null, true);
42 | },
43 | loadMore(e, needRefresh) {
44 | const self = this;
45 | if (!self.data.hasMore) {
46 | return;
47 | }
48 | const loading = self.data.loading;
49 | const data = {
50 | start: self.data.start,
51 | limit: self.data.limit,
52 | order_by: self.data.orderBy,
53 | desc: self.data.desc
54 | };
55 | if (loading
56 | ) {
57 | return;
58 | }
59 | self.setData({
60 | loading: true
61 | });
62 | wx.showToast({
63 | title: '正在加载',
64 | icon: 'loading',
65 | duration: 10000,
66 | });
67 | api.getUsers({
68 | data,
69 | success: (res) => {
70 | let users = res.data.rs;
71 | const hasMore = users.length === self.data.limit;
72 | if (needRefresh) {
73 | wx.stopPullDownRefresh();
74 | } else {
75 | users = self.data.users.concat(users);
76 | }
77 | self.setData({
78 | users, hasMore,
79 | start: self.data.start + self.data.limit,
80 | loading: false
81 | });
82 | wx.hideToast();
83 | },
84 | });
85 | },
86 | bindPickerChange(e) {
87 | const self = this;
88 | const index = [e.detail.value];
89 | this.setData({
90 | orderBy: this.data.sortArray[index].type
91 | })
92 | self.loadData(true);
93 | },
94 | loadData(refresh) {
95 | if (refresh) {
96 | this.setData({
97 | start: 0, users: [], hasMore: true
98 | });
99 | }
100 | this.loadMore(null, !refresh);
101 | },
102 | onViewTap(e) {
103 | const ds = e.currentTarget.dataset;
104 | wx.navigateTo({
105 | url: `../users/user?id=${ds.id}`,
106 | });
107 | },
108 | });
--------------------------------------------------------------------------------
/App/pages/users/users.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigationBarTitleText": "主讲人列表"
3 | }
--------------------------------------------------------------------------------
/App/pages/users/users.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 排序方式: {{sortMap[orderBy]}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/App/pages/users/users.wxss:
--------------------------------------------------------------------------------
1 | @import "../../components/user/user_small.wxss";
2 |
3 | .picker {
4 | color: #fff;
5 | background-color: #4abdcc;
6 | }
7 |
8 | .section {
9 | margin: 20rpx;
10 | position: relative;
11 | }
12 |
13 | .section view {
14 | padding: 10rpx;
15 | }
16 |
17 | .arrow {
18 | border: solid currentColor;
19 | border-width: 0 3rpx 3rpx 0;
20 | display: inline-block;
21 | padding: 6rpx;
22 | transform: rotate(45deg);
23 | position: absolute;
24 | right: 20rpx;
25 | top: 20rpx;
26 | }
27 |
--------------------------------------------------------------------------------
/App/utils/api.js:
--------------------------------------------------------------------------------
1 | const apiURL = 'http://localhost:8300/api/v1';
2 |
3 | const wxRequest = (params, url) => {
4 | wx.request({
5 | url,
6 | method: params.method || 'GET',
7 | data: params.data || {},
8 | header: {
9 | Accept: 'application/json',
10 | 'Content-Type': 'application/json',
11 | },
12 | success(res) {
13 | if (params.success) {
14 | params.success(res);
15 | }
16 | },
17 | fail(res) {
18 | if (params.fail) {
19 | params.fail(res);
20 | }
21 | },
22 | complete(res) {
23 | if (params.complete) {
24 | params.complete(res);
25 | }
26 | },
27 | });
28 | };
29 |
30 | const search = (params) => {
31 | wxRequest(params, `${apiURL}/search`);
32 | };
33 | const suggest = (params) => {
34 | wxRequest(params, `${apiURL}/suggest`);
35 | };
36 | const explore = (params) => {
37 | wxRequest(params, `${apiURL}/explore`);
38 | };
39 | const getLiveInfoById = (params) => {
40 | wxRequest({ success: params.success }, `${apiURL}/live/${params.data.id}`);
41 | };
42 | const getHotTopics = (params) => {
43 | wxRequest(params, `${apiURL}/hot_topics`);
44 | };
45 | const getTopicByName = (params) => {
46 | wxRequest(params, `${apiURL}/topic`);
47 | };
48 | const getUsers = (params) => {
49 | wxRequest(params, `${apiURL}/users`);
50 | };
51 | const getUserInfoById = (params) => {
52 | wxRequest({ success: params.success }, `${apiURL}/user/${params.data.userId}`);
53 | };
54 | const getHotByWeekly = (params) => {
55 | wxRequest(params, `${apiURL}/hot/weekly`);
56 | };
57 | const getHotByMonthly = (params) => {
58 | wxRequest(params, `${apiURL}/hot/monthly`);
59 | };
60 |
61 | module.exports = {
62 | search,
63 | suggest,
64 | explore,
65 | getHotTopics,
66 | getTopicByName,
67 | getUsers,
68 | getUserInfoById,
69 | getHotByWeekly,
70 | getHotByMonthly,
71 | getLiveInfoById
72 | };
73 |
--------------------------------------------------------------------------------
/App/utils/util.js:
--------------------------------------------------------------------------------
1 | function formatTime(date) {
2 | var year = date.getFullYear()
3 | var month = date.getMonth() + 1
4 | var day = date.getDate()
5 |
6 | var hour = date.getHours()
7 | var minute = date.getMinutes()
8 | var second = date.getSeconds()
9 |
10 |
11 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
12 | }
13 |
14 | function formatNumber(n) {
15 | n = n.toString()
16 | return n[1] ? n : '0' + n
17 | }
18 |
19 | function lengthStr(str) {
20 | var m = encodeURIComponent(str).match(/%[89ABab]/g);
21 | return str.length + (m ? m.length : 0);
22 | }
23 |
24 | module.exports = {
25 | formatTime: formatTime,
26 | lengthStr: lengthStr
27 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # weapp-zhihulive
2 |
3 |
4 | 基于Zhihu Live数据的微信小程序.数据归知乎所有,本项目用于技术学习
5 |
6 | ## Preview
7 |
8 | 
9 |
10 | 如果在wifi情况下或者土豪不介意流量的同学可以直接[感受实际使用的动态效果](./screenshot/zhihulive.gif)
11 |
12 | ## Getting started
13 |
14 | 本项目包含服务端和微信小程序全部源代码:
15 |
16 | ```python
17 | ❯ git clone https://github.com/dongweiming/weapp-zhihulive
18 | ❯ cd weapp-zhihulive
19 | ❯ tree -L 1
20 | .
21 | ├── App # 小程序代码
22 | ├── LICENSE
23 | ├── README.md
24 | ├── Server # 服务端+爬虫代码
25 | ├── screenshot # 设计图和动态效果
26 | ```
27 |
28 | 启动服务端:
29 |
30 | ```python
31 | ❯ cd Server
32 | ❯ python3 -m venv venv3 --system-site-packages
33 | ❯ source venv3/bin/activate
34 | ❯ python3 -m pip install -r requirements.txt
35 | # 配置MySQL和Elasticsearch
36 | ❯ python crawl.py # 运行爬虫获取全部Live数据
37 | ❯ python app.py # 启动API服务
38 | ```
39 |
40 | 运行小程序:
41 |
42 | 1. [下载并安装小程序开发工具](https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html)
43 | 2. 启动开发工具,添加项目,目录为App
44 |
--------------------------------------------------------------------------------
/Server/LogGraph.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {
7 | "collapsed": false,
8 | "scrolled": true
9 | },
10 | "outputs": [
11 | {
12 | "data": {
13 | "image/png": "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\n",
14 | "text/plain": [
15 | ""
16 | ]
17 | },
18 | "metadata": {},
19 | "output_type": "display_data"
20 | }
21 | ],
22 | "source": [
23 | "%matplotlib inline\n",
24 | "import matplotlib.pyplot as plt\n",
25 | "import numpy as np\n",
26 | "import array\n",
27 | "\n",
28 | "t1 = np.array([2000, 3000, 5000, 8000, 10000, 30000, 50000, 100000])\n",
29 | "\n",
30 | "l1, = plt.plot(t1, np.log10(t1))\n",
31 | "\n",
32 | "plt.xlabel('Income')\n",
33 | "plt.ylabel('Value')\n",
34 | "plt.title('Log result')\n",
35 | "\n",
36 | "plt.show()\n",
37 | "\n"
38 | ]
39 | },
40 | {
41 | "cell_type": "code",
42 | "execution_count": null,
43 | "metadata": {
44 | "collapsed": true
45 | },
46 | "outputs": [],
47 | "source": []
48 | }
49 | ],
50 | "metadata": {
51 | "kernelspec": {
52 | "display_name": "Python 2",
53 | "language": "python",
54 | "name": "python2"
55 | },
56 | "language_info": {
57 | "codemirror_mode": {
58 | "name": "ipython",
59 | "version": 2
60 | },
61 | "file_extension": ".py",
62 | "mimetype": "text/x-python",
63 | "name": "python",
64 | "nbconvert_exporter": "python",
65 | "pygments_lexer": "ipython2",
66 | "version": "2.7.10"
67 | }
68 | },
69 | "nbformat": 4,
70 | "nbformat_minor": 0
71 | }
72 |
--------------------------------------------------------------------------------
/Server/app.py:
--------------------------------------------------------------------------------
1 | from sanic import Sanic
2 | from elasticsearch_dsl.connections import connections
3 |
4 | from views.api import bp
5 | from views.protocol import JSONHttpProtocol
6 |
7 | app = Sanic(__name__)
8 | app.blueprint(bp)
9 | app.static('/static', './static')
10 |
11 |
12 | def set_loop(sanic, loop):
13 | conns = connections._conns
14 | for c in conns:
15 | conns[c].transport.loop = loop
16 |
17 |
18 | @app.middleware('request')
19 | async def halt_request(request):
20 | request.start = request.args.get('start', 0)
21 | request.limit = request.args.get('limit', 10)
22 |
23 |
24 |
25 | if __name__ == '__main__':
26 | app.run(host='0.0.0.0', port=8300, protocol=JSONHttpProtocol,
27 | before_start=[set_loop], workers=4, debug=True)
28 |
--------------------------------------------------------------------------------
/Server/client.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | import os
3 | import json
4 | import time
5 | import getpass
6 | import requests
7 | from requests.auth import AuthBase
8 | from requests.packages.urllib3.exceptions import InsecureRequestWarning
9 |
10 | from config import (
11 | API_VERSION, APP_VERSION, APP_BUILD, UUID, UA, APP_ZA, CLIENT_ID,
12 | TOKEN_FILE, LOGIN_URL, CAPTCHA_URL)
13 | from utils import gen_signature
14 | from exception import LoginException
15 |
16 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
17 |
18 | LOGIN_DATA = {
19 | 'grant_type': 'password',
20 | 'source': 'com.zhihu.ios',
21 | 'client_id': CLIENT_ID
22 | }
23 |
24 |
25 | class ZhihuOAuth(AuthBase):
26 | def __init__(self, token=None):
27 | self._token = token
28 |
29 | def __call__(self, r):
30 | r.headers['X-API-Version'] = API_VERSION
31 | r.headers['X-APP_VERSION'] = APP_VERSION
32 | r.headers['X-APP-Build'] = APP_BUILD
33 | r.headers['x-app-za'] = APP_ZA
34 | r.headers['X-UDID'] = UUID
35 | r.headers['User-Agent'] = UA
36 | if self._token is None:
37 | auth_str = 'oauth {client_id}'.format(
38 | client_id=CLIENT_ID
39 | )
40 | else:
41 | auth_str = '{type} {token}'.format(
42 | type=str(self._token.token_type.capitalize()),
43 | token=str(self._token.access_token)
44 | )
45 | r.headers['Authorization'] = auth_str
46 | return r
47 |
48 |
49 | class ZhihuToken:
50 | def __init__(self, user_id, uid, access_token, expires_in, token_type,
51 | refresh_token, cookie, lock_in=None, unlock_ticket=None):
52 | self.create_at = time.time()
53 | self.user_id = uid
54 | self.uid = user_id
55 | self.access_token = access_token
56 | self.expires_in = expires_in
57 | self.expires_at = self.create_at + self.expires_in
58 | self.token_type = token_type
59 | self.refresh_token = refresh_token
60 | self.cookie = cookie
61 |
62 | # Not used
63 | self._lock_in = lock_in
64 | self._unlock_ticket = unlock_ticket
65 |
66 | @classmethod
67 | def from_file(cls, filename):
68 | with open(filename) as f:
69 | return cls.from_dict(json.load(f))
70 |
71 | @staticmethod
72 | def save_file(filename, data):
73 | with open(filename, 'w') as f:
74 | json.dump(data, f)
75 |
76 | @classmethod
77 | def from_dict(cls, json_dict):
78 | try:
79 | return cls(**json_dict)
80 | except TypeError:
81 | raise ValueError(
82 | '"{json_dict}" is NOT a valid zhihu token json.'.format(
83 | json_dict=json_dict
84 | ))
85 |
86 | class ZhihuClient:
87 | def __init__(self, token_file=TOKEN_FILE):
88 | self._session = requests.session()
89 | self._session.verify = False
90 | self.token_file = token_file
91 |
92 | if os.path.exists(token_file):
93 | self._token = ZhihuToken.from_file(token_file)
94 | else:
95 | print('----- Zhihu OAuth Login -----')
96 | username = input('Username: ')
97 | password = getpass.getpass('Password: ')
98 | self.login(username, password)
99 | self.auth = ZhihuOAuth(self._token)
100 |
101 | def save_token(self, auth, data):
102 | res = self._session.post(LOGIN_URL, auth=auth, data=data)
103 | try:
104 | json_dict = res.json()
105 | if 'error' in json_dict:
106 | raise LoginException(json_dict['error']['message'])
107 | self._token = ZhihuToken.from_dict(json_dict)
108 | except (ValueError, KeyError) as e:
109 | raise LoginException(str(e))
110 | else:
111 | ZhihuToken.save_file(self.token_file, json_dict)
112 |
113 | def login(self, username, password):
114 | self._login_auth = ZhihuOAuth()
115 | data = LOGIN_DATA.copy()
116 | data['username'] = username
117 | data['password'] = password
118 | gen_signature(data)
119 |
120 | if self.need_captcha():
121 | captcha_image = self.get_captcha()
122 | with open(CAPTCHA_FILE, 'wb') as f:
123 | f.write(captcha_image)
124 | print('Please open {0} for captcha'.format(
125 | os.path.abspath(CAPTCHA_FILE)))
126 |
127 | captcha = input('captcha: ')
128 | os.remove(os.path.abspath(CAPTCHA_FILE))
129 | res = self._session.post(
130 | CAPTCHA_URL,
131 | auth=self._login_auth,
132 | data={'input_text': captcha}
133 | )
134 | try:
135 | json_dict = res.json()
136 | if 'error' in json_dict:
137 | raise LoginException(json_dict['error']['message'])
138 | except (ValueError, KeyError) as e:
139 | raise LoginException('Maybe input wrong captcha value')
140 |
141 | self.save_token(self._login_auth, data)
142 |
143 | def need_captcha(self):
144 | res = self._session.get(CAPTCHA_URL, auth=self._login_auth)
145 | try:
146 | j = res.json()
147 | return j['show_captcha']
148 | except KeyError:
149 | raise LoginException('Show captcha fail!')
150 |
151 | def refresh_token(self):
152 | data = LOGIN_DATA.copy()
153 | data['grant_type'] = 'refresh_token'
154 | data['refresh_token'] = self._token.refresh_token
155 | gen_signature(data)
156 | auth = ZhihuOAuth(self._token)
157 | self.save_token(auth, data)
158 |
159 | if __name__ == '__main__':
160 | client = ZhihuClient()
161 |
--------------------------------------------------------------------------------
/Server/config.py:
--------------------------------------------------------------------------------
1 | API_VERSION = '3.0.42'
2 | APP_VERSION = '3.28.0'
3 | APP_BUILD = 'release'
4 | UUID = 'AJDA7XkI9glLBWc85sk-nJ_6F0jqALu4AlY='
5 | UA = 'osee2unifiedRelease/3.28.0 (iPhone; iOS 10.2; Scale/2.00)'
6 | APP_ZA = 'OS=iOS&Release=10.2&Model=iPhone8,1&VersionName=3.28.0&VersionCode=558&Width=750&Height='
7 | CLIENT_ID = '8d5227e0aaaa4797a763ac64e0c3b8'
8 | APP_SECRET = b'ecbefbf6b17e47ecb9035107866380'
9 |
10 | TOKEN_FILE = 'token.json'
11 |
12 | ZHIHU_API_ROOT = 'https://api.zhihu.com'
13 | PEOPLE_URL = 'https://www.zhihu.com/people/{}'
14 | LIVE_URL = 'https://www.zhihu.com/lives/{}'
15 | LIVE_USER_URL = 'https://www.zhihu.com/lives/users/{}'
16 | ZHUANLAN_URL = 'https://zhuanlan.zhihu.com/p/{}'
17 | TOPIC_URL = 'https://www.zhihu.com/topic{}/'
18 | LOGIN_URL = ZHIHU_API_ROOT + '/sign_in'
19 | CAPTCHA_URL = ZHIHU_API_ROOT + '/captcha'
20 |
21 | DB_URI = 'mysql+pymysql://localhost/test?charset=utf8mb4'
22 |
23 | SPEAKER_KEYS = ['name', 'gender', 'headline', 'avatar_url', 'bio',
24 | 'description']
25 | LIVE_KEYS = ['id', 'feedback_score', 'seats', 'subject', 'fee',
26 | 'description', 'status', 'starts_at', 'outline',
27 | 'speaker_message_count', 'liked_num', 'tags', 'topics']
28 | TOPIC_KEYS = ['id', 'avatar_url', 'best_answerers_count', 'best_answers_count',
29 | 'name', 'questions_count', 'followers_count']
30 | SEARCH_FIELDS = ['subject^5', 'outline^2', 'description', 'topic_names^10',
31 | 'tag_names^5']
32 | SUGGEST_USER_LIMIT = 2
33 | SUGGEST_LIMIT = 6
34 | DOMAIN = 'http://localhost:8300'
35 |
--------------------------------------------------------------------------------
/Server/crawl.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import cgi
4 | import time
5 | import asyncio
6 | import sqlite3
7 | import logging
8 | from asyncio import Queue
9 | from datetime import datetime
10 | from urllib.parse import urlparse, parse_qsl, urlunparse, urlencode
11 |
12 | import aiohttp
13 | from elasticsearch_dsl import Q
14 | from elasticsearch_dsl.connections import connections
15 | from elasticsearch.exceptions import NotFoundError
16 |
17 | from models import User, Live, Topic, session
18 | from models.live import init as live_init
19 | from client import ZhihuClient
20 | from utils import flatten_live_dict
21 | from config import SPEAKER_KEYS, LIVE_KEYS, TOPIC_KEYS, ZHUANLAN_URL
22 |
23 | LIVE_API_URL = 'https://api.zhihu.com/lives/{type}?purchasable=0&limit=10&offset={offset}' # noqa
24 | ZHUANLAN_API_URL = 'https://zhuanlan.zhihu.com/api/columns/zhihulive/posts?limit=20&offset={offset}' # noqa
25 | TOPIC_API_URL = 'https://api.zhihu.com/topics/{}'
26 | LIVE_TYPE = frozenset(['ongoing', 'ended'])
27 | IMAGE_FOLDER = 'static/images/zhihu'
28 | LIVE_REGEX = re.compile(r'
--------------------------------------------------------------------------------
/Server/static/images/weekly.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Server/stopwords-utf8.txt:
--------------------------------------------------------------------------------
1 | --
2 | ?
3 | “
4 | ”
5 | 》
6 | 『
7 | 「
8 | 」
9 | 』
10 | --
11 | able
12 | about
13 | above
14 | according
15 | accordingly
16 | across
17 | actually
18 | after
19 | afterwards
20 | again
21 | against
22 | ain't
23 | all
24 | allow
25 | allows
26 | almost
27 | alone
28 | along
29 | already
30 | also
31 | although
32 | always
33 | am
34 | among
35 | amongst
36 | an
37 | and
38 | another
39 | any
40 | anybody
41 | anyhow
42 | anyone
43 | anything
44 | anyway
45 | anyways
46 | anywhere
47 | apart
48 | appear
49 | appreciate
50 | appropriate
51 | are
52 | aren't
53 | around
54 | as
55 | a's
56 | aside
57 | ask
58 | asking
59 | associated
60 | at
61 | available
62 | away
63 | awfully
64 | be
65 | became
66 | because
67 | become
68 | becomes
69 | becoming
70 | been
71 | before
72 | beforehand
73 | behind
74 | being
75 | believe
76 | below
77 | beside
78 | besides
79 | best
80 | better
81 | between
82 | beyond
83 | both
84 | brief
85 | but
86 | by
87 | came
88 | can
89 | cannot
90 | cant
91 | can't
92 | cause
93 | causes
94 | certain
95 | certainly
96 | changes
97 | clearly
98 | c'mon
99 | co
100 | com
101 | come
102 | comes
103 | concerning
104 | consequently
105 | consider
106 | considering
107 | contain
108 | containing
109 | contains
110 | corresponding
111 | could
112 | couldn't
113 | course
114 | c's
115 | currently
116 | definitely
117 | described
118 | despite
119 | did
120 | didn't
121 | different
122 | do
123 | does
124 | doesn't
125 | doing
126 | done
127 | don't
128 | down
129 | downwards
130 | during
131 | each
132 | edu
133 | eg
134 | eight
135 | either
136 | else
137 | elsewhere
138 | enough
139 | entirely
140 | especially
141 | et
142 | etc
143 | even
144 | ever
145 | every
146 | everybody
147 | everyone
148 | everything
149 | everywhere
150 | ex
151 | exactly
152 | example
153 | except
154 | far
155 | few
156 | fifth
157 | first
158 | five
159 | followed
160 | following
161 | follows
162 | for
163 | former
164 | formerly
165 | forth
166 | four
167 | from
168 | further
169 | furthermore
170 | get
171 | gets
172 | getting
173 | given
174 | gives
175 | go
176 | goes
177 | going
178 | gone
179 | got
180 | gotten
181 | greetings
182 | had
183 | hadn't
184 | happens
185 | hardly
186 | has
187 | hasn't
188 | have
189 | haven't
190 | having
191 | he
192 | hello
193 | help
194 | hence
195 | her
196 | here
197 | hereafter
198 | hereby
199 | herein
200 | here's
201 | hereupon
202 | hers
203 | herself
204 | he's
205 | hi
206 | him
207 | himself
208 | his
209 | hither
210 | hopefully
211 | how
212 | howbeit
213 | however
214 | i'd
215 | ie
216 | if
217 | ignored
218 | i'll
219 | i'm
220 | immediate
221 | in
222 | inasmuch
223 | inc
224 | indeed
225 | indicate
226 | indicated
227 | indicates
228 | inner
229 | insofar
230 | instead
231 | into
232 | inward
233 | is
234 | isn't
235 | it
236 | it'd
237 | it'll
238 | its
239 | it's
240 | itself
241 | i've
242 | just
243 | keep
244 | keeps
245 | kept
246 | know
247 | known
248 | knows
249 | last
250 | lately
251 | later
252 | latter
253 | latterly
254 | least
255 | less
256 | lest
257 | let
258 | let's
259 | like
260 | liked
261 | likely
262 | little
263 | look
264 | looking
265 | looks
266 | ltd
267 | mainly
268 | many
269 | may
270 | maybe
271 | me
272 | mean
273 | meanwhile
274 | merely
275 | might
276 | more
277 | moreover
278 | most
279 | mostly
280 | much
281 | must
282 | my
283 | myself
284 | name
285 | namely
286 | nd
287 | near
288 | nearly
289 | necessary
290 | need
291 | needs
292 | neither
293 | never
294 | nevertheless
295 | new
296 | next
297 | nine
298 | no
299 | nobody
300 | non
301 | none
302 | noone
303 | nor
304 | normally
305 | not
306 | nothing
307 | novel
308 | now
309 | nowhere
310 | obviously
311 | of
312 | off
313 | often
314 | oh
315 | ok
316 | okay
317 | old
318 | on
319 | once
320 | one
321 | ones
322 | only
323 | onto
324 | or
325 | other
326 | others
327 | otherwise
328 | ought
329 | our
330 | ours
331 | ourselves
332 | out
333 | outside
334 | over
335 | overall
336 | own
337 | particular
338 | particularly
339 | per
340 | perhaps
341 | placed
342 | please
343 | plus
344 | possible
345 | presumably
346 | probably
347 | provides
348 | que
349 | quite
350 | qv
351 | rather
352 | rd
353 | re
354 | really
355 | reasonably
356 | regarding
357 | regardless
358 | regards
359 | relatively
360 | respectively
361 | right
362 | said
363 | same
364 | saw
365 | say
366 | saying
367 | says
368 | second
369 | secondly
370 | see
371 | seeing
372 | seem
373 | seemed
374 | seeming
375 | seems
376 | seen
377 | self
378 | selves
379 | sensible
380 | sent
381 | serious
382 | seriously
383 | seven
384 | several
385 | shall
386 | she
387 | should
388 | shouldn't
389 | since
390 | six
391 | so
392 | some
393 | somebody
394 | somehow
395 | someone
396 | something
397 | sometime
398 | sometimes
399 | somewhat
400 | somewhere
401 | soon
402 | sorry
403 | specified
404 | specify
405 | specifying
406 | still
407 | sub
408 | such
409 | sup
410 | sure
411 | take
412 | taken
413 | tell
414 | tends
415 | th
416 | than
417 | thank
418 | thanks
419 | thanx
420 | that
421 | thats
422 | that's
423 | the
424 | their
425 | theirs
426 | them
427 | themselves
428 | then
429 | thence
430 | there
431 | thereafter
432 | thereby
433 | therefore
434 | therein
435 | theres
436 | there's
437 | thereupon
438 | these
439 | they
440 | they'd
441 | they'll
442 | they're
443 | they've
444 | think
445 | third
446 | this
447 | thorough
448 | thoroughly
449 | those
450 | though
451 | three
452 | through
453 | throughout
454 | thru
455 | thus
456 | to
457 | together
458 | too
459 | took
460 | toward
461 | towards
462 | tried
463 | tries
464 | truly
465 | try
466 | trying
467 | t's
468 | twice
469 | two
470 | un
471 | under
472 | unfortunately
473 | unless
474 | unlikely
475 | until
476 | unto
477 | up
478 | upon
479 | us
480 | use
481 | used
482 | useful
483 | uses
484 | using
485 | usually
486 | value
487 | various
488 | very
489 | via
490 | viz
491 | vs
492 | want
493 | wants
494 | was
495 | wasn't
496 | way
497 | we
498 | we'd
499 | welcome
500 | well
501 | we'll
502 | went
503 | were
504 | we're
505 | weren't
506 | we've
507 | what
508 | whatever
509 | what's
510 | when
511 | whence
512 | whenever
513 | where
514 | whereafter
515 | whereas
516 | whereby
517 | wherein
518 | where's
519 | whereupon
520 | wherever
521 | whether
522 | which
523 | while
524 | whither
525 | who
526 | whoever
527 | whole
528 | whom
529 | who's
530 | whose
531 | why
532 | will
533 | willing
534 | wish
535 | with
536 | within
537 | without
538 | wonder
539 | won't
540 | would
541 | wouldn't
542 | yes
543 | yet
544 | you
545 | you'd
546 | you'll
547 | your
548 | you're
549 | yours
550 | yourself
551 | yourselves
552 | you've
553 | zero
554 | zt
555 | ZT
556 | zz
557 | ZZ
558 | 一
559 | 一下
560 | 一些
561 | 一切
562 | 一则
563 | 一天
564 | 一定
565 | 一方面
566 | 一旦
567 | 一时
568 | 一来
569 | 一样
570 | 一次
571 | 一片
572 | 一直
573 | 一致
574 | 一般
575 | 一起
576 | 一边
577 | 一面
578 | 万一
579 | 上下
580 | 上升
581 | 上去
582 | 上来
583 | 上述
584 | 上面
585 | 下列
586 | 下去
587 | 下来
588 | 下面
589 | 不一
590 | 不久
591 | 不仅
592 | 不会
593 | 不但
594 | 不光
595 | 不单
596 | 不变
597 | 不只
598 | 不可
599 | 不同
600 | 不够
601 | 不如
602 | 不得
603 | 不怕
604 | 不惟
605 | 不成
606 | 不拘
607 | 不敢
608 | 不断
609 | 不是
610 | 不比
611 | 不然
612 | 不特
613 | 不独
614 | 不管
615 | 不能
616 | 不要
617 | 不论
618 | 不足
619 | 不过
620 | 不问
621 | 与
622 | 与其
623 | 与否
624 | 与此同时
625 | 专门
626 | 且
627 | 两者
628 | 严格
629 | 严重
630 | 个
631 | 个人
632 | 个别
633 | 中小
634 | 中间
635 | 丰富
636 | 临
637 | 为
638 | 为主
639 | 为了
640 | 为什么
641 | 为什麽
642 | 为何
643 | 为着
644 | 主张
645 | 主要
646 | 举行
647 | 乃
648 | 乃至
649 | 么
650 | 之
651 | 之一
652 | 之前
653 | 之后
654 | 之後
655 | 之所以
656 | 之类
657 | 乌乎
658 | 乎
659 | 乘
660 | 也
661 | 也好
662 | 也是
663 | 也罢
664 | 了
665 | 了解
666 | 争取
667 | 于
668 | 于是
669 | 于是乎
670 | 云云
671 | 互相
672 | 产生
673 | 人们
674 | 人家
675 | 什么
676 | 什么样
677 | 什麽
678 | 今后
679 | 今天
680 | 今年
681 | 今後
682 | 仍然
683 | 从
684 | 从事
685 | 从而
686 | 他
687 | 他人
688 | 他们
689 | 他的
690 | 代替
691 | 以
692 | 以上
693 | 以下
694 | 以为
695 | 以便
696 | 以免
697 | 以前
698 | 以及
699 | 以后
700 | 以外
701 | 以後
702 | 以来
703 | 以至
704 | 以至于
705 | 以致
706 | 们
707 | 任
708 | 任何
709 | 任凭
710 | 任务
711 | 企图
712 | 伟大
713 | 似乎
714 | 似的
715 | 但
716 | 但是
717 | 何
718 | 何况
719 | 何处
720 | 何时
721 | 作为
722 | 你
723 | 你们
724 | 你的
725 | 使得
726 | 使用
727 | 例如
728 | 依
729 | 依照
730 | 依靠
731 | 促进
732 | 保持
733 | 俺
734 | 俺们
735 | 倘
736 | 倘使
737 | 倘或
738 | 倘然
739 | 倘若
740 | 假使
741 | 假如
742 | 假若
743 | 做到
744 | 像
745 | 允许
746 | 充分
747 | 先后
748 | 先後
749 | 先生
750 | 全部
751 | 全面
752 | 兮
753 | 共同
754 | 关于
755 | 其
756 | 其一
757 | 其中
758 | 其二
759 | 其他
760 | 其余
761 | 其它
762 | 其实
763 | 其次
764 | 具体
765 | 具体地说
766 | 具体说来
767 | 具有
768 | 再者
769 | 再说
770 | 冒
771 | 冲
772 | 决定
773 | 况且
774 | 准备
775 | 几
776 | 几乎
777 | 几时
778 | 凭
779 | 凭借
780 | 出去
781 | 出来
782 | 出现
783 | 分别
784 | 则
785 | 别
786 | 别的
787 | 别说
788 | 到
789 | 前后
790 | 前者
791 | 前进
792 | 前面
793 | 加之
794 | 加以
795 | 加入
796 | 加强
797 | 十分
798 | 即
799 | 即令
800 | 即使
801 | 即便
802 | 即或
803 | 即若
804 | 却不
805 | 原来
806 | 又
807 | 及
808 | 及其
809 | 及时
810 | 及至
811 | 双方
812 | 反之
813 | 反应
814 | 反映
815 | 反过来
816 | 反过来说
817 | 取得
818 | 受到
819 | 变成
820 | 另
821 | 另一方面
822 | 另外
823 | 只是
824 | 只有
825 | 只要
826 | 只限
827 | 叫
828 | 叫做
829 | 召开
830 | 叮咚
831 | 可
832 | 可以
833 | 可是
834 | 可能
835 | 可见
836 | 各
837 | 各个
838 | 各人
839 | 各位
840 | 各地
841 | 各种
842 | 各级
843 | 各自
844 | 合理
845 | 同
846 | 同一
847 | 同时
848 | 同样
849 | 后来
850 | 后面
851 | 向
852 | 向着
853 | 吓
854 | 吗
855 | 否则
856 | 吧
857 | 吧哒
858 | 吱
859 | 呀
860 | 呃
861 | 呕
862 | 呗
863 | 呜
864 | 呜呼
865 | 呢
866 | 周围
867 | 呵
868 | 呸
869 | 呼哧
870 | 咋
871 | 和
872 | 咚
873 | 咦
874 | 咱
875 | 咱们
876 | 咳
877 | 哇
878 | 哈
879 | 哈哈
880 | 哉
881 | 哎
882 | 哎呀
883 | 哎哟
884 | 哗
885 | 哟
886 | 哦
887 | 哩
888 | 哪
889 | 哪个
890 | 哪些
891 | 哪儿
892 | 哪天
893 | 哪年
894 | 哪怕
895 | 哪样
896 | 哪边
897 | 哪里
898 | 哼
899 | 哼唷
900 | 唉
901 | 啊
902 | 啐
903 | 啥
904 | 啦
905 | 啪达
906 | 喂
907 | 喏
908 | 喔唷
909 | 嗡嗡
910 | 嗬
911 | 嗯
912 | 嗳
913 | 嘎
914 | 嘎登
915 | 嘘
916 | 嘛
917 | 嘻
918 | 嘿
919 | 因
920 | 因为
921 | 因此
922 | 因而
923 | 固然
924 | 在
925 | 在下
926 | 地
927 | 坚决
928 | 坚持
929 | 基本
930 | 处理
931 | 复杂
932 | 多
933 | 多少
934 | 多数
935 | 多次
936 | 大力
937 | 大多数
938 | 大大
939 | 大家
940 | 大批
941 | 大约
942 | 大量
943 | 失去
944 | 她
945 | 她们
946 | 她的
947 | 好的
948 | 好象
949 | 如
950 | 如上所述
951 | 如下
952 | 如何
953 | 如其
954 | 如果
955 | 如此
956 | 如若
957 | 存在
958 | 宁
959 | 宁可
960 | 宁愿
961 | 宁肯
962 | 它
963 | 它们
964 | 它们的
965 | 它的
966 | 安全
967 | 完全
968 | 完成
969 | 实现
970 | 实际
971 | 宣布
972 | 容易
973 | 密切
974 | 对
975 | 对于
976 | 对应
977 | 将
978 | 少数
979 | 尔后
980 | 尚且
981 | 尤其
982 | 就
983 | 就是
984 | 就是说
985 | 尽
986 | 尽管
987 | 属于
988 | 岂但
989 | 左右
990 | 巨大
991 | 巩固
992 | 己
993 | 已经
994 | 帮助
995 | 常常
996 | 并
997 | 并不
998 | 并不是
999 | 并且
1000 | 并没有
1001 | 广大
1002 | 广泛
1003 | 应当
1004 | 应用
1005 | 应该
1006 | 开外
1007 | 开始
1008 | 开展
1009 | 引起
1010 | 强烈
1011 | 强调
1012 | 归
1013 | 当
1014 | 当前
1015 | 当时
1016 | 当然
1017 | 当着
1018 | 形成
1019 | 彻底
1020 | 彼
1021 | 彼此
1022 | 往
1023 | 往往
1024 | 待
1025 | 後来
1026 | 後面
1027 | 得
1028 | 得出
1029 | 得到
1030 | 心里
1031 | 必然
1032 | 必要
1033 | 必须
1034 | 怎
1035 | 怎么
1036 | 怎么办
1037 | 怎么样
1038 | 怎样
1039 | 怎麽
1040 | 总之
1041 | 总是
1042 | 总的来看
1043 | 总的来说
1044 | 总的说来
1045 | 总结
1046 | 总而言之
1047 | 恰恰相反
1048 | 您
1049 | 意思
1050 | 愿意
1051 | 慢说
1052 | 成为
1053 | 我
1054 | 我们
1055 | 我的
1056 | 或
1057 | 或是
1058 | 或者
1059 | 战斗
1060 | 所
1061 | 所以
1062 | 所有
1063 | 所谓
1064 | 打
1065 | 扩大
1066 | 把
1067 | 抑或
1068 | 拿
1069 | 按
1070 | 按照
1071 | 换句话说
1072 | 换言之
1073 | 据
1074 | 掌握
1075 | 接着
1076 | 接著
1077 | 故
1078 | 故此
1079 | 整个
1080 | 方便
1081 | 方面
1082 | 旁人
1083 | 无宁
1084 | 无法
1085 | 无论
1086 | 既
1087 | 既是
1088 | 既然
1089 | 时候
1090 | 明显
1091 | 明确
1092 | 是
1093 | 是否
1094 | 是的
1095 | 显然
1096 | 显著
1097 | 普通
1098 | 普遍
1099 | 更加
1100 | 曾经
1101 | 替
1102 | 最后
1103 | 最大
1104 | 最好
1105 | 最後
1106 | 最近
1107 | 最高
1108 | 有
1109 | 有些
1110 | 有关
1111 | 有利
1112 | 有力
1113 | 有所
1114 | 有效
1115 | 有时
1116 | 有点
1117 | 有的
1118 | 有着
1119 | 有著
1120 | 望
1121 | 朝
1122 | 朝着
1123 | 本
1124 | 本着
1125 | 来
1126 | 来着
1127 | 极了
1128 | 构成
1129 | 果然
1130 | 果真
1131 | 某
1132 | 某个
1133 | 某些
1134 | 根据
1135 | 根本
1136 | 欢迎
1137 | 正在
1138 | 正如
1139 | 正常
1140 | 此
1141 | 此外
1142 | 此时
1143 | 此间
1144 | 毋宁
1145 | 每
1146 | 每个
1147 | 每天
1148 | 每年
1149 | 每当
1150 | 比
1151 | 比如
1152 | 比方
1153 | 比较
1154 | 毫不
1155 | 没有
1156 | 沿
1157 | 沿着
1158 | 注意
1159 | 深入
1160 | 清楚
1161 | 满足
1162 | 漫说
1163 | 焉
1164 | 然则
1165 | 然后
1166 | 然後
1167 | 然而
1168 | 照
1169 | 照着
1170 | 特别是
1171 | 特殊
1172 | 特点
1173 | 现代
1174 | 现在
1175 | 甚么
1176 | 甚而
1177 | 甚至
1178 | 用
1179 | 由
1180 | 由于
1181 | 由此可见
1182 | 的
1183 | 的话
1184 | 目前
1185 | 直到
1186 | 直接
1187 | 相似
1188 | 相信
1189 | 相反
1190 | 相同
1191 | 相对
1192 | 相对而言
1193 | 相应
1194 | 相当
1195 | 相等
1196 | 省得
1197 | 看出
1198 | 看到
1199 | 看来
1200 | 看看
1201 | 看见
1202 | 真是
1203 | 真正
1204 | 着
1205 | 着呢
1206 | 矣
1207 | 知道
1208 | 确定
1209 | 离
1210 | 积极
1211 | 移动
1212 | 突出
1213 | 突然
1214 | 立即
1215 | 第
1216 | 等
1217 | 等等
1218 | 管
1219 | 紧接着
1220 | 纵
1221 | 纵令
1222 | 纵使
1223 | 纵然
1224 | 练习
1225 | 组成
1226 | 经
1227 | 经常
1228 | 经过
1229 | 结合
1230 | 结果
1231 | 给
1232 | 绝对
1233 | 继续
1234 | 继而
1235 | 维持
1236 | 综上所述
1237 | 罢了
1238 | 考虑
1239 | 者
1240 | 而
1241 | 而且
1242 | 而况
1243 | 而外
1244 | 而已
1245 | 而是
1246 | 而言
1247 | 联系
1248 | 能
1249 | 能否
1250 | 能够
1251 | 腾
1252 | 自
1253 | 自个儿
1254 | 自从
1255 | 自各儿
1256 | 自家
1257 | 自己
1258 | 自身
1259 | 至
1260 | 至于
1261 | 良好
1262 | 若
1263 | 若是
1264 | 若非
1265 | 范围
1266 | 莫若
1267 | 获得
1268 | 虽
1269 | 虽则
1270 | 虽然
1271 | 虽说
1272 | 行为
1273 | 行动
1274 | 表明
1275 | 表示
1276 | 被
1277 | 要
1278 | 要不
1279 | 要不是
1280 | 要不然
1281 | 要么
1282 | 要是
1283 | 要求
1284 | 规定
1285 | 觉得
1286 | 认为
1287 | 认真
1288 | 认识
1289 | 让
1290 | 许多
1291 | 论
1292 | 设使
1293 | 设若
1294 | 该
1295 | 说明
1296 | 诸位
1297 | 谁
1298 | 谁知
1299 | 赶
1300 | 起
1301 | 起来
1302 | 起见
1303 | 趁
1304 | 趁着
1305 | 越是
1306 | 跟
1307 | 转动
1308 | 转变
1309 | 转贴
1310 | 较
1311 | 较之
1312 | 边
1313 | 达到
1314 | 迅速
1315 | 过
1316 | 过去
1317 | 过来
1318 | 运用
1319 | 还是
1320 | 还有
1321 | 这
1322 | 这个
1323 | 这么
1324 | 这么些
1325 | 这么样
1326 | 这么点儿
1327 | 这些
1328 | 这会儿
1329 | 这儿
1330 | 这就是说
1331 | 这时
1332 | 这样
1333 | 这点
1334 | 这种
1335 | 这边
1336 | 这里
1337 | 这麽
1338 | 进入
1339 | 进步
1340 | 进而
1341 | 进行
1342 | 连
1343 | 连同
1344 | 适应
1345 | 适当
1346 | 适用
1347 | 逐步
1348 | 逐渐
1349 | 通常
1350 | 通过
1351 | 造成
1352 | 遇到
1353 | 遭到
1354 | 避免
1355 | 那
1356 | 那个
1357 | 那么
1358 | 那么些
1359 | 那么样
1360 | 那些
1361 | 那会儿
1362 | 那儿
1363 | 那时
1364 | 那样
1365 | 那边
1366 | 那里
1367 | 那麽
1368 | 部分
1369 | 鄙人
1370 | 采取
1371 | 里面
1372 | 重大
1373 | 重新
1374 | 重要
1375 | 鉴于
1376 | 问题
1377 | 防止
1378 | 阿
1379 | 附近
1380 | 限制
1381 | 除
1382 | 除了
1383 | 除此之外
1384 | 除非
1385 | 随
1386 | 随着
1387 | 随著
1388 | 集中
1389 | 需要
1390 | 非但
1391 | 非常
1392 | 非徒
1393 | 靠
1394 | 顺
1395 | 顺着
1396 | 首先
1397 | 高兴
1398 | 是不是
1399 | 说说
1400 |
--------------------------------------------------------------------------------
/Server/test_es.py:
--------------------------------------------------------------------------------
1 | '''测试异步es是否运行正常'''
2 | import asyncio
3 | from elasticsearch_dsl.connections import connections
4 |
5 | from models.live import Live, SEARCH_FIELDS, init as live_init
6 |
7 |
8 | s = Live.search()
9 | es = connections.get_connection(Live._doc_type.using)
10 |
11 |
12 | async def print_info():
13 | rs = await s.query('multi_match', query='python',
14 | fields=SEARCH_FIELDS).execute()
15 | print(rs)
16 |
17 | loop = asyncio.get_event_loop()
18 | loop.run_until_complete(live_init())
19 | loop.run_until_complete(print_info())
20 | loop.close()
21 | es.transport.close()
22 |
--------------------------------------------------------------------------------
/Server/utils.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import hmac
3 | import time
4 |
5 | from config import APP_SECRET
6 |
7 |
8 | def gen_signature(data):
9 | data['timestamp'] = str(int(time.time()))
10 |
11 | params = ''.join([
12 | data['grant_type'],
13 | data['client_id'],
14 | data['source'],
15 | data['timestamp'],
16 | ])
17 |
18 | data['signature'] = hmac.new(
19 | APP_SECRET, params.encode('utf-8'), hashlib.sha1).hexdigest()
20 |
21 |
22 | def flatten_live_dict(d, keys=[]):
23 | def items():
24 | for key, value in d.items():
25 | if key in keys:
26 | yield key, value
27 | elif isinstance(value, dict):
28 | for subkey, subvalue in flatten_live_dict(value, keys).items():
29 | if subkey != 'id' and subkey in keys:
30 | yield subkey, subvalue
31 |
32 | return dict(items())
33 |
--------------------------------------------------------------------------------
/Server/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/Server/views/__init__.py
--------------------------------------------------------------------------------
/Server/views/api.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from sanic.exceptions import ServerError, NotFound
4 | from sanic import Blueprint
5 |
6 | from models import Live, User, session
7 | from config import SUGGEST_LIMIT
8 | from views.schemas import (
9 | LiveFullSchema, UserFullSchema, UserSchema, LiveSchema, TopicSchema)
10 | from views.utils import marshal_with, str2date
11 |
12 | bp = Blueprint('api', url_prefix='/api/v1')
13 |
14 | @bp.route('/search')
15 | @marshal_with([LiveSchema, UserSchema])
16 | async def search(request):
17 | q = request.args.get('q')
18 | status = request.args.get('status')
19 | rs = User.suggest(q, request.start, request.limit)
20 | if status is not None:
21 | status = status == 'ongoing'
22 | lives = await Live.ik_search(q, status, request.start, request.limit)
23 | rs.extend(lives)
24 | return rs
25 |
26 |
27 | @bp.route('/suggest')
28 | @marshal_with(LiveSchema)
29 | async def suggest(request):
30 | q = request.args.get('q')
31 | lives = await Live.ik_suggest(q, request.limit)
32 | return lives
33 |
34 |
35 | @bp.route('/explore')
36 | @marshal_with(LiveFullSchema)
37 | async def explore(request):
38 | from_ = str2date(request.args.get('from'))
39 | to = str2date(request.args.get('to'))
40 | order_by = request.args.get('order_by')
41 | lives = await Live.explore(from_, to, order_by, request.start,
42 | request.limit)
43 | return lives
44 |
45 |
46 | @bp.route('/live/')
47 | @marshal_with(LiveFullSchema)
48 | async def live(request, live_id):
49 | live = await Live.get(live_id)
50 | return live.to_dict()
51 |
52 |
53 | @bp.route('/hot_topics')
54 | @marshal_with(TopicSchema)
55 | async def topics(request):
56 | topics = await Live.get_hot_topics()
57 | return topics
58 |
59 |
60 | @bp.route('/topic')
61 | @marshal_with(LiveFullSchema)
62 | async def topic(request):
63 | from_ = str2date(request.args.get('from'))
64 | to = str2date(request.args.get('to'))
65 | order_by = request.args.get('order_by')
66 | topic_name = request.args.get('topic')
67 | lives = await Live.explore(from_, to, order_by, request.start,
68 | request.limit, topic_name)
69 | return lives
70 |
71 |
72 | @bp.route('/users')
73 | @marshal_with(UserFullSchema)
74 | async def users(request):
75 | order_by = request.args.get('order_by', 'id')
76 | desc = bool(request.args.get('desc', 0))
77 | users = User.get_all(order_by, request.start, request.limit, desc)
78 | return users
79 |
80 |
81 | @bp.route('/user/')
82 | @marshal_with([LiveFullSchema, UserFullSchema])
83 | async def user(request, user_id):
84 | user = session.query(User).get(user_id)
85 | rs = [user.to_dict()]
86 | lives = await Live.ik_search_by_speaker_id(user_id,
87 | order_by='-starts_at')
88 | rs.extend(lives)
89 | return rs
90 |
91 |
92 | @bp.route('/hot/weekly')
93 | @marshal_with(LiveSchema)
94 | async def hot_weekly(request):
95 | return await Live.get_hot_weekly()
96 |
97 |
98 | @bp.route('/hot/monthly')
99 | @marshal_with(LiveSchema)
100 | async def hot_monthly(request):
101 | return await Live.get_hot_monthly()
102 |
--------------------------------------------------------------------------------
/Server/views/protocol.py:
--------------------------------------------------------------------------------
1 | from sanic.server import HttpProtocol, CIMultiDict
2 | from sanic.request import Request as _Request
3 | from sanic.response import text, json
4 |
5 |
6 | class Request(_Request):
7 | __slots__ = (
8 | 'url', 'headers', 'version', 'method', '_cookies',
9 | 'query_string', 'body', 'start', 'limit',
10 | 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
11 | )
12 |
13 |
14 | class JSONHttpProtocol(HttpProtocol):
15 | def on_headers_complete(self):
16 | remote_addr = self.transport.get_extra_info('peername')
17 | if remote_addr:
18 | self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
19 |
20 | self.request = Request(
21 | url_bytes=self.url,
22 | headers=CIMultiDict(self.headers),
23 | version=self.parser.get_http_version(),
24 | method=self.parser.get_method().decode()
25 | )
26 |
27 | def write_response(self, response):
28 | if isinstance(response, str):
29 | response = text(response)
30 | elif isinstance(response, (list, dict)):
31 | response = {'rs': response}
32 | if isinstance(response, dict):
33 | response = json(response)
34 |
35 | return super().write_response(response)
36 |
--------------------------------------------------------------------------------
/Server/views/schemas.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from functools import partialmethod
3 | from marshmallow import Schema, fields
4 |
5 | from models import User, session
6 | from config import DOMAIN
7 | WIDTH = 45
8 |
9 |
10 | def gen_pic_url(path):
11 | if not path.startswith('/'):
12 | path = '/' + path
13 | return '{}{}'.format(DOMAIN, path)
14 |
15 |
16 | def truncate_utf8(str, width=WIDTH):
17 | return str[:width] + '...' if len(str) > width else str
18 |
19 |
20 | class Item(object):
21 | def truncate(self, attr, obj):
22 | if attr not in obj:
23 | return ''
24 | return truncate_utf8(obj[attr], WIDTH)
25 |
26 | def get_pic_url(self, attr, obj, default=None):
27 | return gen_pic_url(obj.get(attr, default))
28 |
29 |
30 | class UserSchema(Schema):
31 | id = fields.Integer()
32 | url = fields.Str()
33 | name = fields.Str()
34 | bio = fields.Method('truncate_bio')
35 | headline = fields.Method('truncate_headline')
36 | description = fields.Method('truncate_description')
37 | avatar_url = fields.Method('get_avatar_url')
38 | live_count = fields.Integer()
39 | type = fields.Str()
40 | truncate_headline = partialmethod(Item.truncate, 'headline')
41 | truncate_bio = partialmethod(Item.truncate, 'bio')
42 | truncate_description = partialmethod(Item.truncate, 'description')
43 | get_avatar_url = partialmethod(Item.get_pic_url, 'avatar_url')
44 |
45 |
46 | class UserFullSchema(UserSchema):
47 | lives_url = fields.Str()
48 | speaker_id = fields.Str()
49 | gender = fields.Integer()
50 | bio = fields.Str()
51 | description = fields.Str()
52 | headline = fields.Str()
53 | gender = fields.Integer()
54 |
55 |
56 | class LiveSchema(Schema):
57 | id = fields.Str()
58 | url = fields.Str()
59 | speaker = fields.Nested(UserSchema)
60 | subject = fields.Str()
61 | feedback_score = fields.Float()
62 | amount = fields.Float()
63 | seats_taken = fields.Integer()
64 | topics = fields.List(fields.String)
65 | cover = fields.Method('get_cover_url', allow_none=True)
66 | starts_at = fields.Method('get_start_time')
67 | outline = fields.Method('truncate_headline')
68 | description = fields.Method('truncate_description')
69 | liked_num = fields.Integer()
70 | type = fields.Str()
71 | truncate_headline = partialmethod(Item.truncate, 'headline')
72 | truncate_description = partialmethod(Item.truncate, 'description')
73 | get_cover_url = partialmethod(Item.get_pic_url, 'cover',
74 | default='/static/images/default-cover.png')
75 |
76 | def get_start_time(self, obj):
77 | return int(obj['starts_at'].strftime('%s'))
78 |
79 |
80 | class LiveFullSchema(LiveSchema):
81 | speaker = fields.Nested(UserFullSchema)
82 | status = fields.Boolean() # public(True)/ended(False)
83 | speaker_message_count = fields.Integer()
84 | tag_names = fields.Str()
85 | description = fields.Str()
86 | outline = fields.Str()
87 | zhuanlan_url = fields.Str(allow_none=True)
88 |
89 |
90 | class TopicSchema(Schema):
91 | id = fields.Integer()
92 | url = fields.Str()
93 | avatar_url = fields.Method('get_avatar_url')
94 | name = fields.Str()
95 | best_answerers_count = fields.Integer()
96 | best_answers_count = fields.Integer()
97 | questions_count = fields.Integer()
98 | followers_count = fields.Integer()
99 | get_avatar_url = partialmethod(Item.get_pic_url, 'avatar_url')
100 |
--------------------------------------------------------------------------------
/Server/views/utils.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from datetime import datetime
3 | from functools import wraps
4 | from collections import OrderedDict
5 |
6 |
7 | def marshal(data, fields):
8 | schemas = [field() for field in fields]
9 | if isinstance(data, (list, tuple)):
10 | return [marshal(d, fields) for d in data]
11 |
12 | type = data.get('type')
13 | for schema in schemas:
14 | if type in schema.__class__.__name__.lower():
15 | result, errors = schema.dump(data)
16 | if errors:
17 | for item in errors.items():
18 | print('{}: {}'.format(*item))
19 | return result
20 |
21 |
22 | class marshal_with(object):
23 | def __init__(self, fields):
24 | if not isinstance(fields, list):
25 | fields = [fields]
26 | self.fields = fields
27 |
28 | def __call__(self, f):
29 | @wraps(f)
30 | async def wrapper(*args, **kwargs):
31 | resp = await f(*args, **kwargs)
32 | return marshal(resp, self.fields)
33 | return wrapper
34 |
35 |
36 | def str2date(string):
37 | if string is None:
38 | return None
39 | return datetime.strptime(string, '%Y-%m-%d').date()
40 |
--------------------------------------------------------------------------------
/screenshot/zhihulive.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/screenshot/zhihulive.gif
--------------------------------------------------------------------------------
/screenshot/zhihulive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dongweiming/weapp-zhihulive/e1449f05deb61c06f7375a22354756e373f3e16d/screenshot/zhihulive.png
--------------------------------------------------------------------------------