├── .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 | ![QRCode](resources/qrcode.jpg) 20 | 21 | ## 部分截图展示 22 | 23 | ### 主页 24 | 25 | ![Home](resources/Home.jpg) 26 | 27 | ### 查看帖子 28 | 29 | ![Thread Preview](resources/Thread.gif) 30 | 31 | ### 板块查看 32 | 33 | ![Section](resources/Section.gif) 34 | 35 | ### 微信分享 36 | 37 | ![Share](resources/Share.jpg) 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 | 32 | 33 |