├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── assets ├── qrcode.jpg ├── s-content.png └── s-index.png ├── config ├── dev.js ├── index.js └── prod.js ├── docs ├── CHANGELOG.md ├── DATA.md └── api-web.log.2019-12-09.log ├── global.d.ts ├── package-lock.json ├── package.json ├── project.config.json ├── src ├── app.scss ├── app.tsx ├── components │ ├── Example │ │ ├── index.scss │ │ └── index.tsx │ ├── FixedBtn │ │ ├── index.scss │ │ └── index.tsx │ └── GoTop │ │ ├── index.scss │ │ └── index.tsx ├── custom-theme.scss ├── index.html ├── pages │ ├── content │ │ ├── index.scss │ │ └── index.tsx │ ├── example │ │ ├── index.scss │ │ └── index.tsx │ ├── help │ │ ├── index.scss │ │ └── index.tsx │ └── index │ │ ├── components │ │ ├── EmptyList │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── ImportGroup │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── recommend.tsx │ │ ├── index.scss │ │ └── index.tsx ├── store │ └── counter.ts └── utils │ ├── index.tsx │ ├── logger.tsx │ └── platform.tsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["taro"], 3 | "rules": { 4 | "import/first": 0, 5 | "react/sort-comp": 0, 6 | // 使用ts来校验 7 | "no-unused-vars": 0, 8 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }] 9 | }, 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "useJSXTextNode": true, 16 | "project": "./tsconfig.json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | deploy_versions/ 3 | .temp/ 4 | .rn_temp/ 5 | node_modules/ 6 | .DS_Store 7 | yarn-error.log 8 | APPLY.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 豆组筛选 2 | 豆瓣小组爬虫微信小程序版本 3 | * 支持添加任意小组(一般就用在租房小组) 4 | * 多关键词置顶、过滤 5 | * 中介过滤 40%+ 6 | 7 | ![微信二维码](https://i.loli.net/2019/12/10/9TKYouELw8HR3Zk.jpg)
8 | 9 | 10 | 11 | 12 | ## 背景 13 | * 豆瓣自带搜索功能太简单了 14 | * 租房小组中介太多,没有自动过滤 15 | * 其它工具会限定在某些小组里面,无法自定义 16 | 17 | 18 | ## Todo 19 | * [x] 订阅小组管理 20 | * [x] 置顶关键词 21 | * [x] 屏蔽关键词 22 | * [x] pages/帖子详情页 23 | * [x] pages/帮助说明 24 | * [x] 刷新列表 25 | * [x] 加载下一页 26 | * [ ] 中介过滤规则 27 | * [x] 名字、发帖数、回帖数 28 | * [x] 发布者豆瓣资料分析 29 | * 注册时间 30 | * 常去小组 31 | * [ ] 缓存中介数据 32 | * [ ] 贝叶斯垃圾邮件 33 | * [ ] 测试用例 34 | * [x] 快速导入小组:根据城市导入常用小组 35 | * [ ] 根据定位获取周边小区信息 36 | * 过滤特殊符号 37 | * [ ] 标签选择导入置顶关键词、屏蔽关键词 38 | 39 | * [x] 小组id显示为小组名称 40 | * [ ] 订阅小组等Input组件交互优化 41 | * [ ] 收藏 42 | * [ ] 分享、客服按钮 43 | * [ ] [水木社区](http://www.newsmth.net/nForum/#!board/HouseRent) 44 | 45 | 46 | #### 其它 47 | * xxx 48 | 49 | 50 | ## 启动 51 | * [npm i -g @tarojs/cli@1.3.46](https://nervjs.github.io/taro/docs/1.x/GETTING-STARTED) 52 | * npm i 53 | * npm run dev:weapp 54 | * 微信开发者工具导入项目`./dist/weapp` 55 | 56 | 57 | ## 最后 58 | 本项目仅供学习使用,请勿商用,否则后果自负(狗头 59 | -------------------------------------------------------------------------------- /assets/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahui92/taro-douban-group-filter/d67ccad69a4ee4a93e74479c3e7aa7f33a0f2f05/assets/qrcode.jpg -------------------------------------------------------------------------------- /assets/s-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahui92/taro-douban-group-filter/d67ccad69a4ee4a93e74479c3e7aa7f33a0f2f05/assets/s-content.png -------------------------------------------------------------------------------- /assets/s-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahui92/taro-douban-group-filter/d67ccad69a4ee4a93e74479c3e7aa7f33a0f2f05/assets/s-index.png -------------------------------------------------------------------------------- /config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"' 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'douban-group-filter', 3 | date: '2019-9-5', 4 | designWidth: 750, 5 | deviceRatio: { 6 | '640': 2.34 / 2, 7 | '750': 1, 8 | '828': 1.81 / 2 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: `dist/${process.env.TARO_ENV}`, 12 | plugins: { 13 | babel: { 14 | sourceMap: true, 15 | presets: [ 16 | ['env', { 17 | modules: false 18 | }] 19 | ], 20 | plugins: [ 21 | 'transform-decorators-legacy', 22 | 'transform-class-properties', 23 | 'transform-object-rest-spread' 24 | ] 25 | } 26 | }, 27 | defineConstants: { 28 | }, 29 | copy: { 30 | patterns: [ 31 | ], 32 | options: { 33 | } 34 | }, 35 | weapp: { 36 | module: { 37 | postcss: { 38 | autoprefixer: { 39 | enable: true, 40 | config: { 41 | browsers: [ 42 | 'last 3 versions', 43 | 'Android >= 4.1', 44 | 'ios >= 8' 45 | ] 46 | } 47 | }, 48 | pxtransform: { 49 | enable: true, 50 | config: { 51 | 52 | } 53 | }, 54 | url: { 55 | enable: true, 56 | config: { 57 | limit: 10240 // 设定转换尺寸上限 58 | } 59 | }, 60 | cssModules: { 61 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 62 | config: { 63 | namingPattern: 'module', // 转换模式,取值为 global/module 64 | generateScopedName: '[name]__[local]___[hash:base64:5]' 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | h5: { 71 | publicPath: '/', 72 | staticDirectory: 'static', 73 | esnextModules: ['taro-ui'], 74 | module: { 75 | postcss: { 76 | autoprefixer: { 77 | enable: true, 78 | config: { 79 | browsers: [ 80 | 'last 3 versions', 81 | 'Android >= 4.1', 82 | 'ios >= 8' 83 | ] 84 | } 85 | }, 86 | cssModules: { 87 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 88 | config: { 89 | namingPattern: 'module', // 转换模式,取值为 global/module 90 | generateScopedName: '[name]__[local]___[hash:base64:5]' 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | module.exports = function (merge) { 99 | if (process.env.NODE_ENV === 'development') { 100 | return merge({}, config, require('./dev')) 101 | } 102 | return merge({}, config, require('./prod')) 103 | } 104 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: { 9 | /** 10 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。 11 | * 参考代码如下: 12 | * webpackChain (chain) { 13 | * chain.plugin('analyzer') 14 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) 15 | * } 16 | */ 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 12/12 12:00 2 | * 打包报错 3 | 4 | 5 | ### 12/10 17:00 6 | * 优化引导文案,解决`docs/api-web.log.2019-12-09.log`中的问题 7 | * 豆瓣资料分析 8 | * 注册时间 9 | * 加入小组 10 | 11 | 12 | ### 12/07 12:30 13 | * 一键导入小组:根据城市导入常用小组 14 | * 名称: (id)小组名称 15 | 16 | 17 | ### 11/26 13:40 18 | * 加载下一页 19 | * 数据打点 20 | * 过滤规则调整 21 | -------------------------------------------------------------------------------- /docs/DATA.md: -------------------------------------------------------------------------------- 1 | 2 | ## 数据 3 | 4 | ### 2019/12/01 5 | * 60%的人使用时间少于100s 6 | * 根据logs观察用户输入情况 7 | * 新增留存率10% 8 | * 留存率继续看有没新功能让用户留下来 9 | 10 | ### 2020/01/15 11 | * 存在一部分用户打开后不知道如何使用,优化文案和导入城市小组后这部分用户基本不存在了 12 | * 访问时长">100s"的用户,近7天68%,近30天76% 13 | * 从访问时长来看,确实拉长了用户的使用时间,但是最终也没有新增用户留存下来 14 | * 新增留存率基本没有,活跃留存率倒是有,不过不稳定,大部分在30%~50%之间 15 | * 说明自然搜索过来的新增用户并不是目标用户 或 使用了100s后仍然不知道怎么用 16 | * 数据缺失:新增用户的访问时长,目前的访问时长是包括了老用户的 17 | -------------------------------------------------------------------------------- /docs/api-web.log.2019-12-09.log: -------------------------------------------------------------------------------- 1 | !!!!!!!!!!!!!!!!!!!!!正常用户 2 | { 3 | 2019-12-09 18:01:55,258 INFO 32662 [-/127.0.0.1/-/1ms GET /logger/log?_event=userInputField&_userId=16ed5bc5bc8-41efa5396d6&_platform=WEAPP&_appName=douban-group-filter&groupList=beijingzufang%2Czhufang%2C26926&importantList=%E5%87%BA%E7%A7%9F&blackList=] { _event: 'userInputField', 4 | _userId: '16ed5bc5bc8-41efa5396d6', 5 | _platform: 'WEAPP', 6 | _appName: 'douban-group-filter', 7 | groupList: 'beijingzufang,zhufang,26926', 8 | importantList: '出租', 9 | blackList: '', 10 | _timestamp: 1575885715258, 11 | _timestr: 'Mon Dec 09 2019 18:01:55 GMT+0800 (China Standard Time)' } 12 | 2019-12-09 18:01:58,852 INFO 32662 [-/127.0.0.1/-/1ms GET /logger/log?_event=userInputField&_userId=16ed5bc5bc8-41efa5396d6&_platform=WEAPP&_appName=douban-group-filter&groupList=beijingzufang%2Czhufang%2C26926&importantList=&blackList=] { _event: 'userInputField', 13 | _userId: '16ed5bc5bc8-41efa5396d6', 14 | _platform: 'WEAPP', 15 | _appName: 'douban-group-filter', 16 | groupList: 'beijingzufang,zhufang,26926', 17 | importantList: '', 18 | blackList: '', 19 | _timestamp: 1575885718852, 20 | _timestr: 'Mon Dec 09 2019 18:01:58 GMT+0800 (China Standard Time)' } 21 | } 22 | 23 | 24 | !!!!!!!!!!!!!!!!!!!!!groupList的小组id输入了中文 25 | { 26 | 2019-12-09 02:47:39,238 INFO 32662 [-/127.0.0.1/-/0ms GET /logger/log?_event=userInputField&_userId=16ee6d64465-281662544ef&_platform=WEAPP&_appName=douban-group-filter&groupList=%E7%A7%9F%E6%88%BF%E5%AD%90%E4%BD%8F%E5%A4%A9%E6%B2%B3&importantList=&blackList=] { _event: 'userInputField', 27 | _userId: '16ee6d64465-281662544ef', 28 | _platform: 'WEAPP', 29 | _appName: 'douban-group-filter', 30 | groupList: '租房子住天河', 31 | importantList: '', 32 | blackList: '', 33 | _timestamp: 1575830859238, 34 | _timestr: 'Mon Dec 09 2019 02:47:39 GMT+0800 (China Standard Time)' } 35 | } 36 | 37 | 38 | 39 | 40 | 41 | !!!!!!!!!!!!!!!!!!!!!groupList的小组id输入了中文 42 | !!!!!!!!!!!!!!!!!!!!!importantList没有逗号分隔 43 | { 44 | 2019-12-09 13:59:44,989 INFO 32662 [-/127.0.0.1/-/1ms GET /logger/log?_event=userInputField&_userId=16ee93af020-5ab244eac19&_platform=WEAPP&_appName=douban-group-filter&groupList=%E8%B1%86%E7%93%A3%E7%A7%9F%E6%88%BF%2Cgz_rent%2Cgz020%2Ctianhezufang%2Cbeijingzufang%2Czhufang%2C26926&importantList=%E8%B6%8A%E7%A7%80%E5%8C%BA%E4%BD%8F%E6%88%BF%E4%B8%A4%E5%AE%A4%E4%B8%80%E5%8E%85%E6%95%B4%E7%A7%9F&blackList=] { _event: 'userInputField', 45 | _userId: '16ee93af020-5ab244eac19', 46 | _platform: 'WEAPP', 47 | _appName: 'douban-group-filter', 48 | groupList: 49 | '豆瓣租房,gz_rent,gz020,tianhezufang,beijingzufang,zhufang,26926', 50 | importantList: '越秀区住房两室一厅整租', 51 | blackList: '', 52 | _timestamp: 1575871184989, 53 | _timestr: 'Mon Dec 09 2019 13:59:44 GMT+0800 (China Standard Time)' } 54 | } 55 | 56 | 57 | 58 | 59 | 60 | !!!!!!!!!!!!!!!!!!!!!importantList没有逗号分隔 61 | { 62 | 2019-12-09 08:55:56,774 INFO 32662 [-/127.0.0.1/-/0ms GET /logger/log?_event=userInputField&_userId=16ee5b88f8c-aae35a5aa2&_platform=WEAPP&_appName=douban-group-filter&groupList=beijingzufang&importantList=%E4%BA%AE%E9%A9%AC%E6%A1%A5&blackList=] { _event: 'userInputField', 63 | _userId: '16ee5b88f8c-aae35a5aa2', 64 | _platform: 'WEAPP', 65 | _appName: 'douban-group-filter', 66 | groupList: 'beijingzufang', 67 | importantList: '亮马桥', 68 | blackList: '', 69 | _timestamp: 1575852956774, 70 | _timestr: 'Mon Dec 09 2019 08:55:56 GMT+0800 (China Standard Time)' } 71 | 2019-12-09 08:55:58,979 INFO 32662 [-/127.0.0.1/-/0ms GET /logger/log?_event=userInputField&_userId=16ee5b88f8c-aae35a5aa2&_platform=WEAPP&_appName=douban-group-filter&groupList=beijingzufang&importantList=&blackList=] { _event: 'userInputField', 72 | _userId: '16ee5b88f8c-aae35a5aa2', 73 | _platform: 'WEAPP', 74 | _appName: 'douban-group-filter', 75 | groupList: 'beijingzufang', 76 | importantList: '', 77 | blackList: '', 78 | _timestamp: 1575852958978, 79 | _timestr: 'Mon Dec 09 2019 08:55:58 GMT+0800 (China Standard Time)' } 80 | 2019-12-09 08:56:03,295 INFO 32662 [-/127.0.0.1/-/1ms GET /logger/log?_event=userInputField&_userId=16ee5b88f8c-aae35a5aa2&_platform=WEAPP&_appName=douban-group-filter&groupList=beijingzufang&importantList=%E4%B8%89%E5%85%83%E6%A1%A5&blackList=] { _event: 'userInputField', 81 | _userId: '16ee5b88f8c-aae35a5aa2', 82 | _platform: 'WEAPP', 83 | _appName: 'douban-group-filter', 84 | groupList: 'beijingzufang', 85 | importantList: '三元桥', 86 | blackList: '', 87 | _timestamp: 1575852963294, 88 | _timestr: 'Mon Dec 09 2019 08:56:03 GMT+0800 (China Standard Time)' } 89 | } 90 | 91 | 92 | 93 | 94 | !!!!!!!!!!!!!!!!!!!!!!!!!!什么也没输入就走了? 95 | { 96 | groupList: '豆瓣租房', 97 | importantList: '', 98 | blackList: '', 99 | _timestamp: 1575884407468, 100 | _timestr: 'Mon Dec 09 2019 17:40:07 GMT+0800 (China Standard Time)' } 101 | 2019-12-09 17:41:04,423 INFO 32662 [-/127.0.0.1/-/0ms GET /logger/log?_event=userInputField&_userId=16ed60cd570-6ce713ef519&_platform=WEAPP&_appName=douban-group-filter&groupList=&importantList=&blackList=] { _event: 'userInputField', 102 | _userId: '16ed60cd570-6ce713ef519', 103 | _platform: 'WEAPP', 104 | _appName: 'douban-group-filter', 105 | groupList: '', 106 | importantList: '', 107 | blackList: '', 108 | _timestamp: 1575884464423, 109 | _timestr: 'Mon Dec 09 2019 17:41:04 GMT+0800 (China Standard Time)' } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; 2 | declare module "*.gif"; 3 | declare module "*.jpg"; 4 | declare module "*.jpeg"; 5 | declare module "*.svg"; 6 | declare module "*.css"; 7 | declare module "*.less"; 8 | declare module "*.scss"; 9 | declare module "*.sass"; 10 | declare module "*.styl"; 11 | 12 | declare namespace JSX { 13 | interface IntrinsicElements { 14 | 'import': React.DetailedHTMLProps, HTMLEmbedElement> 15 | } 16 | } 17 | 18 | // @ts-ignore 19 | declare const process: { 20 | env: { 21 | TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt'; 22 | [key: string]: any; 23 | } 24 | } 25 | 26 | declare var wx: any; 27 | declare var my: any; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "douban-group-filter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "douban-group-filter", 6 | "templateInfo": { 7 | "name": "mobx", 8 | "typescript": true, 9 | "css": "scss" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "build:weapp": "taro build --type weapp", 14 | "build:swan": "taro build --type swan", 15 | "build:alipay": "taro build --type alipay", 16 | "build:tt": "taro build --type tt", 17 | "build:h5": "taro build --type h5", 18 | "build:rn": "taro build --type rn", 19 | "dev:weapp": "npm run build:weapp -- --watch", 20 | "dev:swan": "npm run build:swan -- --watch", 21 | "dev:alipay": "npm run build:alipay -- --watch", 22 | "dev:tt": "npm run build:tt -- --watch", 23 | "dev:h5": "npm run build:h5 -- --watch", 24 | "dev:rn": "npm run build:rn -- --watch" 25 | }, 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@jiahuix/mini-html-parser2": "^0.1.5", 30 | "@tarojs/components": "1.3.46", 31 | "@tarojs/mobx": "1.3.46", 32 | "@tarojs/mobx-h5": "1.3.46", 33 | "@tarojs/mobx-rn": "1.3.46", 34 | "@tarojs/rn-runner": "1.3.46", 35 | "@tarojs/router": "1.3.46", 36 | "@tarojs/taro": "1.3.46", 37 | "@tarojs/taro-alipay": "1.3.46", 38 | "@tarojs/taro-h5": "1.3.46", 39 | "@tarojs/taro-swan": "1.3.46", 40 | "@tarojs/taro-tt": "1.3.46", 41 | "@tarojs/taro-weapp": "1.3.46", 42 | "lodash": "4.17.13", 43 | "mobx": "4.8.0", 44 | "nerv-devtools": "^1.5.7", 45 | "nervjs": "^1.5.7", 46 | "node-html-parser": "^1.1.16", 47 | "taro-ui": "^2.2.2" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.6.0", 51 | "@tarojs/plugin-babel": "1.3.46", 52 | "@tarojs/plugin-csso": "1.3.46", 53 | "@tarojs/plugin-less": "1.3.46", 54 | "@tarojs/plugin-sass": "1.3.46", 55 | "@tarojs/plugin-uglifyjs": "1.3.46", 56 | "@tarojs/webpack-runner": "1.3.46", 57 | "@types/react": "16.3.14", 58 | "@types/webpack-env": "^1.13.6", 59 | "@typescript-eslint/parser": "^1.6.0", 60 | "babel-eslint": "^8.2.3", 61 | "babel-plugin-transform-class-properties": "^6.24.1", 62 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 63 | "babel-plugin-transform-jsx-stylesheet": "^0.6.5", 64 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 65 | "babel-preset-env": "^1.6.1", 66 | "eslint": "^5.16.0", 67 | "eslint-config-taro": "1.3.46", 68 | "eslint-plugin-import": "^2.12.0", 69 | "eslint-plugin-react": "^7.8.2", 70 | "eslint-plugin-react-hooks": "^1.6.1", 71 | "eslint-plugin-taro": "1.3.46", 72 | "stylelint": "9.3.0", 73 | "stylelint-config-taro-rn": "1.3.46", 74 | "stylelint-taro-rn": "1.3.46", 75 | "typescript": "^3.6.4" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "dist/", 3 | "projectname": "douban-group-filter", 4 | "description": "douban-group-filter", 5 | "appid": "wxdd21bb813678f478", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "enhance": true, 10 | "postcss": true, 11 | "minified": true, 12 | "newFeature": true, 13 | "coverView": true, 14 | "nodeModules": true, 15 | "autoAudits": false, 16 | "uglifyFileName": false, 17 | "checkInvalidKey": true, 18 | "checkSiteMap": true, 19 | "uploadWithSourceMap": true, 20 | "babelSetting": { 21 | "ignore": [], 22 | "disablePlugins": [], 23 | "outputPath": "" 24 | } 25 | }, 26 | "compileType": "miniprogram", 27 | "simulatorType": "wechat", 28 | "simulatorPluginLibVersion": {}, 29 | "libVersion": "2.2.3", 30 | "condition": { 31 | "search": { 32 | "current": -1, 33 | "list": [] 34 | }, 35 | "conversation": { 36 | "current": -1, 37 | "list": [] 38 | }, 39 | "plugin": { 40 | "current": -1, 41 | "list": [] 42 | }, 43 | "game": { 44 | "list": [] 45 | }, 46 | "miniprogram": { 47 | "current": 2, 48 | "list": [ 49 | { 50 | "id": -1, 51 | "name": "pages/content/index", 52 | "pathName": "pages/content/index", 53 | "query": "cId=151486456", 54 | "scene": null 55 | }, 56 | { 57 | "id": -1, 58 | "name": "pages/index/index", 59 | "pathName": "pages/index/index", 60 | "query": "", 61 | "scene": null 62 | }, 63 | { 64 | "id": -1, 65 | "name": "pages/help/index", 66 | "pathName": "pages/help/index", 67 | "scene": null 68 | } 69 | ] 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahui92/taro-douban-group-filter/d67ccad69a4ee4a93e74479c3e7aa7f33a0f2f05/src/app.scss -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import 'taro-ui/dist/style/index.scss' 2 | 3 | import Taro, { Component, Config } from '@tarojs/taro' 4 | // import { Provider } from '@tarojs/mobx' 5 | // import counterStore from './store/counter' 6 | import Index from './pages/index' 7 | import utils from './utils' 8 | 9 | 10 | // 如果需要在 h5 环境中开启 React Devtools 11 | // 取消以下注释: 12 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') { 13 | // require('nerv-devtools') 14 | // } 15 | 16 | // const store = { 17 | // counterStore 18 | // } 19 | 20 | class App extends Component { 21 | 22 | /** 23 | * 指定config的类型声明为: Taro.Config 24 | * 25 | * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型 26 | * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string 27 | * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型 28 | */ 29 | config: Config = { 30 | pages: [ 31 | 'pages/index/index', 32 | 'pages/content/index', 33 | 'pages/help/index', 34 | ], 35 | window: { 36 | backgroundTextStyle: 'light', 37 | navigationBarBackgroundColor: '#fff', 38 | navigationBarTitleText: 'WeChat', 39 | navigationBarTextStyle: 'black' 40 | } 41 | } 42 | 43 | componentDidMount () {} 44 | 45 | componentDidShow () {} 46 | 47 | componentDidHide () {} 48 | 49 | componentDidCatchError (errMsg) { 50 | utils.log('componentDidCatchError', { errMsg }) 51 | } 52 | 53 | // 在 App 类中的 render() 函数没有实际作用 54 | // 请勿修改此函数 55 | render () { 56 | return ( 57 | 58 | // 59 | // 60 | // 61 | ) 62 | } 63 | } 64 | 65 | Taro.render(, document.getElementById('app')) 66 | -------------------------------------------------------------------------------- /src/components/Example/index.scss: -------------------------------------------------------------------------------- 1 | .comp-example { 2 | } -------------------------------------------------------------------------------- /src/components/Example/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | 3 | import Taro, { Component } from '@tarojs/taro' 4 | import { View } from '@tarojs/components' 5 | 6 | interface Comp { 7 | props: { 8 | callback: Function, 9 | } 10 | } 11 | 12 | 13 | class Comp extends Component { 14 | 15 | constructor (props) { 16 | super(props) 17 | } 18 | 19 | state: any = { 20 | text: 'xxx' 21 | } 22 | 23 | componentDidMount () { 24 | } 25 | 26 | onClick = () => { 27 | } 28 | 29 | render () { 30 | const {text} = this.state 31 | 32 | return ( 33 | 34 | {text} 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default Comp 41 | -------------------------------------------------------------------------------- /src/components/FixedBtn/index.scss: -------------------------------------------------------------------------------- 1 | .comp-fixed-btn { 2 | position: fixed; 3 | opacity: 0.8; 4 | right: 30px; 5 | 6 | &.position-1 { 7 | bottom: 30px; 8 | } 9 | 10 | &.position-2 { 11 | bottom: 150px; 12 | } 13 | 14 | &.position-3 { 15 | bottom: 300px; 16 | } 17 | 18 | .text { 19 | font-size: 26px; 20 | margin: 20px; 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/FixedBtn/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | 3 | import Taro, { Component } from '@tarojs/taro' 4 | import { View } from '@tarojs/components' 5 | import { AtFab, AtIcon } from 'taro-ui' 6 | import { ReactElement } from 'react' 7 | 8 | interface Comp { 9 | props: { 10 | onClick: Function | any, 11 | index?: number, // 排第几个图标 12 | iconType?: string, // iconType和text二选一 13 | text?: ReactElement | 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 | --------------------------------------------------------------------------------