/config/jest/cssTransform.js',
8 | },
9 | transformIgnorePatterns: [
10 | '/node_modules/',
11 | '^.+\\.module\\.(css|sass|scss)$',
12 | ],
13 | moduleNameMapper: {
14 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/public/static/icon/reload.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/components/ActiveLink.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 |
3 | const ActiveLink = ({ children, href }) => {
4 | const router = useRouter()
5 | const active = router.asPath === href
6 | const className = active
7 | ? 'ant-menu-item-selected ant-menu-item'
8 | : 'ant-menu-item'
9 |
10 | const handleClick = e => {
11 | e.preventDefault()
12 | router.push(href).then(() => window.scrollTo(0, 0))
13 | }
14 |
15 | return (
16 |
24 | {children}
25 |
26 | )
27 | }
28 |
29 | export default ActiveLink
30 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { initStore } from '../store'
2 | import { Provider } from 'mobx-react'
3 | import MainList from '../components/MainList'
4 | import { apiBaseUrl, getPageTitle, getInitList } from '../utils'
5 |
6 | function HomePage ({ title, apiUrl, initList }) {
7 | const store = initStore(initList)
8 |
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export async function getServerSideProps () {
17 | const title = getPageTitle()
18 | const apiUrl = `${apiBaseUrl}data/all/20`
19 |
20 | const initList = await getInitList(apiUrl)
21 |
22 | return {
23 | props: {
24 | title,
25 | apiUrl,
26 | initList
27 | }
28 | }
29 | }
30 |
31 | export default HomePage
32 |
--------------------------------------------------------------------------------
/pages/topics/[name].js:
--------------------------------------------------------------------------------
1 | import { initStore } from '../../store'
2 | import { Provider } from 'mobx-react'
3 | import MainList from '../../components/MainList'
4 | import {
5 | apiBaseUrl, getPageTitle, getInitList
6 | } from '../../utils'
7 |
8 | function TopicPage ({ title, apiUrl, initList }) {
9 | const store = initStore(initList)
10 |
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export async function getServerSideProps ({ params }) {
19 | const title = getPageTitle(params.name)
20 | const apiUrl = `${apiBaseUrl}data/${encodeURIComponent(title)}/20`
21 |
22 | const initList = await getInitList(apiUrl)
23 |
24 | return {
25 | props: {
26 | title,
27 | apiUrl,
28 | initList
29 | }
30 | }
31 | }
32 |
33 | export default TopicPage
34 |
--------------------------------------------------------------------------------
/public/static/icon/ios.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/static/icon/android.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | export const apiBaseUrl = 'https://gank.io/api/'
2 |
3 | const topics = [{
4 | title: '首页',
5 | name: ''
6 | }, {
7 | title: '前端',
8 | name: 'fe'
9 | }, {
10 | title: 'Android',
11 | name: 'android'
12 | }, {
13 | title: 'iOS',
14 | name: 'ios'
15 | }, {
16 | title: 'App',
17 | name: 'app'
18 | }, {
19 | title: '拓展资源',
20 | name: 'expand'
21 | }, {
22 | title: '休息视频',
23 | name: 'videos'
24 | }, {
25 | title: '瞎推荐',
26 | name: 'blind'
27 | }, {
28 | title: '福利',
29 | name: 'welfare'
30 | }]
31 |
32 | export const getInitList = async (apiUrl) => {
33 | const res = await fetch(`${apiUrl}/1`)
34 | const json = await res.json()
35 |
36 | return json.results
37 | }
38 |
39 | export const getPageTitle = (topic = '') => {
40 | return topics.find(({ name }) => name === topic).title
41 | }
42 |
43 | export const getPageTopic = (name) => {
44 | return topics.find(({ title }) => title === name).name
45 | }
46 |
--------------------------------------------------------------------------------
/public/static/icon/app.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/day.js:
--------------------------------------------------------------------------------
1 | import Layout from '../components/Layout'
2 | import NormalList from '../components/NormalList'
3 | import { apiBaseUrl } from '../utils'
4 | import { Tabs } from 'antd'
5 |
6 | function DayPage ({ list, category }) {
7 | return (
8 |
9 |
10 | {
11 | category.map(item =>
12 |
13 |
14 |
15 | )
16 | }
17 |
18 |
19 | )
20 | }
21 |
22 | export async function getServerSideProps ({ query }) {
23 | const { date } = query
24 | const year = date.slice(0, 4)
25 | const month = date.slice(5, 7)
26 | const day = date.slice(8, 10)
27 |
28 | const res = await fetch(`${apiBaseUrl}day/${year}/${month}/${day}`)
29 | const { results, category } = await res.json()
30 |
31 | return {
32 | props: {
33 | list: results,
34 | category
35 | }
36 | }
37 | }
38 |
39 | export default DayPage
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present, OrangeXC
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 |
--------------------------------------------------------------------------------
/pages/404.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import Link from 'next/link'
3 | import { Result, Button } from 'antd'
4 | import pkg from '../package.json'
5 |
6 | function NotFoundPage() {
7 | return (
8 |
9 |
10 |
404 - Gank
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 | }
30 | />
31 |
32 | )
33 | }
34 |
35 | export default NotFoundPage
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gank",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "node server",
6 | "build": "next build",
7 | "start": "cross-env NODE_ENV=production node server",
8 | "test": "jest --watch",
9 | "test:ci": "jest --ci",
10 | "heroku-postbuild": "next build"
11 | },
12 | "dependencies": {
13 | "antd": "4.16.13",
14 | "antd-mobile": "2.3.4",
15 | "isomorphic-fetch": "3.0.0",
16 | "mobx": "6.3.3",
17 | "mobx-react": "7.2.0",
18 | "next": "11.1.2",
19 | "next-images": "1.8.1",
20 | "next-offline": "5.0.5",
21 | "nprogress": "0.2.0",
22 | "rc-form": "2.4.12",
23 | "react": "17.0.2",
24 | "react-dom": "17.0.2",
25 | "react-masonry-component": "6.3.0"
26 | },
27 | "devDependencies": {
28 | "@babel/plugin-proposal-class-properties": "7.14.5",
29 | "@babel/plugin-proposal-decorators": "7.15.4",
30 | "@testing-library/jest-dom": "5.14.1",
31 | "@testing-library/react": "12.1.0",
32 | "babel-jest": "27.2.0",
33 | "babel-plugin-import": "1.13.3",
34 | "codecov": "3.8.3",
35 | "cross-env": "7.0.3",
36 | "identity-obj-proxy": "3.0.0",
37 | "immutable": "4.0.0-rc.15",
38 | "jest": "27.2.0",
39 | "react-test-renderer": "17.0.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/public/static/icon/html.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/mobile/Layout.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import Router from 'next/router'
3 | import NProgress from 'nprogress'
4 | import { LocaleProvider } from 'antd-mobile'
5 | import enUS from 'antd-mobile/lib/locale-provider/en_US'
6 | import 'isomorphic-fetch'
7 | import pkg from '../../package.json'
8 |
9 | Router.onRouteChangeStart = () => NProgress.start()
10 | Router.onRouteChangeComplete = () => NProgress.done()
11 | Router.onRouteChangeError = () => NProgress.done()
12 |
13 | const Layout = ({ children }) => (
14 |
15 |
16 |
17 |
Gank
18 |
19 |
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 |
33 |
34 | )
35 |
36 | export default Layout
37 |
--------------------------------------------------------------------------------
/public/static/icon/upload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/upload_active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/timeline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/timeline_active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/video.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const port = parseInt(process.env.PORT, 10) || 3000
2 | const dev = process.env.NODE_ENV !== 'production'
3 |
4 | const { createServer } = require('http')
5 | const { join } = require('path')
6 | const { parse } = require('url')
7 | const next = require('next')
8 | const mobxReact = require('mobx-react')
9 |
10 | const app = next({ dev })
11 | const handle = app.getRequestHandler()
12 |
13 | mobxReact.enableStaticRendering(true)
14 |
15 | app.prepare().then(() => {
16 | createServer((req, res) => {
17 | const parsedUrl = parse(req.url, true)
18 | const { pathname, query } = parsedUrl
19 |
20 | const ua = req.headers['user-agent']
21 |
22 | if (pathname === '/service-worker.js') {
23 | const filePath = join(__dirname, '.next', pathname)
24 |
25 | app.serveStatic(req, res, filePath)
26 | } else if (pathname.startsWith('/static')) {
27 | handle(req, res, parsedUrl)
28 | } else if (/Mobile/i.test(ua) && !pathname.startsWith('/m')) {
29 | const mobilePathname = pathname === '/' ? '/m' : `/m${pathname}`
30 |
31 | app.render(req, res, mobilePathname, query)
32 | } else if (/Mobile/i.test(ua) && pathname === '/m/') {
33 | app.render(req, res, '/m', query)
34 | } else if (!/Mobile/i.test(ua) && pathname.startsWith('/m/')) {
35 | app.render(req, res, pathname.slice(2), query)
36 | } else if (!/Mobile/i.test(ua) && pathname.startsWith('/m')) {
37 | app.render(req, res, '/', query)
38 | } else {
39 | handle(req, res, parsedUrl)
40 | }
41 | }).listen(port, err => {
42 | if (err) throw err
43 |
44 | console.log(`> Ready on http://localhost:${port}`)
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Introduction
10 |
11 | next.js(react ssr repo) and gank api
12 |
13 | ## Features
14 |
15 | * Use react SSR framework [next.js](https://github.com/zeit/next.js)
16 | * UI ant design [antd](https://ant.design/)
17 | * Mobile UI ant design [antd-mobile](https://mobile.ant.design/index-cn)
18 | * Support PWA with [next-offline](https://github.com/hanford/next-offline)
19 | * Progress bar [nprogress](http://ricostacruz.com/nprogress/)
20 | * State management [mobx](https://mobx.js.org/)
21 | * Fetch Api instead ajax
22 |
23 | ## Pages
24 |
25 | * Home page (all types list)
26 | * Front end page
27 | * Android page
28 | * iOS page
29 | * App page
30 | * Expand page
31 | * Free videos page
32 | * Welfare images page
33 | * Timeline page
34 | * Post gank form page
35 | * Search gank page
36 |
37 | ## Develop
38 |
39 | ``` bash
40 | # install dependencies
41 | $ yarn
42 |
43 | # serve with hot reload at localhost:3000
44 | $ yarn dev
45 |
46 | # build for production and launch server
47 | $ yarn build
48 | $ yarn start
49 | ```
50 |
51 | ## License
52 |
53 | Gank is [MIT licensed](https://github.com/OrangeXC/gank/blob/master/LICENSE).
54 |
--------------------------------------------------------------------------------
/public/static/icon/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/home_active.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/timeline.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import { Tabs, Card, Col, Row } from 'antd'
4 | import Layout from '../components/Layout'
5 | import { apiBaseUrl } from '../utils'
6 |
7 | const { TabPane } = Tabs
8 |
9 | function TimeLinePage ({ timeline, years }) {
10 | return (
11 |
12 |
13 | {
14 | years.map(year =>
15 |
16 |
17 | {
18 | timeline
19 | .filter(item => item.includes(year))
20 | .map(item => cards(item))
21 | }
22 |
23 |
24 | )
25 | }
26 |
27 |
28 | )
29 | }
30 |
31 | function cards (item) {
32 | return (
33 |
34 |
35 |
39 | More
40 |
41 | }
42 | bodyStyle={{ padding: 0 }}>
43 |
44 |
45 |
46 | )
47 | }
48 |
49 |
50 | export async function getServerSideProps () {
51 | const res = await fetch(`${apiBaseUrl}day/history`)
52 | const { results } = await res.json()
53 |
54 | let years = []
55 |
56 | results.forEach(element => {
57 | if (years.indexOf(element.slice(0, 4)) === -1) years.push(element.slice(0, 4))
58 | })
59 |
60 | return {
61 | props: {
62 | timeline: results,
63 | years
64 | }
65 | }
66 | }
67 |
68 | export default TimeLinePage
69 |
--------------------------------------------------------------------------------
/pages/m/topics/[name].js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Router from 'next/router'
3 | import { NavBar, Icon } from 'antd-mobile'
4 | import { apiBaseUrl, getPageTitle, getInitList } from '../../../utils'
5 | import Layout from '../../../components/mobile/Layout'
6 | import ScrollList from '../../../components/mobile/ScrollList'
7 |
8 | class MobileTopicPage extends Component {
9 | constructor (props) {
10 | super(props)
11 |
12 | this.state = {
13 | listHeight: 1000
14 | }
15 | }
16 |
17 | componentDidMount () {
18 | const height = document.documentElement.clientHeight - 45
19 |
20 | this.setState({
21 | listHeight: height
22 | })
23 | }
24 |
25 | render () {
26 | const {
27 | title,
28 | apiUrl,
29 | initList
30 | } = this.props
31 |
32 | return (
33 |
34 |
35 | }
38 | onLeftClick={() => Router.push('/m')}
39 | >
40 | {title}
41 |
42 |
47 |
48 |
49 | )
50 | }
51 | }
52 |
53 | export async function getServerSideProps ({ params }) {
54 | const topic = params.name || ''
55 | const title = getPageTitle(topic)
56 | const apiUrl = `${apiBaseUrl}data/${encodeURIComponent(title)}/20`
57 |
58 | const initList = await getInitList(apiUrl)
59 |
60 | return {
61 | props: {
62 | title,
63 | apiUrl,
64 | initList
65 | }
66 | }
67 | }
68 |
69 | export default MobileTopicPage
70 |
--------------------------------------------------------------------------------
/public/static/css/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 20px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
--------------------------------------------------------------------------------
/public/static/css/nprogress.mobile.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 13px;
39 | right: 45px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
--------------------------------------------------------------------------------
/components/mobile/CardItem.js:
--------------------------------------------------------------------------------
1 | import { Card, Tag, Flex } from 'antd-mobile'
2 |
3 | const flexItemStyle = {
4 | flex: '0 0 auto',
5 | width: 100,
6 | textAlign: 'center'
7 | }
8 |
9 | const flexItemImageStyle = {
10 | maxWidth: 100,
11 | maxHeight: 100
12 | }
13 |
14 | const CardItem = rowData => (
15 |
16 | {
17 | rowData.type !== '福利' ? (
18 |
{window.open(rowData.url)}}>
19 |
20 |
21 | {
22 | rowData.images &&
23 |
24 |
e.target.src='/static/icon/image.svg'}
28 | />
29 |
30 | }
31 |
32 | {rowData.desc}
33 |
34 |
35 |
36 |
39 | {rowData.who ? rowData.who : ''}
40 | {rowData.publishedAt.slice(0, 10)}
41 |
42 | }
43 | extra={{rowData.type}}
44 | />
45 |
46 | ) : (
47 |
48 |
49 |
e.target.src='/static/icon/image.svg'}
53 | />
54 |
55 | {rowData.publishedAt.slice(0, 10)}} />
56 |
57 | )
58 | }
59 |
60 | )
61 |
62 | export default CardItem
63 |
--------------------------------------------------------------------------------
/pages/search.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import { Input, message } from 'antd'
3 | import Layout from '../components/Layout'
4 | import NormalList from '../components/NormalList'
5 | import ListSpin from '../components/ListSpin'
6 | import { apiBaseUrl } from '../utils'
7 |
8 | const { Search } = Input
9 |
10 | const searchStyle = {
11 | marginBottom: '50px'
12 | }
13 |
14 | class SearchPage extends Component {
15 | constructor () {
16 | super()
17 |
18 | this.state = {
19 | list: [],
20 | searching: false
21 | }
22 | }
23 |
24 | handleLoadingShow () {
25 | this.setState((preState) => ({
26 | searching: !preState.searching
27 | }))
28 | }
29 |
30 | async handleSearch (val) {
31 | if (!val) {
32 | this.setState({
33 | list: []
34 | })
35 |
36 | message.info(`搜索内容不能为空`)
37 |
38 | return
39 | }
40 |
41 | this.handleLoadingShow()
42 |
43 | const url = `${apiBaseUrl}search/query/${val}/category/all/count/50/page/1`
44 | const res = await fetch(url)
45 | const { count, results } = await res.json()
46 |
47 | if (count) {
48 | this.setState({
49 | list: results
50 | })
51 | } else {
52 | this.setState({
53 | list: []
54 | })
55 |
56 | message.warning(`未找到关键字为(${val})的数据`)
57 | }
58 |
59 | this.handleLoadingShow()
60 | }
61 |
62 | render () {
63 | return (
64 |
65 | this.handleSearch(value)}
70 | />
71 |
72 | {
73 | this.state.searching
74 | ?
75 | :
76 | }
77 |
78 | )
79 | }
80 | }
81 |
82 | export default SearchPage
83 |
--------------------------------------------------------------------------------
/public/static/icon/expand.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/static/icon/welfare.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/m/day.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import { NavBar, Icon, Tabs, WhiteSpace } from 'antd-mobile'
4 | import Layout from '../../components/mobile/Layout'
5 | import CardItem from '../../components/mobile/CardItem'
6 | import { apiBaseUrl } from '../../utils'
7 |
8 | export default class MobileDayPage extends React.Component {
9 | static async getInitialProps ({ query }) {
10 | const { date } = query
11 | const year = date.slice(0, 4)
12 | const month = date.slice(5, 7)
13 | const day = date.slice(8, 10)
14 |
15 | const res = await fetch(`${apiBaseUrl}day/${year}/${month}/${day}`)
16 | const json = await res.json()
17 |
18 | const tabs = json.category.map(item => {
19 | let listItem = {
20 | title: item,
21 | list: json.results[item]
22 | }
23 |
24 | return listItem
25 | })
26 |
27 | return {
28 | date,
29 | tabs
30 | }
31 | }
32 |
33 | constructor (props) {
34 | super(props)
35 |
36 | this.state = {
37 | listHeight: 300
38 | }
39 | }
40 |
41 | componentDidMount () {
42 | const height = document.documentElement.clientHeight - 88.5
43 |
44 | this.setState({
45 | listHeight: height
46 | })
47 | }
48 |
49 | renderContent = tab =>
50 | (
51 | {tab.list.map(item =>
52 |
53 | {CardItem(item)}
54 |
55 |
56 | )}
57 |
)
58 |
59 | render () {
60 | const {
61 | date,
62 | tabs,
63 | language
64 | } = this.props
65 |
66 | return (
67 |
68 |
69 | }
72 | onLeftClick={() => Router.push('/m/timeline')}
73 | >
74 | {date}
75 |
76 |
77 | {this.renderContent}
78 |
79 |
80 |
81 | )
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/components/MasonryList.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import { Card, Modal } from 'antd'
3 | import Masonry from 'react-masonry-component'
4 |
5 | class MasonryList extends Component {
6 | constructor (props) {
7 | super(props)
8 |
9 | this.state = {
10 | showImages: false,
11 | dialogImageVisible: false,
12 | largeImage: ''
13 | }
14 | }
15 |
16 | componentDidMount () {
17 | this.handleLayoutComplete()
18 | }
19 |
20 | handleLayoutComplete () {
21 | this.setState({
22 | showImages: true
23 | })
24 | }
25 |
26 | handleLargeImageShow (url) {
27 | this.setState({
28 | dialogImageVisible: true,
29 | largeImage: url
30 | })
31 | }
32 |
33 | handleLargeImageHide () {
34 | this.setState({
35 | dialogImageVisible: false
36 | })
37 | }
38 |
39 | render () {
40 | const wrapStyle = {
41 | opacity: this.state.showImages ? '1' : '0',
42 | transition: 'opacity 0.5s linear'
43 | }
44 |
45 | return (
46 |
47 |
48 | {
49 | this.props.list.map(element =>
50 | this.handleLargeImageShow(element.url)}
54 | >
55 | e.target.src='/static/icon/image.svg'} />}
57 | hoverable
58 | >
59 |
62 |
63 |
64 | )
65 | }
66 |
67 |
68 |
this.handleLargeImageHide()}
74 | >
75 |
76 |
77 |
78 | )
79 | }
80 | }
81 |
82 | export default MasonryList
83 |
--------------------------------------------------------------------------------
/components/mobile/MenuBar.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Router from 'next/router'
3 | import { TabBar } from 'antd-mobile'
4 |
5 | const tabs = [{
6 | title: '主页',
7 | name: 'home'
8 | }, {
9 | title: '时间轴',
10 | name: 'timeline'
11 | }, {
12 | title: '发布',
13 | name: 'upload'
14 | }]
15 |
16 | const tabData = tabs.map(({ title, name }) => ({
17 | title,
18 | icon: ,
23 | selectedIcon: ,
28 | link: name === 'home' ? '/m' : `/m/${name}`
29 | }))
30 |
31 | class MenuBar extends Component {
32 | constructor (props) {
33 | super(props)
34 |
35 | this.state = {
36 | tabHeight: '100vh'
37 | }
38 | }
39 |
40 | async componentDidMount () {
41 | const height = `${document.documentElement.clientHeight}px`
42 |
43 | this.setState({
44 | tabHeight: height
45 | })
46 | }
47 |
48 | render () {
49 | const {
50 | pathname,
51 | children
52 | } = this.props
53 |
54 | return (
55 |
56 |
57 | {
58 | tabData.map(({ title, icon, selectedIcon, link }) => (
59 | Router.push(link).then(() => window.scrollTo(0, 0))}
66 | >
67 | {children}
68 |
69 | ))
70 | }
71 |
72 |
81 |
82 | )
83 | }
84 | }
85 |
86 | export default MenuBar
87 |
--------------------------------------------------------------------------------
/components/MainList.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Layout from './Layout'
3 | import NormalList from './NormalList'
4 | import MasonryList from './MasonryList'
5 | import ListSpin from './ListSpin'
6 | import { Alert } from 'antd'
7 | import { inject, observer } from 'mobx-react'
8 |
9 | @inject('store')
10 | @observer
11 | class MainList extends Component {
12 | constructor (props) {
13 | super(props)
14 |
15 | this.state = {
16 | currentPage: 1,
17 | hasMore: true
18 | }
19 |
20 | this.handleScroll = this.handleScroll.bind(this)
21 | this.handleLoadMore = this.handleLoadMore.bind(this)
22 | }
23 |
24 | componentDidMount () {
25 | window.addEventListener('scroll', this.handleScroll)
26 | }
27 |
28 | componentWillUnmount () {
29 | window.removeEventListener('scroll', this.handleScroll)
30 | }
31 |
32 | handleScroll () {
33 | const scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop)
34 |
35 | if (
36 | document.documentElement.offsetHeight + scrollTop >
37 | document.documentElement.scrollHeight - 50
38 | ) {
39 | this.handleLoadMore()
40 | }
41 | }
42 |
43 | async handleLoadMore () {
44 | this.setState((prevState) => ({
45 | currentPage: prevState.currentPage + 1
46 | }))
47 |
48 | const currentPage = this.state.currentPage
49 | const apiUrl = this.props.apiUrl
50 |
51 | const res = await fetch(`${apiUrl}/${currentPage}`)
52 | const json = await res.json()
53 |
54 | if (Array.isArray(json.results) && json.results.length) {
55 | this.props.store.loadMoreList(json.results)
56 | } else {
57 | this.setState({
58 | hasMore: false
59 | })
60 | }
61 | }
62 |
63 | render () {
64 | const { list } = this.props.store
65 |
66 | return (
67 |
68 | {
69 | this.props.title === '福利'
70 | ?
71 | :
72 | }
73 |
74 | {
75 | this.state.hasMore
76 | ?
77 | :
78 | }
79 |
80 | )
81 | }
82 | }
83 |
84 | export default MainList
85 |
--------------------------------------------------------------------------------
/components/mobile/ScrollList.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import { ListView, Icon } from 'antd-mobile'
3 | import CardItem from './CardItem'
4 |
5 | class ScrollList extends Component {
6 | constructor (props) {
7 | super(props)
8 |
9 | const dataSource = new ListView.DataSource({
10 | rowHasChanged: (row1, row2) => row1 !== row2,
11 | }).cloneWithRows(props.initList)
12 |
13 | this.state = {
14 | rData: [],
15 | dataSource,
16 | pageIndex: 1,
17 | isLoading: false
18 | }
19 | }
20 |
21 | onEndReached = async () => {
22 | if (this.state.isLoading && !this.state.hasMore) {
23 | return
24 | }
25 |
26 | this.setState((prevState) => ({
27 | isLoading: true,
28 | pageIndex: prevState.pageIndex + 1
29 | }))
30 |
31 | await this.getData(this.state.pageIndex)
32 |
33 | this.setState({
34 | dataSource: this.state.dataSource.cloneWithRows(this.state.rData),
35 | isLoading: false
36 | })
37 | }
38 |
39 | async getData (pageIndex = 0) {
40 | const res = await fetch(`${this.props.apiUrl}/${pageIndex}`)
41 | const json = await res.json()
42 |
43 | this.setState((prevState) => ({
44 | rData: pageIndex === 2
45 | ? this.props.initList.concat(prevState.rData).concat(json.results)
46 | : prevState.rData.concat(json.results)
47 | }))
48 | }
49 |
50 | render () {
51 | const separator = (sectionId, rowId) => (
52 |
61 | )
62 |
63 | return (
64 | (
67 | {this.state.isLoading ? : '滑动加载更多!'}
68 |
)}
69 | renderRow={CardItem}
70 | renderSeparator={separator}
71 | style={{
72 | height: this.props.listHeight,
73 | overflow: 'auto'
74 | }}
75 | pageSize={10}
76 | scrollRenderAheadDistance={500}
77 | onEndReached={this.onEndReached}
78 | onEndReachedThreshold={10}
79 | />
80 | )
81 | }
82 | }
83 |
84 | export default ScrollList
85 |
--------------------------------------------------------------------------------
/components/NormalList.js:
--------------------------------------------------------------------------------
1 | import { List } from 'antd'
2 | import {
3 | UserOutlined, ClockCircleOutlined, TagOutlined
4 | } from '@ant-design/icons'
5 | import Router from 'next/router'
6 | import { getPageTopic } from '../utils'
7 |
8 | const { Item } = List
9 |
10 | const IconText = ({ icon, text, href = '' }) => (
11 | href && Router.push(href).then(() => window.scrollTo(0, 0))}>
12 | {icon}
13 | {text}
14 |
15 | )
16 |
17 | const listImageWrapStyle = {
18 | width: 272,
19 | height: 168,
20 | textAlign: 'center',
21 | lineHeight: '168px',
22 | backgroundColor: 'rgb(234, 237, 242)'
23 | }
24 |
25 | const listImageStyle = {
26 | maxWidth: '100%',
27 | maxHeight: '100%'
28 | }
29 |
30 | const NormalList = ({ list }) => (
31 |
32 |
(
38 | } text={item.who ? item.who : '未知'} />,
42 | }
44 | text={item.publishedAt.slice(0, 10)}
45 | href={`/day?date=${item.publishedAt.slice(0, 10)}`}
46 | />,
47 | }
49 | text={item.type}
50 | href={`/topics/${getPageTopic(item.type)}`}
51 | />
52 | ]}
53 | extra={item.images &&
54 |
55 |

e.target.src='/static/icon/image.svg'}
60 | />
61 |
62 | }
63 | >
64 |
67 |
68 | )}
69 | />
70 |
71 |
82 |
83 | )
84 |
85 | export default NormalList
86 |
--------------------------------------------------------------------------------
/pages/m/search.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Router from 'next/router'
3 | import {
4 | NavBar, SearchBar, Icon, WhiteSpace, Toast
5 | } from 'antd-mobile'
6 | import CardItem from '../../components/mobile/CardItem'
7 | import Layout from '../../components/mobile/Layout'
8 | import { apiBaseUrl } from '../../utils'
9 |
10 | class MobileSearchPage extends Component {
11 | constructor (props) {
12 | super(props)
13 |
14 | this.state = {
15 | value: '',
16 | list: [],
17 | listHeight: 1000
18 | }
19 | }
20 |
21 | componentDidMount() {
22 | this.autoFocusInst.focus()
23 |
24 | const height = document.documentElement.clientHeight - 89
25 |
26 | this.setState({
27 | listHeight: height
28 | })
29 | }
30 |
31 | async handleSearch (val) {
32 | if (!val) {
33 | this.setState(() => ({
34 | list: []
35 | }))
36 |
37 | Toast.fail('搜索内容不能为空!', 2)
38 |
39 | return
40 | }
41 |
42 | Toast.loading('Loading...', 10)
43 |
44 | const url = `${apiBaseUrl}search/query/${val}/category/all/count/50/page/1`
45 | const res = await fetch(url)
46 | const { count, results } = await res.json()
47 |
48 | if (count) {
49 | this.setState(() => ({
50 | list: results
51 | }))
52 |
53 | Toast.hide()
54 | } else {
55 | this.setState(() => ({
56 | list: []
57 | }))
58 |
59 | await Toast.fail(`未找到关键字为(${val})的数据`, 2)
60 | }
61 | }
62 |
63 | render () {
64 | const { language } = this.props
65 |
66 | return (
67 |
68 |
69 |
}
72 | onLeftClick={() => Router.push('/m')}
73 | >
74 | 搜索
75 |
76 |
this.autoFocusInst = ref}
79 | onSubmit={value => this.handleSearch(value)}
80 | />
81 |
82 | {
83 | this.state.list.map((item, index) =>
84 |
85 | {CardItem(item)}
86 |
87 |
88 | )
89 | }
90 |
91 |
92 |
93 | )
94 | }
95 | }
96 |
97 | export async function getServerSideProps ({ req }) {
98 | const language = req
99 | ? req.headers['accept-language']
100 | : navigator.language
101 |
102 | return {
103 | props: {
104 | language
105 | }
106 | }
107 | }
108 |
109 | export default MobileSearchPage
110 |
--------------------------------------------------------------------------------
/public/static/icon/blind.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/m/index.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { withRouter } from 'next/router'
4 | import {
5 | WhiteSpace, NavBar, Icon, Grid
6 | } from 'antd-mobile'
7 | import Layout from '../../components/mobile/Layout'
8 | import MenuBar from '../../components/mobile/MenuBar'
9 | import ScrollList from '../../components/mobile/ScrollList'
10 | import { apiBaseUrl, getInitList } from '../../utils'
11 |
12 | const gridMenu = [
13 | {
14 | icon: '/static/icon/html.svg',
15 | text: '前端',
16 | link: '/m/topics/fe'
17 | }, {
18 | icon: '/static/icon/android.svg',
19 | text: '安卓',
20 | link: '/m/topics/android'
21 | }, {
22 | icon: '/static/icon/ios.svg',
23 | text: 'iOS',
24 | link: '/m/topics/ios'
25 | }, {
26 | icon: '/static/icon/app.svg',
27 | text: 'App',
28 | link: '/m/topics/app'
29 | }, {
30 | icon: '/static/icon/expand.svg',
31 | text: '拓展资源',
32 | link: '/m/topics/expand'
33 | }, {
34 | icon: '/static/icon/video.svg',
35 | text: '休息视频',
36 | link: '/m/topics/videos'
37 | }, {
38 | icon: '/static/icon/blind.svg',
39 | text: '瞎推荐',
40 | link: '/m/topics/blind'
41 | }, {
42 | icon: '/static/icon/welfare.svg',
43 | text: '福利',
44 | link: '/m/topics/welfare'
45 | }
46 | ]
47 |
48 | class MobileHome extends Component {
49 | constructor (props) {
50 | super(props)
51 |
52 | this.router = props.router
53 | this.state = {
54 | listHeight: 1000
55 | }
56 | }
57 |
58 | componentDidMount () {
59 | const height = document.documentElement.clientHeight - 113 -
60 | ReactDOM.findDOMNode(this.grid).getBoundingClientRect().height
61 |
62 | this.setState({
63 | listHeight: height
64 | })
65 | }
66 |
67 | render () {
68 | const {
69 | initList,
70 | apiUrl
71 | } = this.props
72 |
73 | return (
74 |
75 |
78 | }
81 | onLeftClick={() => window.open('https://github.com/OrangeXC/gank')}
82 | rightContent={
84 | this.router
85 | .push('/m/search')
86 | .then(() => window.scrollTo(0, 0))}
87 | type='search'
88 | />}
89 | >
90 | 主页
91 |
92 |
93 | this.grid = el}
95 | data={gridMenu}
96 | hasLine={false}
97 | onClick={(el) => this.router.push(el.link)}
98 | />
99 |
100 |
105 |
106 |
107 |
108 | )
109 | }
110 | }
111 |
112 | export async function getServerSideProps () {
113 | const apiUrl = `${apiBaseUrl}data/all/20`
114 |
115 | const initList = await getInitList(apiUrl)
116 |
117 | return {
118 | props: {
119 | apiUrl,
120 | initList
121 | }
122 | }
123 | }
124 |
125 | export default withRouter(MobileHome)
126 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 | import NProgress from 'nprogress'
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import {
6 | Layout, Menu, Button, BackTop
7 | } from 'antd'
8 | import {
9 | GithubOutlined, SearchOutlined, UploadOutlined
10 | } from '@ant-design/icons'
11 | import 'isomorphic-fetch'
12 | import ActiveLink from './ActiveLink'
13 | import pkg from '../package.json'
14 |
15 | Router.onRouteChangeStart = () => NProgress.start()
16 | Router.onRouteChangeComplete = () => NProgress.done()
17 | Router.onRouteChangeError = () => NProgress.done()
18 |
19 | const { Header, Content, Footer } = Layout
20 |
21 | const headerStyle = {
22 | position: 'fixed',
23 | top: '0',
24 | left: '0',
25 | width: '100%',
26 | zIndex: 10,
27 | minWidth: 1140
28 | }
29 | const headerMenuStyle = {
30 | lineHeight: '64px',
31 | float: 'left'
32 | }
33 | const contentStyle = {
34 | width: 1140,
35 | padding: '80px 50px 64px',
36 | margin: '0 auto',
37 | minHeight: `calc(100vh - 69px)`
38 | }
39 |
40 | const LayoutPage = ({ children, title = '主页' }) => (
41 |
42 |
43 |
{title} - Gank
44 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
73 |
84 |
85 |
86 |
87 | { children }
88 |
89 |
90 |
96 |
97 |
98 |
99 |
114 |
115 | )
116 |
117 | export default LayoutPage
118 |
--------------------------------------------------------------------------------
/pages/m/timeline.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import Link from 'next/link'
3 | import { withRouter } from 'next/router'
4 | import { NavBar, List, ListView } from 'antd-mobile'
5 | import Layout from '../../components/mobile/Layout'
6 | import MenuBar from '../../components/mobile/MenuBar'
7 | import { apiBaseUrl } from '../../utils'
8 |
9 | const { Item } = List
10 |
11 | class MobileTimeLinePage extends Component {
12 | constructor (props) {
13 | super(props)
14 |
15 | const dataSource = new ListView.DataSource({
16 | rowHasChanged: (row1, row2) => row1 !== row2,
17 | }).cloneWithRows(props.timeline)
18 |
19 | this.state = {
20 | dataSource,
21 | listHeight: 1000
22 | }
23 | }
24 |
25 | async componentDidMount () {
26 | const height = document.documentElement.clientHeight - 95
27 |
28 | this.setState({
29 | listHeight: height
30 | })
31 | }
32 |
33 | render () {
34 | const {
35 | router: { pathname }
36 | } = this.props
37 |
38 | const {
39 | dataSource,
40 | listHeight
41 | } = this.state
42 |
43 | return (
44 |
45 |
46 |
49 |
52 | 时间轴
53 |
54 | (
57 |
60 | - {rowData}
61 |
62 | )}
63 | style={{
64 | height: listHeight,
65 | overflow: 'scroll'
66 | }}
67 | initialListSize={30}
68 | pageSize={30}
69 | />
70 |
71 |
72 |
73 |
148 |
149 | )
150 | }
151 | }
152 |
153 | export async function getServerSideProps () {
154 | const res = await fetch(`${apiBaseUrl}day/history`)
155 | const json = await res.json()
156 |
157 | return {
158 | props: {
159 | timeline: json.results
160 | }
161 | }
162 | }
163 |
164 | export default withRouter(MobileTimeLinePage)
165 |
--------------------------------------------------------------------------------
/pages/upload.js:
--------------------------------------------------------------------------------
1 | import { Component, createRef } from 'react'
2 | import {
3 | Form, Input, Select, Switch, Button, message, Alert
4 | } from 'antd'
5 | import Layout from '../components/Layout'
6 | import { apiBaseUrl } from '../utils'
7 |
8 | const FormItem = Form.Item
9 | const Option = Select.Option
10 |
11 | class UploadForm extends Component {
12 | constructor () {
13 | super()
14 |
15 | this.state = {
16 | submitLoading: false
17 | }
18 |
19 | this.formRef = createRef()
20 | this.onFinish = this.onFinish.bind(this)
21 | this.onFinishFailed = this.onFinishFailed.bind(this)
22 | }
23 |
24 | async onFinish (values) {
25 | this.setState({ submitLoading: true })
26 |
27 | let strList = []
28 |
29 | Object.keys(values).forEach(item => {
30 | strList.push(`${item}=${values[item]}`)
31 | })
32 |
33 | const res = await fetch(`${apiBaseUrl}add2gank`, {
34 | method: "POST",
35 | headers: {
36 | 'Content-Type': 'application/x-www-form-urlencoded'
37 | },
38 | body: strList.join('&')
39 | })
40 |
41 | const json = await res.json()
42 |
43 | if (json.error) {
44 | message.error(json.msg)
45 | } else {
46 | message.success(json.msg)
47 |
48 | this.formRef.current.resetFields()
49 | }
50 |
51 | this.setState({ submitLoading: false })
52 | }
53 |
54 | onFinishFailed = ({ errorFields }) => {
55 | this.formRef.current.scrollToField(errorFields[0].name)
56 | }
57 |
58 | async checkUrl (rule, value) {
59 | if (value && !this.validUrl(value)) {
60 | throw new Error('请输入正确的url地址!')
61 | }
62 | }
63 |
64 | validUrl (str) {
65 | const pattern = /^(https?:\/\/)?((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/i
66 |
67 | return !!pattern.test(str)
68 | }
69 |
70 | render () {
71 | const layout = {
72 | labelCol: {
73 | xs: { span: 24 },
74 | sm: { span: 6 },
75 | },
76 | wrapperCol: {
77 | xs: { span: 24 },
78 | sm: { span: 14 },
79 | }
80 | }
81 |
82 | const tailLayout = {
83 | wrapperCol: {
84 | xs: {
85 | span: 24,
86 | offset: 0,
87 | },
88 | sm: {
89 | span: 14,
90 | offset: 6,
91 | }
92 | }
93 | }
94 |
95 | const types = ['Android', 'iOS', '休息视频', '福利', '拓展资源', '前端', '瞎推荐', 'App']
96 |
97 | return (
98 |
99 |
106 |
179 |
180 | )
181 | }
182 | }
183 |
184 | export default UploadForm
185 |
--------------------------------------------------------------------------------
/pages/m/upload.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | NavBar, NoticeBar, List, InputItem, Picker,
4 | Switch, Button, Toast, WingBlank, WhiteSpace
5 | } from 'antd-mobile'
6 | import { createForm } from 'rc-form'
7 | import MenuBar from '../../components/mobile/MenuBar'
8 | import Layout from '../../components/mobile/Layout'
9 | import { apiBaseUrl } from '../../utils'
10 |
11 | const { Item } = List
12 | const types = [
13 | 'Android', 'iOS', '休息视频', '福利',
14 | '拓展资源', '前端', '瞎推荐', 'App'
15 | ].map(item => ({
16 | label: item,
17 | value: item
18 | }))
19 |
20 | class MobileUploadForm extends React.Component {
21 | constructor (props) {
22 | super(props)
23 |
24 | this.state = {
25 | submitLoading: false
26 | }
27 |
28 | this.onReset = this.onReset.bind(this)
29 | this.onSubmit = this.onSubmit.bind(this)
30 | }
31 |
32 | onSubmit () {
33 | this.props.form.validateFields({ force: true }, async (error) => {
34 | if (error) {
35 | Toast.fail('验证失败', 2)
36 |
37 | return
38 | }
39 |
40 | this.setState({
41 | submitLoading: true
42 | })
43 |
44 | const values = this.props.form.getFieldsValue()
45 |
46 | let strList = []
47 |
48 | Object.keys(values).forEach(item => {
49 | if (item === 'type') {
50 | strList.push(`${item}=${values[item][0]}`)
51 | } else {
52 | strList.push(`${item}=${values[item]}`)
53 | }
54 | })
55 |
56 | const res = await fetch(`${apiBaseUrl}add2gank`, {
57 | method: "POST",
58 | headers: {
59 | 'Content-Type': 'application/x-www-form-urlencoded'
60 | },
61 | body: strList.join('&')
62 | })
63 |
64 | const data = await res.json()
65 |
66 | if (data.error) {
67 | Toast.fail(data.msg, 2)
68 | } else {
69 | Toast.success(data.msg, 2)
70 |
71 | this.onReset()
72 | }
73 |
74 | this.setState({
75 | submitLoading: false
76 | })
77 | })
78 | }
79 |
80 | onReset () {
81 | this.props.form.resetFields()
82 | }
83 |
84 | validateUrl (rule, value, callback) {
85 | if (value && this.checkUrl(value)) {
86 | callback()
87 | } else {
88 | callback(new Error('请输入正确的链接'))
89 | }
90 | }
91 |
92 | checkUrl (str) {
93 | const pattern = /^(https?:\/\/)?((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/i
94 |
95 | if (pattern.test(str)) {
96 | return true
97 | }
98 |
99 | return false
100 | }
101 |
102 | render () {
103 | const {
104 | url: { pathname }
105 | } = this.props
106 | const { getFieldProps, getFieldError } = this.props.form
107 |
108 | return (
109 |
110 |
113 |
116 | 发布
117 |
118 |
119 | 本项目 api 由 gank(干货集中营)提供,本着对 api 提供者负责的原则,此提交表单默认是 debug 模式,非正式提交(但走的是真实接口,可控制台查看),如果您认为你的链接的确是高质量的干活,手动将 “测试数据” 设置为否即可,并且欢迎您提供优质的干货分享给大家.
120 |
121 |
122 |
205 |
206 |
207 | )
208 | }
209 | }
210 |
211 | export default createForm()(MobileUploadForm)
212 |
--------------------------------------------------------------------------------