| string,
14 | }
15 | }
16 |
17 | class Comp extends Component {
18 |
19 | constructor (props) {
20 | super(props)
21 | }
22 |
23 | render () {
24 | const { index = 1, iconType = '', text } = this.props
25 |
26 | return (
27 |
28 |
29 | {
30 | text ?
31 | {text} :
32 |
33 | }
34 |
35 |
36 | )
37 | }
38 | }
39 |
40 | export default Comp
41 |
--------------------------------------------------------------------------------
/src/components/GoTop/index.scss:
--------------------------------------------------------------------------------
1 | .comp-go-top {
2 | }
--------------------------------------------------------------------------------
/src/components/GoTop/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component } from '@tarojs/taro'
4 | import { View } from '@tarojs/components';
5 | import FixedBtn from '../FixedBtn'
6 | import utils from '../../utils'
7 |
8 | class Comp extends Component {
9 |
10 | goTop = () => {
11 |
12 | utils.platform.pageScrollTo({
13 | scrollTop: 0
14 | })
15 | }
16 |
17 | render () {
18 | return (
19 |
20 |
21 |
22 | )
23 | }
24 | }
25 |
26 | export default Comp
27 |
--------------------------------------------------------------------------------
/src/custom-theme.scss:
--------------------------------------------------------------------------------
1 | /* Custom Theme */
2 | // $color-brand: #0ebd13;
3 | // $color-brand-light: #4ace4e;
4 | // $color-brand-dark: #0b970f;
5 | $color-brand: #6190e8;
6 | $color-brand-light: #89acee;
7 | $color-brand-dark: #4e73ba;
8 |
9 | /* 覆盖AtTabs组件样式 */
10 | .at-tabs {
11 | border-bottom: 4px solid $color-brand;
12 | // border-left: none;
13 | // border-right: none;
14 | }
15 |
16 | .at-tabs__item--active {
17 | color: white !important;
18 | background: $color-brand;
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/pages/content/index.scss:
--------------------------------------------------------------------------------
1 | .page-content {
2 | padding: 21px 21px 50px 21px;
3 |
4 | .portrait {
5 | border: 1px solid #eee;
6 | border-radius: 10px;
7 | padding: 20px 20px 20px 40px;
8 | margin: 20px 0 100px 0;
9 | font-size: 30px;
10 |
11 | .title {
12 | font-size: 35px;
13 | margin-left: -20px;
14 | margin-bottom: 10px;
15 | }
16 |
17 | .val {
18 | display: inline-block;
19 | margin-right: 30px;
20 | line-height: 50px;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/content/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component, Config } from '@tarojs/taro'
4 |
5 | import utils from '../../utils'
6 | // import parse from '@jiahuix/mini-html-parser2' // 兼容支付宝
7 |
8 | import { View, RichText } from '@tarojs/components'
9 | import { AtButton, AtIcon } from 'taro-ui'
10 | import GoTop from '../../components/GoTop'
11 |
12 |
13 | class Index extends Component {
14 |
15 | config: Config = {
16 | navigationBarTitleText: '豆组筛选',
17 | }
18 |
19 | state: any = {
20 | nodes: '',
21 | portraitData: {
22 | regTime: '正在获取...'
23 | },
24 | }
25 |
26 | componentDidMount () {
27 |
28 | Taro.showLoading({ title: '加载中' })
29 |
30 | utils.crawlToDom(`https://m.douban.com/group/topic/${this.$router.params.cId}/`).then(root => {
31 |
32 | Taro.hideLoading()
33 |
34 | const title = (root.querySelector('.header .title') || {}).text
35 | const authorName = (root.querySelector('.info .name') || {}).text
36 | const timeStr = (root.querySelector('.info .timestamp') || {}).text
37 | let contentStr = root.querySelector('#content').outerHTML
38 | // 评论内容
39 | let replyStr = ''
40 | root.querySelectorAll('#reply-list li').map(t => {
41 | replyStr += `${t.querySelector('.user-name').text}: ${t.querySelector('.content').text}
`
42 | })
43 |
44 | contentStr = `
45 | ${title}
46 |
47 | ${authorName} ${timeStr}
48 |
49 |
50 | ${contentStr}
51 |
52 | ${replyStr ? '
评论
' : ''}
53 | ${replyStr}
54 | `
55 |
56 | contentStr = contentStr.replace(/\
{
59 | // if (!err) {
60 | // this.setState({title, nodes})
61 | // }
62 | // })
63 | this.setState({title, nodes: contentStr})
64 |
65 | })
66 |
67 | this.getAuthorPortrait(this.$router.params.authorId)
68 | }
69 |
70 | // 获取发布者用户画像
71 | getAuthorPortrait = (authorId) => {
72 | if (!authorId) return
73 |
74 | Taro.request({
75 | url: `https://m.douban.com/rexxar/api/v2/user/${authorId}?ck=rbW0&for_mobile=1`
76 | }).then((res) => {
77 | const {portraitData} = this.state
78 | const d = res.data || {};
79 | Object.assign(portraitData, {
80 | regTime: d.reg_time.slice(0, 10),
81 | statusCount: d.statuses_count,
82 | bookCount: d.book_collected_count,
83 | movieCount: d.movie_collected_count
84 | })
85 | this.setState({portraitData})
86 | })
87 |
88 | utils.crawlToDom(`https://www.douban.com/group/people/${authorId}/joins`, false).then((root) => {
89 | const list = (root.querySelectorAll('.group-list li .info .title a') || [])
90 | const rentCount = list.filter((item) => item.text.indexOf('租房') !== -1).length
91 | const {portraitData} = this.state
92 | Object.assign(portraitData, {
93 | rentCount,
94 | joinedGroupCount: list.length
95 | })
96 | this.setState({portraitData})
97 | })
98 | }
99 |
100 | copyLink = () => {
101 | const cId = this.$router.params.cId
102 | const data = `https://m.douban.com/group/topic/${cId}/`
103 |
104 | utils.platform.setClipboardData(data, () => {
105 | utils.showToast('链接复制成功,请粘贴到浏览器打开')
106 | })
107 | }
108 |
109 | onHelp = () => {
110 | Taro.showModal({
111 | showCancel: false,
112 | confirmColor: '#4e73ba',
113 | content: '极可能是中介的情况:最近注册的账号、加入的小组大多是租房的、较少的广播和已看已读',
114 | })
115 | }
116 |
117 | render () {
118 | const {nodes} = this.state
119 | const {regTime, statusCount, bookCount, movieCount, rentCount, joinedGroupCount} = this.state.portraitData
120 |
121 | return (
122 |
123 |
124 | 使用浏览器打开查看完整内容
125 |
126 |
127 |
128 | 发布者信息
129 |
130 |
131 | 注册时间 {regTime}
132 |
133 |
134 | 加入小组 {joinedGroupCount}
135 | 租房小组 {rentCount}
136 |
137 |
138 | 广播 {statusCount}
139 | 已看 {movieCount}
140 | 已读 {bookCount}
141 |
142 | {/* 标记为中介(请求接口) */}
143 |
144 |
145 | {/* 支付宝显示不了图片? */}
146 |
147 |
148 |
149 |
150 |
151 | )
152 | }
153 | }
154 |
155 | export default Index
156 |
--------------------------------------------------------------------------------
/src/pages/example/index.scss:
--------------------------------------------------------------------------------
1 | .page-example {
2 | }
--------------------------------------------------------------------------------
/src/pages/example/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import { observer, inject } from '@tarojs/mobx'
4 | import Taro, { Component, Config } from '@tarojs/taro'
5 | import { View } from '@tarojs/components';
6 |
7 | @inject('counterStore')
8 | @observer
9 | class Index extends Component {
10 |
11 | config: Config = {
12 | navigationBarTitleText: '豆组筛选',
13 | }
14 |
15 | state: any = {
16 | text: 'xxx'
17 | }
18 |
19 | componentDidMount () {
20 | }
21 |
22 | render () {
23 | const {text} = this.state
24 |
25 | return (
26 |
27 | {text}
28 |
29 | )
30 | }
31 | }
32 |
33 | export default Index
34 |
--------------------------------------------------------------------------------
/src/pages/help/index.scss:
--------------------------------------------------------------------------------
1 | .page-help {
2 | .at-card {
3 | display: block;
4 | margin-bottom: 24px;
5 | }
6 | }
--------------------------------------------------------------------------------
/src/pages/help/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component, Config } from '@tarojs/taro'
4 | import { View } from '@tarojs/components';
5 | import { AtCard } from 'taro-ui';
6 |
7 | class Index extends Component {
8 |
9 | config: Config = {
10 | navigationBarTitleText: '使用说明',
11 | }
12 |
13 | componentDidMount () {
14 | }
15 |
16 | render () {
17 |
18 | return (
19 |
20 |
21 |
22 | ➣ 快速导入:点击输入框右侧的“导入”按钮
23 | ➣ 自定义:从豆瓣PC小组主页的url上获取,比如“北京租房”小组的PC主页为“https://www.douban.com/group/beijingzufang/”, 则小组id为“beijingzufang”,然后填入“订阅小组id”栏
24 |
25 |
26 |
27 | 地名、地铁线路、小区名、公司名、求租、合租、整租、室友、女(限女生)
28 |
29 |
30 |
31 | ➣ 名称包含“豆友xxx”、手机号等
32 | ➣ 发布两次及以上帖子
33 | ➣ 帖子回复数超过50
34 |
35 |
36 |
37 | 当操作太频繁时,可能会出现“request url”之类的报错,这时可以尝试切换一下网络(比如从wifi切换到4G),还不行的话休息一会再试吧
38 |
39 |
40 |
41 | )
42 | }
43 | }
44 |
45 | export default Index
46 |
--------------------------------------------------------------------------------
/src/pages/index/components/EmptyList/index.scss:
--------------------------------------------------------------------------------
1 | .comp-empty-list {
2 | text-align: center;
3 | margin-top: 10vh;
4 | color: #ddd;
5 | font-size: 30px;
6 | .big-icon {
7 | margin-bottom: 20px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/index/components/EmptyList/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component } from '@tarojs/taro'
4 | import { View } from '@tarojs/components'
5 | import { AtIcon } from 'taro-ui'
6 |
7 |
8 | class Comp extends Component {
9 | render () {
10 | return (
11 |
12 |
13 | 请先在上方输入“订阅小组id”
14 | 更多问题可查看右下方 “使用说明”
15 |
16 | )
17 | }
18 | }
19 |
20 | export default Comp
21 |
--------------------------------------------------------------------------------
/src/pages/index/components/ImportGroup/index.scss:
--------------------------------------------------------------------------------
1 | .comp-import-group {
2 | }
--------------------------------------------------------------------------------
/src/pages/index/components/ImportGroup/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component } from '@tarojs/taro'
4 | import { View } from '@tarojs/components'
5 | import localRecommendData from './recommend'
6 | import { AtIcon, AtActionSheet, AtActionSheetItem } from 'taro-ui'
7 |
8 | interface Comp {
9 | props: {
10 | callback: Function,
11 | }
12 | }
13 |
14 |
15 | class Comp extends Component {
16 |
17 | constructor (props) {
18 | super(props)
19 | }
20 |
21 | state: any = {
22 | isOpened: false,
23 | recommendData: []
24 | }
25 |
26 | componentDidMount () {
27 | }
28 |
29 | onBtnClick = () => {
30 | const {recommendData} = this.state
31 |
32 | const finallyFn = (data) => {
33 | this.setState({
34 | isOpened: true,
35 | recommendData: data && data.length ? data : localRecommendData,
36 | })
37 | }
38 |
39 | if (recommendData.length) {
40 | // 走缓存
41 | finallyFn(recommendData)
42 | } else {
43 | Taro.request({
44 | url: 'https://api.guangjun.club/doubanGroupFilter/getRecommendGroups'
45 | }).then(({data}) => {
46 | finallyFn(data)
47 | }).catch(finallyFn)
48 | }
49 | }
50 |
51 | onClose = () => {
52 | this.setState({ isOpened: false })
53 | }
54 |
55 | onItemClick = (data) => {
56 | this.onClose()
57 | this.props.callback(data)
58 | }
59 |
60 | render () {
61 | const {isOpened, recommendData} = this.state
62 |
63 | const list = recommendData.map((d) =>
64 | this.onItemClick(d.groups)}>{d.name} ({d.groups.length})
65 | )
66 |
67 | return (
68 |
69 |
70 |
71 |
72 | {list}
73 |
74 |
75 | )
76 | }
77 | }
78 |
79 | export default Comp
80 |
--------------------------------------------------------------------------------
/src/pages/index/components/ImportGroup/recommend.tsx:
--------------------------------------------------------------------------------
1 | export default [{
2 | name: '宝藏测试小组',
3 | groups: ['638298', 'blabla', 'insidestory', 'buybook', 'ShutFuckUp']
4 | }, {
5 | name: '北京',
6 | groups: ['beijingzufang', 'zhufang', '26926']
7 | }, {
8 | name: '上海',
9 | groups: ['shanghaizufang', 'pudongzufang', 'homeatshanghai']
10 | }, {
11 | name: '广州',
12 | groups: ['gz_rent', 'gz020', 'tianhezufang']
13 | }, {
14 | name: '深圳',
15 | groups: ['szsh', '106955', '637628', 'nanshanzufang', 'futianzufang']
16 | }, {
17 | name: '杭州',
18 | groups: ['HZhome', '145219', '467221']
19 | }, {
20 | name: '成都',
21 | groups: ['CDzufang', 'hezu', '343477']
22 | }]
23 |
--------------------------------------------------------------------------------
/src/pages/index/index.scss:
--------------------------------------------------------------------------------
1 | @import '../../custom-theme.scss';
2 |
3 | .page-index {
4 |
5 | .btn-refresh {
6 | display: inline-block;
7 | color: $color-brand-dark;
8 | text-decoration: underline;
9 | }
10 |
11 | .search-result-tip {
12 | text-align: right;
13 | font-size: 24px;
14 | color: grey;
15 | margin: 24px 30px 50px;
16 | }
17 |
18 | .list {
19 | min-height: 80vh;
20 |
21 | .item {
22 | padding: 13px 15px;
23 | border-bottom: 1px solid #eee;
24 |
25 | &.btn-next-page {
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | color: $color-brand-dark;
30 | font-size: 24px;
31 | height: 72px;
32 | }
33 |
34 | .extra-info {
35 | display: flex;
36 | align-items: center;
37 | font-size: 28px;
38 | color: #ccc;
39 |
40 | view {
41 | margin-right: 30px;
42 | }
43 |
44 | .is-agent {
45 | color: red;
46 | }
47 |
48 | .time {
49 | margin-left: auto;
50 | }
51 | }
52 | }
53 |
54 | .title {
55 | display: inline-block;
56 | font-size: 28px;
57 | white-space: nowrap;
58 | max-width: 100%;
59 | overflow: hidden;
60 | text-overflow: ellipsis;
61 | text-decoration: none;
62 |
63 | &.important {
64 | color: $color-brand-dark;
65 | font-weight: bold;
66 | }
67 |
68 | &.visited {
69 | color: #ccc;
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/pages/index/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.scss'
2 |
3 | import Taro, { Component, Config } from '@tarojs/taro'
4 | import utils from '../../utils'
5 | import lodash from 'lodash/core'
6 |
7 | import { View } from '@tarojs/components'
8 | import { AtIcon, AtInput, AtSwitch, AtTabs } from 'taro-ui'
9 | import FixedBtn from '../../components/FixedBtn'
10 | import GoTop from '../../components/GoTop'
11 | import ImportGroup from './components/ImportGroup'
12 | import EmptyList from './components/EmptyList'
13 |
14 | const MAX_PAGE = 10 // 一次加载页数
15 | const PAGE_SIZE = 25 // 每页item个数,该值不可调
16 | const gs = (k, defaultVal?) => Taro.getStorageSync(k) || defaultVal || []
17 | const tabsCache = gs('tabs')
18 | const groupMsg = gs('groupMsg', {})
19 | const cacheObj = {} // state.cache的对象版本,用于优化抓包,判断某些数据是否已经请求过了;以及记录翻页数据
20 |
21 |
22 | class Index extends Component {
23 |
24 | config: Config = {
25 | navigationBarTitleText: '豆组筛选',
26 | }
27 |
28 | state = {
29 | isLoading: false,
30 | activeTab: tabsCache[0] || '', // 当前选中的Tab
31 | tabs: tabsCache, // 小组的tabs数组
32 | cache: {}, // 缓存接口数据
33 | isShowAgent: false, // 是否显示中介信息
34 | importantList: gs('importantList'), // 置顶名单
35 | blackList: gs('blackList', ['关键词1', '关键词2', '关键词3']), // 黑名单
36 | visitedContentIdArr: gs('visitedContentIdArr'), // mock a:visited,记录访问过的a标签
37 | }
38 |
39 | componentDidMount () {
40 | this.fetchList()
41 | }
42 |
43 | // fetchType: prepend是获取最新数据并追加到列表前面,nextPage是翻下一页并追加到列表后面
44 | fetchList = (fetchType?:''|'prepend'|'nextPage') => {
45 | const {cache, activeTab} = this.state
46 | const isPrepend = fetchType == 'prepend' // 是否已经有缓存了,追加请求数据,比如点击刷新按钮时
47 | const list = cache[activeTab] || []
48 |
49 | if (!activeTab) return
50 |
51 | if (!cacheObj[activeTab]) cacheObj[activeTab] = { page: MAX_PAGE }
52 | const co = cacheObj[activeTab]
53 |
54 | // 翻页偏移值 依赖co.page来翻页
55 | let pageStart = 0
56 | if (fetchType == 'nextPage') {
57 | pageStart = PAGE_SIZE * co.page
58 | co.page += MAX_PAGE
59 | }
60 |
61 | const urlArr = Array(MAX_PAGE).fill('').map((_t, i) => {
62 | return `https://www.douban.com/group/${activeTab}/discussion?start=${i * PAGE_SIZE + pageStart}`
63 | })
64 |
65 | function showLoading(i = 0) {
66 | const p = pageStart / PAGE_SIZE
67 | Taro.showLoading({
68 | mask: true,
69 | title: isPrepend ? '加载中' : `加载 ${i + 1 + p}/${MAX_PAGE + p} 页`
70 | })
71 | }
72 |
73 | showLoading()
74 | utils.crawlToDomOnBatch(urlArr, (root, i, stop) => {
75 |
76 | // 记录小组名称
77 | if (!groupMsg[activeTab]) {
78 | groupMsg[activeTab] = {
79 | id: activeTab,
80 | name: root.querySelector('title').text.trim().replace(/小组$/, ''),
81 | }
82 | Taro.setStorage({ key: 'groupMsg', data: groupMsg })
83 | }
84 |
85 | const domList = root.querySelectorAll('table.olt tr').slice(1); // 获取table每一行
86 | domList.forEach(item => {
87 | const arr = item.querySelectorAll('a');
88 | const $title = arr[0]
89 | const $author = arr[1]
90 | const authorId = ($author.attributes.href.match(/(\w+)\/?$/) || [])[1] || ''
91 | const link = $title.attributes.href
92 | const contentId = link.match(/\d+/)[0]
93 | const timeStr = item.querySelector('.time').text
94 |
95 | const d = {
96 | contentId,
97 | timeStr,
98 | // link,
99 | title: $title.attributes.title,
100 | authorName: $author.text,
101 | authorId,
102 | // authorLink: $author.attributes.href,
103 | replyNum: Number(item.querySelectorAll('td')[2].text)
104 | }
105 |
106 | // 如果两者的timeStr都一样,表示这些数据都已经请求过了,不需要再list.push
107 | if (co[contentId]) {
108 | // isPrepend表示追加请求数据,如果此时追加到contentId和timeStr都一样的item,则表示可以结束抓包了(后续的item都是旧数据或者已经请求过的)
109 | if (isPrepend && co[contentId].timeStr === timeStr) {
110 | stop() // 提前结束抓包
111 | i = MAX_PAGE - 1
112 | }
113 | } else {
114 | co[contentId] = d
115 | list.push(d) // 随便push,后续会根据时间来排序的
116 | }
117 |
118 | })
119 |
120 | const isLoading = i < MAX_PAGE - 1
121 | showLoading(i)
122 | if (!isLoading) Taro.hideLoading()
123 |
124 | cache[activeTab] = list
125 | this.setState({
126 | cache,
127 | isLoading,
128 | })
129 |
130 | }, 1000)
131 | }
132 |
133 | // 根据importantList、blackList之类的配置重写计算list数组
134 | getList = () => {
135 | const { cache, activeTab, isShowAgent, importantList, blackList, visitedContentIdArr } = this.state
136 | let cList: any[] = [];
137 | const list = (cache[activeTab] || []);
138 | const countObj = {};
139 |
140 | // 发帖计数,用于辨识是否为中介
141 | list.forEach(item => {
142 | countObj[item.authorName] = (countObj[item.authorName] || 0) + 1;
143 | });
144 |
145 | list.forEach(item => {
146 | const fn = val => item.title.indexOf(val) !== -1
147 | // 黑名单过滤
148 | if (!blackList.some(fn)) {
149 | // 重点关注
150 | const isImportant = importantList.some(fn)
151 | const an = item.authorName
152 | const phoneTester = /1[3-9][0-9]{9}/
153 |
154 | // 是否“疑似中介”
155 | const isAgent =
156 | countObj[an] > 1 || // 看了N多条length==2的数据,发帖数大于1的99.99%不是中介就是管家之类的,尤其是连续发帖那种,直接简单粗暴判断了。。。
157 | item.replyNum > 50 || // 回帖数超过50(回帖太多人一般是中介自动顶帖,就算不是,那么多人问了没租出去,也表示已经有很多竞争者了,或者这房子不好)
158 | /(豆友\d+)|管家|租房|公寓|房屋|出租/.test(an) || // 名称包含“豆友xxx”等
159 | phoneTester.test(an) || // 名称包含手机号
160 | phoneTester.test(item.title) // 标题包含手机号
161 | const xcxLink = `/pages/content/index?cId=${item.contentId}&authorId=${item.authorId}`
162 | const clArr: string[] = []
163 | if (isImportant) clArr.push('important')
164 | if (visitedContentIdArr.indexOf(item.contentId) !== -1) clArr.push('visited')
165 |
166 | cList.push({
167 | ...item,
168 | xcxLink,
169 | isImportant,
170 | isAgent,
171 | className: clArr.join(' '),
172 | });
173 | }
174 | });
175 |
176 | // 过滤中介信息
177 | if (!isShowAgent) {
178 | cList = cList.filter(t => !t.isAgent);
179 | }
180 |
181 | // 重点关注的置顶
182 | lodash.sortBy(cList, 'timeStr')
183 | return lodash.sortBy(cList, (o) => o.isImportant ? 0 : 1)
184 | }
185 |
186 | // 每个filed都必须拥有自己的debounceFn,共用会有bug的,比如填完“置顶关键词”,在2s内再马上填“屏蔽关键词”,那么“置顶关键词”的onChange会被取消执行
187 | onChangeMap = {}
188 | // Input筛选组件的通用props
189 | getInputProps = (field) => {
190 | const onChangeMap = this.onChangeMap
191 | if (!onChangeMap[field]) {
192 | onChangeMap[field] = utils.debounce(this.onFieldChange.bind(this, field), 2000)
193 | }
194 |
195 | return {
196 | name: field,
197 | value: (this.state[field] || []).join(','),
198 | placeholder: '多个输入使用逗号分隔',
199 | maxLength: 10000, // 覆盖默认140
200 | onChange: onChangeMap[field],
201 | // onBlur: this.onFieldChange.bind(this, field)
202 | }
203 | }
204 |
205 | onFieldChange = (field, val) => {
206 | const {activeTab} = this.state
207 |
208 | // 分割成数组 、 替换掉前后空格 、 过滤空字符串
209 | val = val.split(/,|,/).map(s => s.trim()).filter(s => s)
210 |
211 | // 如果更新tabs之后,activeTab被删掉了,则重置为第一个值,并重新请求
212 | if (field === 'tabs' && val.indexOf(activeTab) === -1) {
213 | this.setState({ activeTab: val[0] }, this.fetchList)
214 | }
215 |
216 | this.setState({ [field]: val }, () => {
217 | const {tabs, importantList, blackList} = this.state
218 | // 数据打点
219 | utils.log('userInputField', {
220 | groupList: tabs.join(','),
221 | importantList: importantList.join(','),
222 | blackList: blackList.join(','),
223 | })
224 | })
225 | Taro.setStorage({key: field, data: val})
226 | }
227 |
228 | onTabClick = (i) => {
229 | const {tabs, cache} = this.state
230 | const activeTab = tabs[i]
231 | this.setState({activeTab}, () => {
232 | // tabClick(tabClick有两种可能,刚开始加载和prepend)
233 | // cache[activeTab] ? this.fetchList( ? 'prepend' : '')
234 | if (!cache[activeTab]) {
235 | this.fetchList()
236 | }
237 | })
238 | }
239 |
240 | onNavigatorClick = (t) => {
241 | Taro.navigateTo({url: t.xcxLink})
242 |
243 | const data = this.state.visitedContentIdArr
244 | data.push(t.contentId)
245 |
246 | // 最多只缓存1000个id
247 | if (data.length > 1000) data.shift()
248 | Taro.setStorage({data, key: 'visitedContentIdArr'})
249 | this.setState({visitedContentIdArr: data})
250 | }
251 |
252 | onImportGroup = (data = []) => {
253 | const {tabs} = this.state
254 | const arr = []
255 |
256 | data.forEach(d => {
257 | if (tabs.indexOf(d) === -1) {
258 | arr.push(d)
259 | }
260 | })
261 |
262 | const c = data.length - arr.length
263 | const text = c ? `已存在${c}个,` : ''
264 | utils.showToast(`${text}成功导入${arr.length}个小组`)
265 |
266 | // bugfix: 延时2s执行,正常情况下,点击importBtn后,失焦会触发debound.onFieldChange;极端情况下点击importBtn后,在2s内马上选好导入小组,会使onImportGroup.onFieldChange的执行顺序先于debound.onFieldChange,所以这里setTimeout(2000ms)来保证先执行debound.onFieldChange,再执行onImportGroup.onFieldChange
267 | setTimeout(() => {
268 | this.onFieldChange('tabs', tabs.concat(arr).join(','))
269 | }, 2000)
270 | }
271 |
272 | render () {
273 | const { tabs, activeTab } = this.state
274 |
275 | const list = this.getList()
276 | let mid = list.findIndex(t => !t.isImportant)
277 | mid = mid == -1 ? 0 : mid
278 | list.splice(mid, 0, { isBtnNextPage: true })
279 |
280 | // 帖子列表html
281 | const listHtml = list.length > 1 ? list.map(t => (
282 | t.isBtnNextPage ?
283 | this.fetchList('nextPage')}>
284 |
285 | 加载下一页
286 | :
287 |
288 | { t.title }
289 |
290 | {t.authorName}
291 | {
292 | t.isAgent ? (
293 |
294 |
295 | 疑似中介
296 |
297 | ) : null
298 | }
299 | {t.timeStr}
300 |
301 |
302 | )) :
303 |
304 | const len = list.length - 1 // 减掉的一个是“下一页”按钮
305 | const searchTipHtml = len > 0 ? (
306 |
307 | this.fetchList('prepend')}>刷新列表
308 | ,共有 {len} 个搜索结果
309 |
310 | ) : ''
311 |
312 | const tabList = tabs.map(id => {
313 | const t = groupMsg[id] || {} // 查看是否已经存储了groupName
314 | return {title: t.name ? `[${id}] ${t.name}` : id}
315 | })
316 |
317 | return (
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 | this.setState({isShowAgent})} />
327 | {searchTipHtml}
328 |
329 |
330 |
336 |
337 | {listHtml}
338 |
339 | Taro.navigateTo({url:'/pages/help/index'})} />
340 |
341 |
342 |
343 | )
344 | }
345 | }
346 |
347 | export default Index
348 |
--------------------------------------------------------------------------------
/src/store/counter.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx'
2 |
3 | const counterStore = observable({
4 | counter: 0,
5 | counterStore() {
6 | this.counter++
7 | },
8 | increment() {
9 | this.counter++
10 | },
11 | decrement() {
12 | this.counter--
13 | },
14 | incrementAsync() {
15 | setTimeout(() => {
16 | this.counter++
17 | }, 1000)
18 | }
19 | })
20 | export default counterStore
--------------------------------------------------------------------------------
/src/utils/index.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 | // import { parse, HTMLElement } from 'node-html-parser/dist/umd/index.js'
3 | import { parse, HTMLElement } from 'node-html-parser' // 这个最终没有被uglify压缩,因为不支持压缩es6,待后续taro更换压缩器后即可修复
4 | import {platform} from './platform'
5 | import {log} from './logger'
6 |
7 | /**
8 | * 包含错误兜底和提示的Taro.request函数
9 | */
10 | function request (param: Taro.request.Param, isShowError?) {
11 |
12 | // 兜底错误处理
13 | function fail (e) {
14 | Taro.hideLoading()
15 | if (isShowError) {
16 | // 对用户抛出友好一点的错误提示,而不是e.message
17 | Taro.showToast({ icon: 'none', mask: true, title: '操作可能太频繁,请稍后重试或尝试切换网络到4G/Wifi' })
18 | }
19 | throw new Error(e.message || e.errMsg) // errMsg是Taro抛出来的
20 | }
21 |
22 | return Taro.request(param).then(res => {
23 | if (res.statusCode !== 200) {
24 | throw new Error(`请求发生错误(statusCode为${res.statusCode})`)
25 | }
26 | return res
27 | }).catch(e => fail(e))
28 | }
29 |
30 | /**
31 | * 爬虫并返回解析后的dom
32 | * @param url
33 | */
34 | export function crawlToDom (url: string, isShowError = true) {
35 | return request({
36 | url,
37 | header: {
38 | 'content-type': 'text/html'
39 | }
40 | }, isShowError).then((res: any) => {
41 | return parse(res.data) as HTMLElement
42 | })
43 | }
44 |
45 | /**
46 | * 批量爬虫
47 | * @param urlArr 请求url的数组
48 | * @param callback 每爬取一次都会回调一次接口
49 | * @param delay 默认间隔1000ms爬取一次
50 | */
51 | export function crawlToDomOnBatch (urlArr: string[] = [], callback: Function = () => {}, delay: number = 1000) {
52 | let i = 0
53 | let count = 0 // 因为网络延迟,接口不一定会按顺序完成请求,甚至有时候是并行的,所以不能够返回i给callback,而是依赖count来计算进度
54 | let timer
55 |
56 | const fn = () => {
57 | if (i >= urlArr.length) {
58 | clearInterval(timer)
59 | return
60 | }
61 |
62 | crawlToDom(urlArr[i++])
63 | .then(root => callback(root, count++, () => clearInterval(timer)))
64 | .catch((e) => {
65 | clearInterval(timer)
66 | throw Error(e)
67 | })
68 |
69 | }
70 |
71 | fn() // 先自执行一遍
72 | timer = setInterval(fn, delay)
73 | }
74 |
75 | export const debounce = (callback, delay) => {
76 | let timer;
77 | return (...arg) => {
78 | clearTimeout(timer);
79 | timer = setTimeout(() => {
80 | callback(...arg)
81 | }, delay);
82 | }
83 | }
84 |
85 | export const showToast = (title: string, config?: Object) => {
86 | Taro.showToast({
87 | title,
88 | icon: 'none',
89 | duration: 3000,
90 | ...(config || {})
91 | })
92 | }
93 |
94 |
95 |
96 |
97 | export default {
98 | request, crawlToDom, crawlToDomOnBatch,
99 | debounce, showToast,
100 | platform, log
101 | }
102 |
--------------------------------------------------------------------------------
/src/utils/logger.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 | import { platform } from './platform'
3 |
4 | // 如果以后登录了,可以使用xcx.getUserInfo
5 | let _userId = Taro.getStorageSync('_userId')
6 | if (!_userId) {
7 | _userId = new Date().getTime().toString(16) + '-' + Number((Math.pow(10, 13) * Math.random()).toFixed(0)).toString(16)
8 |
9 | Taro.setStorage({
10 | key: '_userId',
11 | data: _userId
12 | })
13 | }
14 |
15 | // 打点
16 | export function log (_event = '', data = {}) {
17 | return Taro.request({
18 | url: 'https://api.guangjun.club/logger/log',
19 | data: {
20 | _event,
21 | _userId,
22 | // _timestamp: new Date().getTime(),
23 | _platform: platform.name,
24 | _appName: 'douban-group-filter',
25 | ...data,
26 | }
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/platform.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro'
2 | export let platform
3 |
4 | // 在这里抹平掉所有平台的api调用
5 | let plat
6 | const TYPE = Taro.ENV_TYPE
7 | switch (Taro.getEnv()) {
8 | case TYPE.WEAPP: {
9 | plat = wx
10 | break
11 | }
12 | case TYPE.ALIPAY: {
13 | plat = my
14 | break
15 | }
16 | default: {
17 | plat = window
18 | }
19 | }
20 |
21 | platform = {
22 | ...plat,
23 | name: Taro.getEnv(),
24 | }
25 |
26 | platform.setClipboardData = (text, success) => {
27 | const fn = plat.setClipboardData || plat.setClipboard // 支付宝是setClipboard
28 | fn({
29 | text,
30 | data: text,
31 | success
32 | })
33 | }
34 |
35 | export default { platform }
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "commonjs",
5 | "removeComments": false,
6 | "preserveConstEnums": true,
7 | "moduleResolution": "node",
8 | "experimentalDecorators": true,
9 | "noImplicitAny": false,
10 | "allowSyntheticDefaultImports": true,
11 | "outDir": "lib",
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "strictNullChecks": true,
15 | "sourceMap": true,
16 | "baseUrl": ".",
17 | "rootDir": ".",
18 | "jsx": "preserve",
19 | "jsxFactory": "Taro.createElement",
20 | "allowJs": true,
21 | "resolveJsonModule": true,
22 | "typeRoots": [
23 | "node_modules/@types",
24 | "global.d.ts"
25 | ]
26 | },
27 | "exclude": [
28 | "node_modules",
29 | "dist"
30 | ],
31 | "compileOnSave": false
32 | }
33 |
--------------------------------------------------------------------------------