37 changed files with 1680 additions and 148 deletions
@ -0,0 +1,10 @@ |
|||||
|
# http://editorconfig.org |
||||
|
root = true |
||||
|
|
||||
|
[*] |
||||
|
charset = utf-8 |
||||
|
indent_style = space |
||||
|
indent_size = 4 |
||||
|
end_of_line = lf |
||||
|
insert_final_newline = true |
||||
|
trim_trailing_whitespace = true |
@ -0,0 +1,6 @@ |
|||||
|
out |
||||
|
dist |
||||
|
pnpm-lock.yaml |
||||
|
LICENSE.md |
||||
|
tsconfig.json |
||||
|
tsconfig.*.json |
@ -0,0 +1,13 @@ |
|||||
|
{ |
||||
|
"tabWidth": 4, |
||||
|
"useTabs": false, |
||||
|
"semi": false, |
||||
|
"singleQuote": false, |
||||
|
"trailingComma": "all", |
||||
|
"bracketSpacing": true, |
||||
|
"arrowParens": "avoid", |
||||
|
"printWidth": 140, |
||||
|
"htmlWhitespaceSensitivity": "ignore", |
||||
|
"proseWrap": "preserve", |
||||
|
"endOfLine": "auto" |
||||
|
} |
@ -1,50 +1,5 @@ |
|||||
# React + TypeScript + Vite |
## Learn React |
||||
|
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. |
### 主题切换功能 |
||||
|
|
||||
Currently, two official plugins are available: |
[switch theme](https://css-tricks.org.cn/theming-and-theme-switching-with-react-and-styled-components/) |
||||
|
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh |
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh |
|
||||
|
|
||||
## Expanding the ESLint configuration |
|
||||
|
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: |
|
||||
|
|
||||
- Configure the top-level `parserOptions` property like this: |
|
||||
|
|
||||
```js |
|
||||
export default tseslint.config({ |
|
||||
languageOptions: { |
|
||||
// other options... |
|
||||
parserOptions: { |
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'], |
|
||||
tsconfigRootDir: import.meta.dirname, |
|
||||
}, |
|
||||
}, |
|
||||
}) |
|
||||
``` |
|
||||
|
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` |
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked` |
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: |
|
||||
|
|
||||
```js |
|
||||
// eslint.config.js |
|
||||
import react from 'eslint-plugin-react' |
|
||||
|
|
||||
export default tseslint.config({ |
|
||||
// Set the react version |
|
||||
settings: { react: { version: '18.3' } }, |
|
||||
plugins: { |
|
||||
// Add the react plugin |
|
||||
react, |
|
||||
}, |
|
||||
rules: { |
|
||||
// other rules... |
|
||||
// Enable its recommended rules |
|
||||
...react.configs.recommended.rules, |
|
||||
...react.configs['jsx-runtime'].rules, |
|
||||
}, |
|
||||
}) |
|
||||
``` |
|
||||
|
File diff suppressed because it is too large
@ -1,9 +1,5 @@ |
|||||
import AppRouter from "@/router/AppRouter" |
import AppRouter from "@/router/AppRouter" |
||||
|
|
||||
export default function App() { |
export default function App() { |
||||
return ( |
return <AppRouter></AppRouter> |
||||
<> |
|
||||
<AppRouter></AppRouter> |
|
||||
</> |
|
||||
) |
|
||||
} |
} |
||||
|
@ -0,0 +1,25 @@ |
|||||
|
import { FC, ReactNode } from "react" |
||||
|
import { LeftIn, OpacityIn } from "@/effect" |
||||
|
import styled from "styled-components" |
||||
|
|
||||
|
interface PageProps { |
||||
|
meta: Record<string, any> |
||||
|
children: ReactNode |
||||
|
} |
||||
|
|
||||
|
const StyleAnim = styled(OpacityIn)` |
||||
|
position: relative; |
||||
|
` |
||||
|
|
||||
|
function withPage(WrappedComponent: FC<PageProps>) { |
||||
|
return function WithAuthorizationComponent(props: PageProps) { |
||||
|
return ( |
||||
|
<StyleAnim> |
||||
|
<WrappedComponent {...props} /> |
||||
|
</StyleAnim> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { withPage } |
||||
|
export default withPage |
@ -0,0 +1,3 @@ |
|||||
|
import { createContext } from "react"; |
||||
|
|
||||
|
export const PageScrollContext = createContext(() => {}); |
@ -0,0 +1,37 @@ |
|||||
|
import { FC, ReactNode, useEffect } from "react" |
||||
|
import { useLocation } from "react-router-dom" |
||||
|
import util from "lodash" |
||||
|
import { PageScrollContext } from "./PageScrollContext" |
||||
|
|
||||
|
/** |
||||
|
* https://zhuanlan.zhihu.com/p/390378572
|
||||
|
*/ |
||||
|
|
||||
|
const ScrollToPosition: FC<{ children: ReactNode }> = ({ children }) => { |
||||
|
const location = useLocation() |
||||
|
const { pathname } = location |
||||
|
const scrollTo = () => { |
||||
|
const scrollY = sessionStorage.getItem(pathname) |
||||
|
// 为了避免scrollY为0引起的滚动到顶部后没有记录的bug,将交互改为在150px内直接滚动到顶部
|
||||
|
if (scrollY) { |
||||
|
const scrollResult = Number(scrollY) < 150 ? 0 : Number(scrollY) |
||||
|
window.scrollTo(0, scrollResult) |
||||
|
} |
||||
|
} |
||||
|
// 路由切换时重新绑定一次最新的pathname
|
||||
|
useEffect(() => { |
||||
|
const scrollHandler = util.throttle(() => { |
||||
|
// 点击切换路由时会触发一次scroll,此时 scrollY为0
|
||||
|
if (window.scrollY > 0) { |
||||
|
sessionStorage.setItem(pathname, String(window.scrollY)) |
||||
|
} |
||||
|
}) |
||||
|
window.addEventListener("scroll", scrollHandler) |
||||
|
return () => { |
||||
|
// 做好清理
|
||||
|
window.removeEventListener("scroll", scrollHandler) |
||||
|
} |
||||
|
}, [pathname]) |
||||
|
return <PageScrollContext.Provider value={scrollTo}>{children}</PageScrollContext.Provider> |
||||
|
} |
||||
|
export default ScrollToPosition |
@ -0,0 +1,36 @@ |
|||||
|
import { useEffect, useRef } from "react" |
||||
|
import { useLocation } from "react-router-dom" |
||||
|
|
||||
|
const ScrollManager = () => { |
||||
|
const location = useLocation() |
||||
|
const prevLocationRef = useRef(location) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const prevLocation = prevLocationRef.current |
||||
|
prevLocationRef.current = location |
||||
|
|
||||
|
if (prevLocation.pathname !== location.pathname) { |
||||
|
const scrollPosition = sessionStorage.getItem(location.pathname) |
||||
|
|
||||
|
if (scrollPosition) { |
||||
|
window.scrollTo(0, parseInt(scrollPosition, 10)) |
||||
|
} else { |
||||
|
window.scrollTo(0, 0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleScroll = () => { |
||||
|
sessionStorage.setItem(location.pathname, window.scrollY.toString()) |
||||
|
} |
||||
|
|
||||
|
window.addEventListener("scroll", handleScroll) |
||||
|
|
||||
|
return () => { |
||||
|
window.removeEventListener("scroll", handleScroll) |
||||
|
} |
||||
|
}, [location]) |
||||
|
|
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
export default ScrollManager |
@ -0,0 +1,7 @@ |
|||||
|
import { createGlobalStyle } from "styled-components" |
||||
|
|
||||
|
export const GlobalStyles = createGlobalStyle` |
||||
|
html,body, #root{ |
||||
|
height: 100%; |
||||
|
} |
||||
|
` |
@ -0,0 +1,81 @@ |
|||||
|
import { Alignment, Button, Dialog, DialogBody, DialogFooter, Menu, MenuDivider, MenuItem, Navbar, Popover } from "@blueprintjs/core" |
||||
|
import { FC, ReactNode, useState } from "react" |
||||
|
import { NavLink, useHistory } from "react-router-dom" |
||||
|
import styled from "styled-components" |
||||
|
import { GlobalStyles } from "./GlobalStyles" |
||||
|
|
||||
|
interface PageProps { |
||||
|
children: ReactNode |
||||
|
} |
||||
|
|
||||
|
const PageWrapper = styled.div` |
||||
|
position: relative; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
` |
||||
|
const ContentWrapper = styled.div` |
||||
|
position: relative; |
||||
|
flex: 1; |
||||
|
height: 0; |
||||
|
overflow-x: hidden; |
||||
|
` |
||||
|
|
||||
|
function BaseLayout(props: PageProps) { |
||||
|
const [isOpen, setIsOpen] = useState(false) |
||||
|
const [isOpenAbout, setIsOpenAbout] = useState(false) |
||||
|
|
||||
|
const router = useHistory() |
||||
|
|
||||
|
return ( |
||||
|
<PageWrapper> |
||||
|
<GlobalStyles /> |
||||
|
<Navbar> |
||||
|
<Navbar.Group align={Alignment.LEFT}> |
||||
|
<Navbar.Heading>东风天堂</Navbar.Heading> |
||||
|
<Navbar.Divider /> |
||||
|
<NavLink to={"/"}> |
||||
|
<Button className="bp5-minimal" icon="home" text="Home" /> |
||||
|
</NavLink> |
||||
|
<Popover |
||||
|
isOpen={isOpen} |
||||
|
interactionKind="click" |
||||
|
onInteraction={isOpen => setIsOpen(isOpen)} |
||||
|
content={ |
||||
|
<Menu> |
||||
|
<MenuItem icon="graph" text="/project" onClick={() => router.push("/project")} /> |
||||
|
<MenuItem icon="map" text="/project/child" onClick={() => router.push("/project/child")} /> |
||||
|
<MenuItem icon="th" text="Table" shouldDismissPopover={false} /> |
||||
|
<MenuItem icon="zoom-to-fit" text="Browser" disabled={true} /> |
||||
|
<MenuDivider /> |
||||
|
<MenuItem icon="cog" text="Settings..."> |
||||
|
<MenuItem icon="add" text="Add new application" disabled={true} /> |
||||
|
<MenuItem icon="remove" text="Remove application" /> |
||||
|
</MenuItem> |
||||
|
</Menu> |
||||
|
} |
||||
|
fill={true} |
||||
|
placement="bottom" |
||||
|
> |
||||
|
<Button className="bp5-minimal" icon="document" text="Files" rightIcon={isOpen ? "caret-down" : "caret-right"} /> |
||||
|
</Popover> |
||||
|
</Navbar.Group> |
||||
|
<Navbar.Group align={Alignment.RIGHT}> |
||||
|
<Button className="bp5-minimal" icon="settings" /> |
||||
|
<Navbar.Divider /> |
||||
|
{/* <NavLink to={"/about"}> */} |
||||
|
<Button onClick={()=>setIsOpenAbout(true)} className="bp5-minimal" icon="person" text="关于" /> |
||||
|
{/* </NavLink> */} |
||||
|
</Navbar.Group> |
||||
|
</Navbar> |
||||
|
<ContentWrapper>{props.children}</ContentWrapper> |
||||
|
<Dialog isOpen={isOpenAbout} title="通知" icon="info-sign" onClose={() => setIsOpenAbout(false)}> |
||||
|
<DialogBody>{/* body contents here */}</DialogBody> |
||||
|
<DialogFooter actions={<Button intent="primary" text="关闭" onClick={() => setIsOpenAbout(false)} />} /> |
||||
|
</Dialog> |
||||
|
</PageWrapper> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { BaseLayout } |
||||
|
export default BaseLayout |
@ -0,0 +1,51 @@ |
|||||
|
|
||||
|
export default (err) => { |
||||
|
const { response } = err |
||||
|
|
||||
|
if (!response.status) { |
||||
|
err.code = '' |
||||
|
err.message = '有response但没有response.status的情况' |
||||
|
} |
||||
|
err.code = response.status |
||||
|
switch (response.status) { |
||||
|
case 200: |
||||
|
err.message = '错误响应也会有状态码为200的情况' |
||||
|
break |
||||
|
case 400: |
||||
|
err.message = '请求错误(400)' |
||||
|
break |
||||
|
case 401: |
||||
|
err.message = '未授权,请重新登录(401)' |
||||
|
break |
||||
|
case 403: |
||||
|
err.message = '拒绝访问(403)' |
||||
|
break |
||||
|
case 404: |
||||
|
err.message = '请求出错(404)' |
||||
|
break |
||||
|
case 408: |
||||
|
err.message = '请求超时(408)' |
||||
|
break |
||||
|
case 500: |
||||
|
err.message = '服务器错误(500)' |
||||
|
break |
||||
|
case 501: |
||||
|
err.message = '服务未实现(501)' |
||||
|
break |
||||
|
case 502: |
||||
|
err.message = '网络错误(502)' |
||||
|
break |
||||
|
case 503: |
||||
|
err.message = '服务不可用(503)' |
||||
|
break |
||||
|
case 504: |
||||
|
err.message = '网络超时(504)' |
||||
|
break |
||||
|
case 505: |
||||
|
err.message = 'HTTP版本不受支持(505)' |
||||
|
break |
||||
|
default: |
||||
|
err.message = `连接出错,状态码:(${err.response.status})!` |
||||
|
} |
||||
|
return err |
||||
|
} |
@ -0,0 +1,42 @@ |
|||||
|
|
||||
|
// 处理响应错误码
|
||||
|
export default (response) => { |
||||
|
const status = response.status |
||||
|
// 如果http响应状态码response.status正常,则直接返回数据
|
||||
|
if ((status >= 200 && status <= 300) || status === 304) { |
||||
|
return response |
||||
|
} |
||||
|
// status不正常的话,根据与后端约定好的code,做出对应的提示与处理
|
||||
|
// 返回一个带有code和message属性的对象
|
||||
|
else { |
||||
|
const code = parseInt(response.data && response.data.code) |
||||
|
// msg为服务端返回的错误信息,字段名自定义,此处以msg为例
|
||||
|
let message = (response.data || {}).msg |
||||
|
|
||||
|
switch (code) { |
||||
|
case 400: |
||||
|
break |
||||
|
case 4001: |
||||
|
if (process.server) return |
||||
|
message = message || '登录设备数量超出限制' |
||||
|
// store.commit('savehttpResult', { res: response.data })
|
||||
|
break |
||||
|
case 403: |
||||
|
message = message || '未登录' |
||||
|
break |
||||
|
case 404: |
||||
|
message = message || '请求地址错误' |
||||
|
break |
||||
|
case 412: |
||||
|
message = message || '未找到有效session' |
||||
|
break |
||||
|
default: |
||||
|
// message = message || err.response.data.msg
|
||||
|
break |
||||
|
} |
||||
|
return { |
||||
|
code, |
||||
|
message |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,31 @@ |
|||||
|
|
||||
|
/** |
||||
|
* @param {axios} axios实例 |
||||
|
* @param {config} 自定义配置对象,可覆盖掉默认的自定义配置 |
||||
|
*/ |
||||
|
export default (axios, config = {}) => { |
||||
|
|
||||
|
const defaultConfig = { |
||||
|
baseURL: process.env.VUE_APP_BASEURL, |
||||
|
timeout: 10000, |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json;charset=UTF-8', |
||||
|
'custom-defined-header-key': 'custom-defined-header-value', |
||||
|
// 自定义请求头:对所有请求方法生效
|
||||
|
common: { |
||||
|
'common-defined-key-b': 'custom value: for all methods' |
||||
|
}, |
||||
|
// 自定义请求头:只对post方法生效
|
||||
|
post: { |
||||
|
'post-custom-key': 'custom value: only for post method' |
||||
|
}, |
||||
|
// 自定义请求头:只对get方法生效
|
||||
|
get: { |
||||
|
'get-custom-key': 'custom value: only for get method' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Object.assign(axios.defaults, defaultConfig, config) |
||||
|
return axios |
||||
|
} |
@ -0,0 +1,127 @@ |
|||||
|
import axios from "axios" |
||||
|
import setConfig from "@/plugins/axios/axios.setConfig.js" |
||||
|
import handleResponse from "@/plugins/axios/axios.handleResponse.js" |
||||
|
import handleError from "@/plugins/axios/axios.handleError.js" |
||||
|
// import store from '@/store/index'
|
||||
|
// import router from '@/router/index.js'
|
||||
|
// import { Message } from "element-ui"
|
||||
|
const showTip = tip => { |
||||
|
// Message({
|
||||
|
// type: "warning",
|
||||
|
// message: tip || "请求出错啦",
|
||||
|
// duration: 1500,
|
||||
|
// })
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* intactRequest是只在axios基础上更改了请求配置。 |
||||
|
* 而request是基于axios创建的实例,实例只有常见的数据请求方法,没有axios.isCancel/ axios.CancelToken等方法, |
||||
|
* 也就是没有**取消请求**和**批量请求**的方法。 |
||||
|
* 所以如果需要在实例中调用取消某个请求的方法(例如取消上传),请用intactRequest。 |
||||
|
*/ |
||||
|
let intactRequest = setConfig(axios) |
||||
|
let request = setConfig(intactRequest.create()) |
||||
|
|
||||
|
// 请求中的api
|
||||
|
let pendingPool = new Map() |
||||
|
|
||||
|
/** |
||||
|
* 请求拦截 |
||||
|
*/ |
||||
|
const requestInterceptorId = request.interceptors.request.use( |
||||
|
config => { |
||||
|
// 对于异常的响应也需要在pendingPool中将其删除,但响应拦截器中的异常响应有些获取不到请求信息,这里将其保存在实例上
|
||||
|
request.config = Object.assign({}, config) |
||||
|
// 在发送请求之前做些什么
|
||||
|
// config.headers.common['cookie-id'] = cookieId
|
||||
|
config.cancelToken = new axios.CancelToken(cancelFn => { |
||||
|
pendingPool.has(config.url) |
||||
|
? cancelFn(`${config.url}请求重复`) |
||||
|
: pendingPool.set(config.url, { cancelFn, global: config.global }) |
||||
|
}) |
||||
|
return config |
||||
|
}, |
||||
|
err => { |
||||
|
console.log("请求拦截err:", err) |
||||
|
// 对请求错误做些什么
|
||||
|
return Promise.reject(err) |
||||
|
}, |
||||
|
) |
||||
|
/** |
||||
|
* 响应拦截 |
||||
|
*/ |
||||
|
const responseInterceptorId = request.interceptors.response.use( |
||||
|
response => { |
||||
|
const { config } = response |
||||
|
pendingPool.delete(config.url) |
||||
|
|
||||
|
// console.log('响应response suc:', response)
|
||||
|
showTip(err.message) |
||||
|
return Promise.resolve(handleResponse(response)) |
||||
|
}, |
||||
|
// 对异常响应处理
|
||||
|
err => { |
||||
|
const { config } = request |
||||
|
if (!axios.isCancel(err)) pendingPool.delete(config.url) |
||||
|
|
||||
|
if (!err) return Promise.reject(err) |
||||
|
|
||||
|
if (err.response) { |
||||
|
err = handleError(err) |
||||
|
} |
||||
|
// 没有response(没有状态码)的情况
|
||||
|
// eg: 超时;断网;请求重复被取消;主动取消请求;
|
||||
|
else { |
||||
|
// 错误信息err传入isCancel方法,可以判断请求是否被取消
|
||||
|
if (axios.isCancel(err)) { |
||||
|
throw new axios.Cancel(err.message || `请求'${request.config.url}'被取消`) |
||||
|
} else if (err.stack && err.stack.includes("timeout")) { |
||||
|
err.message = "请求超时!" |
||||
|
} else { |
||||
|
err.message = "连接服务器失败!" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
showTip(err.message) |
||||
|
return Promise.reject(err) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
// 移除全局的请求拦截器
|
||||
|
function removeRequestInterceptor() { |
||||
|
request.interceptors.request.eject(requestInterceptorId) |
||||
|
} |
||||
|
|
||||
|
// 移除全局的响应拦截器
|
||||
|
function removeResponseInterceptor() { |
||||
|
request.interceptors.response.eject(responseInterceptorId) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除所有pending状态的请求 |
||||
|
* @param {Array} whiteList 白名单,里面的请求不会被取消 |
||||
|
* 返回值 被取消了的api请求 |
||||
|
*/ |
||||
|
function clearPendingPool(whiteList = []) { |
||||
|
if (!pendingPool.size) return |
||||
|
|
||||
|
// const pendingUrlList = [...pendingPool.keys()].filter((url) => !whiteList.includes(url))
|
||||
|
const pendingUrlList = Array.from(pendingPool.keys()).filter(url => !whiteList.includes(url)) |
||||
|
if (!pendingUrlList.length) return |
||||
|
|
||||
|
pendingUrlList.forEach(pendingUrl => { |
||||
|
// 清除掉所有非全局的pending状态下的请求
|
||||
|
if (!pendingPool.get(pendingUrl).global) { |
||||
|
pendingPool.get(pendingUrl).cancelFn() |
||||
|
pendingPool.delete(pendingUrl) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return pendingUrlList |
||||
|
} |
||||
|
|
||||
|
request.removeRequestInterceptor = removeRequestInterceptor |
||||
|
request.removeResponseInterceptor = removeResponseInterceptor |
||||
|
request.clearPendingPool = clearPendingPool |
||||
|
|
||||
|
export { intactRequest, request } |
@ -0,0 +1,126 @@ |
|||||
|
https://segmentfault.com/a/1190000027078266#item-9 |
||||
|
|
||||
|
在路由的配置文件src/router/idnex.js中,引入request,并在路由全局前置守卫中执行clearPendingPool方法: |
||||
|
``` |
||||
|
|
||||
|
import { request } from '@/plugins/axios/index' |
||||
|
|
||||
|
// 路由全局前置守卫 |
||||
|
router.beforeEach((to, from, next) => { |
||||
|
// 路由变化时取消当前所有非全局的pending状态的请求 |
||||
|
request.clearPendingPool() |
||||
|
next() |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
到这里就实现了路由切换取消pending状态的请求。可以通过两种方式指定某些api不被取消: |
||||
|
|
||||
|
执行clearPendingPool时传入一个白名单列表: |
||||
|
const globalApi = [ |
||||
|
'/global/banner', |
||||
|
'/global/activity' |
||||
|
] |
||||
|
request.clearPendingPool(globalApi) |
||||
|
发起请求的时候携带global参数,默认为false: |
||||
|
this.$request.get('/global/banner',{ |
||||
|
params:{page: 1}, |
||||
|
global: true |
||||
|
}) |
||||
|
|
||||
|
this.$request.post('/user/login',{ |
||||
|
name: 'xxx', |
||||
|
pwd:'123456' |
||||
|
},{ |
||||
|
global: true |
||||
|
}) |
||||
|
|
||||
|
根目录下新建一个src/api/index.js文件: |
||||
|
|
||||
|
``` |
||||
|
/**api管理页面 |
||||
|
* apiMap: 统一管理所有api地址、对应的请求方式及自定义别名 |
||||
|
* 导出一个对象requestMap,属性名为apiMap中定义的别名,也就是调用时的名称,值为实际请求方法 |
||||
|
* 方法接收两个对象参数,第一个为需要传递的数据,第二个为请求头的配置信息。 |
||||
|
* 语法: api[alias](paramsOrData, headersConfig) |
||||
|
* 第一个参数:如果为put/post/patch方法中的一种,会被转化为data属性;其余则是params |
||||
|
* 第二个参数:请求头信息 |
||||
|
* |
||||
|
* let xx = await this.$api.getBanner({ account: '18038018084', psw: '2' }) |
||||
|
* let vv = await this.$api.login({ account: '18038018084', psw: '2' }) |
||||
|
* |
||||
|
* 如果类似post的方法需要通过url后缀形式传递参数,在第二个参数config加上params属性即可: |
||||
|
* let vv = await this.$api.login({ account: '18038018084', psw: '2' },{ params: {} }) |
||||
|
* |
||||
|
* 自定义请求头信息: |
||||
|
* let xx = await this.$api.getBanner({}, {timeout: 1000, headers:{ aaa: 111 }}) |
||||
|
*/ |
||||
|
import { request } from '@/plugins/axios/index' |
||||
|
// import qs from 'qs' |
||||
|
// console.log('qs:', qs) |
||||
|
|
||||
|
const apiMap = { |
||||
|
getBanner: { method: 'get', url: '/home/banner' }, |
||||
|
login: { method: 'post', url: '/login' } |
||||
|
} |
||||
|
|
||||
|
function injectRequest(apiObj) { |
||||
|
const requestMap = {} |
||||
|
Object.keys(apiObj).forEach((alias) => { |
||||
|
let { method, url, config } = apiObj[alias] |
||||
|
method = method.toUpperCase() |
||||
|
requestMap[alias] = (dataOrParams = {}, instanceConf = {}) => { |
||||
|
const keyName = ['PUT', 'POST', 'PATCH'].includes(method) ? 'data' : 'params' |
||||
|
return request({ |
||||
|
method, |
||||
|
url, |
||||
|
// [keyName]: method === 'POST' ? qs.stringify(dataOrParams) : dataOrParams, |
||||
|
[keyName]: dataOrParams, |
||||
|
...Object.assign(config || {}, instanceConf) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
return requestMap |
||||
|
} |
||||
|
|
||||
|
export default injectRequest(apiMap) |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
在mains.js中引入并挂载到vue实例: |
||||
|
|
||||
|
import Vue from 'vue' |
||||
|
import api from '@/api/index.js' |
||||
|
|
||||
|
Vue.prototype.$api = api |
||||
|
调用示例: |
||||
|
|
||||
|
|
||||
|
// 请求头信息 |
||||
|
const headersConfig = { |
||||
|
timeout: 5000, |
||||
|
global: true, |
||||
|
headers:{ |
||||
|
aaa: 'vvv' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// get请求 |
||||
|
this.$api.getBanner({ |
||||
|
page: 1, |
||||
|
}) |
||||
|
// get请求,自定义请求头信息 |
||||
|
this.$api.getBanner({ |
||||
|
page: 1, |
||||
|
}, headersConfig) |
||||
|
|
||||
|
// post请求 |
||||
|
this.$api.login({ |
||||
|
account: 'laowang', |
||||
|
pwd: 'xxxx' |
||||
|
}) |
||||
|
|
||||
|
// post请求,自定义请求头信息 |
||||
|
this.$api.login({ |
||||
|
account: 'laowang', |
||||
|
pwd: 'xxxx' |
||||
|
}, headersConfig) |
@ -0,0 +1,46 @@ |
|||||
|
import { Component } from "react" |
||||
|
// 高阶组件,就是对原来的组件进行封装
|
||||
|
import { Route } from "react-router-dom" |
||||
|
|
||||
|
interface IProps { |
||||
|
needAuth?: boolean |
||||
|
path: string |
||||
|
exact?: boolean |
||||
|
children: any |
||||
|
} |
||||
|
|
||||
|
class Auth extends Component<IProps, { isLogin: boolean }> { |
||||
|
constructor(props: IProps) { |
||||
|
super(props) |
||||
|
this.state = { |
||||
|
isLogin: false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
login() { |
||||
|
this.setState({ |
||||
|
isLogin: true |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { path, needAuth = false, exact = false } = this.props |
||||
|
if (!needAuth || (needAuth && this.state.isLogin)) { |
||||
|
return ( |
||||
|
// 如果已经登陆,就直接显示原来的组件
|
||||
|
<Route exact={exact} path={path}> |
||||
|
{this.props.children} |
||||
|
</Route> |
||||
|
) |
||||
|
} |
||||
|
return ( |
||||
|
<div> |
||||
|
{/* <NavLink to="/login">你还有没有登陆好吧!</NavLink> */} |
||||
|
<div onClick={() => this.setState({ isLogin: true })}>你还有没有登陆好吧!</div> |
||||
|
{/* <Redirect to={{ pathname: '/login', state: { from: { path }} }} > </Redirect> */} |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default Auth |
@ -0,0 +1,51 @@ |
|||||
|
import { createRef, ReactNode, useEffect, useState } from "react" |
||||
|
import styled from "styled-components" |
||||
|
|
||||
|
interface IProps {} |
||||
|
|
||||
|
const LoadingComp = styled.div` |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
left: 0; |
||||
|
background-color: #00000033; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
z-index: 1000; |
||||
|
&:after { |
||||
|
content: ""; |
||||
|
animation: aa 1.2s ease-in infinite; |
||||
|
} |
||||
|
@keyframes aa { |
||||
|
0% { |
||||
|
content: ""; |
||||
|
} |
||||
|
|
||||
|
25% { |
||||
|
content: "."; |
||||
|
} |
||||
|
|
||||
|
50% { |
||||
|
content: ".."; |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
content: "..."; |
||||
|
} |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
export default function Loading(props: IProps): ReactNode { |
||||
|
const [show, setShow] = useState(false) |
||||
|
useEffect(() => { |
||||
|
const timeId = setTimeout(() => { |
||||
|
setShow(true) |
||||
|
}, 800) |
||||
|
return () => { |
||||
|
clearTimeout(timeId) |
||||
|
} |
||||
|
}, []) |
||||
|
return <>{show && <LoadingComp>加载中</LoadingComp>}</> |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
import withPage from "@/base/withPage"; |
||||
|
import { Button, Dialog, DialogBody, DialogFooter } from "@blueprintjs/core"; |
||||
|
import { useCallback, useState } from "react"; |
||||
|
|
||||
|
function About() { |
||||
|
const [isOpen, setIsOpen] = useState(false); |
||||
|
const toggleOverlay = useCallback(() => setIsOpen(open => !open), [setIsOpen]); |
||||
|
const handleClose = useCallback(() => setIsOpen(false), []); |
||||
|
return <> |
||||
|
<Button text="点击打开美丽的按钮" onClick={toggleOverlay} /> |
||||
|
<Dialog isOpen={isOpen} title="关于我" icon="info-sign" onClose={handleClose}> |
||||
|
<DialogBody>{/* body contents here */}</DialogBody> |
||||
|
<DialogFooter actions={<Button intent="primary" text="关闭" onClick={handleClose} />} /> |
||||
|
</Dialog> |
||||
|
</> |
||||
|
} |
||||
|
|
||||
|
export default withPage(About) |
@ -1,15 +1,10 @@ |
|||||
import { Button, Dialog, DialogBody, DialogFooter } from "@blueprintjs/core"; |
import { Button, Dialog, DialogBody, DialogFooter } from "@blueprintjs/core"; |
||||
import { useCallback, useState } from "react"; |
import { useCallback, useState } from "react"; |
||||
|
|
||||
export default function Child() { |
function Child() { |
||||
const [isOpen, setIsOpen] = useState(false); |
|
||||
const toggleOverlay = useCallback(() => setIsOpen(open => !open), [setIsOpen]); |
|
||||
const handleClose = useCallback(() => setIsOpen(false), []); |
|
||||
return <> |
return <> |
||||
<Button text="点击打开美丽的按钮" onClick={toggleOverlay} /> |
<Button text="点击打开美丽的按钮"/> |
||||
<Dialog isOpen={isOpen} title="通知" icon="info-sign" onClose={handleClose}> |
|
||||
<DialogBody>{/* body contents here */}</DialogBody> |
|
||||
<DialogFooter actions={<Button intent="primary" text="关闭" onClick={handleClose} />} /> |
|
||||
</Dialog> |
|
||||
</> |
</> |
||||
} |
} |
||||
|
|
||||
|
export default Child |
||||
|
@ -0,0 +1,7 @@ |
|||||
|
|
||||
|
## asda |
||||
|
|
||||
|
*React-Markdown* is **Awesome** |
||||
|
``` |
||||
|
sads |
||||
|
``` |
@ -0,0 +1,52 @@ |
|||||
|
import withPage from "@/base/withPage" |
||||
|
import { useHistory } from "react-router-dom" |
||||
|
import styled from "styled-components" |
||||
|
import ReactMarkdown from "react-markdown" |
||||
|
import MdText from "./index.md?raw" |
||||
|
|
||||
|
const Title = styled.h1` |
||||
|
font-size: 1.5em; |
||||
|
text-align: center; |
||||
|
color: #bf4f74; |
||||
|
` |
||||
|
|
||||
|
const Wrapper = styled.section` |
||||
|
padding: 4em; |
||||
|
background: papayawhip; |
||||
|
` |
||||
|
|
||||
|
export default withPage(function Project({ children }) { |
||||
|
const router = useHistory() |
||||
|
function toChild() { |
||||
|
router.push("/project/child") |
||||
|
} |
||||
|
return ( |
||||
|
<> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<Wrapper> |
||||
|
<Title>HOME</Title> |
||||
|
</Wrapper> |
||||
|
<ReactMarkdown>{MdText}</ReactMarkdown> |
||||
|
<div> |
||||
|
<div onClick={toChild} style={{ height: "200px" }}> |
||||
|
vaas |
||||
|
</div> |
||||
|
{children} |
||||
|
</div> |
||||
|
</> |
||||
|
) |
||||
|
}) |
@ -1,6 +1,11 @@ |
|||||
|
import withPage from "@/base/withPage" |
||||
|
|
||||
export default function PlayGround() { |
function PlayGround() { |
||||
return <> |
return ( |
||||
|
<> |
||||
<div>PlayGround</div> |
<div>PlayGround</div> |
||||
</> |
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
|
export default withPage(PlayGround) |
||||
|
@ -1,13 +1,34 @@ |
|||||
|
import withPage from "@/base/withPage" |
||||
|
import { useHistory } from "react-router-dom" |
||||
|
import styled from "styled-components" |
||||
|
|
||||
export default function Project(props: any) { |
const Title = styled.h1` |
||||
console.log(props); |
font-size: 1.5em; |
||||
|
text-align: center; |
||||
|
color: #bf4f74; |
||||
|
` |
||||
|
|
||||
return <> |
const Wrapper = styled.section` |
||||
<div> |
padding: 4em; |
||||
Project |
background: papayawhip; |
||||
|
` |
||||
|
|
||||
|
export default withPage(function Project({ children }) { |
||||
|
const router = useHistory() |
||||
|
function toChild() { |
||||
|
router.push("/project/child") |
||||
|
} |
||||
|
return ( |
||||
|
<> |
||||
|
<Wrapper> |
||||
|
<Title>Hello World!</Title> |
||||
|
</Wrapper> |
||||
<div> |
<div> |
||||
{props?.children} |
<div onClick={toChild} style={{ height: "200px" }}> |
||||
|
vaas |
||||
</div> |
</div> |
||||
|
{children} |
||||
</div> |
</div> |
||||
</> |
</> |
||||
} |
) |
||||
|
}) |
||||
|
@ -0,0 +1,12 @@ |
|||||
|
import { LeftIn } from "@/effect" |
||||
|
|
||||
|
export default function Page404() { |
||||
|
return ( |
||||
|
<div className="h-1/1 flex" style={{display: "flex",alignItems: "center", justifyContent: "center", height: "100%"}}> |
||||
|
<LeftIn className="m-auto"> |
||||
|
404 |
||||
|
{/* <img className="p-40px" src="static/404.jpg" alt="" /> */} |
||||
|
</LeftIn> |
||||
|
</div> |
||||
|
) |
||||
|
} |
@ -1,11 +0,0 @@ |
|||||
import { LeftIn } from "@/effect" |
|
||||
|
|
||||
export default function Page404() { |
|
||||
return ( |
|
||||
<div className="h-1/1 flex"> |
|
||||
<LeftIn className="m-auto"> |
|
||||
<img className="p-40px" src="static/404.jpg" alt="" /> |
|
||||
</LeftIn> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
Loading…
Reference in new issue