├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── config ├── dev.js ├── index.js └── prod.js ├── package.json ├── project.config.json ├── src ├── app.config.ts ├── app.css ├── app.tsx ├── components │ ├── loading.css │ ├── loading.tsx │ ├── thread.css │ ├── thread.tsx │ ├── thread_list.tsx │ └── thread_props.d.ts ├── index.html ├── interfaces │ ├── member.d.ts │ ├── node.d.ts │ └── thread.d.ts ├── pages │ ├── hot │ │ ├── hot.config.ts │ │ ├── hot.tsx │ │ └── index.css │ ├── index │ │ ├── index.config.ts │ │ ├── index.css │ │ └── index.tsx │ ├── node_detail │ │ ├── index.css │ │ ├── node_detail.config.ts │ │ └── node_detail.tsx │ ├── nodes │ │ ├── all_node.ts │ │ ├── nodes.config.ts │ │ ├── nodes.css │ │ └── nodes.tsx │ └── thread_detail │ │ ├── index.css │ │ ├── thread_detail.config.ts │ │ └── thread_detail.tsx ├── resource │ ├── hotest.png │ ├── hotest_on.png │ ├── lastest_on.png │ ├── latest.png │ ├── node.png │ ├── node_on.png │ └── spiner.gif └── utils │ ├── api.ts │ └── index.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | // add more generic rulesets here, such as: 4 | // 'eslint:recommended', 5 | 'taro/react' 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .temp/ 3 | node_modules/ 4 | .idea/ 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taro-V2EX 2 | 3 | 这个项目使用了 Taro 构建了一个 [V2ex](www.v2ex.com) 论坛小程序。主要目的在于展示如何使用 TypeScript 构建 Taro 项目和使用内置的事件中心跨组件/路由传递消息。 4 | 5 | [![v2ex.gif](https://i.loli.net/2018/08/15/5b73d86a54514.gif)](https://i.loli.net/2018/08/15/5b73d86a54514.gif) 6 | 7 | 8 | ## 运行 9 | 10 | ```bash 11 | $ npm install 12 | $ npm i -g @tarojs/cli 13 | $ taro build --type weapp --watch 14 | ``` 15 | 16 | ## 限制 17 | 18 | 宥于 V2EX API 的限制,本项目有几个限制: 19 | 20 | 1. 没有「获取更多」的这个 API,除了回复可以全部载入之外所有 API 都不能加载更多信息; 21 | 2. 每个 IP 每小时只能访问 API 100 次,超过便无法访问; 22 | 3. 无法跨域,因此没有 h5 版本; 23 | 24 | ## 建议 25 | 26 | 对于在 Taro 中使用 TypeScript 有一些建议: 27 | 28 | * 使用 tslint 作为编辑器内置的 linter 29 | * 使用 eslint 命令行工具配合 `typescript-eslint-parser` 和 `eslint-config-taro`(见 [.eslintrc](./eslintrc)) 作为 `precommit` 或者 `prepush` 的钩子,在提交或 commit 或编译出现问题时检查代码是否符合 Taro 规范 30 | * 不要在 TypeScript 使用 Redux 的 `connect` 装饰器,使用普通的函数写法,详情见: [#9951](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/9951) 31 | * 当你的项目不那么复杂时,可以不使用 Redux 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['taro', { 4 | framework: 'react', 5 | ts: true 6 | }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /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: 'v2ex', 3 | date: '2018-8-3', 4 | designWidth: 750, 5 | sourceRoot: 'src', 6 | outputRoot: 'dist', 7 | framework: 'react', 8 | babel: { 9 | sourceMap: true, 10 | presets: [ 11 | 'env' 12 | ], 13 | plugins: [ 14 | 'transform-class-properties', 15 | 'transform-decorators-legacy', 16 | 'transform-object-rest-spread' 17 | ] 18 | }, 19 | typescript: { 20 | compilerOptions: { 21 | allowSyntheticDefaultImports: true, 22 | baseUrl: '.', 23 | declaration: false, 24 | experimentalDecorators: true, 25 | jsx: 'react', 26 | jsxFactory: 'Nerv.createElement', 27 | module: 'commonjs', 28 | moduleResolution: 'node', 29 | noImplicitAny: false, 30 | noUnusedLocals: true, 31 | outDir: './dist/', 32 | preserveConstEnums: true, 33 | removeComments: false, 34 | rootDir: '.', 35 | sourceMap: true, 36 | strictNullChecks: true, 37 | target: 'es6' 38 | }, 39 | include: [ 40 | 'src/**/*' 41 | ], 42 | exclude: [ 43 | 'node_modules' 44 | ], 45 | compileOnSave: false 46 | }, 47 | plugins: [], 48 | defineConstants: { 49 | }, 50 | weapp: { 51 | 52 | }, 53 | h5: { 54 | publicPath: '/', 55 | staticDirectory: 'static', 56 | module: { 57 | postcss: { 58 | autoprefixer: { 59 | enable: true 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | module.exports = function (merge) { 67 | if (process.env.NODE_ENV === 'development') { 68 | return merge({}, config, require('./dev')) 69 | } 70 | return merge({}, config, require('./prod')) 71 | } 72 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v2ex", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "v2ex-taro", 6 | "main": "index.js", 7 | "scripts": { 8 | "build:weapp": "taro build --type weapp", 9 | "build:h5": "taro build --type h5", 10 | "dev:weapp": "npm run build:weapp -- --watch", 11 | "dev:h5": "npm run build:h5 -- --watch", 12 | "test": "eslint src/ --ext .tsx" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@tarojs/components": "^3.2.10", 18 | "@tarojs/plugin-framework-react": "^3.6.8", 19 | "@tarojs/react": "^3.2.10", 20 | "@tarojs/runtime": "^3.2.10", 21 | "@tarojs/taro": "^3.2.10", 22 | "@tarojs/webpack-runner": "^3.2.10", 23 | "react": "^16.12.0", 24 | "react-dom": "^17.0.2", 25 | "timeago.js": "^3.0.2" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.8.0", 29 | "@tarojs/mini-runner": "^3.2.10", 30 | "@types/node": "^10.5.8", 31 | "@types/react": "^16.4.8", 32 | "@types/redux-actions": "^2.3.0", 33 | "@types/webpack-env": "^1.13.6", 34 | "@typescript-eslint/eslint-plugin": "^4.26.1", 35 | "babel-preset-taro": "3.2.10", 36 | "eslint": "^6.8.0", 37 | "eslint-config-taro": "3.2.10", 38 | "eslint-plugin-import": "^2.12.0", 39 | "eslint-plugin-react": "^7.8.2", 40 | "eslint-plugin-react-hooks": "^1.6.1", 41 | "postcss": "^8.4.25", 42 | "postcss-loader": "^7.3.3", 43 | "stylelint": "9.3.0", 44 | "typescript": "^3.8.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "dist/", 3 | "projectname": "taro-v2ex", 4 | "description": "v2ex-taro", 5 | "appid": "touristappid", 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false, 11 | "newFeature": true 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.2.2", 15 | "condition": {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | 'pages/index/index', 4 | 'pages/nodes/nodes', 5 | 'pages/hot/hot', 6 | 'pages/node_detail/node_detail', 7 | 'pages/thread_detail/thread_detail' 8 | ], 9 | tabBar: { 10 | list: [{ 11 | 'iconPath': 'resource/latest.png', 12 | 'selectedIconPath': 'resource/lastest_on.png', 13 | pagePath: 'pages/index/index', 14 | text: '最新' 15 | }, { 16 | 'iconPath': 'resource/hotest.png', 17 | 'selectedIconPath': 'resource/hotest_on.png', 18 | pagePath: 'pages/hot/hot', 19 | text: '热门' 20 | }, { 21 | 'iconPath': 'resource/node.png', 22 | 'selectedIconPath': 'resource/node_on.png', 23 | pagePath: 'pages/nodes/nodes', 24 | text: '节点' 25 | }], 26 | 'color': '#000', 27 | 'selectedColor': '#56abe4', 28 | 'backgroundColor': '#fff', 29 | 'borderStyle': 'white' 30 | }, 31 | window: { 32 | backgroundTextStyle: 'light', 33 | navigationBarBackgroundColor: '#fff', 34 | navigationBarTitleText: 'V2EX', 35 | navigationBarTextStyle: 'black' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/app.css -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './app.css' 3 | 4 | class App extends React.Component { 5 | render () { 6 | return ( 7 | this.props.children 8 | ) 9 | } 10 | } 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /src/components/loading.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | justify-content: center; 3 | align-items: center; 4 | display: flex; 5 | height: 300px; 6 | } 7 | 8 | .img { 9 | width: 150px; 10 | height: 150px; 11 | } -------------------------------------------------------------------------------- /src/components/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View, Image } from '@tarojs/components' 3 | const url = require('../resource/spiner.gif') 4 | import './loading.css' 5 | 6 | class Loading extends React.Component { 7 | render () { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | } 15 | 16 | export { Loading } 17 | -------------------------------------------------------------------------------- /src/components/thread.css: -------------------------------------------------------------------------------- 1 | .thread { 2 | display: flex; 3 | padding: 15px; 4 | border-bottom: 1px solid #f1f1f1; 5 | flex-direction: column; 6 | } 7 | 8 | .info { 9 | display: flex; 10 | height: 65px; 11 | margin-bottom: 5px; 12 | margin-top: 5px; 13 | } 14 | 15 | .info .author { 16 | font-size: 28px; 17 | } 18 | 19 | .bold { 20 | font-size: 32px; 21 | font-weight: bold; 22 | } 23 | 24 | .avatar { 25 | max-width: 65px; 26 | max-height: 65px; 27 | border-radius: 50%; 28 | margin-right: 10px; 29 | } 30 | 31 | .middle { 32 | flex: 1; 33 | display: flex; 34 | margin-left: 10px; 35 | flex-direction: column; 36 | } 37 | 38 | .mr10 { 39 | margin-right: 10px; 40 | } 41 | 42 | .info .replies { 43 | font-size: 20px; 44 | color: darkgray; 45 | } 46 | 47 | .node { 48 | float: right; 49 | } 50 | 51 | .node .tag { 52 | font-size: 24px; 53 | padding: 5px 15px; 54 | float: right; 55 | background-color: #eeeeee; 56 | border-radius: 5px; 57 | } 58 | 59 | .title { 60 | margin-top: 10px; 61 | font-size: 30px; 62 | } -------------------------------------------------------------------------------- /src/components/thread.tsx: -------------------------------------------------------------------------------- 1 | import Taro, { eventCenter } from '@tarojs/taro' 2 | import React from 'react' 3 | import { View, Text, Navigator, Image } from '@tarojs/components' 4 | 5 | import api from '../utils/api' 6 | import { timeagoInst, Thread_DETAIL_NAVIGATE } from '../utils' 7 | import { IMember } from '../interfaces/member' 8 | import { INode } from '../interfaces/node' 9 | 10 | import './thread.css' 11 | 12 | interface IProps { 13 | title: string, 14 | member: IMember, 15 | node: INode, 16 | last_modified: number, 17 | tid: number, 18 | replies: number, 19 | key?: number, 20 | not_navi?: boolean // 不导航到 detail 21 | } 22 | 23 | class Thread extends React.Component { 24 | 25 | handleNavigate = () => { 26 | // 这里必须显式指名 this.props 包含 tid 27 | // 或设置 defaultProps 28 | const { tid, not_navi } = this.props 29 | if (not_navi) { 30 | return 31 | } 32 | // 懒得用 redux 了 33 | eventCenter.trigger(Thread_DETAIL_NAVIGATE, this.props) 34 | Taro.navigateTo({ 35 | url: '/pages/thread_detail/thread_detail' 36 | }) 37 | } 38 | 39 | render () { 40 | const { title, member, last_modified, replies, node, not_navi } = this.props 41 | const time = timeagoInst.format(last_modified * 1000, 'zh') 42 | const usernameCls = `author ${not_navi ? 'bold' : ''}` 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {member.username} 53 | 54 | 55 | 56 | {time} 57 | 58 | 59 | 评论 {replies} 60 | 61 | 62 | 63 | 64 | 65 | {node.title} 66 | 67 | 68 | 69 | 70 | {title} 71 | 72 | 73 | ) 74 | } 75 | } 76 | 77 | export { Thread } 78 | -------------------------------------------------------------------------------- /src/components/thread_list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { View, Text } from '@tarojs/components' 3 | import { Thread } from './thread' 4 | import { Loading } from './loading' 5 | import { IMember } from '../interfaces/member' 6 | import { INode } from '../interfaces/node' 7 | 8 | import './thread.css' 9 | 10 | interface IProps { 11 | threads: IThread[], 12 | loading: boolean 13 | } 14 | 15 | interface IThread { 16 | title: string, 17 | member: IMember, 18 | node: INode, 19 | last_modified: number, 20 | id: number, 21 | replies: number 22 | key?: number 23 | } 24 | 25 | class ThreadList extends React.Component { 26 | static defaultProps = { 27 | threads: [], 28 | loading: true 29 | } 30 | 31 | render () { 32 | const { loading, threads } = this.props 33 | 34 | if (loading) { 35 | return 36 | } 37 | 38 | const element = threads.map((thread, index) => { 39 | return ( 40 | 49 | ) 50 | }) 51 | 52 | return ( 53 | 54 | {element} 55 | 56 | ) 57 | } 58 | } 59 | 60 | export { ThreadList } 61 | -------------------------------------------------------------------------------- /src/components/thread_props.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/components/thread_props.d.ts -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Taro 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/interfaces/member.d.ts: -------------------------------------------------------------------------------- 1 | export interface IMember { 2 | username: string; 3 | website?: null; 4 | github?: null; 5 | psn?: null; 6 | avatar_normal: string; 7 | bio?: null; 8 | url: string; 9 | tagline?: null; 10 | twitter?: null; 11 | created: number; 12 | avatar_large: string; 13 | avatar_mini: string; 14 | location?: null; 15 | btc?: null; 16 | id: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/interfaces/node.d.ts: -------------------------------------------------------------------------------- 1 | export interface INode { 2 | avatar_large: string, 3 | name: string, 4 | avatar_normal: string, 5 | title: string, 6 | url: string, 7 | topics: number, 8 | footer: string, 9 | header: string, 10 | title_alternative: string, 11 | avatar_mini: string, 12 | stars: number, 13 | root: boolean, 14 | id: number, 15 | parent_node_name: string, 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces/thread.d.ts: -------------------------------------------------------------------------------- 1 | import { INode } from './node' 2 | import { IMember } from './member' 3 | 4 | export interface IThread { 5 | node: INode, 6 | member: IMember, 7 | last_reply_by: string; 8 | last_touched: number; 9 | title: string; 10 | url: string; 11 | created: number; 12 | content: string; 13 | content_rendered: string; 14 | last_modified: number; 15 | replies: number; 16 | id: number; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/pages/hot/hot.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '热门' 3 | } -------------------------------------------------------------------------------- /src/pages/hot/hot.tsx: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | import React from 'react' 3 | import { View } from '@tarojs/components' 4 | import { ThreadList } from '../../components/thread_list' 5 | import { IThread } from '../../interfaces/thread' 6 | import api from '../../utils/api' 7 | 8 | import './index.css' 9 | 10 | interface IState { 11 | loading: boolean, 12 | threads: IThread[] 13 | } 14 | 15 | class Hot extends React.Component<{}, IState> { 16 | state = { 17 | loading: true, 18 | threads: [] 19 | } 20 | 21 | async componentDidMount () { 22 | try { 23 | const res = await Taro.request({ 24 | url: api.getHotNodes() 25 | }) 26 | this.setState({ 27 | threads: res.data, 28 | loading: false 29 | }) 30 | } catch (error) { 31 | Taro.showToast({ 32 | title: '载入远程数据错误' 33 | }) 34 | } 35 | } 36 | 37 | render () { 38 | const { loading, threads } = this.state 39 | return ( 40 | 41 | 45 | 46 | ) 47 | } 48 | } 49 | 50 | export default Hot 51 | -------------------------------------------------------------------------------- /src/pages/hot/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/pages/hot/index.css -------------------------------------------------------------------------------- /src/pages/index/index.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '首页' 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/index/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/pages/index/index.css -------------------------------------------------------------------------------- /src/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | import React from 'react' 3 | import { View } from '@tarojs/components' 4 | import { ThreadList } from '../../components/thread_list' 5 | import { IThread } from '../../interfaces/thread' 6 | import api from '../../utils/api' 7 | 8 | import './index.css' 9 | 10 | interface IState { 11 | loading: boolean, 12 | threads: IThread[] 13 | } 14 | 15 | class Index extends React.Component<{}, IState> { 16 | config = { 17 | navigationBarTitleText: '首页' 18 | } 19 | 20 | state = { 21 | loading: true, 22 | threads: [] 23 | } 24 | 25 | async componentDidMount () { 26 | try { 27 | const res = await Taro.request({ 28 | url: api.getLatestTopic() 29 | }) 30 | this.setState({ 31 | threads: res.data, 32 | loading: false 33 | }) 34 | } catch (error) { 35 | Taro.showToast({ 36 | title: '载入远程数据错误' 37 | }) 38 | } 39 | } 40 | 41 | render () { 42 | const { loading, threads } = this.state 43 | return ( 44 | 45 | 49 | 50 | ) 51 | } 52 | } 53 | 54 | export default Index 55 | -------------------------------------------------------------------------------- /src/pages/node_detail/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/pages/node_detail/index.css -------------------------------------------------------------------------------- /src/pages/node_detail/node_detail.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | } 3 | -------------------------------------------------------------------------------- /src/pages/node_detail/node_detail.tsx: -------------------------------------------------------------------------------- 1 | import Taro, { Current } from '@tarojs/taro' 2 | import React from 'react' 3 | import { View } from '@tarojs/components' 4 | import { ThreadList } from '../../components/thread_list' 5 | import { IThread } from '../../interfaces/thread' 6 | import api from '../../utils/api' 7 | 8 | import './index.css' 9 | 10 | interface IState { 11 | loading: boolean, 12 | threads: IThread[] 13 | } 14 | 15 | class NodeDetail extends React.Component<{}, IState> { 16 | state = { 17 | loading: true, 18 | threads: [] 19 | } 20 | 21 | componentWillMount () { 22 | const { full_name } = Current.router.params 23 | Taro.setNavigationBarTitle({ 24 | title: decodeURI(full_name) 25 | }) 26 | } 27 | 28 | async componentDidMount () { 29 | const { short_name } = Current.router.params 30 | try { 31 | const { data: { id } } = await Taro.request({ 32 | url: api.getNodeInfo({ 33 | name: short_name 34 | }) 35 | }) 36 | const res = await Taro.request({ 37 | url: api.getTopics({ 38 | node_id: id 39 | }) 40 | }) 41 | this.setState({ 42 | threads: res.data, 43 | loading: false 44 | }) 45 | } catch (error) { 46 | Taro.showToast({ 47 | title: '载入远程数据错误' 48 | }) 49 | } 50 | } 51 | 52 | render () { 53 | const { loading, threads } = this.state 54 | return ( 55 | 56 | 60 | 61 | ) 62 | } 63 | } 64 | 65 | export default NodeDetail 66 | -------------------------------------------------------------------------------- /src/pages/nodes/all_node.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | 'title': '分享与探索', 4 | 'nodes': [ 5 | { 6 | 'short_name': 'qna', 7 | 'full_name': '问与答' 8 | }, 9 | { 10 | 'short_name': 'share', 11 | 'full_name': '分享发现' 12 | }, 13 | { 14 | 'short_name': 'create', 15 | 'full_name': '分享创造' 16 | }, 17 | { 18 | 'short_name': 'in', 19 | 'full_name': '分享邀请码' 20 | }, 21 | { 22 | 'short_name': 'ideas', 23 | 'full_name': '奇思妙想' 24 | }, 25 | { 26 | 'short_name': 'autistic', 27 | 'full_name': '自言自语' 28 | }, 29 | { 30 | 'short_name': 'random', 31 | 'full_name': '随想' 32 | }, 33 | { 34 | 'short_name': 'design', 35 | 'full_name': '设计' 36 | }, 37 | { 38 | 'short_name': 'blog', 39 | 'full_name': 'Blog' 40 | } 41 | ] 42 | }, 43 | { 44 | 'title': 'V2EX', 45 | 'nodes': [ 46 | { 47 | 'short_name': 'v2ex', 48 | 'full_name': 'V2EX' 49 | }, 50 | { 51 | 'short_name': 'dns', 52 | 'full_name': 'DNS' 53 | }, 54 | { 55 | 'short_name': 'babel', 56 | 'full_name': 'Project Babel' 57 | }, 58 | { 59 | 'short_name': 'feedback', 60 | 'full_name': '反馈' 61 | }, 62 | { 63 | 'short_name': 'gae', 64 | 'full_name': 'Google App Engine' 65 | }, 66 | { 67 | 'short_name': 'guide', 68 | 'full_name': '使用指南' 69 | } 70 | ] 71 | }, 72 | { 73 | 'title': 'iOS', 74 | 'nodes': [ 75 | { 76 | 'short_name': 'idev', 77 | 'full_name': 'iDev' 78 | }, 79 | { 80 | 'short_name': 'icode', 81 | 'full_name': 'iCode' 82 | }, 83 | { 84 | 'short_name': 'imarketing', 85 | 'full_name': 'iMarketing' 86 | }, 87 | { 88 | 'short_name': 'iad', 89 | 'full_name': 'iAd' 90 | }, 91 | { 92 | 'short_name': 'itransfer', 93 | 'full_name': 'iTransfer' 94 | } 95 | ] 96 | }, 97 | { 98 | 'title': 'Geek', 99 | 'nodes': [ 100 | { 101 | 'short_name': 'programmer', 102 | 'full_name': '程序员' 103 | }, 104 | { 105 | 'short_name': 'python', 106 | 'full_name': 'Python' 107 | }, 108 | { 109 | 'short_name': 'android', 110 | 'full_name': 'Android' 111 | }, 112 | { 113 | 'short_name': 'bb', 114 | 'full_name': '宽带症候群' 115 | }, 116 | { 117 | 'short_name': 'linux', 118 | 'full_name': 'Linux' 119 | }, 120 | { 121 | 'short_name': 'php', 122 | 'full_name': 'PHP' 123 | }, 124 | { 125 | 'short_name': 'cloud', 126 | 'full_name': '云计算' 127 | }, 128 | { 129 | 'short_name': 'outsourcing', 130 | 'full_name': '外包' 131 | }, 132 | { 133 | 'short_name': 'hardware', 134 | 'full_name': '硬件' 135 | }, 136 | { 137 | 'short_name': 'java', 138 | 'full_name': 'Java' 139 | }, 140 | { 141 | 'short_name': 'nodejs', 142 | 'full_name': 'Node.js' 143 | }, 144 | { 145 | 'short_name': 'server', 146 | 'full_name': '服务器' 147 | }, 148 | { 149 | 'short_name': 'bitcoin', 150 | 'full_name': 'Bitcoin' 151 | }, 152 | { 153 | 'short_name': 'mysql', 154 | 'full_name': 'MySQL' 155 | }, 156 | { 157 | 'short_name': 'programming', 158 | 'full_name': '编程' 159 | }, 160 | { 161 | 'short_name': 'linode', 162 | 'full_name': 'Linode' 163 | }, 164 | { 165 | 'short_name': 'car', 166 | 'full_name': '汽车' 167 | }, 168 | { 169 | 'short_name': 'designer', 170 | 'full_name': '设计师' 171 | }, 172 | { 173 | 'short_name': 'kindle', 174 | 'full_name': 'Kindle' 175 | }, 176 | { 177 | 'short_name': 'markdown', 178 | 'full_name': 'Markdown' 179 | }, 180 | { 181 | 'short_name': 'mongodb', 182 | 'full_name': 'MongoDB' 183 | }, 184 | { 185 | 'short_name': 'tornado', 186 | 'full_name': 'Tornado' 187 | }, 188 | { 189 | 'short_name': 'redis', 190 | 'full_name': 'Redis' 191 | }, 192 | { 193 | 'short_name': 'ror', 194 | 'full_name': 'Ruby on Rails' 195 | }, 196 | { 197 | 'short_name': 'minecraft', 198 | 'full_name': 'Minecraft' 199 | }, 200 | { 201 | 'short_name': 'typography', 202 | 'full_name': '字体排印' 203 | }, 204 | { 205 | 'short_name': 'business', 206 | 'full_name': '商业模式' 207 | }, 208 | { 209 | 'short_name': 'ruby', 210 | 'full_name': 'Ruby' 211 | }, 212 | { 213 | 'short_name': 'math', 214 | 'full_name': '数学' 215 | }, 216 | { 217 | 'short_name': 'photoshop', 218 | 'full_name': 'Photoshop' 219 | }, 220 | { 221 | 'short_name': 'amazon', 222 | 'full_name': 'Amazon' 223 | }, 224 | { 225 | 'short_name': 'nlp', 226 | 'full_name': '自然语言处理' 227 | }, 228 | { 229 | 'short_name': 'lego', 230 | 'full_name': 'LEGO' 231 | }, 232 | { 233 | 'short_name': 'sony', 234 | 'full_name': 'SONY' 235 | }, 236 | { 237 | 'short_name': 'csharp', 238 | 'full_name': 'C#' 239 | }, 240 | { 241 | 'short_name': 'serverless', 242 | 'full_name': 'Serverless' 243 | } 244 | ] 245 | }, 246 | { 247 | 'title': '游戏', 248 | 'nodes': [ 249 | { 250 | 'short_name': 'games', 251 | 'full_name': '游戏' 252 | }, 253 | { 254 | 'short_name': 'steam', 255 | 'full_name': 'Steam' 256 | }, 257 | { 258 | 'short_name': 'ps4', 259 | 'full_name': 'PlayStation 4' 260 | }, 261 | { 262 | 'short_name': 'lol', 263 | 'full_name': '英雄联盟' 264 | }, 265 | { 266 | 'short_name': 'igame', 267 | 'full_name': 'iGame' 268 | }, 269 | { 270 | 'short_name': 'bf3', 271 | 'full_name': 'Battlefield 3' 272 | }, 273 | { 274 | 'short_name': 'sc2', 275 | 'full_name': 'StarCraft 2' 276 | }, 277 | { 278 | 'short_name': 'ps3', 279 | 'full_name': 'PlayStation 3' 280 | }, 281 | { 282 | 'short_name': 'wow', 283 | 'full_name': 'World of Warcraft' 284 | }, 285 | { 286 | 'short_name': 'switch', 287 | 'full_name': 'Nintendo Switch' 288 | }, 289 | { 290 | 'short_name': 'eve', 291 | 'full_name': 'EVE' 292 | }, 293 | { 294 | 'short_name': 'xbox360', 295 | 'full_name': 'Xbox 360' 296 | }, 297 | { 298 | 'short_name': '5v5', 299 | 'full_name': '王者荣耀' 300 | }, 301 | { 302 | 'short_name': 'bf4', 303 | 'full_name': 'Battlefield 4' 304 | }, 305 | { 306 | 'short_name': 'gt', 307 | 'full_name': 'Gran Turismo' 308 | }, 309 | { 310 | 'short_name': 'wii', 311 | 'full_name': 'Wii' 312 | }, 313 | { 314 | 'short_name': 'wiiu', 315 | 'full_name': 'Wii U' 316 | } 317 | ] 318 | }, 319 | { 320 | 'title': 'Apple', 321 | 'nodes': [ 322 | { 323 | 'short_name': 'macos', 324 | 'full_name': 'macOS' 325 | }, 326 | { 327 | 'short_name': 'iphone', 328 | 'full_name': 'iPhone' 329 | }, 330 | { 331 | 'short_name': 'mbp', 332 | 'full_name': 'MacBook Pro' 333 | }, 334 | { 335 | 'short_name': 'ipad', 336 | 'full_name': 'iPad' 337 | }, 338 | { 339 | 'short_name': 'macbook', 340 | 'full_name': 'MacBook' 341 | }, 342 | { 343 | 'short_name': 'accessory', 344 | 'full_name': '配件' 345 | }, 346 | { 347 | 'short_name': 'mba', 348 | 'full_name': 'MacBook Air' 349 | }, 350 | { 351 | 'short_name': 'imac', 352 | 'full_name': 'iMac' 353 | }, 354 | { 355 | 'short_name': 'macmini', 356 | 'full_name': 'Mac mini' 357 | }, 358 | { 359 | 'short_name': 'ipod', 360 | 'full_name': 'iPod' 361 | }, 362 | { 363 | 'short_name': 'macpro', 364 | 'full_name': 'Mac Pro' 365 | }, 366 | { 367 | 'short_name': 'mobileme', 368 | 'full_name': 'MobileMe' 369 | }, 370 | { 371 | 'short_name': 'iwork', 372 | 'full_name': 'iWork' 373 | }, 374 | { 375 | 'short_name': 'ilife', 376 | 'full_name': 'iLife' 377 | }, 378 | { 379 | 'short_name': 'garageband', 380 | 'full_name': 'GarageBand' 381 | } 382 | ] 383 | }, 384 | { 385 | 'title': '生活', 386 | 'nodes': [ 387 | { 388 | 'short_name': 'all4all', 389 | 'full_name': '二手交易' 390 | }, 391 | { 392 | 'short_name': 'jobs', 393 | 'full_name': '酷工作' 394 | }, 395 | { 396 | 'short_name': 'afterdark', 397 | 'full_name': '天黑以后' 398 | }, 399 | { 400 | 'short_name': 'free', 401 | 'full_name': '免费赠送' 402 | }, 403 | { 404 | 'short_name': 'music', 405 | 'full_name': '音乐' 406 | }, 407 | { 408 | 'short_name': 'movie', 409 | 'full_name': '电影' 410 | }, 411 | { 412 | 'short_name': 'exchange', 413 | 'full_name': '物物交换' 414 | }, 415 | { 416 | 'short_name': 'tv', 417 | 'full_name': '剧集' 418 | }, 419 | { 420 | 'short_name': 'invest', 421 | 'full_name': '投资' 422 | }, 423 | { 424 | 'short_name': 'creditcard', 425 | 'full_name': '信用卡' 426 | }, 427 | { 428 | 'short_name': 'tuan', 429 | 'full_name': '团购' 430 | }, 431 | { 432 | 'short_name': 'taste', 433 | 'full_name': '美酒与美食' 434 | }, 435 | { 436 | 'short_name': 'travel', 437 | 'full_name': '旅行' 438 | }, 439 | { 440 | 'short_name': 'reading', 441 | 'full_name': '阅读' 442 | }, 443 | { 444 | 'short_name': 'photograph', 445 | 'full_name': '摄影' 446 | }, 447 | { 448 | 'short_name': 'soccer', 449 | 'full_name': '绿茵场' 450 | }, 451 | { 452 | 'short_name': 'baby', 453 | 'full_name': 'Baby' 454 | }, 455 | { 456 | 'short_name': 'pet', 457 | 'full_name': '宠物' 458 | }, 459 | { 460 | 'short_name': 'coffee', 461 | 'full_name': '咖啡' 462 | }, 463 | { 464 | 'short_name': 'lohas', 465 | 'full_name': '乐活' 466 | }, 467 | { 468 | 'short_name': 'love', 469 | 'full_name': '非诚勿扰' 470 | }, 471 | { 472 | 'short_name': 'bike', 473 | 'full_name': '骑行' 474 | }, 475 | { 476 | 'short_name': 'diary', 477 | 'full_name': '日记' 478 | }, 479 | { 480 | 'short_name': 'plant', 481 | 'full_name': '植物' 482 | }, 483 | { 484 | 'short_name': 'mushroom', 485 | 'full_name': '蘑菇' 486 | }, 487 | { 488 | 'short_name': 'mileage', 489 | 'full_name': '行程控' 490 | } 491 | ] 492 | }, 493 | { 494 | 'title': 'Internet', 495 | 'nodes': [ 496 | { 497 | 'short_name': 'google', 498 | 'full_name': 'Google' 499 | }, 500 | { 501 | 'short_name': 'twitter', 502 | 'full_name': 'Twitter' 503 | }, 504 | { 505 | 'short_name': 'coding', 506 | 'full_name': 'Coding' 507 | }, 508 | { 509 | 'short_name': 'facebook', 510 | 'full_name': 'Facebook' 511 | }, 512 | { 513 | 'short_name': 'wikipedia', 514 | 'full_name': 'Wikipedia' 515 | }, 516 | { 517 | 'short_name': 'reddit', 518 | 'full_name': 'reddit' 519 | } 520 | ] 521 | }, 522 | { 523 | 'title': '城市', 524 | 'nodes': [ 525 | { 526 | 'short_name': 'beijing', 527 | 'full_name': '北京' 528 | }, 529 | { 530 | 'short_name': 'shanghai', 531 | 'full_name': '上海' 532 | }, 533 | { 534 | 'short_name': 'shenzhen', 535 | 'full_name': '深圳' 536 | }, 537 | { 538 | 'short_name': 'hangzhou', 539 | 'full_name': '杭州' 540 | }, 541 | { 542 | 'short_name': 'chengdu', 543 | 'full_name': '成都' 544 | }, 545 | { 546 | 'short_name': 'guangzhou', 547 | 'full_name': '广州' 548 | }, 549 | { 550 | 'short_name': 'wuhan', 551 | 'full_name': '武汉' 552 | }, 553 | { 554 | 'short_name': 'kunming', 555 | 'full_name': '昆明' 556 | }, 557 | { 558 | 'short_name': 'tianjin', 559 | 'full_name': '天津' 560 | }, 561 | { 562 | 'short_name': 'qingdao', 563 | 'full_name': '青岛' 564 | }, 565 | { 566 | 'short_name': 'nyc', 567 | 'full_name': 'New York' 568 | }, 569 | { 570 | 'short_name': 'sanfrancisco', 571 | 'full_name': 'San Francisco' 572 | }, 573 | { 574 | 'short_name': 'la', 575 | 'full_name': 'Los Angeles' 576 | }, 577 | { 578 | 'short_name': 'boston', 579 | 'full_name': 'Boston' 580 | } 581 | ] 582 | } 583 | ] 584 | -------------------------------------------------------------------------------- /src/pages/nodes/nodes.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '节点' 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/nodes/nodes.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 32px; 3 | border-left: 5px solid #333333; 4 | margin-bottom: 15px; 5 | margin-left: 5px; 6 | } 7 | 8 | .container { 9 | display: flex; 10 | flex-direction: column; 11 | margin-bottom: 15px; 12 | } 13 | 14 | .nodes { 15 | margin-left: 15px; 16 | } 17 | 18 | .nodes .tag { 19 | display: inline-block; 20 | font-size: 24px; 21 | padding: 5px 15px; 22 | border-radius: 5px; 23 | background-color: #eeeeee; 24 | margin-right: 15px; 25 | margin-bottom: 15px; 26 | vertical-align: middle; 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/nodes/nodes.tsx: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | import React from 'react' 3 | import { View, Text, Navigator } from '@tarojs/components' 4 | import allNodes from './all_node' 5 | import api from '../../utils/api' 6 | 7 | import './nodes.css' 8 | 9 | class Nodes extends React.Component<{}, {}> { 10 | config = { 11 | navigationBarTitleText: '节点' 12 | } 13 | 14 | render () { 15 | const element = allNodes.map(item => { 16 | const nodes = item.nodes.map(node => { 17 | return ( 18 | 23 | 24 | {node.full_name} 25 | 26 | 27 | ) 28 | }) 29 | return ( 30 | 31 | 32 | 33 | {item.title} 34 | 35 | 36 | 37 | {nodes} 38 | 39 | 40 | ) 41 | }) 42 | return ( 43 | 44 | {element} 45 | 46 | ) 47 | } 48 | } 49 | 50 | export default Nodes 51 | -------------------------------------------------------------------------------- /src/pages/thread_detail/index.css: -------------------------------------------------------------------------------- 1 | .replies { 2 | margin-bottom: 50px; 3 | } 4 | 5 | .reply { 6 | display: flex; 7 | } 8 | 9 | .main-content { 10 | font-size: 26px; 11 | border-bottom: 1px solid #f1f1f1; 12 | padding: 20px 15px; 13 | } 14 | 15 | .main-content .line { 16 | margin: 5px 0; 17 | } 18 | 19 | .main-content .img { 20 | max-width: 100%; 21 | height: auto; 22 | } 23 | 24 | .main-tent .hr { 25 | width: 50%; 26 | } 27 | 28 | .avatar { 29 | max-width: 50px; 30 | max-height: 50px; 31 | border-radius: 50%; 32 | margin: 10px; 33 | margin-top: 15px; 34 | } 35 | 36 | .main { 37 | flex: 1; 38 | display: flex; 39 | border-bottom: 1px solid #f1f1f1; 40 | flex-direction: column; 41 | margin-left: 10px; 42 | margin-top: 10px; 43 | } 44 | 45 | .main .author { 46 | font-size: 24px; 47 | } 48 | 49 | .main .time { 50 | font-size: 18px; 51 | color: darkgray; 52 | } 53 | 54 | .main .content { 55 | font-size: 24px; 56 | margin: 10px; 57 | margin-left: 0; 58 | } 59 | 60 | .main .floor { 61 | color: darkgray; 62 | font-size: 20px; 63 | margin-bottom: 10px; 64 | } -------------------------------------------------------------------------------- /src/pages/thread_detail/thread_detail.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '话题' 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/thread_detail/thread_detail.tsx: -------------------------------------------------------------------------------- 1 | import Taro from '@tarojs/taro' 2 | import React from 'react' 3 | import { View, RichText, Image } from '@tarojs/components' 4 | import { Thread } from '../../components/thread' 5 | import { Loading } from '../../components/loading' 6 | import { IThread } from '../../interfaces/thread' 7 | import api from '../../utils/api' 8 | import { timeagoInst, GlobalState, IThreadProps } from '../../utils' 9 | 10 | import './index.css' 11 | 12 | interface IState { 13 | loading: boolean 14 | replies: IThread[], 15 | content: string, 16 | thread: IThreadProps 17 | } 18 | 19 | function prettyHTML (str: string) { 20 | const lines = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] 21 | 22 | lines.forEach(line => { 23 | const regex = new RegExp(`<${line}`, 'gi') 24 | 25 | str = str.replace(regex, `<${line} class="line"`) 26 | }) 27 | 28 | return str.replace(/ { 32 | state = { 33 | loading: true, 34 | replies: [], 35 | content: '', 36 | thread: {} as IThreadProps 37 | } as IState 38 | 39 | config = { 40 | navigationBarTitleText: '话题' 41 | } 42 | 43 | componentWillMount () { 44 | this.setState({ 45 | thread: GlobalState.thread 46 | }) 47 | } 48 | 49 | async componentDidMount () { 50 | try { 51 | const id = GlobalState.thread.tid 52 | const [{ data }, { data: [ { content_rendered } ] } ] = await Promise.all([ 53 | Taro.request({ 54 | url: api.getReplies({ 55 | 'topic_id': id 56 | }) 57 | }), 58 | Taro.request({ 59 | url: api.getTopics({ 60 | id 61 | }) 62 | }) 63 | ]) 64 | this.setState({ 65 | loading: false, 66 | replies: data, 67 | content: prettyHTML(content_rendered) 68 | }) 69 | } catch (error) { 70 | Taro.showToast({ 71 | title: '载入远程数据错误' 72 | }) 73 | } 74 | } 75 | 76 | render () { 77 | const { loading, replies, thread, content } = this.state 78 | 79 | const replieEl = replies.map((reply, index) => { 80 | const time = timeagoInst.format(reply.last_modified * 1000, 'zh') 81 | return ( 82 | 83 | 84 | 85 | 86 | {reply.member.username} 87 | 88 | 89 | {time} 90 | 91 | {Taro.getEnv() === Taro.ENV_TYPE.ALIPAY 92 | ? {reply.content} 93 | : 94 | } 95 | 96 | {index + 1} 楼 97 | 98 | 99 | 100 | ) 101 | }) 102 | 103 | const contentEl = loading 104 | ? 105 | : ( 106 | 107 | 108 | {Taro.getEnv() === Taro.ENV_TYPE.ALIPAY 109 | ? {content} 110 | : 111 | } 112 | 113 | 114 | {replieEl} 115 | 116 | 117 | ) 118 | 119 | return ( 120 | 121 | 130 | {contentEl} 131 | 132 | ) 133 | } 134 | } 135 | 136 | export default ThreadDetail 137 | -------------------------------------------------------------------------------- /src/resource/hotest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/hotest.png -------------------------------------------------------------------------------- /src/resource/hotest_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/hotest_on.png -------------------------------------------------------------------------------- /src/resource/lastest_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/lastest_on.png -------------------------------------------------------------------------------- /src/resource/latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/latest.png -------------------------------------------------------------------------------- /src/resource/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/node.png -------------------------------------------------------------------------------- /src/resource/node_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/node_on.png -------------------------------------------------------------------------------- /src/resource/spiner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NervJS/taro-v2ex/132ff4c3ef816dd015c8f456970542fb7448ca67/src/resource/spiner.gif -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | const HOST_URI = 'https://www.v2ex.com/api/' 2 | 3 | // 获取节点 4 | // 所有的节点 5 | const ALL_NODE = 'nodes/all.json' 6 | // 获取节点信息 7 | // 节点id :id 节点名 :name 8 | const NODE_INFO = 'nodes/show.json' 9 | 10 | // 获取主题 11 | // 取最新的主题 12 | const LATEST_TOPIC = 'topics/latest.json' 13 | // 获取热议主题 14 | const HOT_TOPIC = 'topics/hot.json' 15 | // 获取主题信息 :id | (:username | :node_id | :node_name) 16 | const GET_TOPICS = 'topics/show.json' 17 | 18 | // 获取回复 :topic_id (:page , :page_size)? 19 | const GET_REPLIES = 'replies/show.json' 20 | 21 | // 获取用户信息 22 | const GET_USERINFO = 'members/show.json' 23 | 24 | function queryString (obj?: Object) { 25 | if (!obj) { 26 | return '' 27 | } 28 | return '?' + Object.keys(obj).map(function (k) { 29 | return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]) 30 | }).join('&') 31 | } 32 | 33 | function getAllNode () { 34 | return HOST_URI + ALL_NODE 35 | } 36 | 37 | function getNodeInfo (o?) { 38 | return HOST_URI + NODE_INFO + queryString(o) 39 | } 40 | 41 | function getHotNodes () { 42 | return HOST_URI + HOT_TOPIC 43 | } 44 | 45 | function getLatestTopic (o?) { 46 | return HOST_URI + LATEST_TOPIC + queryString(o) 47 | } 48 | 49 | function getReplies (o?) { 50 | return HOST_URI + GET_REPLIES + queryString(o) 51 | } 52 | 53 | function getTopics (o?) { 54 | return HOST_URI + GET_TOPICS + queryString(o) 55 | } 56 | 57 | export default { 58 | getAllNode, 59 | getNodeInfo, 60 | getLatestTopic, 61 | getReplies, 62 | getHotNodes, 63 | queryString, 64 | getTopics 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import timeago from 'timeago.js' 2 | 3 | import { IThread } from '../interfaces/thread' 4 | 5 | import { eventCenter } from '@tarojs/taro' 6 | 7 | // tslint:disable-next-line 8 | export const Thread_DETAIL_NAVIGATE = 'thread_detail_navigate' 9 | 10 | export interface IThreadProps extends IThread { 11 | tid: string 12 | } 13 | 14 | eventCenter.on(Thread_DETAIL_NAVIGATE, (thread: IThreadProps) => { 15 | GlobalState.thread = thread 16 | }) 17 | 18 | export const GlobalState = { 19 | thread: {} as IThreadProps 20 | } 21 | 22 | export const timeagoInst = timeago() 23 | 24 | // 数字/英文与中文之间需要加空格 25 | const betterChineseDict = (_, index) => { 26 | return [ 27 | ['刚刚', '片刻后'], 28 | ['%s 秒前', '%s 秒后'], 29 | ['1 分钟前', '1 分钟后'], 30 | ['%s 分钟前', '%s 分钟后'], 31 | ['1 小时前', '1小 时后'], 32 | ['%s 小时前', '%s 小时后'], 33 | ['1 天前', '1 天后'], 34 | ['%s 天前', '%s 天后'], 35 | ['1 周前', '1 周后'], 36 | ['%s 周前', '%s 周后'], 37 | ['1 月前', '1 月后'], 38 | ['%s 月前', '%s 月后'], 39 | ['1 年前', '1 年后'], 40 | ['%s 年前', '%s 年后'] 41 | ][index] 42 | } 43 | 44 | timeago.register('zh', betterChineseDict) 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": false, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "preserve", 19 | "jsxFactory": "React.createElement", 20 | "allowJs": true, 21 | "typeRoots": [ 22 | "node_modules/@types" 23 | ] 24 | }, 25 | "exclude": [ 26 | "__tests__", 27 | "node_modules", 28 | "dist", 29 | "tests", 30 | "jest", 31 | "lib", 32 | "**/*.test.ts", 33 | "**/*.spec.ts" 34 | ], 35 | "compileOnSave": false 36 | } 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-react"] 3 | } --------------------------------------------------------------------------------