├── .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 | [](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 | }
--------------------------------------------------------------------------------