✨ adding useRequest

This commit is contained in:
zuiidea
2021-04-11 22:14:18 +08:00
parent 1dbeb05005
commit 12eab5f5c7
22 changed files with 386 additions and 239 deletions
+3
View File
@@ -25,6 +25,9 @@ const config: IConfig = {
type: 'none',
exclude: [],
},
mock: {
exclude: ['utils/index'],
},
extraBabelPresets: ['@lingui/babel-preset-react'],
extraBabelPlugins: [
'macros',
+3 -2
View File
@@ -1,4 +1,5 @@
import { Mock, Constant } from './_utils'
import Mock from 'mockjs'
import { Constant } from './utils'
const { ApiPrefix, Color } = Constant
@@ -135,7 +136,7 @@ const Dashboard = Mock.mock({
],
})
module.exports = {
export default {
[`GET ${ApiPrefix}/dashboard`](req, res) {
res.json(Dashboard)
},
+2
View File
@@ -0,0 +1,2 @@
declare module 'mockjs'
declare module 'qs'
+2 -2
View File
@@ -1,4 +1,4 @@
import { Constant } from './_utils'
import { Constant } from './utils'
const { ApiPrefix } = Constant
const database = [
@@ -148,7 +148,7 @@ const database = [
},
]
module.exports = {
export default {
[`GET ${ApiPrefix}/routes`](req, res) {
res.status(200).json(database)
},
+46 -34
View File
@@ -1,9 +1,10 @@
import { Mock, Constant, randomAvatar } from './_utils'
import Mock from 'mockjs'
import qs from 'qs'
import { Constant, randomAvatar, waitTime } from './utils'
const { ApiPrefix } = Constant
let usersListData = Mock.mock({
const usersListData = Mock.mock({
'data|80-100': [
{
id: '@id',
@@ -91,10 +92,28 @@ const NOTFOUND = {
documentation_url: 'http://localhost:8000/request',
}
module.exports = {
[`POST ${ApiPrefix}/user/login`](req, res) {
const authorized = (req, res) => {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
if (!cookies.token) {
res.status(401).send({ message: 'Not Login' })
return false
}
const token = JSON.parse(cookies.token)
if (cookies.token && token && token.deadline > new Date().getTime()) {
return token
}
res.status(401).send({ message: 'Not Login' })
return false
}
export default {
async [`POST ${ApiPrefix}/user/login`](req, res) {
await waitTime()
const { username, password } = req.body
const user = adminUsers.filter(item => item.username === username)
const user = adminUsers.filter((item) => item.username === username)
if (user.length > 0 && user[0].password === password) {
const now = new Date()
@@ -113,33 +132,27 @@ module.exports = {
}
},
[`GET ${ApiPrefix}/user/logout`](req, res) {
async [`GET ${ApiPrefix}/user/logout`](req, res) {
await waitTime()
res.clearCookie('token')
res.status(200).end()
},
[`GET ${ApiPrefix}/user`](req, res) {
const cookie = req.headers.cookie || ''
const cookies = qs.parse(cookie.replace(/\s/g, ''), { delimiter: ';' })
const response = {}
let user = {}
if (!cookies.token) {
res.status(200).send({ message: 'Not Login' })
async [`GET ${ApiPrefix}/user`](req, res) {
await waitTime()
const token = authorized(req, res)
if (!token) {
return
}
const token = JSON.parse(cookies.token)
if (token) {
response.success = token.deadline > new Date().getTime()
const userItem = adminUsers.find((_) => _.id === token.id)
if (userItem) {
const { password, ...rest } = userItem
res.json(rest)
} else {
res.status(401).send({ message: 'Not Login' })
}
if (response.success) {
const userItem = adminUsers.find(_ => _.id === token.id)
if (userItem) {
const { password, ...other } = userItem
user = other
}
}
response.user = user
res.json(response)
},
[`GET ${ApiPrefix}/users`](req, res) {
@@ -151,10 +164,10 @@ module.exports = {
let newData = database
for (let key in other) {
if ({}.hasOwnProperty.call(other, key)) {
newData = newData.filter(item => {
newData = newData.filter((item) => {
if ({}.hasOwnProperty.call(item, key)) {
if (key === 'address') {
return other[key].every(iitem => item[key].indexOf(iitem) > -1)
return other[key].every((iitem) => item[key].indexOf(iitem) > -1)
} else if (key === 'createTime') {
const start = new Date(other[key][0]).getTime()
const end = new Date(other[key][1]).getTime()
@@ -166,9 +179,8 @@ module.exports = {
return true
}
return (
String(item[key])
.trim()
.indexOf(decodeURI(other[key]).trim()) > -1
String(item[key]).trim().indexOf(decodeURI(other[key]).trim()) >
-1
)
}
return true
@@ -183,8 +195,8 @@ module.exports = {
},
[`POST ${ApiPrefix}/users/delete`](req, res) {
const { ids=[] } = req.body
database = database.filter(item => !ids.some(_ => _ === item.id))
const { ids = [] } = req.body
database = database.filter((item) => !ids.some((_) => _ === item.id))
res.status(204).end()
},
@@ -221,7 +233,7 @@ module.exports = {
const { id } = req.params
const data = queryArray(database, id, 'id')
if (data) {
database = database.filter(item => item.id !== id)
database = database.filter((item) => item.id !== id)
res.status(204).end()
} else {
res.status(200).json(NOTFOUND)
@@ -233,7 +245,7 @@ module.exports = {
const editItem = req.body
let isExist = false
database = database.map(item => {
database = database.map((item) => {
if (item.id === id) {
isExist = true
return Object.assign({}, item, editItem)
+13 -6
View File
@@ -9,7 +9,7 @@ export function queryArray(array, key, value) {
if (!Array.isArray(array)) {
return
}
return array.filter(_ => _[key] === value)
return array.filter((_) => _[key] === value)
}
export function randomNumber(min, max) {
@@ -30,9 +30,19 @@ export function randomAvatar() {
'photo-1551438632-e8c7d9a5d1b7.jpeg',
'photo-1525879000488-bff3b1c387cf.jpeg',
'photo-1487412720507-e7ab37603c6f.jpeg',
'photo-1510227272981-87123e259b17.jpeg'
'photo-1510227272981-87123e259b17.jpeg',
]
return `//image.zuiidea.com/${avatarList[randomNumber(0, avatarList.length - 1)]}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim`
return `//image.zuiidea.com/${
avatarList[randomNumber(0, avatarList.length - 1)]
}?imageView2/1/w/200/h/200/format/webp/q/75|imageslim`
}
export const waitTime = (time: number = 300) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
}
export const Constant = {
@@ -54,6 +64,3 @@ export const Constant = {
sky: '#c1e0fc',
},
}
export Mock from 'mockjs'
export qs from 'qs'
+1
View File
@@ -20,6 +20,7 @@ export const config: IConfig = {
logo: 'logo.svg',
copyright: 'Ant Design Admin © 2021 zuiidea',
language: language as ISupportedLocales,
apiPrefix: '/api/v1',
/* Layout configuration, specify which layout to use for route. */
layouts: [
+10 -2
View File
@@ -1,4 +1,12 @@
import { useHistory, useLocation } from 'umi'
import { useHistory, useLocation, useRequest, UseRequestProvider } from 'umi'
import useIntlProvider from './useIntlProvider'
import useConfig from './useConfig'
export { useIntlProvider, useHistory, useLocation }
export {
useIntlProvider,
useHistory,
useLocation,
useRequest,
UseRequestProvider,
useConfig,
}
+8
View File
@@ -0,0 +1,8 @@
import { useContext } from 'react'
import { ConfigContext } from '@/utils/context'
const useConfig = () => {
return useContext(ConfigContext)
}
export default useConfig
+1 -1
View File
@@ -3,7 +3,7 @@ import { useLocation } from '@/hooks'
import { queryLayout } from '@/utils'
import { config } from '@/configs'
import PublicLayout from './PublicLayout'
import PrimaryLayout from './PrimaryLayout'
import PrimaryLayout from './PrimaryLayout/index'
import './BaseLayout.less'
const LayoutMap = {
@@ -0,0 +1,69 @@
import React from 'react'
import { Dropdown, Menu, Avatar } from 'antd'
import { language, languages } from '@/configs'
import { ISupportedLocales } from '@/typings'
import { Trans } from '@lingui/macro'
import { useRequest, useHistory, useConfig } from '@/hooks'
import { logoutUser } from '@/services'
import styles from './index.less'
const { SubMenu } = Menu
const NotFoundPage: React.FC = () => {
const { language: locale, setLanguage, userInfo } = useConfig()
const currentLanguage = language[locale]
const history = useHistory()
const { run: runLogoutUser } = useRequest(logoutUser, {
manual: true,
onSuccess: () => history.push('/login'),
})
return (
<>
<Dropdown
overlay={
<Menu
selectedKeys={[currentLanguage.value]}
onClick={(data) => {
setLanguage(data.key as ISupportedLocales)
}}
>
{languages.map((item) => (
<Menu.Item key={item.key}>
<Avatar
size="small"
style={{ marginRight: 8 }}
src={item.flag}
/>
{item.value}
</Menu.Item>
))}
</Menu>
}
>
<div className={styles.headerButton}>
<Avatar size="small" src={currentLanguage.flag} />
</div>
</Dropdown>
<Menu key="user" mode="horizontal">
<SubMenu
title={
<>
<span style={{ color: '#999', marginRight: 4 }}>
<Trans>Hi,</Trans>
</span>
<span>{userInfo?.username}</span>
<Avatar style={{ marginLeft: 8 }} src={userInfo?.avatar} />
</>
}
>
<Menu.Item key="SignOut" onClick={() => runLogoutUser()}>
<Trans>Sign out</Trans>
</Menu.Item>
</SubMenu>
</Menu>
</>
)
}
export default NotFoundPage
@@ -1,5 +1,9 @@
@import '~themes/vars.less';
@import '~@/themes/vars.less';
:global {
#root {
height: 100%;
}
}
.backTop {
right: 50px;
@@ -1,24 +1,15 @@
import React, {
useState,
useEffect,
useMemo,
useCallback,
useContext,
} from 'react'
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import ProBasicLayout, {
getMenuData,
MenuDataItem,
SettingDrawerProps,
} from '@ant-design/pro-layout'
import { Link } from 'umi'
import { Dropdown, Menu, Avatar } from 'antd'
import { useLocation } from '@/hooks'
import { ConfigContext } from '@/utils/context'
import { menus, menuIcon, language, languages, config } from '@/configs'
import { menus, menuIcon, config } from '@/configs'
import defaultSettings from '@/defaultSettings'
import { ISupportedLocales } from '@/typings'
import type { MenuProps } from 'antd/es/menu'
import styles from './index.less'
import HeaderRightContent from './HeaderRightContent'
const renderMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
menus.map(({ icon, children, ...item }) => ({
@@ -34,8 +25,6 @@ const PrimaryLayout: React.FC = (props) => {
const [selectedKeys, setSelectedKeys] = useState<string[]>(['/'])
const { pathname } = location
const { breadcrumbMap, menuData } = useMemo(() => getMenuData(menus), [])
const { language: locale, setLanguage } = useContext(ConfigContext)
const currentLanguage = language[locale]
useEffect(() => {
const select = breadcrumbMap.get(pathname)
@@ -68,30 +57,6 @@ const PrimaryLayout: React.FC = (props) => {
[handleOnOpenChange, openKeys, selectedKeys]
)
const rightContentRender = () => (
<Dropdown
overlay={
<Menu
selectedKeys={[currentLanguage.value]}
onClick={(data) => {
setLanguage(data.key as ISupportedLocales)
}}
>
{languages.map((item) => (
<Menu.Item key={item.key}>
<Avatar size="small" style={{ marginRight: 8 }} src={item.flag} />
{item.value}
</Menu.Item>
))}
</Menu>
}
>
<div className={styles.headerButton}>
<Avatar size="small" src={currentLanguage.flag} />
</div>
</Dropdown>
)
return (
<>
<ProBasicLayout
@@ -101,7 +66,7 @@ const PrimaryLayout: React.FC = (props) => {
menuDataRender={menuDataRender}
menuItemRender={menuItemRender}
menuProps={menuProps}
rightContentRender={rightContentRender}
rightContentRender={() => <HeaderRightContent />}
>
{props.children}
</ProBasicLayout>
+36 -2
View File
@@ -1,21 +1,55 @@
import React from 'react'
import { ConfigProvider } from 'antd'
import { I18nProvider } from '@lingui/react'
import { useIntlProvider } from '@/hooks'
import {
useIntlProvider,
useRequest,
UseRequestProvider,
useHistory,
} from '@/hooks'
import { ConfigContext } from '@/utils/context'
import request from '@/utils/request'
import { queryUserInfo, IUserInfo } from '@/services'
import BaseLayout from './BaseLayout'
const Layout: React.FC = (props) => {
const { language, setLanguage, i18n, locale } = useIntlProvider()
const history = useHistory()
const {
data: userInfo,
run: runQueryUserInfo,
loading,
} = useRequest<IUserInfo>(queryUserInfo, {
onError: (data: any) => {
if (data.statusCode === 401) {
history.push('/login')
}
},
})
const globalConfig = {
language,
setLanguage,
userInfo: userInfo as IUserInfo,
queryUserInfo: runQueryUserInfo,
}
if (loading) {
return <div>loading</div>
}
return (
<ConfigContext.Provider value={globalConfig}>
<I18nProvider i18n={i18n}>
<ConfigProvider locale={locale}>
<BaseLayout>{props.children}</BaseLayout>
<UseRequestProvider
value={{
requestMethod: request,
}}
>
<BaseLayout>{props.children}</BaseLayout>
</UseRequestProvider>
</ConfigProvider>
</I18nProvider>
</ConfigContext.Provider>
+32 -17
View File
@@ -4,27 +4,37 @@ import { GithubOutlined } from '@ant-design/icons'
import { GlobalFooter } from '@/components'
import { Trans, t } from '@lingui/macro'
import { config, languages } from '@/configs'
import { useRequest, useHistory, useConfig } from '@/hooks'
import { loginUser, ILoginUserParams } from '@/services'
import styles from './index.less'
const FormItem = Form.Item
const Login: React.FC = () => {
const footerLinks = [
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/zuiidea/antd-admin',
blankTarget: true,
},
...languages.map((item) => ({
key: item.key,
title: <span>{item.value}</span>,
})),
]
const footerLinks = [
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/zuiidea/antd-admin',
blankTarget: true,
},
...languages.map((item) => ({
key: item.key,
title: <span>{item.value}</span>,
})),
]
const handleFinish = (values) => {
// eslint-disable-next-line no-console
console.log(values)
const Login: React.FC = () => {
const { run: runLogin, loading } = useRequest(loginUser, {
manual: true,
})
const history = useHistory()
const { queryUserInfo } = useConfig()
const handleFinish = (values: ILoginUserParams) => {
runLogin(values).then(() => {
queryUserInfo()
history.push('/dashborad')
})
}
return (
@@ -50,7 +60,12 @@ const Login: React.FC = () => {
<Input.Password placeholder={t`Password`} />
</FormItem>
<Row>
<Button type="primary" htmlType="submit">
<Button
type="primary"
htmlType="submit"
loading={loading}
disabled={loading}
>
<Trans>Sign in</Trans>
</Button>
<p>
-38
View File
@@ -1,38 +0,0 @@
import request from 'utils/request'
import { apiPrefix } from 'utils/config'
import api from './api'
const gen = (params) => {
let url = apiPrefix + params
let method = 'GET'
const paramsArray = params.split(' ')
if (paramsArray.length === 2) {
method = paramsArray[0]
url = apiPrefix + paramsArray[1]
}
return function (data) {
return request({
url,
data,
method,
})
}
}
const APIFunction = {}
for (const key in api) {
APIFunction[key] = gen(api[key])
}
APIFunction.queryWeather = (params) => {
params.key = 'i7sau1babuzwhycn'
return request({
url: `${apiPrefix}/weather/now.json`,
data: params,
})
}
export default APIFunction
+44
View File
@@ -0,0 +1,44 @@
import user, { TUserAPI } from '@/services/user'
import createService, { TService } from '@/utils/createService'
const APIs = {
...user,
}
type RecordService<T> = {
[P in keyof T]: TService
}
type TAPIService = RecordService<TUserAPI>
const APIService = {} as TAPIService
for (const key in APIs) {
APIService[key] = createService(APIs[key])
}
export const {
logoutUser,
queryUser,
queryUserList,
updateUser,
createUser,
removeUser,
removeUserList,
} = APIService
export interface IUserInfo {
avatar: string
id: number
username: string
permissions: {
role?: string
visit?: string[]
}
}
export const queryUserInfo = createService<IUserInfo>(user.queryUserInfo)
export interface ILoginUserParams {
password: string
username: string
}
export const loginUser = createService<any, ILoginUserParams>(user.loginUser)
@@ -1,18 +1,24 @@
export default {
queryRouteList: '/routes',
export type TUserAPIProperty =
| 'queryUserInfo'
| 'logoutUser'
| 'loginUser'
| 'queryUser'
| 'queryUserList'
| 'updateUser'
| 'createUser'
| 'removeUser'
| 'removeUserList'
export type TUserAPI = Record<TUserAPIProperty, string>
export default {
queryUserInfo: '/user',
logoutUser: '/user/logout',
loginUser: 'POST /user/login',
queryUser: '/user/:id',
queryUserList: '/users',
updateUser: 'Patch /user/:id',
createUser: 'POST /user',
removeUser: 'DELETE /user/:id',
removeUserList: 'POST /users/delete',
queryPostList: '/posts',
queryDashboard: '/dashboard',
}
} as TUserAPI
+1
View File
@@ -12,5 +12,6 @@ export interface IConfig {
title: string
logo: string
copyright: string
apiPrefix: string
layouts: ILayouItem[]
}
+5
View File
@@ -1,13 +1,18 @@
import React from 'react'
import { config } from '@/configs'
import { ISupportedLocales } from '@/typings'
import { IUserInfo } from '@/services'
interface IConfigContext {
language: ISupportedLocales
setLanguage: (language: ISupportedLocales) => void
userInfo: IUserInfo | null
queryUserInfo: () => void
}
export const ConfigContext = React.createContext<IConfigContext>({
language: config.language,
setLanguage: (language) => null,
userInfo: null,
queryUserInfo: () => null,
})
+25
View File
@@ -0,0 +1,25 @@
import request from '@/utils/request'
import { AxiosPromise, AxiosRequestConfig } from 'axios'
import { config } from '@/configs'
const createService = <T, U = any>(params: string) => {
let url = params
let method = 'GET'
const paramsArray = params.split(' ')
if (paramsArray.length === 2) {
method = paramsArray[0]
url = paramsArray[1]
}
return (data: U) =>
request<T>({
url: config.apiPrefix + url,
data,
method: method as AxiosRequestConfig['method'],
})
}
export type TService = <T>(data?: any) => AxiosPromise<T>
export default createService
+61 -86
View File
@@ -1,92 +1,67 @@
import axios from 'axios'
import { cloneDeep } from 'lodash'
import { message } from 'antd'
import { CANCEL_REQUEST_MESSAGE } from '@/utils/constant'
const { parse, compile } = require('path-to-regexp')
import axios, { AxiosRequestConfig, AxiosPromise } from 'axios'
const { CancelToken } = axios
window.cancelRequest = new Map()
export default function request(options) {
let { data, url } = options
const cloneData = cloneDeep(data)
try {
let domain = ''
const urlMatch = url.match(/[a-zA-z]+:\/\/[^/]*/)
if (urlMatch) {
;[domain] = urlMatch
url = url.slice(domain.length)
}
const match = parse(url)
url = compile(url)(data)
for (const item of match) {
if (item instanceof Object && item.name in cloneData) {
delete cloneData[item.name]
}
}
url = domain + url
} catch (e) {
message.error(e.message)
const transformURL = (url: string, data: any) => {
if (!data) {
return { url, data }
}
let cloneData = data
let cloneUrl = url
try {
cloneData = JSON.parse(JSON.stringify(data))
} catch (error) {}
options.url = url
options.cancelToken = new CancelToken((cancel) => {
window.cancelRequest.set(Symbol(Date.now()), {
pathname: window.location.pathname,
cancel,
})
Object.entries(cloneData).forEach(([key]) => {
const reg = new RegExp(`/:${key}`)
if (reg.test(cloneUrl)) {
cloneUrl = cloneUrl.replace(`/:${key}`, `/${cloneData[key]}`)
delete cloneData[key]
}
})
return axios(options)
.then((response) => {
const { statusText, status, data } = response
let result = {}
if (typeof data === 'object') {
result = data
if (Array.isArray(data)) {
result.list = data
}
} else {
result.data = data
}
return Promise.resolve({
success: true,
message: statusText,
statusCode: status,
...result,
})
})
.catch((error) => {
const { response, message } = error
if (String(message) === CANCEL_REQUEST_MESSAGE) {
return {
success: false,
}
}
let msg
let statusCode
if (response && response instanceof Object) {
const { data, statusText } = response
statusCode = response.status
msg = data.message || statusText
} else {
statusCode = 600
msg = error.message || 'Network Error'
}
/* eslint-disable */
return Promise.reject({
success: false,
statusCode,
message: msg,
})
})
return { url: cloneUrl, data: cloneData }
}
export type RequestConfig = AxiosRequestConfig | string
const request = <T>(config: RequestConfig): AxiosPromise<T> => {
let params = config
if (typeof config === 'object') {
const axiosConfig = config as AxiosRequestConfig
const { data, url } = transformURL(
axiosConfig.url as string,
axiosConfig.data
)
params = {
...axiosConfig,
data,
url,
}
if (
(!params.method || params.method.toLocaleUpperCase() === 'GET') &&
!params.params
) {
params.params = data
}
}
return axios(params as AxiosRequestConfig).catch((error) => {
const { response } = error
let message
let statusCode = 0
if (response && response instanceof Object) {
const { data, statusText } = response
statusCode = response.status
message = data.message || statusText
} else {
message = error.message
}
return Promise.reject({
statusCode,
message,
})
})
}
export default request