├── .editorconfig
├── .eslintrc
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
├── dev.js
├── index.js
└── prod.js
├── global.d.ts
├── package.json
├── project.config.json
├── resources
├── Home.jpg
├── Section.gif
├── Share.jpg
├── Thread.gif
└── qrcode.jpg
├── src
├── actions
│ ├── account.ts
│ └── system.ts
├── app.scss
├── app.tsx
├── assets
│ └── images
│ │ └── tab
│ │ ├── home.png
│ │ ├── home_selected.png
│ │ ├── hot.png
│ │ ├── hot_selected.png
│ │ ├── new.png
│ │ ├── new_selected.png
│ │ ├── profile.png
│ │ ├── profile_selected.png
│ │ ├── section.png
│ │ └── section_selected.png
├── components
│ ├── ParserRichText
│ │ ├── Parser
│ │ │ ├── CssTokenizer.js
│ │ │ ├── DomHandler.js
│ │ │ ├── Parser.js
│ │ │ ├── Tokenizer.js
│ │ │ ├── api.js
│ │ │ ├── index.js
│ │ │ ├── index.json
│ │ │ ├── index.wxml
│ │ │ ├── index.wxss
│ │ │ └── trees
│ │ │ │ ├── cssHandler.wxs
│ │ │ │ ├── trees.js
│ │ │ │ ├── trees.json
│ │ │ │ ├── trees.wxml
│ │ │ │ └── trees.wxss
│ │ ├── parserRichText.scss
│ │ └── parserRichText.tsx
│ ├── ReplyCard
│ │ ├── replyCard.scss
│ │ └── replyCard.tsx
│ ├── SectionGroupList
│ │ ├── assets
│ │ │ ├── f127.png
│ │ │ ├── f129.png
│ │ │ ├── f140.png
│ │ │ ├── f148.png
│ │ │ ├── f161.png
│ │ │ ├── f189.png
│ │ │ ├── f197.png
│ │ │ ├── f200.png
│ │ │ ├── f201.png
│ │ │ ├── f232.png
│ │ │ ├── f234.png
│ │ │ ├── f235.png
│ │ │ ├── f238.png
│ │ │ ├── f244.png
│ │ │ ├── f245.png
│ │ │ ├── f246.png
│ │ │ ├── f248.png
│ │ │ ├── f251.png
│ │ │ ├── f254.png
│ │ │ ├── f257.png
│ │ │ ├── f259.png
│ │ │ ├── f271.png
│ │ │ ├── f273.png
│ │ │ ├── f274.png
│ │ │ ├── f275.png
│ │ │ ├── f276.png
│ │ │ ├── f277.png
│ │ │ ├── f291.png
│ │ │ ├── f299.png
│ │ │ ├── f301.png
│ │ │ ├── f302.png
│ │ │ ├── f303.png
│ │ │ ├── f304.png
│ │ │ ├── f305.png
│ │ │ ├── f311.png
│ │ │ ├── f312.png
│ │ │ ├── f316.png
│ │ │ ├── f318.png
│ │ │ ├── f319.png
│ │ │ ├── f322.png
│ │ │ ├── f325.png
│ │ │ ├── f326.png
│ │ │ ├── f328.png
│ │ │ ├── f330.png
│ │ │ ├── f332.png
│ │ │ └── f335.png
│ │ ├── sectionGroupList.scss
│ │ └── sectionGroupList.tsx
│ └── ThreadCard
│ │ ├── threadCard.scss
│ │ └── threadCard.tsx
├── constants
│ ├── account.ts
│ └── system.ts
├── index.html
├── interfaces
│ ├── account.d.ts
│ ├── respond.d.ts
│ └── thread.d.ts
├── pages
│ ├── account
│ │ ├── about.scss
│ │ ├── about.tsx
│ │ ├── account.scss
│ │ ├── account.tsx
│ │ ├── assets
│ │ │ └── empty_avatar_user.png
│ │ ├── history.scss
│ │ ├── history.tsx
│ │ ├── login.scss
│ │ ├── login.tsx
│ │ ├── setting.scss
│ │ └── setting.tsx
│ ├── hot
│ │ ├── hot.scss
│ │ └── hot.tsx
│ ├── index
│ │ ├── index.scss
│ │ └── index.tsx
│ ├── new
│ │ ├── new.scss
│ │ └── new.tsx
│ ├── section
│ │ ├── section.scss
│ │ ├── section.tsx
│ │ ├── sectionThreadList.scss
│ │ └── sectionThreadList.tsx
│ └── thread
│ │ ├── thread.scss
│ │ └── thread.tsx
├── reducers
│ ├── account.ts
│ ├── index.ts
│ └── system.ts
├── store
│ └── index.ts
└── utils
│ └── cleaner.ts
├── tsconfig.json
└── yarn.lock
/.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": [
3 | "taro",
4 | "plugin:@typescript-eslint/recommended",
5 | "prettier",
6 | "prettier/@typescript-eslint"
7 | ],
8 | "parser": "@typescript-eslint/parser",
9 | "plugins": ["@typescript-eslint"],
10 | "rules": {
11 | "no-unused-vars": ["error", { "varsIgnorePattern": "Taro" }],
12 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }],
13 | },
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "useJSXTextNode": true,
19 | "project": "./tsconfig.json"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | .temp/
3 | .rn_temp/
4 | node_modules/
5 | .DS_Store
6 | yarn-error.log
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v0.2.6 (2019-7-13,低调发布)
2 |
3 | ### 添加功能
4 |
5 | - 在主题详情页右下角添加 FAB 分享按钮,可以更方便地将帖子分享给微信好友/群组
6 | - 使用自定义导航栏,使状态栏沉浸,浏览更多内容
7 |
8 | ### Bug 修复
9 |
10 | - 修复无法清理历史记录及登出的问题
11 |
12 | ### 程序改进
13 |
14 | - 使用更高效的 [Parser](https://github.com/jin-yufeng/Parser) 作为 HTML 富文本显示组件
15 | - 帖子主题及回复内容可长按选择复制(由新的富文本显示组件支持)
16 | - Taro 升级至 1.3.8
17 |
18 | ## v0.2.5 (2019-7-10,未发布)
19 |
20 | ### 程序改进
21 |
22 | - 重构并清理代码
23 | - 改进 dayjs 初始化过程,#49
24 | - Taro 升级至 1.3.6,Taro-UI 升级至 2.2.1
25 |
26 | ## v0.2.4 (2019-5-19)
27 |
28 | ### Bug 修复
29 |
30 | - 登录时一直提示输入用户名,[NervJS/taro-ui#583](https://github.com/NervJS/taro-ui/issues/583)
31 | - 固定登录页中的 Footer
32 |
33 | ### 程序改进
34 |
35 | - 登出按钮移至设置中
36 | - 在程序进行网络请求,需要等待时,添加 Loading 提示
37 | - 减小打包体积
38 |
39 | ## v0.2.3 (2019-5-18)
40 |
41 | ### Bug 修复
42 |
43 | - 修复无安全问题时,因 `must be a number` 无法登录问题
44 |
45 | ## v0.2.2 (2019-05-18)
46 |
47 | ### 添加功能
48 |
49 | - 添加用户凭据过期处理
50 |
51 | ### 程序改进
52 |
53 | - 美化帖子回复卡片
54 | - 添加板块帖子列表下拉刷新时的加载提示
55 | - 历史记录去除重复查看的帖子
56 | - 历史记录分段加载,提升渲染长列表性能
57 |
58 | ## v0.2.1 (2019-05-17)
59 |
60 | ### 添加功能
61 |
62 | - 添加登录安全问题
63 |
64 | ### Bug 修复
65 |
66 | - (于 v0.2.4 中修复)登录时一直提示输入用户名
67 |
68 | # v0.2.0 (2019-05-16)
69 |
70 | ### 添加功能
71 |
72 | - 使用论坛 App 的 API 获取帖子列表及帖子内容等数据
73 | - 添加登录页面及登录、登出功能,现在登陆后可以查看具有权限的内容,如交易中心板块、有阅读权限的帖子等
74 | - 在帖子列表中添加发帖时间
75 | - 使用 [dayjs](https://github.com/iamkun/dayjs) 显示更人性化的时间,如“3 分钟前”、“5 小时前”、“2 天前”等
76 | - 添加加载更多回复时候的加载动画
77 | - 添加已经加载完所有回复的提示
78 | - 在板块的帖子列表中添加加载更多动画
79 | - 直接显示 `spoiler` 的折叠内容
80 |
81 | ### Bug 修复
82 |
83 | - 添加不可访问板块/帖子的提示
84 | - 获取板块帖子列表及查看帖子内容时,添加已登录用户的 token 信息,以查看具有权限的板块及帖子内容
85 | - 使用接口中的 `pic` 来作为首页轮播图片的图片 URL 来源,`coverPath` 字段有时候会提供多余的信息
86 | - 去除 Steam Widget 产生的空白
87 | - 替换接口中默认的 `none.png` 图片为真实图片
88 | - 恢复板块列表中,以前未登录无法访问的板块,并更新板块描述
89 | - 修复登陆后仍然显示默认头像的问题
90 |
91 | ### 程序改进
92 |
93 | - 使用微信云存储功能存放部分小程序图片,大大减小小程序体积,加快小程序下载、启动和加载速度
94 | - 使用 Redux 管理已登录用户的凭据
95 |
96 | # v0.1.0 (2019-04-09)
97 |
98 | ### 添加功能:
99 |
100 | - 使用 [Taro](https://github.com/NervJS/taro) 小程序框架代替小程序原生开发
101 | - 使用 [Taro-ui](https://github.com/NervJS/taro-ui) 组件库配合 Taro 进行界面开发
102 | - 使用 [node-html-parser](https://github.com/taoqf/node-html-parser) 解析 HTML 字符串为 DOM 对象
103 | - 使用 wxParse 作为富文本显示组件
104 | - 添加首页轮播图片
105 | - 添加首页最新主题页
106 | - 添加最新回复主题页
107 | - 添加近期热门主题页
108 | - 添加论坛板块列表页
109 | - 添加个人中心页
110 | - 记录个人浏览记录
111 | - 设置中可清除浏览记录
112 | - 添加最新主题页、最新回复页、近期热门页下拉刷新
113 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Cloud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SteamCN 蒸汽动力论坛 微信小程序
2 |
3 | 这是 SteamCN 蒸汽动力论坛微信小程序。您可以使用本小程序查看 SteamCN 论坛上的帖子。当然前提是您有能正常使用的微信~
4 |
5 | ## 小程序功能
6 |
7 | - 浏览 SteamCN 论坛中帖子的内容及坛友回复
8 | - 登录论坛账号,访问有阅读权限的帖子
9 | - 查看最新有价值的帖子
10 | - 查看近期最热门的帖子
11 | - 查看最新回复的帖子
12 | - 分板块查阅帖子
13 | - 记录小程序中的看帖历史
14 |
15 | 更多开发计划见:https://github.com/xPixv/SteamCN-Mini-Program/projects/1
16 |
17 | ## 现在就扫码体验吧~
18 |
19 | 
20 |
21 | ## 部分截图展示
22 |
23 | ### 主页
24 |
25 | 
26 |
27 | ### 查看帖子
28 |
29 | 
30 |
31 | ### 板块查看
32 |
33 | 
34 |
35 | ### 微信分享
36 |
37 | 
38 |
39 | ## 更新日志
40 |
41 | 见 CHANGELOG:https://github.com/xPixv/SteamCN-Mini-Program/blob/master/CHANGELOG.md
42 |
43 | ## 反馈建议
44 |
45 | 在 issue 中进行反馈:https://github.com/xPixv/SteamCN-Mini-Program/issues
46 |
47 | ## 开发步骤
48 |
49 | 开发环境:
50 | - [Node.js](https://nodejs.org) (>=8.0.0)
51 | - [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
52 |
53 | 1. Clone 仓库 `master` 分支到本地
54 | 2. 安装项目,执行 `npm install` 或者 `yarn install` (推荐使用 yarn),等待安装完成
55 | 3. 进入项目目录,运行 `npm run dev:weapp` 或者 `yarn dev:weapp`,等待 Taro 编译项目为微信小程序
56 | 4. 使用微信开发者工具,打开项目目录下的 `dist` 文件夹即可预览和调试
57 |
58 | ## 开源许可
59 |
60 | 本小程序使用 [MIT](https://github.com/xPixv/SteamCN-Mini-Program/blob/master/LICENSE) 许可发布源代码
61 |
62 | ## Open Source Credit ❤
63 |
64 | - [Taro](https://github.com/NervJS/taro) —— MIT
65 | - [Taro UI](https://github.com/NervJS/taro-ui) —— MIT
66 | - [Parser](https://github.com/jin-yufeng/Parser) —— Unlicensed
67 | - [dayjs](https://github.com/iamkun/dayjs) —— MIT
68 | - [node-html-parser](https://github.com/taoqf/node-html-parser) —— Unlicensed
69 | - [SteamCN 论坛](https://steamcn.com)及 SteamCN 论坛 App 资源
70 |
--------------------------------------------------------------------------------
/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: 'SteamCN-Mini-Program',
3 | date: '2019-1-24',
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',
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 | { from: 'src/components/SectionGroupList/assets/', to: 'dist/components/SectionGroupList/assets/' },
32 | { from: 'src/components/ParserRichText/Parser/', to: 'dist/components/ParserRichText/Parser/' }
33 | ],
34 | options: {
35 | }
36 | },
37 | weapp: {
38 | module: {
39 | postcss: {
40 | autoprefixer: {
41 | enable: true,
42 | config: {
43 | browsers: [
44 | 'last 3 versions',
45 | 'Android >= 4.1',
46 | 'ios >= 8'
47 | ]
48 | }
49 | },
50 | pxtransform: {
51 | enable: true,
52 | config: {
53 |
54 | }
55 | },
56 | url: {
57 | enable: true,
58 | config: {
59 | limit: 10240 // 设定转换尺寸上限
60 | }
61 | },
62 | cssModules: {
63 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
64 | config: {
65 | namingPattern: 'module', // 转换模式,取值为 global/module
66 | generateScopedName: '[name]__[local]___[hash:base64:5]'
67 | }
68 | }
69 | }
70 | }
71 | },
72 | h5: {
73 | publicPath: '/',
74 | staticDirectory: 'static',
75 | module: {
76 | postcss: {
77 | autoprefixer: {
78 | enable: true,
79 | config: {
80 | browsers: [
81 | 'last 3 versions',
82 | 'Android >= 4.1',
83 | 'ios >= 8'
84 | ]
85 | }
86 | },
87 | cssModules: {
88 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
89 | config: {
90 | namingPattern: 'module', // 转换模式,取值为 global/module
91 | generateScopedName: '[name]__[local]___[hash:base64:5]'
92 | }
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | module.exports = function (merge) {
100 | if (process.env.NODE_ENV === 'development') {
101 | return merge({}, config, require('./dev'))
102 | }
103 | return merge({}, config, require('./prod'))
104 | }
105 |
--------------------------------------------------------------------------------
/config/prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | NODE_ENV: '"production"'
4 | },
5 | defineConstants: {
6 | },
7 | weapp: {},
8 | h5: {}
9 | }
10 |
--------------------------------------------------------------------------------
/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 | // @ts-ignore
13 | declare const process: {
14 | env: {
15 | TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq';
16 | [key: string]: any;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "steamcn-mini-program",
3 | "version": "0.2.6",
4 | "private": true,
5 | "description": "SteamCN Forum Official Mini Program",
6 | "templateInfo": {
7 | "name": "redux",
8 | "typescript": true,
9 | "css": "sass"
10 | },
11 | "scripts": {
12 | "build:weapp": "taro build --type weapp",
13 | "build:swan": "taro build --type swan",
14 | "build:alipay": "taro build --type alipay",
15 | "build:tt": "taro build --type tt",
16 | "build:h5": "taro build --type h5",
17 | "build:rn": "taro build --type rn",
18 | "build:qq": "taro build --type qq",
19 | "build:quickapp": "taro build --type quickapp",
20 | "dev:weapp": "npm run build:weapp -- --watch",
21 | "dev:swan": "npm run build:swan -- --watch",
22 | "dev:alipay": "npm run build:alipay -- --watch",
23 | "dev:tt": "npm run build:tt -- --watch",
24 | "dev:h5": "npm run build:h5 -- --watch",
25 | "dev:rn": "npm run build:rn -- --watch",
26 | "dev:qq": "npm run build:qq -- --watch",
27 | "dev:quickapp": "npm run build:quickapp -- --watch",
28 | "update": "taro update project",
29 | "doctor": "taro doctor"
30 | },
31 | "author": "Cloud",
32 | "license": "MIT",
33 | "dependencies": {
34 | "@tarojs/async-await": "1.3.18",
35 | "@tarojs/components": "1.3.18",
36 | "@tarojs/redux": "1.3.18",
37 | "@tarojs/redux-h5": "1.3.18",
38 | "@tarojs/router": "1.3.18",
39 | "@tarojs/taro": "1.3.18",
40 | "@tarojs/taro-alipay": "1.3.18",
41 | "@tarojs/taro-h5": "1.3.18",
42 | "@tarojs/taro-qq": "1.3.18",
43 | "@tarojs/taro-quickapp": "1.3.18",
44 | "@tarojs/taro-swan": "1.3.18",
45 | "@tarojs/taro-tt": "1.3.18",
46 | "@tarojs/taro-weapp": "1.3.18",
47 | "dayjs": "^1.8.16",
48 | "nerv-devtools": "^1.4.4",
49 | "nervjs": "^1.4.4",
50 | "node-html-parser": "^1.1.16",
51 | "redux": "^4.0.4",
52 | "redux-logger": "^3.0.6",
53 | "redux-thunk": "^2.3.0",
54 | "taro-ui": "^2.2.2"
55 | },
56 | "devDependencies": {
57 | "@tarojs/cli": "1.3.18",
58 | "@tarojs/plugin-babel": "1.3.18",
59 | "@tarojs/plugin-csso": "1.3.18",
60 | "@tarojs/plugin-sass": "1.3.18",
61 | "@tarojs/plugin-uglifyjs": "1.3.18",
62 | "@tarojs/webpack-runner": "1.3.18",
63 | "@types/react": "^16.9.2",
64 | "@types/webpack-env": "^1.14.0",
65 | "@typescript-eslint/eslint-plugin": "^2.2.0",
66 | "@typescript-eslint/parser": "^2.2.0",
67 | "babel-eslint": "^10.0.3",
68 | "babel-plugin-transform-class-properties": "^6.24.1",
69 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
70 | "babel-plugin-transform-jsx-stylesheet": "^0.6.8",
71 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
72 | "babel-preset-env": "^1.7.0",
73 | "eslint": "^6.4.0",
74 | "eslint-config-prettier": "^6.3.0",
75 | "eslint-config-taro": "1.3.18",
76 | "eslint-plugin-import": "^2.18.2",
77 | "eslint-plugin-react": "^7.14.3",
78 | "eslint-plugin-react-hooks": "^2.0.1",
79 | "eslint-plugin-taro": "1.3.18",
80 | "typescript": "^3.6.3"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "miniprogramRoot": "./dist",
3 | "projectname": "SteamCN-Mini-Program",
4 | "description": "SteamCN Forum Official Mini Program",
5 | "appid": "wx55a5a0eab124806d",
6 | "setting": {
7 | "urlCheck": true,
8 | "es6": false,
9 | "postcss": false,
10 | "minified": false
11 | },
12 | "compileType": "miniprogram"
13 | }
14 |
--------------------------------------------------------------------------------
/resources/Home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/resources/Home.jpg
--------------------------------------------------------------------------------
/resources/Section.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/resources/Section.gif
--------------------------------------------------------------------------------
/resources/Share.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/resources/Share.jpg
--------------------------------------------------------------------------------
/resources/Thread.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/resources/Thread.gif
--------------------------------------------------------------------------------
/resources/qrcode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/resources/qrcode.jpg
--------------------------------------------------------------------------------
/src/actions/account.ts:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import {
3 | INIT_CREDENTIAL,
4 | INVALID_CREDENTIAL,
5 | LOGIN,
6 | LOGIN_SUCCESS,
7 | LOGIN_ERROR,
8 | LOGOUT,
9 | LOGOUT_SUCCESS,
10 | LOGOUT_ERROR
11 | } from '../constants/account';
12 | import { IAccount } from '../interfaces/account';
13 |
14 | export const initCredential = () => {
15 | return dispatch => {
16 | Taro.getStorage({
17 | key: 'auth'
18 | }).then(
19 | res => {
20 | const auth = res.data;
21 | if (auth) {
22 | Taro.getStorage({
23 | key: 'account'
24 | }).then(res => {
25 | const account = res.data;
26 | dispatch({
27 | type: INIT_CREDENTIAL,
28 | payload: {
29 | auth: true,
30 | account
31 | }
32 | });
33 | });
34 | } else {
35 | dispatch({
36 | type: INIT_CREDENTIAL,
37 | payload: {
38 | auth: false,
39 | account: {
40 | uid: 0,
41 | username: '',
42 | email: '',
43 | avatar: '',
44 | groupid: 0,
45 | createdAt: '',
46 | UpdatedAt: '',
47 | accessToken: ''
48 | }
49 | }
50 | });
51 | }
52 | },
53 | () => {
54 | Taro.setStorageSync('auth', false);
55 | dispatch({
56 | type: INIT_CREDENTIAL,
57 | payload: {
58 | auth: false,
59 | account: {
60 | uid: 0,
61 | username: '',
62 | email: '',
63 | avatar: '',
64 | groupid: 0,
65 | createdAt: '',
66 | UpdatedAt: '',
67 | accessToken: ''
68 | }
69 | }
70 | });
71 | }
72 | );
73 | };
74 | };
75 |
76 | export const invalidCredential = (): { type: string } => {
77 | return {
78 | type: INVALID_CREDENTIAL
79 | };
80 | };
81 |
82 | export const login = (): { type: string } => {
83 | return {
84 | type: LOGIN
85 | };
86 | };
87 |
88 | export const loginSuccess = (
89 | account: IAccount
90 | ): { type: string; payload?: { auth: boolean; account: IAccount } } => {
91 | Taro.setStorage({
92 | key: 'auth',
93 | data: true
94 | });
95 | Taro.setStorage({
96 | key: 'account',
97 | data: account
98 | });
99 | return {
100 | type: LOGIN_SUCCESS,
101 | payload: {
102 | auth: true,
103 | account
104 | }
105 | };
106 | };
107 |
108 | export const loginError = (): { type: string } => {
109 | return {
110 | type: LOGIN_ERROR
111 | };
112 | };
113 |
114 | export const logout = (): { type: string } => {
115 | return {
116 | type: LOGOUT
117 | };
118 | };
119 |
120 | export const logoutSuccess = (): { type: string } => {
121 | Taro.setStorage({
122 | key: 'auth',
123 | data: false
124 | });
125 | Taro.removeStorage({
126 | key: 'account'
127 | });
128 | return {
129 | type: LOGOUT_SUCCESS
130 | };
131 | };
132 |
133 | export const logoutError = (): { type: string } => {
134 | return {
135 | type: LOGOUT_ERROR
136 | };
137 | };
138 |
--------------------------------------------------------------------------------
/src/actions/system.ts:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { GET_SYSTEM_INFO } from '../constants/system';
3 |
4 | export interface SystemInfo {
5 | statusBarHeight: number;
6 | menuButtonBoundingClientRect: Taro.getMenuButtonBoundingClientRect.Return;
7 | }
8 |
9 | // eslint-disable-next-line import/prefer-default-export
10 | export const getSystemInfo = (): {
11 | type: string;
12 | payload: {
13 | statusBarHeight: number;
14 | menuButtonBoundingClientRect: Taro.getMenuButtonBoundingClientRect.Return;
15 | };
16 | } => {
17 | const statusBarHeight = Taro.getSystemInfoSync().statusBarHeight;
18 | const menuButtonBoundingClientRect = Taro.getMenuButtonBoundingClientRect();
19 | return {
20 | type: GET_SYSTEM_INFO,
21 | payload: { statusBarHeight, menuButtonBoundingClientRect }
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | /* Custom Theme */
2 | $color-brand: #57bae8;
3 | $color-brand-light: #81cbee;
4 | $color-brand-dark: #4695ba;
5 |
6 | /* Default Theme*/
7 | @import '~taro-ui/dist/style/index.scss';
8 |
9 | ::-webkit-scrollbar {
10 | display: none;
11 | }
12 |
13 | page {
14 | -webkit-font-smoothing: antialiased;
15 | font-family: 'PingHei', 'Helvetica Neue', 'Helvetica', 'Arial', 'Verdana',
16 | 'sans-serif';
17 | }
18 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import '@tarojs/async-await';
2 | import Taro, { Component, Config } from '@tarojs/taro';
3 | import { Provider } from '@tarojs/redux';
4 | import dayjs from 'dayjs';
5 | import 'dayjs/locale/zh-cn';
6 | import relativeTime from 'dayjs/plugin/relativeTime';
7 |
8 | import Index from './pages/index';
9 |
10 | import configStore from './store';
11 |
12 | import './app.scss';
13 |
14 | // 如果需要在 h5 环境中开启 React Devtools
15 | // 取消以下注释:
16 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') {
17 | // require('nerv-devtools')
18 | // }
19 |
20 | const store = configStore();
21 |
22 | class App extends Component {
23 | /**
24 | * 指定config的类型声明为: Taro.Config
25 | *
26 | * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
27 | * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
28 | * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
29 | */
30 | public config: Config = {
31 | window: {
32 | navigationStyle: 'custom',
33 | backgroundTextStyle: 'dark',
34 | navigationBarBackgroundColor: '#57bae8',
35 | navigationBarTitleText: 'SteamCN 蒸汽动力',
36 | navigationBarTextStyle: 'white'
37 | },
38 | pages: [
39 | 'pages/index/index',
40 | 'pages/new/new',
41 | 'pages/hot/hot',
42 | 'pages/section/section',
43 | 'pages/section/sectionThreadList',
44 | 'pages/account/account',
45 | 'pages/account/about',
46 | 'pages/account/setting',
47 | 'pages/account/history',
48 | 'pages/account/login',
49 | 'pages/thread/thread'
50 | ],
51 | tabBar: {
52 | custom: false,
53 | color: '#abb4bf',
54 | selectedColor: '#57bae8',
55 | borderStyle: 'white',
56 | backgroundColor: '#fff',
57 | list: [
58 | {
59 | pagePath: 'pages/index/index',
60 | text: '首页',
61 | iconPath: './assets/images/tab/home.png',
62 | selectedIconPath: './assets/images/tab/home_selected.png'
63 | },
64 | {
65 | pagePath: 'pages/new/new',
66 | text: '最新',
67 | iconPath: './assets/images/tab/new.png',
68 | selectedIconPath: './assets/images/tab/new_selected.png'
69 | },
70 | {
71 | pagePath: 'pages/hot/hot',
72 | text: '热门',
73 | iconPath: './assets/images/tab/hot.png',
74 | selectedIconPath: './assets/images/tab/hot_selected.png'
75 | },
76 | {
77 | pagePath: 'pages/section/section',
78 | text: '板块',
79 | iconPath: './assets/images/tab/section.png',
80 | selectedIconPath: './assets/images/tab/section_selected.png'
81 | },
82 | {
83 | pagePath: 'pages/account/account',
84 | text: '我的',
85 | iconPath: './assets/images/tab/profile.png',
86 | selectedIconPath: './assets/images/tab/profile_selected.png'
87 | }
88 | ]
89 | }
90 | };
91 |
92 | public componentDidMount(): void {
93 | dayjs.locale('zh-cn');
94 | dayjs.extend(relativeTime);
95 | this.updateApp();
96 | }
97 |
98 | /*更新小程序*/
99 | private updateApp(): void {
100 | if (Taro.canIUse('getUpdateManager')) {
101 | const updateManager = Taro.getUpdateManager();
102 |
103 | updateManager.onCheckForUpdate((res): void => {
104 | // 请求完新版本信息的回调
105 | console.log('是否有新版本:', res.hasUpdate);
106 | });
107 |
108 | updateManager.onUpdateReady((): void => {
109 | Taro.showModal({
110 | title: '更新提示',
111 | content: '新版本已经准备好,是否重启小程序?',
112 | success(res): void {
113 | if (res.confirm) {
114 | updateManager.applyUpdate();
115 | }
116 | }
117 | });
118 | });
119 |
120 | updateManager.onUpdateFailed((): void => {
121 | console.error('App Update Failed!');
122 | });
123 | }
124 | }
125 |
126 | // 在 App 类中的 render() 函数没有实际作用
127 | // 请勿修改此函数
128 | public render(): JSX.Element {
129 | return (
130 |
131 |
132 |
133 | );
134 | }
135 | }
136 |
137 | Taro.render(, document.getElementById('app'));
138 |
--------------------------------------------------------------------------------
/src/assets/images/tab/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/home.png
--------------------------------------------------------------------------------
/src/assets/images/tab/home_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/home_selected.png
--------------------------------------------------------------------------------
/src/assets/images/tab/hot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/hot.png
--------------------------------------------------------------------------------
/src/assets/images/tab/hot_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/hot_selected.png
--------------------------------------------------------------------------------
/src/assets/images/tab/new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/new.png
--------------------------------------------------------------------------------
/src/assets/images/tab/new_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/new_selected.png
--------------------------------------------------------------------------------
/src/assets/images/tab/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/profile.png
--------------------------------------------------------------------------------
/src/assets/images/tab/profile_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/profile_selected.png
--------------------------------------------------------------------------------
/src/assets/images/tab/section.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/section.png
--------------------------------------------------------------------------------
/src/assets/images/tab/section_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/assets/images/tab/section_selected.png
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/CssTokenizer.js:
--------------------------------------------------------------------------------
1 | //CssTokenizer.js
2 | function CssTokenizer(style = '', tagStyle = {}) {
3 | this.res = JSON.parse(JSON.stringify(tagStyle));
4 | this._state = "SPACE";
5 | this._buffer = style;
6 | this._sectionStart = 0;
7 | this._index = 0;
8 | this._name = '';
9 | this._content = '';
10 | this._list = [];
11 | this._comma = false;
12 | }
13 | CssTokenizer.prototype.SPACE = function(c) {
14 | if (/[a-zA-Z.#]/.test(c)) {
15 | this._sectionStart = this._index;
16 | this._state = "InName";
17 | } else if (c == '@') this._state = "Ignore1";
18 | else if (c == '/') this._state = "BeforeComment";
19 | };
20 | CssTokenizer.prototype.BeforeComment = function(c) {
21 | if (c == '*') this._state = "InComment";
22 | else {
23 | this._index--;
24 | this._state = "SPACE";
25 | }
26 | };
27 | CssTokenizer.prototype.InComment = function(c) {
28 | if (c == '*') this._state = "AfterComment";
29 | };
30 | CssTokenizer.prototype.AfterComment = function(c) {
31 | if (c == '/') this._state = "SPACE";
32 | else {
33 | this._index--;
34 | this._state = "InComment"
35 | }
36 | };
37 | CssTokenizer.prototype.InName = function(c) {
38 | if (c == '{') {
39 | this._list.push(this._buffer.substring(this._sectionStart, this._index))
40 | this._sectionStart = this._index + 1;
41 | this._state = "InContent";
42 | } else if (c == ',') {
43 | this._list.push(this._buffer.substring(this._sectionStart, this._index));
44 | this._sectionStart = this._index + 1;
45 | this._comma = true;
46 | } else if ((c == '.' || c == '#') && !this._comma) {
47 | this._buffer = this._buffer.splice(this._index, 1, ' ');
48 | } else if (/\s/.test(c)) {
49 | this._name = this._buffer.substring(this._sectionStart, this._index);
50 | this._state = "NameSpace";
51 | } else if (/[>:\[]/.test(c)) {
52 | if (this._list.length) this._state = "IgnoreName";
53 | else this._state = "Ignore1";
54 | } else this._comma = false;
55 | };
56 | CssTokenizer.prototype.NameSpace = function(c) {
57 | if (c == '{') {
58 | this._list.push(this._name);
59 | this._sectionStart = this._index + 1;
60 | this._state = "InContent";
61 | } else if (c == ',') {
62 | this._comma = true;
63 | this._list.push(this._name);
64 | this._sectionStart = this._index + 1;
65 | this._state = "InName"
66 | } else if (/\S/.test(c)) {
67 | if (this._comma) {
68 | this._sectionStart = this._index;
69 | this._index--;
70 | this._state = "InName";
71 | } else if (this._list.length) this._state = "IgnoreName";
72 | else this._state = "Ignore1"
73 | }
74 | };
75 | CssTokenizer.prototype.InContent = function(c) {
76 | if (c == '}') {
77 | this._content = this._buffer.substring(this._sectionStart, this._index);
78 | for (let item of this._list)
79 | this.res[item] = (this.res[item] || '') + this._content;
80 | this._list = [];
81 | this._comma = false;
82 | this._state = "SPACE";
83 | }
84 | };
85 | CssTokenizer.prototype.IgnoreName = function(c) {
86 | if (c == ',') {
87 | this._sectionStart = this._index + 1;
88 | this._state = "InName";
89 | } else if (c == '{') {
90 | this._sectionStart = this._index + 1;
91 | this._state = "InContent";
92 | }
93 | }
94 | CssTokenizer.prototype.Ignore1 = function(c) {
95 | if (c == ';') {
96 | this._state = "SPACE";
97 | this._sectionStart = this._index + 1;
98 | } else if (c == '{') this._state = "Ignore2";
99 | };
100 | CssTokenizer.prototype.Ignore2 = function(c) {
101 | if (c == '}') {
102 | this._state = "SPACE";
103 | this._sectionStart = this._index + 1;
104 | } else if (c == '{') this._state = "Ignore3";
105 | };
106 | CssTokenizer.prototype.Ignore3 = function(c) {
107 | if (c == '}') this._state = "Ignore2";
108 | };
109 | CssTokenizer.prototype.parse = function() {
110 | for (; this._index < this._buffer.length; this._index++)
111 | this[this._state](this._buffer[this._index]);
112 | return this.res;
113 | };
114 | module.exports = CssTokenizer;
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/DomHandler.js:
--------------------------------------------------------------------------------
1 | //DomHandler.js
2 | const CssTokenizer = require('./CssTokenizer.js');
3 | const CanIUse = require('./api.js').versionHigherThan('2.7.1');
4 | const trustTag = {
5 | a: 0,
6 | abbr: 1,
7 | ad: 0,
8 | audio: 0,
9 | b: 1,
10 | blockquote: 1,
11 | br: 0,
12 | code: 1,
13 | col: 0,
14 | colgroup: 0,
15 | dd: 1,
16 | del: 1,
17 | dl: 1,
18 | dt: 1,
19 | div: 1,
20 | em: 1,
21 | fieldset: 0,
22 | font: 1,
23 | h1: 0,
24 | h2: 0,
25 | h3: 0,
26 | h4: 0,
27 | h5: 0,
28 | h6: 0,
29 | hr: 0,
30 | i: 1,
31 | img: 1,
32 | ins: 1,
33 | label: 1,
34 | legend: 0,
35 | li: 0,
36 | ol: 0,
37 | p: 1,
38 | q: 1,
39 | source: 0,
40 | span: 1,
41 | strong: 1,
42 | sub: 0,
43 | sup: 0,
44 | table: 0,
45 | tbody: 0,
46 | td: 0,
47 | tfoot: 0,
48 | th: 0,
49 | thead: 0,
50 | tr: 0,
51 | u: 1,
52 | ul: 0,
53 | video: 1
54 | };
55 | const blockTag = {
56 | address: true,
57 | article: true,
58 | aside: true,
59 | body: true,
60 | center: true,
61 | cite: true,
62 | footer: true,
63 | header: true,
64 | html: true,
65 | nav: true,
66 | pre: true,
67 | section: true
68 | }
69 | const textTag = {
70 | a: true,
71 | abbr: true,
72 | b: true,
73 | big: true,
74 | code: true,
75 | del: true,
76 | em: true,
77 | font: true,
78 | i: true,
79 | ins: true,
80 | label: true,
81 | mark: true,
82 | q: true,
83 | s: true,
84 | small: true,
85 | span: true,
86 | strong: true,
87 | u: true
88 | };
89 | const ignoreTag = {
90 | area: true,
91 | base: true,
92 | basefont: true,
93 | canvas: true,
94 | circle: true,
95 | command: true,
96 | ellipse: true,
97 | embed: true,
98 | frame: true,
99 | head: true,
100 | iframe: true,
101 | input: true,
102 | isindex: true,
103 | keygen: true,
104 | line: true,
105 | link: true,
106 | map: true,
107 | meta: true,
108 | param: true,
109 | path: true,
110 | polygon: true,
111 | polyline: true,
112 | rect: true,
113 | script: true,
114 | stop: true,
115 | textarea: true,
116 | title: true,
117 | track: true,
118 | use: true,
119 | wbr: true
120 | };
121 | if (CanIUse) {
122 | trustTag.bdi = 0;
123 | trustTag.bdo = 0;
124 | trustTag.caption = 0;
125 | trustTag.rt = 0;
126 | trustTag.ruby = 0;
127 | ignoreTag.rp = true;
128 | trustTag.big = 1;
129 | trustTag.small = 1;
130 | trustTag.pre = 0;
131 | delete blockTag.pre;
132 | }
133 | //添加默认值
134 | function initStyle(tagStyle) {
135 | tagStyle.a = "display:inline;color:#366092;word-break:break-all;" + (tagStyle.a || "");
136 | tagStyle.address = "font-style:italic;" + (tagStyle.address || "");
137 | tagStyle.blockquote = tagStyle.blockquote || 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px;';
138 | tagStyle.center = 'text-align:center;' + (tagStyle.center || "");
139 | tagStyle.cite = "font-style:italic;" + (tagStyle.cite || "");
140 | tagStyle.code = tagStyle.code || 'padding:0 1px 0 1px;margin-left:2px;margin-right:2px;background-color:#f8f8f8;border:1px solid #cccccc;border-radius:3px;';
141 | tagStyle.dd = "margin-left:40px;" + (tagStyle.dd || "");
142 | tagStyle.img = "max-width:100%;" + (tagStyle.img || "");
143 | tagStyle.mark = "display:inline;background-color:yellow;" + (tagStyle.mark || "");
144 | tagStyle.pre = "overflow:scroll;" + (tagStyle.pre || 'background-color:#f6f8fa;padding:5px;border-radius:5px;');
145 | tagStyle.s = "display:inline;text-decoration:line-through;" + (tagStyle.s || "");
146 | tagStyle.u = "display:inline;text-decoration:underline;" + (tagStyle.u || "");
147 | //低版本兼容
148 | if (!CanIUse) {
149 | blockTag.caption = true;
150 | tagStyle.big = "display:inline;font-size:1.2em;" + (tagStyle.big || "");
151 | tagStyle.small = "display:inline;font-size:0.8em;" + (tagStyle.small || "");
152 | tagStyle.pre = "font-family:monospace;white-space:pre;" + tagStyle.pre;
153 | }
154 | return tagStyle;
155 | }
156 |
157 | function DomHandler(style, tagStyle = {}) {
158 | this.imgList = [];
159 | this.imgIndex = 0;
160 | this.nodes = [];
161 | this.title = "";
162 | this._videoNum = 0;
163 | this._audioNum = 0;
164 | this._style = new CssTokenizer(style, initStyle(tagStyle)).parse();
165 | this._tagStack = [];
166 | this._whiteSpace = false;
167 | }
168 | DomHandler.prototype._addDomElement = function(element) {
169 | if (element.name == 'pre' || (element.attrs && /white-space\s*:\s*pre/.test(element.attrs.style))) {
170 | this._whiteSpace = true;
171 | element.pre = true;
172 | }
173 | let parent = this._tagStack[this._tagStack.length - 1];
174 | let siblings = parent ? parent.children : this.nodes;
175 | siblings.push(element);
176 | };
177 | DomHandler.prototype._bubbling = function() {
178 | for (let i = this._tagStack.length - 1; i >= 0; i--) {
179 | if (trustTag[this._tagStack[i].name]) {
180 | this._tagStack[i].continue = true;
181 | if (i == this._tagStack.length - 1) { // 同级标签中若有文本标签
182 | for (var node of this._tagStack[i].children)
183 | if (textTag[node.name])
184 | node.continue = true;
185 | }
186 | } else return this._tagStack[i].name;
187 | }
188 | }
189 | DomHandler.prototype.onopentag = function(name, attrs) {
190 | let element = {
191 | children: []
192 | };
193 | //匹配样式
194 | let matched = this._style[name] ? (this._style[name] + ';') : '';
195 | if (attrs.id)
196 | matched += (this._style['#' + attrs.id] ? (this._style['#' + attrs.id] + ';') : '');
197 | if (attrs.class) {
198 | for (var Class of attrs.class.split(' ')) {
199 | matched += (this._style['.' + Class] ? (this._style['.' + Class] + ';') : '');
200 | }
201 | delete attrs.class;
202 | }
203 | //处理属性
204 | switch (name) {
205 | case 'div':
206 | case 'p':
207 | if (attrs.align) {
208 | attrs.style += (';text-align:' + attrs.align);
209 | delete attrs.align;
210 | }
211 | break;
212 | case 'img':
213 | if (attrs.width) {
214 | attrs.style = 'width:' + attrs.width + (/[0-9]/.test(attrs.width[attrs.width.length - 1]) ? 'px' : '') + ';' + attrs.style;
215 | delete attrs.width;
216 | }
217 | if (attrs['data-src']) {
218 | attrs.src = attrs.src || attrs['data-src'];
219 | delete attrs['data-src'];
220 | }
221 | if (!attrs.hasOwnProperty('ignore') && attrs.src) {
222 | if (this.imgList.indexOf(attrs.src) != -1)
223 | attrs.src = attrs.src + "?index=" + this.imgIndex++;
224 | this.imgList.push(attrs.src);
225 | if (this._bubbling() == 'a') attrs.ignore = ""; // 图片在链接中不可预览
226 | };
227 | break;
228 | case 'font':
229 | name = 'span';
230 | if (attrs.color) {
231 | attrs.style += (';color:' + attrs.color);
232 | delete attrs.color;
233 | }
234 | if (attrs.face) {
235 | attrs.style += (";font-family:" + attrs.face);
236 | delete attrs.face;
237 | }
238 | if (attrs.size) {
239 | var size = parseInt(attrs.size);
240 | if (size < 1) size = 1;
241 | else if (size > 7) size = 7;
242 | let map = [10, 13, 16, 18, 24, 32, 48];
243 | attrs.style += (";font-size:" + map[size - 1] + "px");
244 | delete attrs.size;
245 | }
246 | break;
247 | case 'a':
248 | case 'ad':
249 | this._bubbling();
250 | break;
251 | case 'video':
252 | case 'audio':
253 | attrs.loop = attrs.hasOwnProperty('loop');
254 | attrs.controls = attrs.hasOwnProperty('controls');
255 | attrs.autoplay = attrs.hasOwnProperty('autoplay');
256 | if (name == 'video') {
257 | attrs.muted = attrs.hasOwnProperty('muted');
258 | if (attrs.width) {
259 | attrs.style = 'width:' + parseFloat(attrs.width) + 'px;' + attrs.style;
260 | delete attrs.width;
261 | }
262 | if (attrs.height) {
263 | attrs.style = 'height:' + parseFloat(attrs.height) + 'px;' + attrs.style;
264 | delete attrs.height;
265 | }
266 | }
267 | attrs.id = (name + (++this['_' + name + 'Num']));
268 | attrs.source = [];
269 | if (attrs.src) attrs.source.push(attrs.src);
270 | if (!attrs.controls && !attrs.autoplay)
271 | console.warn('存在没有controls属性的' + name + '标签,可能导致无法播放', attrs);
272 | this._bubbling();
273 | break;
274 | case 'source':
275 | let parent = this._tagStack[this._tagStack.length - 1];
276 | if (parent && (parent.name == 'video' || parent.name == 'audio')) {
277 | parent.attrs.source.push(attrs.src);
278 | if (!parent.attrs.src) parent.attrs.src = attrs.src;
279 | }
280 | this._tagStack.push(element);
281 | return;
282 | }
283 | attrs.style = matched + attrs.style;
284 | if (textTag[name]) {
285 | if (!this._tagStack.length || this._tagStack[this._tagStack.length - 1].continue)
286 | element.continue = true;
287 | } else if (blockTag[name]) name = 'div';
288 | else if (!trustTag.hasOwnProperty(name)) name = 'span';
289 | element.name = name;
290 | element.attrs = attrs;
291 | this._addDomElement(element);
292 | this._tagStack.push(element);
293 | };
294 | DomHandler.prototype.ontext = function(data) {
295 | if (!this._whiteSpace){
296 | if(!/\S/.test(data))
297 | return;
298 | data = data.replace(/\s+/g, " ");
299 | }
300 | let element = {
301 | text: data.replace(/ /g, '\u00A0'), // 解决连续 失效问题
302 | type: 'text'
303 | };
304 | if (/*((?!sp|lt|gt).){2,5};/.test(data)) element.decode = true;
305 | this._addDomElement(element);
306 | };
307 | DomHandler.prototype.onclosetag = function(name) {
308 | let element = this._tagStack.pop();
309 | if (ignoreTag[name]) {
310 | if (name == 'title') {
311 | try {
312 | this.title = element.children[0].text;
313 | } catch (e) {}
314 | }
315 | let parent = this._tagStack[this._tagStack.length - 1];
316 | let siblings = parent ? parent.children : this.nodes;
317 | siblings.pop();
318 | }
319 | // 合并一些不必要的层,减小节点深度
320 | if (element.children.length == 1 && element.name == 'div' && element.children[0].name == 'div' && !(/padding/.test(element.attrs.style)) && !(/margin/.test(element.attrs.style) && /margin/.test(element.children[0].attrs.style)) && !(/display/.test(element.attrs.style)) && !(/display/.test(element.children[0].attrs.style)) && !(element.attrs.id && element.children[0].attrs.id)) {
321 | let parent = this._tagStack.length ? this._tagStack[this._tagStack.length - 1].children : this.nodes;
322 | let i = parent.indexOf(element);
323 | if (/padding/.test(element.children[0].attrs.style))
324 | element.children[0].attrs.style = ";box-sizing:border-box;" + element.children[0].attrs.style;
325 | element.children[0].attrs.style = element.attrs.style + ";" + element.children[0].attrs.style;
326 | element.children[0].attrs.id = (element.children[0].attrs.id || "") + (element.attrs.id || "");
327 | parent[i] = element.children[0];
328 | }
329 | if (element.pre) {
330 | this._whiteSpace = false;
331 | for (var ele of this._tagStack)
332 | if (ele.pre)
333 | this._whiteSpace = true;
334 | delete element.pre;
335 | }
336 | };
337 | module.exports = DomHandler;
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/Parser.js:
--------------------------------------------------------------------------------
1 | //Parser.js
2 | const Tokenizer = require("./Tokenizer.js");
3 | const DomHandler = require("./DomHandler.js");
4 | const trustAttrs = {
5 | align: true,
6 | alt: true,
7 | author: true,
8 | autoplay: true,
9 | class: true,
10 | color: true,
11 | colspan: true,
12 | controls: true,
13 | "data-src": true,
14 | dir: true,
15 | face: true,
16 | height: true,
17 | href: true,
18 | id: true,
19 | ignore: true,
20 | loop: true,
21 | muted: true,
22 | name: true,
23 | poster: true,
24 | rowspan: true,
25 | size: true,
26 | span: true,
27 | src: true,
28 | start: true,
29 | style: true,
30 | type: true,
31 | "unit-id":true,
32 | width: true,
33 | };
34 | const voidTag = {
35 | area: true,
36 | base: true,
37 | basefont: true,
38 | br: true,
39 | col: true,
40 | circle: true,
41 | command: true,
42 | ellipse: true,
43 | embed: true,
44 | frame: true,
45 | hr: true,
46 | img: true,
47 | input: true,
48 | isindex: true,
49 | keygen: true,
50 | line: true,
51 | link: true,
52 | meta: true,
53 | param: true,
54 | path: true,
55 | polygon: true,
56 | polyline: true,
57 | rect: true,
58 | source: true,
59 | stop: true,
60 | track: true,
61 | use: true,
62 | wbr: true
63 | };
64 |
65 | function Parser(cbs, callback) {
66 | this._cbs = cbs;
67 | this._callback = callback;
68 | this._tagname = "";
69 | this._attribname = "";
70 | this._attribvalue = "";
71 | this._attribs = null;
72 | this._stack = [];
73 | this._tokenizer = new Tokenizer(this);
74 | }
75 | Parser.prototype.ontext = function(data) {
76 | this._cbs.ontext(data);
77 | };
78 | Parser.prototype.onopentagname = function(name) {
79 | name = name.toLowerCase();
80 | this._tagname = name;
81 | this._attribs = {
82 | style: ''
83 | };
84 | if (!voidTag[name]) this._stack.push(name);
85 | };
86 | Parser.prototype.onopentagend = function() {
87 | if (this._attribs) {
88 | this._cbs.onopentag(this._tagname, this._attribs);
89 | this._attribs = null;
90 | }
91 | if (voidTag[this._tagname]) this._cbs.onclosetag(this._tagname);
92 | this._tagname = "";
93 | };
94 | Parser.prototype.onclosetag = function(name) {
95 | name = name.toLowerCase();
96 | if (this._stack.length && !voidTag[name]) {
97 | var pos = this._stack.lastIndexOf(name);
98 | if (pos !== -1) {
99 | pos = this._stack.length - pos;
100 | while (pos--) this._cbs.onclosetag(this._stack.pop());
101 | } else if (name === "p") {
102 | this.onopentagname(name);
103 | this._closeCurrentTag();
104 | }
105 | } else if (name === "br" || name === "hr" || name === "p") {
106 | this.onopentagname(name);
107 | this._closeCurrentTag();
108 | }
109 | };
110 | Parser.prototype._closeCurrentTag = function() {
111 | let name = this._tagname;
112 | this.onopentagend();
113 | if (this._stack[this._stack.length - 1] === name) {
114 | this._cbs.onclosetag(name);
115 | this._stack.pop();
116 | }
117 | };
118 | Parser.prototype.onattribend = function() {
119 | this._attribvalue = this._attribvalue.replace(/"/g, '"');
120 | if (this._attribs && trustAttrs[this._attribname]) {
121 | this._attribs[this._attribname] = this._attribvalue;
122 | }
123 | this._attribname = "";
124 | this._attribvalue = "";
125 | };
126 | Parser.prototype.onend = function() {
127 | for (
128 | var i = this._stack.length; i > 0; this._cbs.onclosetag(this._stack[--i])
129 | );
130 | this._callback({
131 | 'nodes': this._cbs.nodes,
132 | 'title': this._cbs.title,
133 | 'imgList': this._cbs.imgList
134 | });
135 | };
136 | Parser.prototype.write = function(chunk) {
137 | this._tokenizer.parse(chunk);
138 | };
139 |
140 | function html2nodes(data, tagStyle) {
141 | return new Promise(function(resolve, reject) {
142 | try {
143 | let style = '';
144 | data = data.replace(/([\s\S]*?)<\/style>/gi, function() {
145 | style += arguments[1];
146 | return '';
147 | });
148 | try {
149 | var emoji = require("./emoji.js");
150 | data = emoji.parseEmoji(data);
151 | } catch (err) {}
152 | let handler = new DomHandler(style, tagStyle);
153 | new Parser(handler, (res) => {
154 | return resolve(res);
155 | }).write(data);
156 | } catch (err) {
157 | return reject(err);
158 | }
159 | })
160 | }
161 | module.exports = html2nodes;
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/Tokenizer.js:
--------------------------------------------------------------------------------
1 | //Tokenizer.js
2 | function Tokenizer(cbs) {
3 | this._state = "TEXT";
4 | this._buffer = "";
5 | this._sectionStart = 0;
6 | this._index = 0;
7 | this._cbs = cbs;
8 | }
9 | Tokenizer.prototype.TEXT = function(c) {
10 | var index = this._buffer.indexOf("<", this._index);
11 | if (index != -1) {
12 | this._index = index;
13 | this._cbs.ontext(this._getSection());
14 | this._state = "BeforeTag";
15 | this._sectionStart = this._index;
16 | } else this._index = this._buffer.length;
17 | };
18 | Tokenizer.prototype.BeforeTag = function(c) {
19 | switch (c) {
20 | case "/":
21 | this._state = "BeforeCloseTag";
22 | break;
23 | case "!":
24 | this._state = "BeforeDeclaration";
25 | break;
26 | case "?":
27 | let index = this._buffer.indexOf(">", this._index);
28 | if (index != -1) {
29 | this._index = index;
30 | this._sectionStart = this._index + 1;
31 | } else this._sectionStart = this._index = this._buffer.length;
32 | this._state = "TEXT";
33 | break;
34 | case ">":
35 | this._state = "TEXT";
36 | break;
37 | case "<":
38 | this._cbs.ontext(this._getSection());
39 | this._sectionStart = this._index;
40 | break;
41 | default:
42 | if (/\s/.test(c)) this._state = "TEXT";
43 | else {
44 | this._state = "InTag";
45 | this._sectionStart = this._index;
46 | }
47 | }
48 | };
49 | Tokenizer.prototype.InTag = function(c) {
50 | if (c === "/" || c === ">" || /\s/.test(c)) {
51 | this._cbs.onopentagname(this._getSection());
52 | this._state = "BeforeAttrsName";
53 | this._index--;
54 | }
55 | };
56 | Tokenizer.prototype.BeforeAttrsName = function(c) {
57 | if (c === ">") {
58 | this._cbs.onopentagend();
59 | this._state = "TEXT";
60 | this._sectionStart = this._index + 1;
61 | } else if (c === "/") {
62 | this._state = "InSelfCloseTag";
63 | } else if (!(/\s/.test(c))) {
64 | this._state = "InAttrsName";
65 | this._sectionStart = this._index;
66 | }
67 | };
68 | Tokenizer.prototype.InAttrsName = function(c) {
69 | if (c === "=" || c === "/" || c === ">" || /\s/.test(c)) {
70 | this._cbs._attribname = this._getSection().toLowerCase();
71 | this._sectionStart = -1;
72 | this._state = "AfterAttrsName";
73 | this._index--;
74 | }
75 | };
76 | Tokenizer.prototype.AfterAttrsName = function(c) {
77 | if (c === "=") {
78 | this._state = "BeforeAttrsValue";
79 | } else if (c === "/" || c === ">") {
80 | this._cbs.onattribend();
81 | this._state = "BeforeAttrsName";
82 | this._index--;
83 | } else if (!(/\s/.test(c))) {
84 | this._cbs.onattribend();
85 | this._state = "InAttrsName";
86 | this._sectionStart = this._index;
87 | }
88 | };
89 | Tokenizer.prototype.BeforeAttrsValue = function(c) {
90 | if (c === '"') {
91 | this._state = "InAttrsValueDQ";
92 | this._sectionStart = this._index + 1;
93 | } else if (c === "'") {
94 | this._state = "InAttrsValueSQ";
95 | this._sectionStart = this._index + 1;
96 | } else if (!(/\s/.test(c))) {
97 | this._state = "InAttrsValueNQ";
98 | this._sectionStart = this._index;
99 | this._index--;
100 | }
101 | };
102 | Tokenizer.prototype.InAttrsValueDQ = function(c) {
103 | if (c === '"') {
104 | this._cbs._attribvalue += this._getSection();
105 | this._cbs.onattribend();
106 | this._state = "BeforeAttrsName";
107 | }
108 | };
109 | Tokenizer.prototype.InAttrsValueSQ = function(c) {
110 | if (c === "'") {
111 | this._cbs._attribvalue += this._getSection();
112 | this._cbs.onattribend();
113 | this._state = "BeforeAttrsName";
114 | }
115 | };
116 | Tokenizer.prototype.InAttrsValueNQ = function(c) {
117 | if (/\s/.test(c) || c === ">") {
118 | this._cbs._attribvalue += this._getSection();
119 | this._cbs.onattribend();
120 | this._state = "BeforeAttrsName";
121 | this._index--;
122 | }
123 | };
124 | Tokenizer.prototype.BeforeCloseTag = function(c) {
125 | if (/\s/.test(c));
126 | else if (c === ">") {
127 | this._state = "TEXT";
128 | } else {
129 | this._state = "InCloseTag";
130 | this._sectionStart = this._index;
131 | }
132 | };
133 | Tokenizer.prototype.InCloseTag = function(c) {
134 | if (c === ">" || /\s/.test(c)) {
135 | this._cbs.onclosetag(this._getSection());
136 | this._state = "AfterCloseTag";
137 | this._index--;
138 | }
139 | };
140 | Tokenizer.prototype.InSelfCloseTag = function(c) {
141 | if (c === ">") {
142 | this._cbs.onopentagend();
143 | this._state = "TEXT";
144 | this._sectionStart = this._index + 1;
145 | } else if (!(/\s/.test(c))) {
146 | this._state = "BeforeAttrsName";
147 | this._index--;
148 | }
149 | };
150 | Tokenizer.prototype.AfterCloseTag = function(c) {
151 | if (c === ">") {
152 | this._state = "TEXT";
153 | this._sectionStart = this._index + 1;
154 | }
155 | };
156 | Tokenizer.prototype.BeforeDeclaration = function(c) {
157 | if (c == '-') this._state = "InComment";
158 | else if (c == '[') this._state = "BeforeCDATA1";
159 | else this._state = "InDeclaration";
160 | };
161 | Tokenizer.prototype.InDeclaration = function(c) {
162 | var index = this._buffer.indexOf(">", this._index);
163 | if (index != -1) {
164 | this._index = index;
165 | this._sectionStart = index + 1;
166 | } else this._sectionStart = this._index = this._buffer.length;
167 | this._state = "TEXT";
168 | };
169 | Tokenizer.prototype.InComment = function(c) {
170 | let key = (c == '-' ? '-->' : '>');
171 | let index = this._buffer.indexOf(key, this._index);
172 | if (index != -1) {
173 | this._index = index + key.length - 1;
174 | this._sectionStart = this._index + 1;
175 | } else this._sectionStart = this._index = this._buffer.length;
176 | this._state = "TEXT";
177 | };
178 | Tokenizer.prototype.BeforeCDATA1 = function(c) {
179 | if (c == 'C') this._state = "BeforeCDATA2";
180 | else this._state = "InDeclaration";
181 | };
182 | Tokenizer.prototype.BeforeCDATA2 = function(c) {
183 | if (c == 'D') this._state = "BeforeCDATA3";
184 | else this._state = "InDeclaration";
185 | };
186 | Tokenizer.prototype.BeforeCDATA3 = function(c) {
187 | if (c == 'A') this._state = "BeforeCDATA4";
188 | else this._state = "InDeclaration";
189 | };
190 | Tokenizer.prototype.BeforeCDATA4 = function(c) {
191 | if (c == 'T') this._state = "BeforeCDATA5";
192 | else this._state = "InDeclaration";
193 | };
194 | Tokenizer.prototype.BeforeCDATA5 = function(c) {
195 | if (c == 'A') this._state = "InCDATA";
196 | else this._state = "InDeclaration";
197 | };
198 | Tokenizer.prototype.InCDATA = function(c) {
199 | let key = (c == '[' ? ']]>' : '>');
200 | let index = this._buffer.indexOf(key, this._index);
201 | if (index != -1) {
202 | this._index = index + key.length - 1;
203 | this._sectionStart = this._index + 1;
204 | } else this._sectionStart = this._index = this._buffer.length;
205 | this._state = "TEXT";
206 | };
207 | Tokenizer.prototype.parse = function(chunk) {
208 | this._buffer += chunk;
209 | for (; this._index < this._buffer.length; this._index++)
210 | this[this._state](this._buffer[this._index]);
211 | if (this._state === "TEXT" && this._sectionStart !== this._index)
212 | this._cbs.ontext(this._buffer.substr(this._sectionStart));
213 | this._cbs.onend();
214 | };
215 | Tokenizer.prototype._getSection = function() {
216 | return this._buffer.substring(this._sectionStart, this._index);
217 | };
218 | module.exports = Tokenizer;
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/api.js:
--------------------------------------------------------------------------------
1 | String.prototype.splice = function(start = 0, deleteCount = 0, addStr = '') {
2 | if (start < 0) start = this.length + start;
3 | if (deleteCount < 0) deleteCount = 0;
4 | return this.substring(0, start) + addStr + this.substring(start + deleteCount);
5 | }
6 | module.exports = {
7 | versionHigherThan(version = '') {
8 | var v1 = wx.getSystemInfoSync().SDKVersion.split('.');
9 | var v2 = version.split('.');
10 | const len = Math.max(v1.length, v2.length);
11 | while (v1.length < len) {
12 | v1.push('0');
13 | }
14 | while (v2.length < len) {
15 | v2.push('0');
16 | }
17 | for (let i = 0; i < len; i++) {
18 | const num1 = parseInt(v1[i]);
19 | const num2 = parseInt(v2[i]);
20 | if (num1 > num2) {
21 | return true;
22 | } else if (num1 < num2) {
23 | return false;
24 | }
25 | }
26 | return true;
27 | },
28 | html2nodes(html, tagStyle) {
29 | const Parser = require('./Parser.js');
30 | return Parser(html, tagStyle);
31 | },
32 | css2object(style, tagStyle) {
33 | const CssTokenizer = require('./CssTokenizer.js');
34 | return new CssTokenizer(style, tagStyle).parse();
35 | }
36 | }
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/index.js:
--------------------------------------------------------------------------------
1 | //Parser组件
2 | const html2nodes = require('./Parser.js');
3 | const initData = function(Component) {
4 | setTimeout(() => {
5 | Component.createSelectorQuery().select('#contain').boundingClientRect(res => {
6 | Component.triggerEvent('ready', res);
7 | }).exec();
8 | Component.videoContext = [];
9 | let nodes = [Component.selectComponent('#contain')];
10 | nodes = nodes.concat(Component.selectAllComponents('#contain>>>#node'));
11 | for (let node of nodes) {
12 | for (let item of node.data.nodes) {
13 | if (item.name == 'video') {
14 | Component.videoContext.push({
15 | id: item.attrs.id,
16 | context: wx.createVideoContext(item.attrs.id, node)
17 | });
18 | } else if (item.name == 'audio' && item.attrs.autoplay)
19 | wx.createAudioContext(item.attrs.id, node).play();
20 | }
21 | }
22 | }, 10)
23 | }
24 | Component({
25 | created() {
26 | try {
27 | const Document = require("./document.js");
28 | this.document = new Document();
29 | } catch (e) {}
30 | },
31 | properties: {
32 | 'html': {
33 | type: null,
34 | value: '',
35 | observer: function(html) {
36 | let hideAnimation = {},
37 | showAnimation = {};
38 | if (this.data.showWithAnimation) {
39 | hideAnimation = wx.createAnimation({
40 | duration: this.data.animationDuration,
41 | timingFunction: "ease"
42 | }).opacity(0).step().export();
43 | showAnimation = wx.createAnimation({
44 | duration: this.data.animationDuration,
45 | timingFunction: "ease"
46 | }).opacity(1).step().export();
47 | }
48 | if (!html) {
49 | this.setData({
50 | nodes: []
51 | })
52 | } else if (typeof html == 'string') {
53 | html2nodes(html, this.data.tagStyle).then(res => {
54 | this.setData({
55 | nodes: res.nodes,
56 | controls: {
57 | imgMode: this.data.imgMode
58 | },
59 | showAnimation,
60 | hideAnimation
61 | }, initData(this))
62 | if (this.document) this.document.init("nodes", res.nodes, this);
63 | if (res.title && this.data.autosetTitle) {
64 | wx.setNavigationBarTitle({
65 | title: res.title
66 | })
67 | }
68 | this.imgList = res.imgList;
69 | this.triggerEvent('parse', res);
70 | }).catch(err => {
71 | this.triggerEvent('error', {
72 | source: "parse",
73 | errMsg: err
74 | });
75 | })
76 | } else if (html.constructor == Array) {
77 | this.setData({
78 | controls: {
79 | imgMode: this.data.imgMode
80 | },
81 | showAnimation,
82 | hideAnimation
83 | }, initData(this))
84 | if (this.document) this.document.init("html", html, this);
85 | this.imgList = [];
86 | } else if (typeof html == 'object') {
87 | if (!html.nodes || html.nodes.constructor != Array) {
88 | if ((html.name && html.children && html.attrs) || (html.type == "text"))
89 | return;
90 | this.triggerEvent('error', {
91 | source: "parse",
92 | errMsg: "传入的nodes数组格式不正确!应该传入的类型是array,实际传入的类型是:" + typeof html.nodes
93 | });
94 | return;
95 | }
96 | this.setData({
97 | controls: {
98 | imgMode: this.data.imgMode
99 | },
100 | showAnimation,
101 | hideAnimation
102 | }, initData(this))
103 | if (this.document) this.document.init("html.nodes", html.nodes, this);
104 | if (html.title && this.data.autosetTitle)
105 | wx.setNavigationBarTitle({
106 | title: html.title
107 | })
108 | this.imgList = html.imgList || [];
109 | } else {
110 | this.triggerEvent('error', {
111 | source: "parse",
112 | errMsg: "错误的html类型:" + typeof html
113 | });
114 | }
115 | }
116 | },
117 | 'autocopy': {
118 | type: Boolean,
119 | value: true
120 | },
121 | 'autopause': {
122 | type: Boolean,
123 | value: true
124 | },
125 | 'autopreview': {
126 | type: Boolean,
127 | value: true
128 | },
129 | 'autosetTitle': {
130 | type: Boolean,
131 | value: true
132 | },
133 | 'imgMode': {
134 | type: String,
135 | value: "default"
136 | },
137 | 'selectable': {
138 | type: Boolean,
139 | value: false
140 | },
141 | 'tagStyle': {
142 | type: Object,
143 | value: {}
144 | },
145 | 'showWithAnimation': {
146 | type: Boolean,
147 | value: false
148 | },
149 | 'animationDuration': {
150 | type: Number,
151 | value: 400
152 | }
153 | },
154 | methods: {
155 | //事件
156 | tapEvent(e) {
157 | let src = e.detail;
158 | if(src.match(/^https?:\/\/steamcn.com\/t(\d+)-(\d+)-(\d+)/)){
159 | let tid = src.match(/^https?:\/\/steamcn.com\/t(\d+)-(\d+)-(\d+)/)[1]
160 | wx.navigateTo({
161 | url: `/pages/thread/thread?tid=${tid}`
162 | })
163 | }else if(src.match(/^https?:\/\/steamcn.com\/forum.php\?mod=viewthread&tid=(\d+)/)){
164 | let tid = src.match(/^https?:\/\/steamcn.com\/forum.php\?mod=viewthread&tid=(\d+)/)[1]
165 | wx.navigateTo({
166 | url: `/pages/thread/thread?tid=${tid}`
167 | })
168 | } else if(src.match(/^https?:\/\/steamcn.com\/forum.php\?mod=redirect&goto=findpost&ptid=(\d+)/)){
169 | let tid = src.match(/^https?:\/\/steamcn.com\/forum.php\?mod=redirect&goto=findpost&ptid=(\d+)/)[1]
170 | wx.navigateTo({
171 | url: `/pages/thread/thread?tid=${tid}`
172 | })
173 | } else if (this.data.autocopy && e.detail && /^http/.test(e.detail)) {
174 | wx.setClipboardData({
175 | data: e.detail,
176 | success() {
177 | wx.showToast({
178 | title: '链接已复制',
179 | icon: 'success',
180 | duration: 600
181 | })
182 | }
183 | })
184 | }
185 | this.triggerEvent('linkpress', e.detail);
186 | },
187 | errorEvent(e) {
188 | this.triggerEvent('error', e.detail);
189 | },
190 | previewEvent(e) {
191 | if (this.data.autopreview) {
192 | wx.previewImage({
193 | current: e.detail,
194 | urls: this.imgList.length ? this.imgList : [e.detail],
195 | })
196 | }
197 | this.triggerEvent('imgtap', e.detail);
198 | },
199 | //内部方法
200 | _playVideo(e) {
201 | if (this.videoContext.length > 1 && this.data.autopause) {
202 | for (let video of this.videoContext) {
203 | if (video.id == e.detail) continue;
204 | video.context.pause();
205 | }
206 | }
207 | }
208 | }
209 | })
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {
4 | "trees": "./trees/trees"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/index.wxss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | overflow: scroll;
4 | -webkit-overflow-scrolling: touch;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/trees/cssHandler.wxs:
--------------------------------------------------------------------------------
1 | // Parser/trees/cssHandler.wxs
2 | module.exports = {
3 | getStyle: function(style, display) {
4 | var res = "";
5 | var reg = getRegExp("float\s*:\s*[^;]*", "i");
6 | if (reg.test(style)) res += reg.exec(style)[0];
7 | reg = getRegExp("margin[^;]*auto", "i");
8 | if (reg.test(style)) res += (";" + reg.exec(style)[0]);
9 | reg = getRegExp("display\s*:\s*([^;]*)", "i");
10 | if (reg.test(style) && reg.exec(style)[1]!="flex") res += (';' + reg.exec(style)[0]);
11 | else res += (';display:' + display);
12 | reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig");
13 | var width = reg.exec(style);
14 | while (width) {
15 | res += (';' + width[0]);
16 | width = reg.exec(style);
17 | }
18 | return res;
19 | },
20 | setImgStyle: function(item, imgMode) {
21 | if (getRegExp("[^-]width\s*:\s*[^px]*;", "i").test(';' + item.attrs.style))
22 | item.attrs.style += ';width:100%';
23 | if (imgMode == "widthFix")
24 | item.attrs.style += ";height:auto !important";
25 | return [item];
26 | },
27 | setStyle: function(item) {
28 | if (getRegExp("[^-]width\s*:\s*[^;]*", "i").test(';' + item.attrs.style))
29 | item.attrs.style += ';width:100%';
30 | return [item];
31 | }
32 | }
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/trees/trees.js:
--------------------------------------------------------------------------------
1 | // Parser/trees/trees.js
2 | // 提交错误事件
3 | const triggerError = function(Component, source, target, errMsg, errCode) {
4 | Component.triggerEvent('error', {
5 | source,
6 | target,
7 | errMsg,
8 | errCode
9 | }, {
10 | bubbles: true,
11 | composed: true
12 | });
13 | }
14 | // 加载其他源(音视频)
15 | const loadSource = function (Component, currentTarget) {
16 | if (!Component.data.controls[currentTarget.id] && currentTarget.source.length > 1) {
17 | Component.data.controls[currentTarget.id] = {
18 | play: false,
19 | index: 1
20 | }
21 | } else if (Component.data.controls[currentTarget.id] && currentTarget.source.length > (Component.data.controls[currentTarget.id].index + 1)) {
22 | Component.data.controls[currentTarget.id].index++;
23 | }
24 | Component.setData({
25 | controls: Component.data.controls
26 | })
27 | }
28 | Component({
29 | properties: {
30 | nodes: {
31 | type: Array,
32 | value: []
33 | },
34 | controls: {
35 | type: Object,
36 | value: {}
37 | }
38 | },
39 | methods: {
40 | //冒泡事件
41 | playEvent(e) {
42 | this.triggerEvent('play', e.currentTarget.dataset.id, {
43 | bubbles: true,
44 | composed: true
45 | });
46 | },
47 | previewEvent(e) {
48 | if (!e.target.dataset.hasOwnProperty('ignore')) {
49 | this.triggerEvent('preview', e.currentTarget.dataset.src, {
50 | bubbles: true,
51 | composed: true
52 | });
53 | }
54 | },
55 | tapEvent(e) {
56 | this.triggerEvent('linkpress', e.currentTarget.dataset.href, {
57 | bubbles: true,
58 | composed: true
59 | });
60 | },
61 | adError(e) {
62 | triggerError(this, "ad", e.currentTarget, e.detail.errMsg, e.detail.errCode);
63 | },
64 | videoError(e) {
65 | loadSource(this,e.currentTarget.dataset);
66 | triggerError(this, "video", e.currentTarget, e.detail.errMsg);
67 | },
68 | audioError(e){
69 | loadSource(this, e.currentTarget.dataset);
70 | triggerError(this, "audio", e.currentTarget, e.detail.errMsg);
71 | },
72 | //内部方法:加载视频
73 | _loadVideo(e) {
74 | this.data.controls[e.currentTarget.dataset.id] = {
75 | play: true,
76 | index: 0
77 | }
78 | this.setData({
79 | controls: this.data.controls
80 | })
81 | },
82 | }
83 | })
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/trees/trees.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true,
3 | "usingComponents": {
4 | "trees":"./trees"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/trees/trees.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{item.text}}
9 |
10 |
11 | \n
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/ParserRichText/Parser/trees/trees.wxss:
--------------------------------------------------------------------------------
1 | /* 链接受到点击的hover-class,可自定义修改 */
2 | .navigator-hover {
3 | opacity: 0.7;
4 | text-decoration: underline;
5 | }
6 | /* 以下内容不建议修改 */
7 | :host {
8 | display: inherit;
9 | float: inherit;
10 | }
11 | .sub, .sup, .bdo, .bdi, .ruby, .rt {
12 | display: inline-block !important;
13 | }
14 | .div, .blockquote, .p{
15 | display:block;
16 | }
17 | .b, .strong {
18 | display: inline;
19 | font-weight: bold;
20 | }
21 | .em, .i {
22 | display: inline;
23 | font-style: italic;
24 | }
25 | .del {
26 | display: inline;
27 | text-decoration: line-through;
28 | }
29 | .ins {
30 | display: inline;
31 | text-decoration: underline;
32 | }
33 | .code {
34 | display: inline;
35 | font-family: monospace;
36 | }
37 | .big {
38 | font-size:1.2em;
39 | display:inline;
40 | }
41 | .small {
42 | font-size:0.8em;
43 | display:inline;
44 | }
45 | .q, .span, .label, .abbr {
46 | display: inline;
47 | }
48 | .q::before {
49 | content: '"';
50 | }
51 | .q::after {
52 | content: '"';
53 | }
54 | .video {
55 | background-color: black;
56 | width: 300px;
57 | height: 225px;
58 | display: inline-block;
59 | position: relative;
60 | }
61 | .video-triangle {
62 | border-width: 15px 0 15px 30px;
63 | border-style: solid;
64 | border-color: transparent transparent transparent white;
65 | position: absolute;
66 | left: 50%;
67 | top: 50%;
68 | margin: -15px 0 0 -15px;
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ParserRichText/parserRichText.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/ParserRichText/parserRichText.scss
--------------------------------------------------------------------------------
/src/components/ParserRichText/parserRichText.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 |
3 | import './parserRichText.scss';
4 |
5 | interface Props {
6 | /**
7 | * 富文本数据,可以是 HTML 字符串、节点对象、节点数组
8 | *
9 | * @default []
10 | */
11 | html: string | object | object[];
12 |
13 | /**
14 | * 是否允许链接受到点击时自动复制链接(仅限http(s)开头的网络链接)
15 | *
16 | * @default true
17 | */
18 | autocopy?: boolean;
19 |
20 | /**
21 | * 是否允许播放视频时自动暂停其他视频
22 | *
23 | * @default true
24 | */
25 | autopause?: boolean;
26 |
27 | /**
28 | * 是否自动将``标签的内容设置到页面标题上
29 | *
30 | * @default true
31 | */
32 | autosetTitle?: boolean;
33 |
34 | /**
35 | * 是否允许长按复制内容
36 | *
37 | * @default false
38 | */
39 | selectable?: boolean;
40 |
41 | /**
42 | * 标签的默认样式
43 | *
44 | * @default {}
45 | */
46 | tagStyle?: object;
47 |
48 | /**
49 | * 图片显示模式
50 | *
51 | * @default 'default'
52 | */
53 | imgMode?:
54 | | 'default'
55 | | 'widthFix'
56 | | 'scaleToFill'
57 | | 'aspectFit'
58 | | 'aspectFill'
59 | | 'top'
60 | | 'bottom'
61 | | 'center'
62 | | 'left'
63 | | 'right'
64 | | 'top left'
65 | | 'top right'
66 | | 'bottom left'
67 | | 'bottom right';
68 |
69 | /**
70 | * 是否使用渐显动画
71 | *
72 | * @default false
73 | */
74 | showWithAnimation?: boolean;
75 |
76 | /**
77 | * 渐显动画持续时间
78 | *
79 | * @default 400
80 | */
81 | animationDuration?: number;
82 | }
83 |
84 | /**
85 | * ParserRichText 富文本组件
86 | */
87 | class ParserRichText extends Taro.Component {
88 | public static defaultProps = {
89 | html: [],
90 | autocopy: true,
91 | autopause: true,
92 | autosetTitle: true,
93 | showWithAnimation: false,
94 | animationDuration: 400,
95 | selectable: false,
96 | tagStyle: {},
97 | imgMode: 'default'
98 | };
99 |
100 | public config: Taro.Config = {
101 | usingComponents: {
102 | parser: './Parser/index'
103 | }
104 | };
105 |
106 | public render(): JSX.Element {
107 | const {
108 | html,
109 | autocopy,
110 | autopause,
111 | autosetTitle,
112 | selectable,
113 | tagStyle,
114 | imgMode,
115 | showWithAnimation,
116 | animationDuration
117 | } = this.props;
118 | return (
119 |
130 | );
131 | }
132 | }
133 |
134 | export default ParserRichText as Taro.ComponentClass;
135 |
--------------------------------------------------------------------------------
/src/components/ReplyCard/replyCard.scss:
--------------------------------------------------------------------------------
1 | .reply {
2 | border-radius: 5px;
3 | box-shadow: 2px 2px 2px 2px rgba(136, 136, 136, 0.3);
4 |
5 | .user {
6 | height: 100px;
7 | margin: 3px 25px;
8 | display: flex;
9 | align-items: center;
10 |
11 | .info {
12 | display: flex;
13 | margin-left: 25px;
14 | flex-direction: column;
15 | justify-content: space-between;
16 |
17 | .name {
18 | font-size: 25px;
19 | font-weight: 700;
20 | margin-bottom: 7px;
21 | }
22 |
23 | .time {
24 | font-size: 20px;
25 | color: #666;
26 | }
27 | }
28 | }
29 |
30 | .floor {
31 | margin: 15px;
32 | font-size: 25px;
33 | color: #abb4bf;
34 | }
35 | }
36 |
37 | .content {
38 | margin: 15px 15px 5px 85px;
39 | padding-bottom: 25px;
40 | font-size: 28px;
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/ReplyCard/replyCard.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { View, Text } from '@tarojs/components';
3 | import { AtAvatar } from 'taro-ui';
4 | import dayjs from 'dayjs';
5 |
6 | import './replyCard.scss';
7 | import ParserRichText from '../ParserRichText/parserRichText';
8 | import { IReply } from '../../interfaces/thread';
9 |
10 | interface Props {
11 | reply: IReply;
12 | }
13 |
14 | class ReplyCard extends Taro.Component {
15 | public static defaultProps = {
16 | reply: {
17 | user: {
18 | username: '',
19 | uid: 0,
20 | avatar: ''
21 | },
22 | content: '',
23 | timestamp: 0,
24 | position: 0
25 | }
26 | };
27 |
28 | public static options = {
29 | addGlobalClass: true
30 | };
31 |
32 | public render(): JSX.Element {
33 | const { user, content, timestamp, position } = this.props.reply;
34 | return (
35 |
36 |
37 |
38 |
39 |
40 | {user.username}
41 | {dayjs.unix(timestamp).fromNow()}
42 |
43 |
44 | {`#${position}`}
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | export default ReplyCard as Taro.ComponentClass;
55 |
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f127.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f127.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f129.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f129.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f140.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f140.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f148.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f148.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f161.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f161.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f189.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f189.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f197.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f197.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f200.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f201.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f201.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f232.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f232.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f234.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f234.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f235.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f235.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f238.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f238.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f244.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f244.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f245.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f245.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f246.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f246.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f248.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f248.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f251.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f251.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f254.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f254.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f257.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f259.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f259.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f271.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f271.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f273.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f273.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f274.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f274.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f275.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f275.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f276.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f276.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f277.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f277.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f291.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f291.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f299.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f299.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f301.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f301.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f302.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f302.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f303.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f303.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f304.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f304.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f305.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f305.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f311.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f311.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f312.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f312.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f316.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f316.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f318.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f318.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f319.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f319.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f322.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f322.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f325.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f325.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f326.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f326.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f328.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f328.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f330.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f330.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f332.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f332.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/assets/f335.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/components/SectionGroupList/assets/f335.png
--------------------------------------------------------------------------------
/src/components/SectionGroupList/sectionGroupList.scss:
--------------------------------------------------------------------------------
1 | @import '~taro-ui/dist/style/components/flex.scss';
2 |
3 | .section {
4 | height: 110px;
5 | border-radius: 5px;
6 | box-shadow: 2px 2px 2px 2px rgba(136, 136, 136, 0.3);
7 | background: #fff;
8 |
9 | .icon {
10 | width: 40px;
11 | height: 40px;
12 | margin-left: 25px;
13 | padding: 15px;
14 | border-radius: 50%;
15 | background: #57bae8;
16 |
17 | image {
18 | width: 40px;
19 | height: 40px;
20 | }
21 | }
22 |
23 | .info {
24 | display: flex;
25 | height: 80px;
26 | margin-bottom: 20px;
27 | margin-left: 30px;
28 | flex-direction: column;
29 | justify-content: center;
30 |
31 | .title {
32 | font-size: 27px;
33 | font-weight: 700;
34 | margin-top: 25px;
35 | }
36 |
37 | .desc {
38 | margin-right: 25px;
39 | font-size: 21px;
40 | color: #666;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/SectionGroupList/sectionGroupList.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { View, Image, Text } from '@tarojs/components';
3 |
4 | import './sectionGroupList.scss';
5 |
6 | interface Props {
7 | list: {
8 | title: string;
9 | desc: string;
10 | fid: number;
11 | }[];
12 | }
13 |
14 | class SectionGroupList extends Taro.Component {
15 | public static defaultProps = {
16 | list: [
17 | {
18 | title: '',
19 | desc: '',
20 | fid: 0
21 | }
22 | ]
23 | };
24 |
25 | private toSectionThreadList(fid: number, title: string): void {
26 | Taro.navigateTo({
27 | url: `/pages/section/sectionThreadList?fid=${fid}&title=${title}`
28 | });
29 | }
30 |
31 | public render(): JSX.Element {
32 | const { list } = this.props;
33 | const sections = list.map(
34 | (item): JSX.Element => {
35 | return (
36 |
40 |
41 |
42 |
46 |
47 |
48 | {item.title}
49 | {item.desc}
50 |
51 |
52 |
53 | );
54 | }
55 | );
56 | return {sections};
57 | }
58 | }
59 |
60 | export default SectionGroupList as Taro.ComponentClass;
61 |
--------------------------------------------------------------------------------
/src/components/ThreadCard/threadCard.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 0 30px;
3 | margin: 0px 10px 20px;
4 | border-radius: 5px;
5 | box-shadow: 2px 2px 2px 2px rgba(136, 136, 136, 0.3);
6 | box-sizing: border-box;
7 |
8 | .header {
9 | align-items: center;
10 | height: 65px;
11 |
12 | .author {
13 | display: flex;
14 | align-items: center;
15 | font-size: 22px;
16 | color: #333;
17 |
18 | .avatar {
19 | width: 40px;
20 | height: 40px;
21 | border-radius: 50%;
22 | margin-right: 10px;
23 | }
24 | }
25 |
26 | .section {
27 | font-size: 22px;
28 | color: #abb4bf;
29 | }
30 | }
31 | }
32 |
33 | .title {
34 | font-size: 28px;
35 | }
36 |
37 | .footer {
38 | height: 60px;
39 | font-size: 22px;
40 | color: #abb4bf;
41 |
42 | .icon {
43 | margin-right: 8px;
44 | margin-left: 15px;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/ThreadCard/threadCard.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { View, Image, Text } from '@tarojs/components';
3 | import dayjs from 'dayjs';
4 |
5 | import { IThreadMeta } from '../../interfaces/thread';
6 |
7 | import './threadCard.scss';
8 |
9 | interface Props {
10 | threadMeta: IThreadMeta;
11 | }
12 |
13 | class ThreadCard extends Taro.Component {
14 | public static defaultProps = {
15 | threadMeta: {
16 | title: '',
17 | tid: 1,
18 | url: '',
19 | section: '',
20 | timestamp: 0,
21 | author: {
22 | username: '',
23 | uid: 1,
24 | avatar: ''
25 | },
26 | stats: {
27 | viewed: 0,
28 | replied: 0
29 | }
30 | }
31 | };
32 |
33 | public static options = {
34 | addGlobalClass: true
35 | };
36 |
37 | private toThread(): void {
38 | this.addToHistory();
39 | const { tid } = this.props.threadMeta;
40 | Taro.navigateTo({
41 | url: `/pages/thread/thread?tid=${tid}`
42 | });
43 | }
44 |
45 | private addToHistory(): void {
46 | const { threadMeta } = this.props;
47 | Taro.getStorage({
48 | key: 'history'
49 | }).then(
50 | (res): void => {
51 | let history = (res.data as unknown) as IThreadMeta[];
52 |
53 | history = history.filter((i): boolean => {
54 | if (i.tid === threadMeta.tid) {
55 | return false;
56 | } else {
57 | return true;
58 | }
59 | });
60 |
61 | history.push(threadMeta);
62 | Taro.setStorage({
63 | key: 'history',
64 | data: history
65 | });
66 | },
67 | (): void => {
68 | let history = Array();
69 | history.push(threadMeta);
70 | Taro.setStorage({
71 | key: 'history',
72 | data: history
73 | });
74 | }
75 | );
76 | }
77 |
78 | public render(): JSX.Element {
79 | const { title, section, timestamp, author, stats } = this.props.threadMeta;
80 | return (
81 |
82 |
83 |
84 | {author.avatar && (
85 |
90 | )}
91 | {author.username}
92 |
93 | {section}
94 |
95 |
96 |
97 | {title}
98 |
99 |
100 |
101 |
102 | {dayjs.unix(timestamp).fromNow()}
103 |
104 |
105 |
106 | {stats.viewed}
107 |
108 | {stats.replied}
109 |
110 |
111 |
112 | );
113 | }
114 | }
115 |
116 | export default ThreadCard as Taro.ComponentClass;
117 |
--------------------------------------------------------------------------------
/src/constants/account.ts:
--------------------------------------------------------------------------------
1 | export const INIT_CREDENTIAL = 'INIT_CREDENTIAL';
2 |
3 | export const INVALID_CREDENTIAL = 'INVALID_CREDENTIAL';
4 |
5 | export const LOGIN = 'LOGIN';
6 |
7 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
8 |
9 | export const LOGIN_ERROR = 'LOGIN_ERROR';
10 |
11 | export const LOGOUT = 'LOGOUT';
12 |
13 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
14 |
15 | export const LOGOUT_ERROR = 'LOGOUT_ERROR';
16 |
--------------------------------------------------------------------------------
/src/constants/system.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/prefer-default-export
2 | export const GET_SYSTEM_INFO = 'GET_SYSTEM_INFO';
3 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | SteamCN
12 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/interfaces/account.d.ts:
--------------------------------------------------------------------------------
1 | export interface IAccount {
2 | uid: number,
3 | username: string,
4 | email: string,
5 | avatar: string,
6 | groupid: number,
7 | createdAt: string,
8 | updatedAt: string,
9 | accessToken: string
10 | }
11 |
--------------------------------------------------------------------------------
/src/interfaces/respond.d.ts:
--------------------------------------------------------------------------------
1 | export interface IHotThreadItemRespond {
2 | itemid: string,
3 | id: string,
4 | idtype: string,
5 | title: string,
6 | url: string,
7 | pic: string,
8 | displayorder: number,
9 | coverpath: string,
10 | content: string,
11 | hasAuth: boolean,
12 | fields: {
13 | fulltitle: string,
14 | threads: string,
15 | author: string,
16 | authorid: string,
17 | avatar: string,
18 | avatar_middle: string,
19 | avatar_big: string,
20 | posts: string,
21 | todayposts: string,
22 | lastpost: string,
23 | lastposter: string,
24 | dateline: string,
25 | replies: string,
26 | forumurl: string,
27 | forumname: string,
28 | typename: string,
29 | typeicon: string,
30 | typeurl: string,
31 | sortname: string,
32 | sorturl: string,
33 | views: string,
34 | heats: string,
35 | recommends: string,
36 | hourviews: string,
37 | todayviews: string,
38 | weekviews: string,
39 | monthviews: string
40 | }
41 | }
42 |
43 | export interface IThreadRespond {
44 | floors: {
45 | pid: string,
46 | fid: string,
47 | tid: string,
48 | author: string,
49 | authorid: string,
50 | subject: string,
51 | dateline: string,
52 | message: string,
53 | avatar: string,
54 | uid: string,
55 | position: string,
56 | views: number,
57 | groupid: string,
58 | number: string,
59 | dbdateline: string,
60 | authortitle: string,
61 | ratelog?: {},
62 | ratelogextcredits?: {
63 | '1'?: number,
64 | '3'?: number,
65 | '4'?: number
66 | },
67 | totalrate: string[]
68 | }[],
69 | thread: {
70 | tid: string,
71 | fid: string,
72 | author: string,
73 | authorid: string,
74 | subject: string,
75 | dateline: string,
76 | lastpost: string,
77 | lastposter: string,
78 | views: number,
79 | replies: number,
80 | displayorder: string,
81 | heats: string,
82 | favtimes: string,
83 | maxposition: string
84 | },
85 | replyadd: boolean
86 | }
87 |
88 | export interface ISectionThreadListItem {
89 | tid: string,
90 | fid: string,
91 | author: string,
92 | authorid: string,
93 | subject: string,
94 | dateline: string,
95 | lastpost: string,
96 | lastposter: string,
97 | views: number,
98 | replies: string,
99 | displayorder: string,
100 | heats: string,
101 | favtimes: string,
102 | maxposition: string,
103 | typename: string,
104 | dbdateline: string,
105 | coverpath: string,
106 | content: string,
107 | forumname: string,
108 | hasAuth: boolean
109 | }
110 |
--------------------------------------------------------------------------------
/src/interfaces/thread.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Thread metadata type
3 | */
4 | export interface IThreadMeta {
5 | /**
6 | * Thread title
7 | */
8 | title: string,
9 | /**
10 | * Thread ID
11 | */
12 | tid: number,
13 | /**
14 | * Thread URL
15 | */
16 | url: string,
17 | /**
18 | * Thread image, only for Swiper
19 | */
20 | image?: string,
21 | /**
22 | * Thread section
23 | */
24 | section: string,
25 | /**
26 | * Thread post time
27 | */
28 | timestamp: number,
29 | /**
30 | * Thread author info
31 | */
32 |
33 | author: {
34 | /**
35 | * Author's username
36 | */
37 | username: string,
38 | /**
39 | * Author's UID
40 | */
41 | uid?: number,
42 | /**
43 | * Author's avatar
44 | */
45 | avatar?: string,
46 | },
47 | /**
48 | * Thread stats info
49 | */
50 | stats: {
51 | /**
52 | * Viewed ammount
53 | */
54 | viewed: number
55 | /**
56 | * Replied ammount
57 | */
58 | replied: number
59 | }
60 | }
61 |
62 | /**
63 | * Thread content type
64 | */
65 | export interface IThread {
66 | title: string,
67 | tid: number,
68 | timestamp: number,
69 | viewed: number,
70 | replied: number,
71 | content: string,
72 | maxPosition: number,
73 | author: {
74 | username: string,
75 | uid: number,
76 | avatar: string
77 | },
78 | replies: IReply[]
79 | }
80 |
81 | export interface IReply {
82 | user: {
83 | username: string,
84 | uid: number,
85 | avatar: string
86 | },
87 | content: string,
88 | timestamp: number,
89 | position: number
90 | }
91 |
--------------------------------------------------------------------------------
/src/pages/account/about.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding-top: 180px;
3 | font-size: 28px;
4 | line-height: 2.2em;
5 |
6 | .header {
7 | text-align: center;
8 | }
9 |
10 | .item {
11 | padding: 10px 30px 20px;
12 |
13 | .logo {
14 | width: 128px;
15 | height: 128px;
16 | border-radius: 50%;
17 | display: block;
18 | margin: auto;
19 | }
20 |
21 | .title {
22 | font-size: 32px;
23 | color: #57bae8;
24 | margin: 26px 0;
25 | }
26 |
27 | .text {
28 | flex: 1;
29 | display: flex;
30 | flex-direction: column;
31 | z-index: 1;
32 | }
33 |
34 | .qrcode {
35 | width: 420px;
36 | height: 420px;
37 | display: block;
38 | margin: auto;
39 | }
40 |
41 | .content {
42 | display: flex;
43 | flex-direction: row;
44 | justify-content: flex-start;
45 | align-items: flex-start;
46 | position: relative;
47 | padding-bottom: 16px;
48 |
49 | .icon {
50 | display: flex;
51 | justify-content: space-around;
52 | align-items: center;
53 | height: 2.2em;
54 | }
55 |
56 | image {
57 | width: 32px;
58 | height: 32px;
59 | margin-right: 20px;
60 | }
61 | }
62 | }
63 |
64 | .footer {
65 | height: 80px;
66 | margin-top: 30px;
67 | display: flex;
68 | justify-content: space-around;
69 | align-items: center;
70 | font-size: 24px;
71 | color: #333;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/pages/account/about.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { View, Image } from '@tarojs/components';
3 |
4 | import './about.scss';
5 |
6 | class About extends Taro.Component {
7 | public config: Taro.Config = {
8 | navigationBarTitleText: '关于'
9 | };
10 |
11 | public render(): JSX.Element {
12 | return (
13 |
14 |
15 |
19 | 蒸汽动力 SteamCN.com
20 | Version 0.2.6
21 |
22 |
23 |
24 | 项目地址
25 |
26 |
27 |
28 |
29 |
30 | GayHub
31 | https://github.com/xPixv/SteamCN-Mini-Program
32 |
33 |
34 |
35 |
36 |
37 | 反馈建议
38 |
39 |
40 |
41 |
42 |
43 |
44 | SteamCN
45 | https://steamcn.com/t477292-1-1
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Steam
55 | https://s.team/p/cdcc-dnbw
56 |
57 |
58 |
59 |
60 |
61 | 小程序二维码
62 |
66 |
67 | 蒸汽动力 · SteamCN
68 |
69 | );
70 | }
71 | }
72 |
73 | export default About as Taro.ComponentClass;
74 |
--------------------------------------------------------------------------------
/src/pages/account/account.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding-top: 20px;
3 | }
4 |
5 | .profile {
6 | display: flex;
7 | height: 180px;
8 | margin-bottom: 20px;
9 | padding: 0 40px;
10 | justify-content: space-between;
11 | align-items: center;
12 |
13 | .avatar {
14 | margin-right: 40px;
15 | }
16 |
17 | .info {
18 | display: flex;
19 | font-size: 24px;
20 | color: #abb4bf;
21 |
22 | .text {
23 | flex-direction: column;
24 | justify-content: space-around;
25 | }
26 | }
27 |
28 | .name {
29 | margin-bottom: 8px;
30 | font-size: 40px;
31 | color: #333;
32 | }
33 | }
34 |
35 | .forum-area {
36 | margin-bottom: 20px;
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/account/account.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View, Text } from '@tarojs/components';
4 | import { AtList, AtListItem, AtAvatar, AtNavBar } from 'taro-ui';
5 |
6 | import { IAccount } from '../../interfaces/account';
7 | import { initCredential } from '../../actions/account';
8 | import EmptyAvatar from './assets/empty_avatar_user.png';
9 |
10 | import './account.scss';
11 |
12 | interface Props {
13 | auth: boolean;
14 | account: IAccount;
15 | initCredential: () => void;
16 | }
17 |
18 | interface State {
19 | history: number;
20 | statusBarHeight: number;
21 | }
22 |
23 | @connect(
24 | ({ account }) => ({
25 | auth: account.auth,
26 | account: account.account
27 | }),
28 | dispatch => ({
29 | initCredential() {
30 | dispatch(initCredential());
31 | }
32 | })
33 | )
34 | class Account extends Taro.Component {
35 | public config: Taro.Config = {
36 | navigationBarTitleText: '我的'
37 | };
38 |
39 | public state = {
40 | history: 0,
41 | statusBarHeight: 20
42 | };
43 |
44 | public constructor(props: Props | undefined) {
45 | super(props);
46 | this.setState({
47 | statusBarHeight: Taro.getSystemInfoSync().statusBarHeight
48 | });
49 | }
50 |
51 | public componentDidShow(): void {
52 | Taro.getStorage({
53 | key: 'history'
54 | }).then(
55 | (res): void => {
56 | this.setState({
57 | history: res.data.length
58 | });
59 | },
60 | (): void => {
61 | this.setState({
62 | history: 0
63 | });
64 | }
65 | );
66 |
67 | this.props.initCredential();
68 | }
69 |
70 | private navigator(addr: string): void {
71 | Taro.navigateTo({
72 | url: `/pages/account/${addr}`
73 | });
74 | }
75 |
76 | private handleProfile(): void {
77 | const { auth } = this.props;
78 | if (auth) {
79 | // this.navigator('profile')
80 | } else {
81 | this.navigator('login');
82 | }
83 | }
84 |
85 | private joking(): void {
86 | Taro.showToast({
87 | title: '这里还没抛瓦 QAQ',
88 | icon: 'none',
89 | duration: 1500
90 | });
91 | }
92 |
93 | public render(): JSX.Element {
94 | const { auth, account } = this.props;
95 | const { history, statusBarHeight } = this.state;
96 | return (
97 |
98 |
103 |
104 |
105 |
106 |
107 |
113 |
114 | {auth ? account.username : '登录'}
115 | {auth ? (
116 | 充满抛瓦!(๑•̀ㅂ•́)و✧
117 | ) : (
118 | 一直未登录你怎么变强?w(゚Д゚)w
119 | )}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
133 |
139 |
145 |
146 |
147 |
148 |
149 |
150 |
155 |
160 |
161 |
162 |
163 |
164 | );
165 | }
166 | }
167 |
168 | export default Account as Taro.ComponentClass;
169 |
--------------------------------------------------------------------------------
/src/pages/account/assets/empty_avatar_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/umaim/SteamCN-Mini-Program/e7a9c258570eceafe9f2fa8ce12cba2cdbe29b71/src/pages/account/assets/empty_avatar_user.png
--------------------------------------------------------------------------------
/src/pages/account/history.scss:
--------------------------------------------------------------------------------
1 | .thread-list {
2 | padding-top: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/account/history.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { View } from '@tarojs/components';
3 | import { AtMessage, AtNavBar } from 'taro-ui';
4 |
5 | import './history.scss';
6 | import ThreadCard from '../../components/ThreadCard/threadCard';
7 | import { IThreadMeta } from '../../interfaces/thread';
8 |
9 | interface State {
10 | historyThreadList: IThreadMeta[];
11 | history: IThreadMeta[];
12 | page: number;
13 | statusBarHeight: number;
14 | }
15 |
16 | class History extends Taro.Component<{}, State> {
17 | public config: Taro.Config = {
18 | navigationBarTitleText: '历史记录',
19 | onReachBottomDistance: 300
20 | };
21 |
22 | public state = {
23 | historyThreadList: Array(),
24 | history: Array(),
25 | page: 1,
26 | statusBarHeight: 20
27 | };
28 |
29 | public constructor(props: undefined) {
30 | super(props);
31 | this.setState({
32 | statusBarHeight: Taro.getSystemInfoSync().statusBarHeight
33 | });
34 | }
35 |
36 | public componentDidMount(): void {
37 | Taro.getStorage({
38 | key: 'history'
39 | }).then(
40 | (res): void => {
41 | const history = (res.data as unknown) as IThreadMeta[];
42 | this.setState(
43 | {
44 | history: history.reverse()
45 | },
46 | (): void => {
47 | this.showMoreHistory();
48 | }
49 | );
50 | },
51 | (): void => {
52 | Taro.atMessage({
53 | message: '似乎没有留下你的足迹',
54 | type: 'info',
55 | duration: 1500
56 | });
57 | }
58 | );
59 | }
60 |
61 | public onReachBottom(): void {
62 | this.showMoreHistory();
63 | }
64 |
65 | private showMoreHistory(): void {
66 | const { history, historyThreadList, page } = this.state;
67 | if ((page - 1) * 10 >= history.length) {
68 | return;
69 | } else if (page * 10 < history.length) {
70 | this.setState({
71 | historyThreadList: historyThreadList.concat(
72 | history.slice((page - 1) * 10, page * 10)
73 | ),
74 | page: page + 1
75 | });
76 | } else {
77 | this.setState({
78 | historyThreadList: historyThreadList.concat(
79 | history.slice((page - 1) * 10, history.length)
80 | ),
81 | page: page + 1
82 | });
83 | }
84 | }
85 |
86 | public render(): JSX.Element {
87 | const { historyThreadList, statusBarHeight } = this.state;
88 | const threadCards = historyThreadList.map(
89 | (item): JSX.Element => {
90 | return ;
91 | }
92 | );
93 | return (
94 |
95 | {
101 | Taro.navigateBack({ delta: 1 });
102 | }}
103 | border={false}
104 | />
105 |
106 |
107 |
108 | {threadCards}
109 |
110 |
111 | );
112 | }
113 | }
114 |
115 | export default History as Taro.ComponentClass;
116 |
--------------------------------------------------------------------------------
/src/pages/account/login.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | height: 100%;
3 | margin: 0;
4 |
5 | .content {
6 | min-height: calc(100vh - 160px);
7 |
8 | .backgroundImage {
9 | width: 100%;
10 | }
11 |
12 | .login-button {
13 | width: 70%;
14 | margin-top: 20px;
15 | }
16 | }
17 |
18 | .footer {
19 | height: 35px;
20 | text-align: center;
21 | font-size: 24px;
22 | color: #333;
23 | }
24 | }
25 |
26 | .list-item {
27 | position: relative;
28 | display: flex;
29 | padding: 24px 0;
30 | margin-left: 32px;
31 |
32 | &::before,
33 | &::after {
34 | position: absolute;
35 | top: auto;
36 | left: 0;
37 | right: 0;
38 | bottom: 0;
39 | transform: scaleY(0.5);
40 | transform-origin: center;
41 | box-sizing: border-box;
42 | pointer-events: none;
43 | }
44 |
45 | &::before {
46 | top: 0;
47 | bottom: auto;
48 | }
49 |
50 | &__label,
51 | &__value {
52 | color: #333;
53 | font-size: 32px;
54 | line-height: 1.5;
55 | }
56 |
57 | &__label {
58 | margin-right: 16px;
59 | width: 172px;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/pages/account/login.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View, Image, Picker } from '@tarojs/components';
4 | import { AtInput, AtButton, AtMessage, AtNavBar } from 'taro-ui';
5 |
6 | import './login.scss';
7 | import {
8 | initCredential,
9 | login,
10 | loginSuccess,
11 | loginError
12 | } from '../../actions/account';
13 | import { IAccount } from '../../interfaces/account';
14 |
15 | interface Props {
16 | initCredential: () => void;
17 | login: () => void;
18 | loginSuccess: (account: IAccount) => void;
19 | loginError: () => void;
20 | }
21 |
22 | interface State {
23 | username: string;
24 | password: string;
25 | background: string;
26 | questions: string[];
27 | questionid: number;
28 | answer: string;
29 | statusBarHeight: number;
30 | }
31 |
32 | @connect(
33 | () => ({}),
34 | dispatch => ({
35 | initCredential() {
36 | dispatch(initCredential());
37 | },
38 | login() {
39 | dispatch(login());
40 | },
41 | loginSuccess(account: IAccount) {
42 | dispatch(loginSuccess(account));
43 | },
44 | loginError() {
45 | dispatch(loginError());
46 | }
47 | })
48 | )
49 | class Login extends Taro.Component {
50 | public config: Taro.Config = {
51 | navigationBarTitleText: '登录'
52 | };
53 |
54 | public state = {
55 | username: '',
56 | password: '',
57 | background: '',
58 | questions: [
59 | '未设置',
60 | '母亲的名字',
61 | '爷爷的名字',
62 | '父亲出生的城市',
63 | '您其中一位老师的名字',
64 | '您个人计算机的型号',
65 | '您最喜欢的餐馆名称',
66 | '驾驶执照最后四位数字'
67 | ],
68 | questionid: 0,
69 | answer: '',
70 | statusBarHeight: 20
71 | };
72 |
73 | public constructor(props: Props | undefined) {
74 | super(props);
75 | this.setState({
76 | statusBarHeight: Taro.getSystemInfoSync().statusBarHeight
77 | });
78 | }
79 |
80 | public componentDidShow(): void {
81 | this.setState({
82 | background: `cloud://steamcn.7374-steamcn/assets/img/login/background${Math.floor(
83 | Math.random() * 9
84 | )}.jpg`
85 | });
86 | }
87 |
88 | private handleUsernameChange(value: string): string {
89 | this.setState({
90 | username: value
91 | });
92 | return value;
93 | }
94 |
95 | private handlePasswordChange(value: string): string {
96 | this.setState({
97 | password: value
98 | });
99 | return value;
100 | }
101 |
102 | private handleQuestionsChange(e: { detail: { value: string } }): void {
103 | this.setState({
104 | questionid: parseInt(e.detail.value)
105 | });
106 | }
107 |
108 | private handleAnswerChange(value: string): string {
109 | this.setState({
110 | answer: value
111 | });
112 | return value;
113 | }
114 |
115 | private handleAnswerBlur(value: string): string {
116 | this.setState({
117 | answer: value
118 | });
119 | return value;
120 | }
121 |
122 | private login(): void {
123 | const { username, password, questionid, answer } = this.state;
124 |
125 | if (username.length === 0) {
126 | Taro.atMessage({
127 | message: `请输入用户名😟`,
128 | type: 'error',
129 | duration: 1500
130 | });
131 | return;
132 | } else if (password.length === 0) {
133 | Taro.atMessage({
134 | message: `请输入密码😟`,
135 | type: 'error',
136 | duration: 1500
137 | });
138 | return;
139 | } else if (questionid !== 0 && answer.trim() === '') {
140 | Taro.atMessage({
141 | message: `请输入安全问题答案😟`,
142 | type: 'error',
143 | duration: 1500
144 | });
145 | return;
146 | }
147 |
148 | this.props.login();
149 | Taro.showLoading({
150 | title: '正在登录 💦'
151 | });
152 |
153 | Taro.request({
154 | url: 'https://vnext.steamcn.com/v1/auth/login',
155 | data: {
156 | username: username,
157 | password: password,
158 | loginfield: username,
159 | questionid: questionid,
160 | answer: answer
161 | },
162 | header: {
163 | 'content-type': 'application/x-www-form-urlencoded'
164 | },
165 | method: 'POST',
166 | dataType: 'json',
167 | responseType: 'text'
168 | }).then(
169 | (res): void => {
170 | if (res.statusCode === 200) {
171 | const account: IAccount = res.data;
172 | this.props.loginSuccess(account);
173 | Taro.hideLoading();
174 | Taro.navigateBack();
175 | } else {
176 | const data = res.data;
177 | this.props.loginError();
178 | Taro.hideLoading();
179 | Taro.atMessage({
180 | message: `登录失败😱,${data.message}`,
181 | type: 'error',
182 | duration: 2000
183 | });
184 | }
185 | },
186 | (): void => {
187 | this.props.loginError();
188 | Taro.hideLoading();
189 | Taro.atMessage({
190 | message: '网络连接中断😭',
191 | type: 'error',
192 | duration: 2000
193 | });
194 | }
195 | );
196 | }
197 |
198 | public render(): JSX.Element {
199 | const {
200 | username,
201 | password,
202 | background,
203 | questions,
204 | questionid,
205 | answer,
206 | statusBarHeight
207 | } = this.state;
208 | return (
209 |
210 |
211 |
212 | {
218 | Taro.navigateBack({ delta: 1 });
219 | }}
220 | border={false}
221 | />
222 |
223 |
224 |
225 |
226 |
231 |
232 |
241 |
250 |
256 |
257 | 安全问题:
258 |
259 | {questions[questionid]}
260 |
261 |
262 |
263 | {questionid !== 0 && (
264 |
274 | )}
275 |
282 | 登录
283 |
284 |
285 | 蒸汽动力 · SteamCN.com
286 |
287 |
288 | );
289 | }
290 | }
291 |
292 | export default Login as Taro.ComponentClass;
293 |
--------------------------------------------------------------------------------
/src/pages/account/setting.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | margin: 15px 15px;
3 | }
4 |
5 | .logout-button {
6 | margin-top: 25px;
7 | width: 70%;
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/account/setting.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View } from '@tarojs/components';
4 | import { AtList, AtListItem, AtMessage, AtButton, AtNavBar } from 'taro-ui';
5 |
6 | import { IAccount } from '../../interfaces/account';
7 | import {
8 | initCredential,
9 | logout,
10 | logoutSuccess,
11 | logoutError
12 | } from '../../actions/account';
13 |
14 | import './setting.scss';
15 |
16 | interface Props {
17 | auth: boolean;
18 | account: IAccount;
19 | initCredential: () => void;
20 | logout: () => void;
21 | logoutSuccess: () => void;
22 | logoutError: () => void;
23 | }
24 |
25 | interface State {
26 | size: number;
27 | statusBarHeight: number;
28 | }
29 |
30 | @connect(
31 | ({ account }) => ({
32 | auth: account.auth,
33 | account: account.account
34 | }),
35 | dispatch => ({
36 | initCredential() {
37 | dispatch(initCredential());
38 | },
39 | logout() {
40 | dispatch(logout());
41 | },
42 | logoutSuccess() {
43 | dispatch(logoutSuccess());
44 | },
45 | logoutError() {
46 | dispatch(logoutError());
47 | }
48 | })
49 | )
50 | class Setting extends Taro.Component {
51 | public config: Taro.Config = {
52 | navigationBarTitleText: '设置'
53 | };
54 |
55 | public state = {
56 | size: 0,
57 | statusBarHeight: 20
58 | };
59 |
60 | public constructor(props: Props | undefined) {
61 | super(props);
62 | this.setState({
63 | statusBarHeight: Taro.getSystemInfoSync().statusBarHeight
64 | });
65 | }
66 |
67 | public componentDidMount(): void {
68 | Taro.getStorageInfo({
69 | success: (res): void => {
70 | if (!res.keys.includes('history')) {
71 | this.setState({
72 | size: 0
73 | });
74 | } else {
75 | this.setState({
76 | size: res.currentSize
77 | });
78 | }
79 | }
80 | });
81 |
82 | this.props.initCredential();
83 | }
84 |
85 | private clearHistory(): void {
86 | Taro.removeStorage({
87 | key: 'history'
88 | }).then((): void => {
89 | this.setState({
90 | size: 0
91 | });
92 | });
93 | }
94 |
95 | private logout(): void {
96 | Taro.showModal({
97 | title: '提示',
98 | content: '确认退出登录?',
99 | cancelText: '取消',
100 | confirmText: '确定',
101 | confirmColor: '#E64340'
102 | }).then((res): void => {
103 | if (res.confirm) {
104 | this.doLogout();
105 | }
106 | });
107 | }
108 |
109 | private doLogout(): void {
110 | const { account } = this.props;
111 | this.props.logout();
112 | Taro.showLoading({
113 | title: '正在登出 💦'
114 | });
115 | Taro.request({
116 | url: 'https://vnext.steamcn.com/v1/auth/logout',
117 | data: {},
118 | header: {
119 | authorization: account.accessToken
120 | },
121 | method: 'POST',
122 | dataType: 'json',
123 | responseType: 'text'
124 | }).then(
125 | (res): void => {
126 | if (res.statusCode === 200 || res.statusCode === 401) {
127 | this.props.logoutSuccess();
128 | Taro.hideLoading();
129 | Taro.atMessage({
130 | message: '已退出登录ヾ(•ω•`)o',
131 | type: 'success',
132 | duration: 2000
133 | });
134 | } else {
135 | this.props.logoutError();
136 | const data = res.data.message;
137 | Taro.hideLoading();
138 | Taro.atMessage({
139 | message: `登出失败😱,${data}`,
140 | type: 'error',
141 | duration: 3000
142 | });
143 | }
144 | },
145 | (): void => {
146 | this.props.logoutError();
147 | Taro.hideLoading();
148 | Taro.atMessage({
149 | message: '网络连接中断😭',
150 | type: 'error',
151 | duration: 2000
152 | });
153 | }
154 | );
155 | }
156 |
157 | public render(): JSX.Element {
158 | const { auth } = this.props;
159 | const { size, statusBarHeight } = this.state;
160 | return (
161 |
162 |
163 |
164 | {
170 | Taro.navigateBack({ delta: 1 });
171 | }}
172 | border={false}
173 | />
174 |
175 |
176 |
177 |
182 |
183 |
184 |
185 | {auth && (
186 |
192 | 退出登录 ヾ(•ω•`)o
193 |
194 | )}
195 |
196 | );
197 | }
198 | }
199 |
200 | export default Setting as Taro.ComponentClass;
201 |
--------------------------------------------------------------------------------
/src/pages/hot/hot.scss:
--------------------------------------------------------------------------------
1 | .thread-list {
2 | padding-top: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/hot/hot.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View } from '@tarojs/components';
4 | import { AtMessage, AtNavBar } from 'taro-ui';
5 |
6 | import ThreadCard from '../../components/ThreadCard/threadCard';
7 | import { IAccount } from '../../interfaces/account';
8 | import { IThreadMeta } from '../../interfaces/thread';
9 | import { IHotThreadItemRespond } from '../../interfaces/respond';
10 |
11 | import './hot.scss';
12 |
13 | interface Props {
14 | auth: boolean;
15 | account: IAccount;
16 | statusBarHeight: number;
17 | }
18 |
19 | interface State {
20 | hotThreadList: IThreadMeta[];
21 | }
22 |
23 | @connect(({ account, system }) => ({
24 | auth: account.auth,
25 | account: account.account,
26 | statusBarHeight: system.statusBarHeight
27 | }))
28 | class Hot extends Taro.Component {
29 | public config: Taro.Config = {
30 | navigationBarTitleText: '热门主题',
31 | enablePullDownRefresh: true
32 | };
33 |
34 | public state = {
35 | hotThreadList: Array()
36 | };
37 |
38 | public componentDidMount(): void {
39 | this.initHot();
40 | }
41 |
42 | public onShareAppMessage(): {
43 | title: string;
44 | path: string;
45 | } {
46 | return {
47 | title: 'SteamCN 蒸汽动力 - 热门主题',
48 | path: 'pages/hot/hot'
49 | };
50 | }
51 |
52 | public onPullDownRefresh(): void {
53 | this.initHot();
54 | }
55 |
56 | private initHot(): void {
57 | Taro.showLoading({
58 | title: '努力加载中 💦'
59 | });
60 | this.requestHotThreadList();
61 | }
62 |
63 | private requestHotThreadList(): void {
64 | this.requestHot(434).then((res): void => {
65 | if (res) {
66 | this.setState(
67 | {
68 | hotThreadList: res
69 | },
70 | this.isFinish
71 | );
72 | }
73 | });
74 | }
75 |
76 | private requestHot(bid: number): Promise {
77 | const { account } = this.props;
78 | return Taro.request({
79 | url: `https://vnext.steamcn.com/v1/forum/hot/${bid}`,
80 | data: {},
81 | header: {
82 | authorization: account.accessToken
83 | },
84 | method: 'GET',
85 | dataType: 'json',
86 | responseType: 'text'
87 | }).then(
88 | (res): IThreadMeta[] | undefined => {
89 | if (res.statusCode === 200) {
90 | console.log(res.data);
91 | const itemlist = res.data.itemlist as IHotThreadItemRespond[];
92 | let thraedList = Array();
93 | itemlist.forEach((item): void => {
94 | const title = item.title;
95 | const tid = parseInt(item.id);
96 | const url = `https://steamcn.com/t${tid}-1-1`;
97 | const image = item.coverpath;
98 | const section = item.fields.forumname;
99 | const timestamp = parseInt(item.fields.dateline);
100 | const username = item.fields.author;
101 | const uid = parseInt(item.fields.authorid);
102 | const avatar = item.fields.avatar_middle;
103 | const viewed = parseInt(item.fields.views);
104 | const replied = parseInt(item.fields.replies);
105 | thraedList.push({
106 | title,
107 | tid,
108 | url,
109 | image,
110 | section,
111 | timestamp,
112 | author: {
113 | username,
114 | uid,
115 | avatar
116 | },
117 | stats: {
118 | viewed,
119 | replied
120 | }
121 | });
122 | });
123 | return thraedList;
124 | } else {
125 | Taro.atMessage({
126 | message: `刷新失败😱`,
127 | type: 'error',
128 | duration: 2000
129 | });
130 | }
131 | },
132 | (): void => {
133 | Taro.atMessage({
134 | message: '网络连接中断😭',
135 | type: 'error',
136 | duration: 2000
137 | });
138 | }
139 | );
140 | }
141 |
142 | private isFinish(): void {
143 | const { hotThreadList } = this.state;
144 | if (hotThreadList.length > 0) {
145 | Taro.stopPullDownRefresh();
146 | Taro.hideLoading();
147 | Taro.atMessage({
148 | message: `刷新成功😁`,
149 | type: 'success',
150 | duration: 1500
151 | });
152 | }
153 | }
154 |
155 | public render(): JSX.Element {
156 | const { hotThreadList } = this.state;
157 | const { statusBarHeight } = this.props;
158 | const threadCards = hotThreadList.map(
159 | (item): JSX.Element => {
160 | return ;
161 | }
162 | );
163 | return (
164 |
165 |
166 |
167 |
172 |
173 | {threadCards}
174 |
175 | );
176 | }
177 | }
178 |
179 | export default Hot as Taro.ComponentClass;
180 |
--------------------------------------------------------------------------------
/src/pages/index/index.scss:
--------------------------------------------------------------------------------
1 | .index-swiper {
2 | height: 450px;
3 |
4 | .swiper-item-image {
5 | display: block;
6 | width: 100%;
7 | height: 450px;
8 | }
9 |
10 | .swiper-item-title {
11 | position: absolute;
12 | width: 100%;
13 | top: 400px;
14 | color: #fff;
15 | background: linear-gradient(bottom, black, rgba(0, 0, 0, 0));
16 | font-size: 26px;
17 | font-weight: 580;
18 | line-height: 50px;
19 | text-align: center;
20 | }
21 | }
22 |
23 | .thread-list {
24 | padding-top: 10px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/index/index.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View, Swiper, SwiperItem, Text, Image } from '@tarojs/components';
4 | import { AtMessage, AtNavBar } from 'taro-ui';
5 |
6 | import ThreadCard from '../../components/ThreadCard/threadCard';
7 | import { IThreadMeta } from '../../interfaces/thread';
8 | import { IHotThreadItemRespond } from '../../interfaces/respond';
9 | import { IAccount } from '../../interfaces/account';
10 | import { initCredential } from '../../actions/account';
11 | import { getSystemInfo } from '../../actions/system';
12 |
13 | import './index.scss';
14 |
15 | interface Props {
16 | auth: boolean;
17 | account: IAccount;
18 | statusBarHeight: number;
19 | initCredential: () => void;
20 | getSystemInfo: () => void;
21 | }
22 |
23 | interface State {
24 | bannerThreadList: IThreadMeta[];
25 | indexThreadList: IThreadMeta[];
26 | }
27 |
28 | @connect(
29 | ({ account, system }) => ({
30 | auth: account.auth,
31 | account: account.account,
32 | statusBarHeight: system.statusBarHeight
33 | }),
34 | dispatch => ({
35 | initCredential() {
36 | dispatch(initCredential());
37 | },
38 | getSystemInfo() {
39 | dispatch(getSystemInfo());
40 | }
41 | })
42 | )
43 | class Index extends Taro.Component {
44 | public config: Taro.Config = {
45 | navigationBarTitleText: 'SteamCN 蒸汽动力',
46 | enablePullDownRefresh: true
47 | };
48 |
49 | public state = {
50 | bannerThreadList: Array(),
51 | indexThreadList: Array()
52 | };
53 |
54 | public constructor(props: Props) {
55 | super(props);
56 | this.props.getSystemInfo();
57 | }
58 |
59 | public componentDidShow(): void {
60 | this.props.initCredential();
61 | }
62 |
63 | public componentDidMount(): void {
64 | this.initHome();
65 | }
66 |
67 | public onPullDownRefresh(): void {
68 | this.initHome();
69 | }
70 |
71 | private initHome(): void {
72 | Taro.showLoading({
73 | title: '努力加载中 💦'
74 | });
75 | this.requestBannerThreadList();
76 | this.requestIndexThreadList();
77 | }
78 |
79 | private requestBannerThreadList(): void {
80 | this.requestHot(431).then((res): void => {
81 | if (res) {
82 | this.setState(
83 | {
84 | bannerThreadList: res
85 | },
86 | this.isFinish
87 | );
88 | }
89 | });
90 | }
91 |
92 | public requestIndexThreadList(): void {
93 | this.requestHot(432).then((res): void => {
94 | if (res) {
95 | this.setState(
96 | {
97 | indexThreadList: res
98 | },
99 | this.isFinish
100 | );
101 | }
102 | });
103 | }
104 |
105 | private requestHot(bid: number): Promise {
106 | const { account } = this.props;
107 | return Taro.request({
108 | url: `https://vnext.steamcn.com/v1/forum/hot/${bid}`,
109 | data: {},
110 | header: {
111 | authorization: account.accessToken
112 | },
113 | method: 'GET',
114 | dataType: 'json',
115 | responseType: 'text'
116 | }).then(
117 | (res): IThreadMeta[] | undefined => {
118 | if (res.statusCode === 200) {
119 | console.log(res.data);
120 | const itemlist = res.data.itemlist as IHotThreadItemRespond[];
121 | let thraedList = Array();
122 | itemlist.forEach((item): void => {
123 | const title = item.title;
124 | const tid = parseInt(item.id);
125 | const url = `https://steamcn.com/t${tid}-1-1`;
126 | const image =
127 | item.pic.indexOf('https://blob.steamcn.com') === -1
128 | ? `https://blob.steamcn.com/${item.pic}`
129 | : item.pic;
130 | const section = item.fields.forumname;
131 | const timestamp = parseInt(item.fields.dateline);
132 | const username = item.fields.author;
133 | const uid = parseInt(item.fields.authorid);
134 | const avatar = item.fields.avatar_middle;
135 | const viewed = parseInt(item.fields.views);
136 | const replied = parseInt(item.fields.replies);
137 | thraedList.push({
138 | title,
139 | tid,
140 | url,
141 | image,
142 | section,
143 | timestamp,
144 | author: {
145 | username,
146 | uid,
147 | avatar
148 | },
149 | stats: {
150 | viewed,
151 | replied
152 | }
153 | });
154 | });
155 | return thraedList;
156 | } else {
157 | Taro.atMessage({
158 | message: `刷新失败😱`,
159 | type: 'error',
160 | duration: 2000
161 | });
162 | }
163 | },
164 | (): void => {
165 | Taro.atMessage({
166 | message: '网络连接中断😭',
167 | type: 'error',
168 | duration: 2000
169 | });
170 | }
171 | );
172 | }
173 |
174 | private isFinish(): void {
175 | const { bannerThreadList, indexThreadList } = this.state;
176 | if (bannerThreadList.length > 0 && indexThreadList.length > 0) {
177 | Taro.stopPullDownRefresh();
178 | Taro.hideLoading();
179 | Taro.atMessage({
180 | message: `刷新成功😁`,
181 | type: 'success',
182 | duration: 1500
183 | });
184 | }
185 | }
186 |
187 | public onShareAppMessage(): {
188 | title: string;
189 | path: string;
190 | } {
191 | return {
192 | title: 'SteamCN 蒸汽动力',
193 | path: '/pages/index/index'
194 | };
195 | }
196 |
197 | private toThread(tid: number): void {
198 | Taro.navigateTo({
199 | url: `/pages/thread/thread?tid=${tid}`
200 | });
201 | }
202 |
203 | public render(): JSX.Element {
204 | const { bannerThreadList, indexThreadList } = this.state;
205 | const { statusBarHeight } = this.props;
206 | const swiperItems = bannerThreadList.map(
207 | (item): JSX.Element => {
208 | return (
209 |
213 |
218 | {item.title}
219 |
220 | );
221 | }
222 | );
223 | const threadCards = indexThreadList.map(
224 | (item): JSX.Element => {
225 | return ;
226 | }
227 | );
228 |
229 | return (
230 |
231 |
232 |
233 |
238 |
239 |
246 | {swiperItems}
247 |
248 | {threadCards}
249 |
250 | );
251 | }
252 | }
253 |
254 | export default Index as Taro.ComponentClass;
255 |
--------------------------------------------------------------------------------
/src/pages/new/new.scss:
--------------------------------------------------------------------------------
1 | .thread-list {
2 | padding-top: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/new/new.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View } from '@tarojs/components';
4 | import { AtMessage, AtNavBar } from 'taro-ui';
5 |
6 | import ThreadCard from '../../components/ThreadCard/threadCard';
7 | import { IThreadMeta } from '../../interfaces/thread';
8 | import { IHotThreadItemRespond } from '../../interfaces/respond';
9 | import { IAccount } from '../../interfaces/account';
10 |
11 | import './new.scss';
12 |
13 | interface Props {
14 | auth: boolean;
15 | account: IAccount;
16 | statusBarHeight: number;
17 | }
18 |
19 | interface State {
20 | newThreadList: IThreadMeta[];
21 | }
22 |
23 | @connect(({ account, system }) => ({
24 | auth: account.auth,
25 | account: account.account,
26 | statusBarHeight: system.statusBarHeight
27 | }))
28 | class New extends Taro.Component {
29 | public config: Taro.Config = {
30 | navigationBarTitleText: '最新回复',
31 | enablePullDownRefresh: true
32 | };
33 |
34 | public state = {
35 | newThreadList: Array()
36 | };
37 |
38 | public componentDidMount(): void {
39 | this.initNew();
40 | }
41 |
42 | public onShareAppMessage(): {
43 | title: string;
44 | path: string;
45 | } {
46 | return {
47 | title: 'SteamCN 蒸汽动力 - 最新回复',
48 | path: 'pages/new/new'
49 | };
50 | }
51 |
52 | public onPullDownRefresh(): void {
53 | this.initNew();
54 | }
55 |
56 | private initNew(): void {
57 | Taro.showLoading({
58 | title: '努力加载中 💦'
59 | });
60 | this.requestNewThreadList();
61 | }
62 |
63 | private requestNewThreadList(): void {
64 | this.requestHot(433).then((res): void => {
65 | if (res) {
66 | this.setState(
67 | {
68 | newThreadList: res
69 | },
70 | this.isFinish
71 | );
72 | }
73 | });
74 | }
75 |
76 | private requestHot(bid: number): Promise {
77 | const { account } = this.props;
78 | return Taro.request({
79 | url: `https://vnext.steamcn.com/v1/forum/hot/${bid}`,
80 | data: {},
81 | header: {
82 | authorization: account.accessToken
83 | },
84 | method: 'GET',
85 | dataType: 'json',
86 | responseType: 'text'
87 | }).then(
88 | (res): IThreadMeta[] | undefined => {
89 | if (res.statusCode === 200) {
90 | console.log(res.data);
91 | const itemlist = res.data.itemlist as IHotThreadItemRespond[];
92 | let thraedList = Array();
93 | itemlist.forEach((item): void => {
94 | const title = item.title;
95 | const tid = parseInt(item.id);
96 | const url = `https://steamcn.com/t${tid}-1-1`;
97 | const image = item.coverpath;
98 | const section = item.fields.forumname;
99 | const timestamp = parseInt(item.fields.dateline);
100 | const username = item.fields.author;
101 | const uid = parseInt(item.fields.authorid);
102 | const avatar = item.fields.avatar_middle;
103 | const viewed = parseInt(item.fields.views);
104 | const replied = parseInt(item.fields.replies);
105 | thraedList.push({
106 | title,
107 | tid,
108 | url,
109 | image,
110 | section,
111 | timestamp,
112 | author: {
113 | username,
114 | uid,
115 | avatar
116 | },
117 | stats: {
118 | viewed,
119 | replied
120 | }
121 | });
122 | });
123 | return thraedList;
124 | } else {
125 | Taro.atMessage({
126 | message: `刷新失败😱`,
127 | type: 'error',
128 | duration: 2000
129 | });
130 | }
131 | },
132 | (): void => {
133 | Taro.atMessage({
134 | message: '网络连接中断😭',
135 | type: 'error',
136 | duration: 2000
137 | });
138 | }
139 | );
140 | }
141 |
142 | private isFinish(): void {
143 | const { newThreadList } = this.state;
144 | if (newThreadList.length > 0) {
145 | Taro.stopPullDownRefresh();
146 | Taro.hideLoading();
147 | Taro.atMessage({
148 | message: `刷新成功😁`,
149 | type: 'success',
150 | duration: 1500
151 | });
152 | }
153 | }
154 |
155 | public render(): JSX.Element {
156 | const { newThreadList } = this.state;
157 | const { statusBarHeight } = this.props;
158 | const threadCards = newThreadList.map(
159 | (item): JSX.Element => {
160 | return ;
161 | }
162 | );
163 | return (
164 |
165 |
166 |
167 |
172 |
173 | {threadCards}
174 |
175 | );
176 | }
177 | }
178 |
179 | export default New as Taro.ComponentClass;
180 |
--------------------------------------------------------------------------------
/src/pages/section/section.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 10px 5px;
3 |
4 | .group {
5 | margin: 0px 5px 25px 5px;
6 |
7 | .groupTitle {
8 | margin: 20px 40px;
9 | font-size: 35px;
10 | color: #666;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/section/section.tsx:
--------------------------------------------------------------------------------
1 | import Taro from '@tarojs/taro';
2 | import { connect } from '@tarojs/redux';
3 | import { Text, View } from '@tarojs/components';
4 | import { AtNavBar } from 'taro-ui';
5 |
6 | import SectionGroupList from '../../components/SectionGroupList/sectionGroupList';
7 |
8 | import './section.scss';
9 |
10 | interface Props {
11 | statusBarHeight: number;
12 | }
13 |
14 | @connect(({ system }) => ({
15 | statusBarHeight: system.statusBarHeight
16 | }))
17 | class Section extends Taro.Component {
18 | public config: Taro.Config = {
19 | navigationBarTitleText: '板块'
20 | };
21 |
22 | private _sectionMeta = {
23 | forum: [
24 | //平台周边
25 | {
26 | title: '热点聚焦',
27 | desc: '周边热门话题讨论',
28 | fid: 161
29 | },
30 | {
31 | title: '平台研讨',
32 | desc: '进阶的技术性交流',
33 | fid: 127
34 | },
35 | {
36 | title: '华语汉化',
37 | desc: '适用于正版游戏的汉化',
38 | fid: 257
39 | },
40 | {
41 | title: '成就指南',
42 | desc: '游戏成就和达成攻略',
43 | fid: 235
44 | },
45 | {
46 | title: '游戏互鉴',
47 | desc: '发布原创游戏评测',
48 | fid: 129
49 | },
50 | {
51 | title: '福利放送',
52 | desc: '免费领取游戏的机会',
53 | fid: 319
54 | },
55 | {
56 | title: '购物心得',
57 | desc: '购物经验和优惠信息',
58 | fid: 234
59 | },
60 | {
61 | title: '慈善包',
62 | desc: '物美价廉的游戏集合',
63 | fid: 271
64 | },
65 | {
66 | title: '交易中心',
67 | desc: '游戏、卡片、道具、市场',
68 | fid: 201
69 | },
70 | {
71 | title: '分享互赠',
72 | desc: '送出余裕的游戏或道具',
73 | fid: 254
74 | },
75 | {
76 | title: '资源载点',
77 | desc: '备份、美化、媒体等资源',
78 | fid: 189
79 | }
80 | ],
81 | problem: [
82 | //问题互助
83 | {
84 | title: '技术问题',
85 | desc: '异常故障和账户帮助',
86 | fid: 301
87 | },
88 | {
89 | title: '购物问题',
90 | desc: '跨区、支付、慈善包等问题',
91 | fid: 302
92 | },
93 | {
94 | title: '社区问题',
95 | desc: '积分、账号和制度的疑问',
96 | fid: 304
97 | },
98 | {
99 | title: '资源 / 汉化问题',
100 | desc: '寻求资源/汉化相关问题',
101 | fid: 318
102 | },
103 | {
104 | title: '游戏 / 成就问题',
105 | desc: '针对某一游戏的特定问题',
106 | fid: 303
107 | },
108 | {
109 | title: '福利 / 软硬 / 其它',
110 | desc: '福利,软硬网络或其他问题',
111 | fid: 322
112 | },
113 | {
114 | title: '魔法学园',
115 | desc: '呼啦呼,雪尼拉,噜巴拉',
116 | fid: 311
117 | }
118 | ],
119 | discussion: [
120 | //游戏讨论
121 | {
122 | title: '综合讨论',
123 | desc: '一般游戏交流区',
124 | fid: 251
125 | },
126 | {
127 | title: '刀塔',
128 | desc: 'DOTA',
129 | fid: 305
130 | },
131 | {
132 | title: '全球攻势',
133 | desc: 'Global Offensive',
134 | fid: 299
135 | },
136 | {
137 | title: '生存类游戏',
138 | desc: 'Survival 类游戏',
139 | fid: 291
140 | },
141 | {
142 | title: '侠盗猎车手',
143 | desc: 'Grand Theft Auto',
144 | fid: 312
145 | },
146 | {
147 | title: '威乐',
148 | desc: 'Valve 旗下游戏讨论',
149 | fid: 244
150 | },
151 | {
152 | title: '艺电',
153 | desc: 'EA 旗下游戏讨论',
154 | fid: 246
155 | },
156 | {
157 | title: '育碧',
158 | desc: 'Ubisoft 旗下游戏讨论',
159 | fid: 245
160 | },
161 | {
162 | title: '动视暴雪',
163 | desc: 'Activision Blizzard 游戏',
164 | fid: 248
165 | }
166 | ],
167 | peripheral: [
168 | //论坛周边
169 | {
170 | title: '谈天说地',
171 | desc: '论坛指定水区',
172 | fid: 148
173 | },
174 | {
175 | title: '旅游摄影',
176 | desc: '摄影作品 | 旅游图文',
177 | fid: 259
178 | },
179 | {
180 | title: '美眉美食',
181 | desc: '美眉鲜肉 | 美食图片',
182 | fid: 273
183 | },
184 | {
185 | title: '软硬兼施',
186 | desc: '软件硬件 | 苹果世界 | 安卓天地',
187 | fid: 200
188 | },
189 | {
190 | title: '开箱晒物',
191 | desc: '开箱分享',
192 | fid: 330
193 | }
194 | ],
195 | platform: [
196 | //友商平台
197 | {
198 | title: 'Origin',
199 | desc: '艺电旗下游戏平台',
200 | fid: 232
201 | },
202 | {
203 | title: 'Uplay',
204 | desc: '育碧旗下游戏平台',
205 | fid: 274
206 | },
207 | {
208 | title: 'GOG',
209 | desc: '无 DRM 机制的自由平台',
210 | fid: 276
211 | },
212 | {
213 | title: '杉果',
214 | desc: '中电博亚旗下代理平台',
215 | fid: 316
216 | },
217 | {
218 | title: 'Epic Games',
219 | desc: '虚幻引擎开发商的新推平台',
220 | fid: 335
221 | },
222 | {
223 | title: '其它平台',
224 | desc: '其他销售网站旗下平台',
225 | fid: 277
226 | },
227 | {
228 | title: 'Windows 商店',
229 | desc: '微软游戏与软件平台',
230 | fid: 326
231 | },
232 | {
233 | title: '方块',
234 | desc: '方块游戏平台',
235 | fid: 332
236 | },
237 | {
238 | title: 'WeGame',
239 | desc: '腾讯游戏平台',
240 | fid: 325
241 | },
242 | {
243 | title: '主机平台',
244 | desc: 'PS 与 XBOX 等主机',
245 | fid: 275
246 | },
247 | {
248 | title: '移动平台',
249 | desc: '安卓与 iOS 手机游戏',
250 | fid: 328
251 | }
252 | ],
253 | official: [
254 | //社区服务
255 | {
256 | title: '公告发布',
257 | desc: '社区告示和动态',
258 | fid: 140
259 | },
260 | {
261 | title: '投诉反馈',
262 | desc: '意见、建议和投诉',
263 | fid: 197
264 | },
265 | {
266 | title: '社区活动',
267 | desc: '社区与节日活动',
268 | fid: 238
269 | }
270 | ]
271 | };
272 |
273 | public render(): JSX.Element {
274 | const { statusBarHeight } = this.props;
275 | return (
276 |
277 |
282 |
283 |
284 |
285 |
286 | 平台周边
287 |
288 |
289 |
290 |
291 |
292 | 问题互助
293 |
294 |
297 |
298 |
299 |
300 | 游戏讨论
301 |
302 |
305 |
306 |
307 |
308 | 论坛周边
309 |
310 |
313 |
314 |
315 |
316 | 友商平台
317 |
318 |
321 |
322 |
323 |
324 | 社区服务
325 |
326 |
329 |
330 |
331 |
332 | );
333 | }
334 | }
335 |
336 | export default Section as Taro.ComponentClass;
337 |
--------------------------------------------------------------------------------
/src/pages/section/sectionThreadList.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/section/sectionThreadList.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View } from '@tarojs/components';
4 | import { AtMessage, AtLoadMore, AtNavBar } from 'taro-ui';
5 |
6 | import ThreadCard from '../../components/ThreadCard/threadCard';
7 | import { IThreadMeta } from '../../interfaces/thread';
8 | import { IAccount } from '../../interfaces/account';
9 | import { ISectionThreadListItem } from '../../interfaces/respond';
10 |
11 | import './sectionThreadList.scss';
12 |
13 | interface Props {
14 | auth: boolean;
15 | account: IAccount;
16 | statusBarHeight: number;
17 | }
18 |
19 | interface State {
20 | sectionThreadList: IThreadMeta[];
21 | pageNum: number;
22 | loadMoreVisibility: boolean;
23 | loadMoreStatus: 'more' | 'loading' | 'noMore';
24 | }
25 |
26 | @connect(({ account, system }) => ({
27 | auth: account.auth,
28 | account: account.account,
29 | statusBarHeight: system.statusBarHeight
30 | }))
31 | class SectionThreadList extends Taro.Component {
32 | public config: Taro.Config = {
33 | navigationBarTitleText: '板块',
34 | enablePullDownRefresh: true,
35 | onReachBottomDistance: 300
36 | };
37 |
38 | public state = {
39 | sectionThreadList: Array(),
40 | pageNum: 1,
41 | loadMoreVisibility: false,
42 | loadMoreStatus: 'loading' as 'more' | 'loading' | 'noMore'
43 | };
44 |
45 | public componentDidMount(): void {
46 | const { pageNum } = this.state;
47 | Taro.showLoading({
48 | title: '努力加载中 💦'
49 | });
50 | this.fetchSection(parseInt(this.$router.params.fid), pageNum);
51 | }
52 |
53 | public onPullDownRefresh(): void {
54 | Taro.showLoading({
55 | title: '努力加载中 💦'
56 | });
57 | this.setState(
58 | {
59 | sectionThreadList: Array(),
60 | pageNum: 1
61 | },
62 | (): void => {
63 | const { pageNum } = this.state;
64 | this.fetchSection(parseInt(this.$router.params.fid), pageNum);
65 | }
66 | );
67 | }
68 |
69 | public onReachBottom(): void {
70 | const currentPageNum = this.state.pageNum;
71 | this.setState(
72 | {
73 | pageNum: currentPageNum + 1,
74 | loadMoreVisibility: true
75 | },
76 | (): void => {
77 | const { pageNum } = this.state;
78 | this.fetchSection(parseInt(this.$router.params.fid), pageNum);
79 | }
80 | );
81 | }
82 |
83 | private fetchSection(fid: number, page: number): void {
84 | const { account } = this.props;
85 | Taro.request({
86 | url: `https://vnext.steamcn.com/v1/forum/thread?fid=${fid}&page=${page}&orderby=dateline`,
87 | data: {},
88 | header: {
89 | authorization: account.accessToken
90 | },
91 | method: 'GET',
92 | dataType: 'json',
93 | responseType: 'text'
94 | }).then(
95 | (res): void => {
96 | if (res.statusCode === 200) {
97 | const threadListRespond = res.data as ISectionThreadListItem[];
98 |
99 | let threadList = Array();
100 | for (let i = 0; i < threadListRespond.length; i++) {
101 | const sectionThreadListItem = threadListRespond[i];
102 |
103 | // 跳过置顶帖
104 | if (sectionThreadListItem.displayorder !== '0') {
105 | continue;
106 | }
107 |
108 | const title = sectionThreadListItem.subject;
109 | const tid = parseInt(sectionThreadListItem.tid);
110 | const url = `https://steamcn.com/t${tid}-1-1`;
111 | const image = sectionThreadListItem.coverpath;
112 | const section = sectionThreadListItem.forumname;
113 | const timestamp = parseInt(sectionThreadListItem.dbdateline);
114 | const username = sectionThreadListItem.author;
115 | const uid = parseInt(sectionThreadListItem.authorid);
116 | const avatar = `https://steamcn.com/uc_server/avatar.php?uid=${uid}&size=middle`;
117 | const viewed = sectionThreadListItem.views;
118 | const replied = parseInt(sectionThreadListItem.replies);
119 |
120 | threadList.push({
121 | title,
122 | tid,
123 | url,
124 | image,
125 | section,
126 | timestamp,
127 | author: {
128 | username,
129 | uid,
130 | avatar
131 | },
132 | stats: {
133 | viewed,
134 | replied
135 | }
136 | });
137 | }
138 | const { sectionThreadList } = this.state;
139 | this.setState(
140 | {
141 | sectionThreadList: sectionThreadList.concat(threadList),
142 | loadMoreVisibility: false
143 | },
144 | (): void => {
145 | Taro.hideLoading();
146 | Taro.stopPullDownRefresh();
147 | }
148 | );
149 | } else {
150 | const { auth } = this.props;
151 | Taro.hideLoading();
152 | Taro.stopPullDownRefresh();
153 | if (auth) {
154 | Taro.atMessage({
155 | message: `登录凭据过期,请重新登录🥀`,
156 | type: 'error',
157 | duration: 3000
158 | });
159 | } else {
160 | let message = res.data.message as string;
161 | message = message.replace('
', '');
162 | Taro.atMessage({
163 | message: `无法查看本板块😱,${message}`,
164 | type: 'error',
165 | duration: 3000
166 | });
167 | }
168 | }
169 | },
170 | (): void => {
171 | Taro.hideLoading();
172 | Taro.stopPullDownRefresh();
173 | Taro.atMessage({
174 | message: '网络连接中断😭',
175 | type: 'error',
176 | duration: 2000
177 | });
178 | }
179 | );
180 | }
181 |
182 | public render(): JSX.Element {
183 | const {
184 | sectionThreadList,
185 | loadMoreStatus,
186 | loadMoreVisibility
187 | } = this.state;
188 | const { statusBarHeight } = this.props;
189 | const threadCards = sectionThreadList.map(
190 | (item): JSX.Element => {
191 | return ;
192 | }
193 | );
194 | return (
195 |
196 |
197 |
198 | {
204 | Taro.navigateBack({ delta: 1 });
205 | }}
206 | border={false}
207 | />
208 |
209 | {threadCards}
210 | {loadMoreVisibility && (
211 |
216 | )}
217 |
218 | );
219 | }
220 | }
221 |
222 | export default SectionThreadList as Taro.ComponentClass;
223 |
--------------------------------------------------------------------------------
/src/pages/thread/thread.scss:
--------------------------------------------------------------------------------
1 | .fab {
2 | position: fixed;
3 | right: 48px;
4 | bottom: 48px;
5 | z-index: 100;
6 |
7 | .fab-bottom {
8 | box-shadow: 0 6px 10px -2px rgba(0, 0, 0, 0.2),
9 | 0 12px 20px 0 rgba(0, 0, 0, 0.14), 0 2px 36px 0 rgba(0, 0, 0, 0.12);
10 | box-sizing: border-box;
11 | }
12 | }
13 |
14 | .header {
15 | margin: 15px 25px;
16 |
17 | .title {
18 | font-size: 35px;
19 | font-weight: 700;
20 | }
21 | }
22 |
23 | .author {
24 | display: flex;
25 | margin: 15px;
26 | align-items: center;
27 | border-radius: 5px;
28 | box-shadow: 2px 2px 2px 2px rgba(136, 136, 136, 0.3);
29 |
30 | .avatar {
31 | margin-left: 25px;
32 | }
33 |
34 | .name {
35 | font-size: 27px;
36 | font-weight: 700;
37 | margin-bottom: 7px;
38 | }
39 |
40 | .info {
41 | display: flex;
42 | margin: 35px 0px 35px 20px;
43 | flex-direction: column;
44 | justify-content: space-between;
45 | }
46 |
47 | .others {
48 | font-size: 20px;
49 | color: #666;
50 |
51 | .time {
52 | margin-right: 20px;
53 | }
54 | }
55 | }
56 |
57 | .content {
58 | margin: 0px 22px;
59 | font-size: 28px;
60 | }
61 |
--------------------------------------------------------------------------------
/src/pages/thread/thread.tsx:
--------------------------------------------------------------------------------
1 | import { connect } from '@tarojs/redux';
2 | import Taro from '@tarojs/taro';
3 | import { View, Text } from '@tarojs/components';
4 | import { AtDivider, AtAvatar, AtMessage, AtLoadMore, AtNavBar } from 'taro-ui';
5 | import dayjs from 'dayjs';
6 |
7 | import { IThread, IReply } from '../../interfaces/thread';
8 | import { IThreadRespond } from '../../interfaces/respond';
9 | import { IAccount } from '../../interfaces/account';
10 | import ParserRichText from '../../components/ParserRichText/parserRichText';
11 | import ReplyCard from '../../components/ReplyCard/replyCard';
12 | import contentCleaner from '../../utils/cleaner';
13 |
14 | import './thread.scss';
15 |
16 | interface Props {
17 | auth: boolean;
18 | account: IAccount;
19 | statusBarHeight: number;
20 | }
21 |
22 | interface State {
23 | pageNum: number;
24 | loadedPosition: number;
25 | thread: IThread;
26 | loadMoreVisibility: boolean;
27 | loadMoreStatus: 'more' | 'loading' | 'noMore';
28 | }
29 |
30 | @connect(({ account, system }) => ({
31 | auth: account.auth,
32 | account: account.account,
33 | statusBarHeight: system.statusBarHeight
34 | }))
35 | class Thread extends Taro.Component {
36 | public config: Taro.Config = {
37 | navigationBarTitleText: '主题',
38 | onReachBottomDistance: 300
39 | };
40 |
41 | public state = {
42 | pageNum: 1,
43 | loadedPosition: 0,
44 | loadMoreVisibility: false,
45 | loadMoreStatus: 'loading' as 'more' | 'loading' | 'noMore',
46 | thread: {
47 | title: '',
48 | tid: 0,
49 | timestamp: 0,
50 | viewed: 0,
51 | replied: 0,
52 | content: '',
53 | maxPosition: 0,
54 | author: {
55 | username: '',
56 | uid: 0,
57 | avatar: ''
58 | },
59 | replies: [
60 | {
61 | user: {
62 | username: '',
63 | uid: 0,
64 | avatar: ''
65 | },
66 | content: '',
67 | timestamp: 0,
68 | position: 0
69 | }
70 | ]
71 | }
72 | };
73 |
74 | public componentDidMount(): void {
75 | const { pageNum } = this.state;
76 | Taro.showLoading({
77 | title: '努力加载中 💦'
78 | });
79 | this.fetchThread(parseInt(this.$router.params.tid), pageNum);
80 | }
81 |
82 | public onShareAppMessage(): {
83 | title: string;
84 | path: string;
85 | } {
86 | const { thread } = this.state;
87 | return {
88 | title: `${thread.title} - SteamCN 蒸汽动力`,
89 | path: `/pages/thread/thread?tid=${this.$router.params.tid}`
90 | };
91 | }
92 |
93 | public onReachBottom(): void {
94 | const { loadedPosition, thread } = this.state;
95 | const currentPageNum = this.state.pageNum;
96 | if (loadedPosition < thread.maxPosition) {
97 | this.setState(
98 | {
99 | pageNum: currentPageNum + 1,
100 | loadMoreVisibility: true,
101 | loadMoreStatus: 'loading'
102 | },
103 | (): void => {
104 | const { pageNum } = this.state;
105 | this.fetchThread(parseInt(this.$router.params.tid), pageNum);
106 | }
107 | );
108 | } else {
109 | this.setState({
110 | loadMoreVisibility: true,
111 | loadMoreStatus: 'noMore'
112 | });
113 | }
114 | }
115 |
116 | private fetchThread(tid: number, pageNum: number): void {
117 | const { account } = this.props;
118 | Taro.request({
119 | url: `https://vnext.steamcn.com/v1/forum/thread/${tid}?page=${pageNum}`,
120 | data: {},
121 | header: {
122 | authorization: account.accessToken
123 | },
124 | method: 'GET',
125 | dataType: 'json',
126 | responseType: 'text'
127 | }).then(
128 | (res): void => {
129 | if (res.statusCode === 200) {
130 | const threadData = res.data as IThreadRespond;
131 | const currentPageNum = this.state.pageNum;
132 | if (currentPageNum === 1) {
133 | const { loadedPosition } = this.state;
134 | const floors = threadData.floors;
135 | const title = threadData.thread.subject;
136 | const tid = parseInt(threadData.thread.tid);
137 | const timestamp = parseInt(threadData.thread.dateline);
138 | const viewed = threadData.thread.views;
139 | const replied = threadData.thread.replies;
140 | const content = contentCleaner(threadData.floors[0].message);
141 | const maxPosition = parseInt(threadData.thread.maxposition);
142 | const username = threadData.thread.author;
143 | const uid = parseInt(threadData.thread.authorid);
144 | const avatar = `https://steamcn.com/uc_server/avatar.php?uid=${uid}&size=middle`;
145 |
146 | // console.log(content) // 打印主贴 HTML 代码
147 |
148 | let replies = Array();
149 | for (let i = 1; i < floors.length; i++) {
150 | const floor = floors[i];
151 | const username = floor.author;
152 | const uid = parseInt(floor.authorid);
153 | const avatar = `https://steamcn.com/uc_server/avatar.php?uid=${uid}&size=middle`;
154 | const content = contentCleaner(floor.message);
155 | const timestamp = parseInt(floor.dbdateline);
156 | const position = parseInt(floor.position);
157 | replies.push({
158 | user: {
159 | username,
160 | uid,
161 | avatar
162 | },
163 | content,
164 | timestamp,
165 | position
166 | });
167 | }
168 | this.setState({
169 | loadedPosition: loadedPosition + floors.length,
170 | thread: {
171 | title,
172 | tid,
173 | timestamp,
174 | viewed,
175 | replied,
176 | content,
177 | maxPosition,
178 | author: {
179 | username,
180 | uid,
181 | avatar
182 | },
183 | replies
184 | }
185 | });
186 | Taro.hideLoading();
187 | } else {
188 | const { loadedPosition, thread } = this.state;
189 | const floors = threadData.floors;
190 |
191 | let replies = Array();
192 | for (let i = 0; i < floors.length; i++) {
193 | const floor = floors[i];
194 | const username = floor.author;
195 | const uid = parseInt(floor.authorid);
196 | const avatar = `https://steamcn.com/uc_server/avatar.php?uid=${uid}&size=middle`;
197 | const content = contentCleaner(floor.message);
198 | const timestamp = parseInt(floor.dbdateline);
199 | const position = parseInt(floor.position);
200 | replies.push({
201 | user: {
202 | username,
203 | uid,
204 | avatar
205 | },
206 | content,
207 | timestamp,
208 | position
209 | });
210 | }
211 | this.setState({
212 | loadedPosition: loadedPosition + floors.length,
213 | loadMoreVisibility: false,
214 | loadMoreStatus: 'more',
215 | thread: {
216 | ...thread,
217 | replies: thread.replies.concat(replies)
218 | }
219 | });
220 | }
221 | } else {
222 | const { auth } = this.props;
223 | Taro.hideLoading();
224 | if (auth) {
225 | Taro.atMessage({
226 | message: `登录凭据过期,请重新登录🥀`,
227 | type: 'error',
228 | duration: 3000
229 | });
230 | } else {
231 | let message = res.data.message as string;
232 | message = message.replace('
', '');
233 | Taro.atMessage({
234 | message: `无法查看帖子😱,${message}`,
235 | type: 'error',
236 | duration: 3000
237 | });
238 | }
239 | }
240 | },
241 | (): void => {
242 | Taro.atMessage({
243 | message: '网络连接中断😭',
244 | type: 'error',
245 | duration: 2000
246 | });
247 | }
248 | );
249 | }
250 |
251 | public render(): JSX.Element {
252 | const pageDepth = Taro.getCurrentPages().length;
253 | const { thread, loadMoreStatus, loadMoreVisibility } = this.state;
254 | const { statusBarHeight } = this.props;
255 | const repliesArea = thread.replies.map(
256 | (reply): JSX.Element => {
257 | return (
258 |
259 |
260 |
261 | );
262 | }
263 | );
264 | return (
265 | // // 组件报错,不可用
266 | // // 效果挺好
267 | // //最方便,没有任何排版,样式原始,没有表格,图片不自适应
268 | //
269 |
270 |
271 |
272 | {
280 | Taro.switchTab({ url: '/pages/index/index' });
281 | }
282 | : (): void => {
283 | Taro.navigateBack({ delta: 1 });
284 | }
285 | }
286 | border={false}
287 | />
288 |
289 |
290 |
291 | {thread.title}
292 |
293 |
294 |
295 |
296 |
302 |
303 | {thread.author.username}
304 |
305 |
306 | {dayjs.unix(thread.timestamp as number).fromNow()}
307 |
308 |
309 | 阅读 {thread.viewed} · 回复 {thread.replied}
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 | {repliesArea}
324 |
325 | {loadMoreVisibility && (
326 |
331 | )}
332 |
333 | );
334 | }
335 | }
336 |
337 | export default Thread as Taro.ComponentClass;
338 |
--------------------------------------------------------------------------------
/src/reducers/account.ts:
--------------------------------------------------------------------------------
1 | import {
2 | INIT_CREDENTIAL,
3 | INVALID_CREDENTIAL,
4 | LOGIN,
5 | LOGIN_SUCCESS,
6 | LOGIN_ERROR,
7 | LOGOUT,
8 | LOGOUT_SUCCESS,
9 | LOGOUT_ERROR
10 | } from '../constants/account';
11 |
12 | const INITIAL_STATE = {
13 | auth: false,
14 | account: {
15 | uid: 0,
16 | username: '',
17 | email: '',
18 | avatar: '',
19 | groupid: 0,
20 | createAt: '',
21 | updatedAt: '',
22 | accessToken: ''
23 | }
24 | };
25 |
26 | export default function account(
27 | state = INITIAL_STATE,
28 | action
29 | ): {
30 | auth: boolean;
31 | account: {
32 | uid: number;
33 | username: string;
34 | email: string;
35 | avatar: string;
36 | groupid: number;
37 | createAt: string;
38 | updatedAt: string;
39 | accessToken: string;
40 | };
41 | } {
42 | switch (action.type) {
43 | case INIT_CREDENTIAL:
44 | return {
45 | ...state,
46 | auth: action.payload.auth,
47 | account: action.payload.account
48 | };
49 | case INVALID_CREDENTIAL:
50 | return INITIAL_STATE;
51 | case LOGIN:
52 | return state;
53 | case LOGIN_SUCCESS:
54 | return {
55 | ...state,
56 | auth: action.payload.auth,
57 | account: action.payload.account
58 | };
59 | case LOGIN_ERROR:
60 | return state;
61 | case LOGOUT:
62 | return state;
63 | case LOGOUT_SUCCESS:
64 | return INITIAL_STATE;
65 | case LOGOUT_ERROR:
66 | return state;
67 | default:
68 | return state;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import account from './account';
3 | import system from './system';
4 |
5 | export default combineReducers({
6 | account,
7 | system
8 | });
9 |
--------------------------------------------------------------------------------
/src/reducers/system.ts:
--------------------------------------------------------------------------------
1 | import { GET_SYSTEM_INFO } from '../constants/system';
2 | import { SystemInfo } from '../actions/system';
3 |
4 | const INITIAL_STATE = {
5 | statusBarHeight: 0,
6 | menuButtonBoundingClientRect: {
7 | width: 0,
8 | height: 0,
9 | top: 0,
10 | right: 0,
11 | bottom: 0,
12 | left: 0
13 | }
14 | };
15 |
16 | export default function system(
17 | state = INITIAL_STATE,
18 | action: { type: string; payload: SystemInfo }
19 | ): SystemInfo {
20 | switch (action.type) {
21 | case GET_SYSTEM_INFO:
22 | return action.payload;
23 | default:
24 | return state;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose, AnyAction, Store } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 |
5 | const composeEnhancers =
6 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
7 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
8 | // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
9 | })
10 | : compose;
11 |
12 | const middlewares = [thunkMiddleware];
13 |
14 | if (process.env.NODE_ENV === 'development') {
15 | middlewares.push(require('redux-logger').createLogger());
16 | }
17 |
18 | const enhancer = composeEnhancers(
19 | applyMiddleware(...middlewares)
20 | // other store enhancers if any
21 | );
22 |
23 | export default function configStore() {
24 | const store = createStore(rootReducer, enhancer);
25 | return store;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/cleaner.ts:
--------------------------------------------------------------------------------
1 | // import { parse, HTMLElement } from 'node-html-parser';
2 |
3 | export default function contentCleaner(html: string): string {
4 | html = html
5 | .replace(/[\r\n]/g, '') //去掉回车
6 | .replace(
7 | /