第一章 项目框架搭建
yarn create @vitejs/app react-vite-h5 --template react
yarn
yarn run dev
为了在项目过程中不出现千奇百怪问题,这里贴出项目的包版本。
{
"name": "react-vite-h5",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^0.26.1",
"classnames": "^2.3.1",
"dayjs": "^1.11.0",
"global": "^4.4.0",
"lib-flexible": "0.3.2",
"pm2": "^5.2.0",
"postcss-pxtorem": "6.0.0",
"pushstate-server": "^3.1.0",
"query-string": "^7.1.1",
"rc-form": "^2.4.12",
"react": "^17.0.2",
"react-captcha-code": "^1.0.7",
"react-dom": "^17.0.2",
"react-router-dom": "5.2.0",
"zarm": "^2.9.13"
},
"devDependencies": {
"@vitejs/plugin-react": "^1.0.7",
"@vitejs/plugin-react-refresh": "^1.3.6",
"less": "4.1.1",
"vite": "^2.8.0",
"vite-plugin-style-import": "0.10.1"
}
}
- 引入路由插件
react-router-dom
yarn add react-router-dom@5 -S
cd src
mkdir container && cd container
mkdir Index About
cd Index && touch index.jsx
import React from "react";
const Index = () => {
return <div>
<h1>Index</h1>
</div>
}
export default Index;
cd About && touch index.jsx
import React from "react";
const About = () => {
return <div>
<h1>About</h1>
</div>
}
export default About;
cd src && mkdir router && touch index.jsx
import Index from '../container/Index'
import About from '../container/About'
const routes = [
{
path: "/",
component: Index
},{
path: "/about",
component: About
}
];
export default routes;
vim App.jsx
import React, { useState } from 'react'
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom"
import routes from '../src/router'
function App() {
return <Router>
<Switch>
{
routes.map(route => <Route exact key={route.path} path={route.path}>
<route.component />
</Route>)
}
</Switch>
</Router>
}
export default App
启动项目 npm run dev
- 引入
Zarm UI 组件库
yarn add zarm -S
vim App.jsx
import React, { useState } from 'react'
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom"
+ import { ConfigProvider } from 'zarm'
+ import zhCN from 'zarm/lib/config-provider/locale/zh_CN'
+ import 'zarm/dist/zarm.css'
import routes from '../src/router'
function App() {
return <Router>
+ <ConfigProvider locale={zhCN} primaryColor={'#007fff'}>
<Switch>
{
routes.map(route => <Route exact key={route.path} path={route.path}>
<route.component />
</Route>)
}
</Switch>
+ </ConfigProvider>
</Router>
}
export default App
此时 zarm 的样式,已经全局引入了,我们先查看在 /container/Index/index.jsx 添加一个按钮是否生效:
- 使用less
yarn add less -D
vim vite.config.js
{
plugins: [...]
css: {
modules: {
localsConvention: 'dashesOnly'
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
}
}
},
}
- 移动端项目适配rem
yarn add lib-flexible@0.3.2 -S
vim main.jsx
import React from 'react'
import ReactDOM from 'react-dom'
+ import 'lib-flexible/flexible'
import './index.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
yarn add postcss-pxtorem@6.0.0
cd / && vim postcss.config.js
module.exports = {
"plugins": [
require("postcss-pxtorem")({
rootValue: 37.5,
propList: ['*'],
selectorBlackList: ['.norem']
})
]
}
- 按需引入css
yarn add vite-plugin-style-import@0.10.1 -D
vim vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import styleImport from 'vite-plugin-style-import'
export default defineConfig({
plugins: [react(),styleImport({
libs:[{
libraryName:'zarm',
esModule:true,
resolveStyle:(name)=>{
return `zarm/es/${name}/style/css`;
}
}]
})],
css: {
modules: {
localsConvention: 'dashesOnly'
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
}
}
}
})
yarn run build
- 二次封装axios
后端接口:http://dualseason.com:7001
yarn add axios -S
cd src && mkdir utils && vim axios.js
import axios from 'axios'
import { Toast } from 'zarm'
const MODE = import.meta.env.MODE
axios.defaults.baseURL = 'http://dualseason.com:7001'
axios.defaults.withCredentials = true
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.interceptors.response.use(res => {
if (typeof res.data !== 'object') {
Toast.show('服务端异常!')
return Promise.reject(res)
}
if (res.data.code != 200) {
if (res.data.msg) Toast.show(res.data.msg)
if (res.data.code == 401) {
window.location.href = '/login'
}
return Promise.reject(res.data)
}
return res.data
})
export default axios;
代码解释:
const MODE = import.meta.env.MODE
MODE 是一个环境变量,通过 Vite 构建的项目中,环境变量在项目中,可以通过 import.meta.env.MODE 获取,环境变量的作用就是判断当前代码运行在开发环境还是生产环境。
axios.defaults.baseURL = 'http://dualseason.com:7001'
baseURL 是 axios 的配置项,它的作用就是设置请求的基础路径,后续我们会在项目实战中有所体现。配置基础路径的好处就是,当请求地址修改的时候,可以在此统一配置。
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'
上述三个配置是用于请求头的设置,Authorization 是我们在服务端鉴权的时候用到的,我们在前端设置好 token ,服务端通过获取请求头中的 token 去验证每一次请求是否合法。最后一行是配置 post 请求是,使用的请求体,这里默认设置成 application/json 的形式。
axios.interceptors.response.use(res => {
if (typeof res.data !== 'object') {
Toast.show('服务端异常!')
return Promise.reject(res)
}
if (res.data.code != 200) {
if (res.data.msg) Toast.show(res.data.msg)
if (res.data.code == 401) {
window.location.href = '/login'
}
return Promise.reject(res.data)
}
return res.data
})
interceptors 为拦截器,拦截器的作用是帮你拦截每一次请求,你可以在回调函数中做一些“手脚”,再将数据 return 回去。上述代码就是拦截了响应内容,统一判断请求内容,如果非 200,则提示错误信息,401 的话,就是没有登录的用户,默认跳到 /login 页面。如果是正常的响应,则 retrun res.data 。
最后我们将这个 axios 抛出,供页面组件请求使用。
cd src && mkdir utils && vim index.js
import axios from './axios'
export const get = axios.get
export const post = axios.post
- 代理配置(项目不需要代理,后端已处理跨域)
vim vite.config.js
server: {
proxy: {
'/api': {
target: 'http://api.chennick.wang/api/',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
- resolve.alias别名设置
vim vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import styleImport from 'vite-plugin-style-import'
+ import path from 'path'
export default defineConfig({
plugins: [react(),styleImport({
libs:[{
libraryName:'zarm',
esModule:true,
resolveStyle:(name)=>{
return `zarm/es/${name}/style/css`;
}
}]
})],
css: {
modules: {
localsConvention: 'dashesOnly'
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
}
}
},
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ 'utils': path.resolve(__dirname, 'src/utils')
+ }
+ }
})
此时我们便可以修改之前的代码如下:
vim router/index.jsx
import Index from '@/container/Index'
import About from '@/container/About'
vim App.jsx
import routes from '@/router'
行文至此,我们的基础开发环境已经搭建完毕,涉及构建工具、前端框架、UI 组件库、HTTP 请求库、CSS 预加载器、跨域代理、移动端分辨率适配,这些知识都是一个合格的前端工程师应该具备的,所以请大家加油,将他们都通通拿下,下载代码。
第二章 底部导航栏实现
- 预期效果
cd src && mkdir components && cd components && mkdir NavBar && cd NavBar && touch index.jsx style.module.less
zarm文档这里要做底部导航栏最适合的就是TabBar(众安科技移动端组件库例子)
我们从架构的层面来看,这三个按钮分别对应跳转三个页面,分别是home 、data 以及user ,所以我们应该先创建三个路由,不过创建路由前需要创建对应的组件,用于跳转。
cd src/container && mkdir Home && cd Home && touch index.jsx style.module.less
vim src/container/Data/index.jsx
import React from "react";
const Data = () => {
return <div>
<h1>Data</h1>
</div>
}
export default Data;
cd src/container && mkdir Data && cd Data && touch index.jsx style.module.less
vim src/container/Home/index.jsx
import React from "react";
const Home = () => {
return <div>
<h1>Home</h1>
</div>
}
export default Home;
cd src/container && mkdir User && cd User && touch index.jsx style.module.less
vim src/container/User/index.jsx
import React from "react";
const User = () => {
return <div>
<h1>User</h1>
</div>
}
export default User;
vim src/router/index.js
- import Index from "@/container/Index";
import About from "@/container/About";
import Home from "@/container/Home";
import Data from "@/container/Data";
import User from "@/container/User";
const router = [
- {
- path: '/',
- component: Index,
- }, {
path: '/about',
component: About,
}, {
path: '/user',
component: User,
}, {
path: '/data',
component: Data,
}, {
path: '/',
component: Home,
}
];
export default router;
确保路由能够正常跳转后再进行其他操作,接下来我们对NavBar 组件进行编写,编写前我们查看文档,先确保能够显示静态再做动态。
vim NarBar/index.jsx
import React from "react";
import { TabBar } from "zarm";
const NavBar = () => {
return <TabBar>
<TabBar.Item itemKey={'/'} title={'账单'}/>
<TabBar.Item itemKey={'/data'} title={'统计'}/>
<TabBar.Item itemKey={'/user'} title={'我的'}/>
</TabBar>
}
export default NavBar;
vim App.jsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import router from "@/router";
import { ConfigProvider } from 'zarm'
import zhCN from 'zarm/lib/config-provider/locale/zh_CN'
import 'zarm/dist/zarm.css'
+ import NavBar from "@/components/NavBar";
const App = () => {
return <Router>
<ConfigProvider primaryColor={'#007fff'} locale={zhCN}>
<Switch>
{router.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}
</Switch>
</ConfigProvider>
+ <NavBar/>
</Router>;
}
export default App;
到此为止我们已经可以使用组件进行代码编写了,可以写一个静态页面了,代码我已经放在了gitee上,跑不起来这些代码的可以去获取,直接下载下来运行即可。
接下来我们要实现点击NavBar 进行路由跳转,只需要在上一步基础上加上如下代码,如果你不能出现跳转可能是环境没配置好,我们要明确目标,环境只是为了写代码的,我们不必在环境上花太多时间,所以我建议直接下载代码直接使用即可,接下来每一步我都会把对应代码放上来,出现问题先不要管,删掉自己的代码然后用我提供的代码进行替换即可,后面有基础后再自己搞。
vim NavBar/index.jsx
import React from "react";
import { TabBar } from "zarm";
import { useHistory } from 'react-router-dom'
const NavBar = () => {
+ const history = useHistory();
+ const changeTab = (path) => [
+ history.push(path)
+ ]
+ return <TabBar onChange={changeTab}>
<TabBar.Item itemKey={'/'} title={'账单'} />
<TabBar.Item itemKey={'/data'} title={'统计'} />
<TabBar.Item itemKey={'/user'} title={'我的'} />
</TabBar>
}
export default NavBar;
useHistory() 是react-router-dom 提供的一个路由组件,可以使用push() 方法进行路由插入history 中。
接下来我们实现功能:当点击NavBar 时,我们的蓝色active 会标亮选中。
import React, { useState } from "react";
import { TabBar } from "zarm";
import { useHistory } from 'react-router-dom'
const NavBar = () => {
+ const [activeKey, setActiveKey] = useState("/");
const history = useHistory();
const changeTab = (path) => {
+ setActiveKey(path)
history.push(path)
}
return <TabBar activeKey={activeKey} onChange={changeTab}>
<TabBar.Item itemKey={'/'} title={'账单'} />
<TabBar.Item itemKey={'/data'} title={'统计'} />
<TabBar.Item itemKey={'/user'} title={'我的'} />
</TabBar>
}
export default NavBar;
- 添加底部导航栏图标
cd src/components && mkdir CustomIcon && touch index.jsx
import { Icon } from "zarm";
export default Icon.createFromIconfont('//at.alicdn.com/t/font_2236655_w1mpqp7n1ni.js');
import React, { useState } from "react";
import { TabBar } from "zarm";
import { useHistory } from 'react-router-dom'
+ import CustomIcon from "@/components/CustomIcon";
const NavBar = () => {
const [activeKey, setActiveKey] = useState("/");
const history = useHistory();
const changeTab = (path) => {
setActiveKey(path)
history.push(path)
}
return <TabBar activeKey={activeKey} onChange={changeTab}>
+ <TabBar.Item itemKey={'/'} title={'账单'} icon={<CustomIcon type="zhangdan"/>}/>
+ <TabBar.Item itemKey={'/data'} title={'统计'} icon={<CustomIcon type="tongji"/>}/>
+ <TabBar.Item itemKey={'/user'} title={'我的'} icon={<CustomIcon type="wode"/>}/>
</TabBar>
}
export default NavBar;
- 底部导航栏的显示与隐藏
我们要做到动态隐藏显示NavBar 可以使用文档里面提到的visible 属性。
我们要实现的功能:当用户强行在地址栏输入我们不存在的路由时,我们不仅不显示页面的内容,还要把底部的NavBar 给屏蔽掉。
如图,我们就没有/detail 路径,也就是说当用户来到这个路径时会产生bug ,只显示底部导航栏但没内容,这是不行的,而且以后我们还要做登陆页面也是没有NavBar 的,改!
vim App.jsx
import React, { useState, useEffect } from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
useLocation
} from "react-router-dom"
import { ConfigProvider } from 'zarm'
import routes from '@/router'
import NavBar from '@/components/NavBar'
const App = () => {
const location = useLocation()
const { pathname } = location
const needNav = ['/', '/data', '/user']
const [showNav, setShowNav] = useState(false)
useEffect(() => {
setShowNav(needNav.includes(pathname))
}, [pathname])
return <Router>
<ConfigProvider primaryColor={'#007fff'}>
<Switch>
{
routes.map(route => <Route exact key={route.path} path={route.path}>
<route.component />
</Route>)
}
</Switch>
</ConfigProvider>
<NavBar showNav={showNav}/>
</Router>
}
export default App
我们按照修改思路进行编写代码,结果发现报错了,那问题出在哪呢?
我们来看报错信息Cannot read properties of undefined (reading 'location')
这是因为想要在函数组件内执行 useLocation ,该组件必须被 Router 高阶组件包裹,我们做如下改动,将 App.jsx 的 Router 组件,前移到 main.jsx 内。
vim App.jsx
import React, { useState, useEffect } from 'react'
import {
Switch,
Route,
useLocation
} from "react-router-dom"
import { ConfigProvider } from 'zarm'
import routes from '@/router'
import NavBar from '@/components/NavBar'
const App = () => {
const location = useLocation()
const { pathname } = location
const needNav = ['/', '/data', '/user']
const [showNav, setShowNav] = useState(false)
useEffect(() => {
setShowNav(needNav.includes(pathname))
}, [pathname])
return <>
<ConfigProvider primaryColor={'#007fff'}>
<Switch>
{
routes.map(route => <Route exact key={route.path} path={route.path}>
<route.component />
</Route>)
}
</Switch>
</ConfigProvider>
<NavBar showNav={showNav}/>
</>
}
export default App
vim main.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'lib-flexible/flexible'
import './index.css'
import App from './App'
import { BrowserRouter as Router } from 'react-router-dom'
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
)
vim NavBar/index.jsx
import React, { useState } from "react";
import { TabBar } from "zarm";
import { useHistory } from 'react-router-dom'
import CustomIcon from "@/components/CustomIcon";
const NavBar = ({showNav}) => {
const [activeKey, setActiveKey] = useState("/");
const history = useHistory();
const changeTab = (path) => {
setActiveKey(path)
history.push(path)
}
return <TabBar activeKey={activeKey} onChange={changeTab} visible={showNav}>
<TabBar.Item itemKey={'/'} title={'账单'} icon={<CustomIcon type="zhangdan"/>}/>
<TabBar.Item itemKey={'/data'} title={'统计'} icon={<CustomIcon type="tongji"/>}/>
<TabBar.Item itemKey={'/user'} title={'我的'} icon={<CustomIcon type="wode"/>}/>
</TabBar>
}
export default NavBar;
这里说明一下传入参数showNav 为什么要加一个{} ,这是ES6 语法中的结构,App.jsx 中<NavBar showNav={showNav}/> 这里的showNav={} 传给NavBar 组件的其实是一个props ,所以原本的写法是:
const NavBar = (props) = >{
const {showNav} = props
...
}
这里我们使用简写方式,能偷懒就不多写任何东西。
导航栏可以用在很多地方,映射到 PC 网页就是左侧侧边导航,道理都是相通的。移动端放在下面控制,PC 端放在左边或者右边控制罢了。所以再次强调不要学完了一个知识点,就思维定势地认为只能用在某一个需求上,能做到融会贯通,才是判断一个好程序员的标准,下载代码。
第三章 登陆注册页面
cd src/container && mkdir Login && cd Login && touch index.jsx style.module.less
vim Login/index.jsx
import React from 'react';
const Login = () => {
return <div>
<h1>Login</h1>
</div>
}
export default Login;
vim router/index.js
import Data from '@/container/Data'
import Home from '@/container/Home'
import User from '@/container/User'
+ import Login from '@/container/Login'
const routes = [
{
path: "/",
component: Home
},
{
path: "/user",
component: User
},
{
path: "/data",
component: Data
},
+ {
+ path:'/login',
+ component: Login
+ }
];
export default routes
通过上面的学习我们可以得出一个开发顺序的思路:先路由后页面,先静态后动态。首先要确保这个页面可以正常跳转了,能够出来基础页面了,最后我们才去对页面进行丰富以及美化,否则做了一堆结果页面没出来,一切都是无效的。
vim Login/index.jsx
import React from 'react'
import { Button, Cell, Checkbox, Input } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
const Login = () => {
return <div>
<div />
<div>
<span>注册</span>
</div>
<div>
<Cell icon={<CustomIcon type={'zhanghao'} />}><Input clearable type='text' placeholder='账号'/></Cell>
<Cell icon={<CustomIcon type={'mima'} />}><Input clearable type='text' placeholder='密码'/></Cell>
<Cell icon={<CustomIcon type={'mima'} />}><Input clearable type='text' placeholder='验证码'/></Cell>
</div>
<div>
<div>
<Checkbox />
<label>阅读并同意 <a>《掘掘记账本条款》</a> </label>
</div>
<Button block theme='primary'>注册</Button>
</div>
</div>
}
export default Login;
上述代码中,关键部分是账号输入、密码输入、验证码输入,这三个输入框是需要获取数据作为接口的参数提交上去的。
很多时候,服务端没有开发好接口的时候,我们前端要做的任务就是先还原 UI 稿,把该切的页面都切出来,并且预留好需要给接口提交的数据交互,比如上述三个输入框。
接下来我们来让页面变好看:
vim ./style.module.less
.auth {
min-height: 100vh;
background-image: linear-gradient(217deg, #6fb9f8, #3daaf85e, #49d3fc1a, #3fd3ff00);
.head {
height: 200px;
background: url('//s.yezgea02.com/1616032174786/cryptocurrency.png') no-repeat center;
background-size: 120%;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
img {
width: 34px;
margin: 15px 0 0 15px;
}
}
.tab {
color: #597fe7;
padding: 30px 24px 10px 24px;
> span {
margin-right: 10px;
font-size: 14px;
font-weight: bold;
&.avtive {
font-size: 20px;
border-bottom: 2PX solid #597fe7;
padding-bottom: 6px;
}
}
}
.form {
padding: 0 6px;
:global {
.za-cell {
background-color: transparent;
&::after {
border-top: none;
}
}
}
}
.operation {
padding: 10px 24px 0 24px;
.agree {
display: flex;
align-items: center;
margin-bottom: 10px;
label {
margin-left: 10px;
font-size: 14px;
}
}
}
}
由于我们采用的是 CSS Module 的形式进行开发,也就是你在页面中声明的类名都会根据当前页面,打一个唯一的 hash 值,比如我们页面中声明的 className={s.form} ,最终在浏览器中显示的是这样的:
_form_1h5us_30 是已经被编译过的样式,这样做的目的是避免和别的页面的样式重名,这是目前样式管理的一个诟病,当多人参与项目开发的时候,很难做到不污染全局样式名称,除非很小心的命名样式名称。
所以经过编译之后,想要修改 .form 下的 .za-cell ,如下写法,将无法修改成功:
.form {
.za-cell {
color: red;
}
}
原因是,上述写法,.za-cell 会被编译加上 hash ,组件库 Zarm 内的 dom 类名还是叫 za-cell ,如上图所示。所以为了不加 hash ,就需要这样操作:
.form {
:global {
.za-cell {
color: red;
}
}
}
这样 .za-cell 就不会被加上 hash ,如上图的.za-cell 所示。
接下来我们添加验证码:
yarn add react-captcha-code -S
由于代码太长加的又少,就贴图了。
我们给页面加上相应的逻辑,首先是账号、密码、验证码:
vim Login/index.jsx
import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'
const Login = () => {
const [captcha, setCaptcha] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [verify, setVerify] = useState('');
const handleChange = useCallback((captcha) => {
setCaptcha(captcha)
}, []);
return <div className={s.auth}>
<div className={s.head} />
<div className={s.tab}>
<span>登陆</span>
<span>注册</span>
</div>
<div className={s.form}>
<Cell icon={<CustomIcon type="zhanghao" />}>
<Input
clearable
type="text"
placeholder="请输入账号"
onChange={(value) => setUsername(value)}
/>
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="password"
placeholder="请输入密码"
onChange={(value) => setPassword(value)}
/>
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>
<Captcha charNum={4} onChange={handleChange} />
</Cell>
</div>
<div className={s.operation}>
<div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div>
<Button block theme="primary">{'登录'}</Button>
</div>
</div>
}
export default Login;
到此为止,我们可以获取的参数有用户输入的账号、密码、验证码以及验证码图片上的验证码。
接下来我们该写一下注册方法了,前端只需要把参数传给后端即可注册:
import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input, Toast } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'
import { post } from '@/utils'
const Login = () => {
const [captcha, setCaptcha] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [verify, setVerify] = useState('');
const handleChange = useCallback((captcha) => {
setCaptcha(captcha)
}, []);
const onSubmit = async () => {
if (!username) {
Toast.show('请输入账号')
return
}
if (!password) {
Toast.show('请输入密码')
return
}
if (!verify) {
Toast.show('请输入验证码')
return
};
if (verify != captcha) {
Toast.show('验证码错误')
return
};
try {
const { data } = await post('/api/user/register', {
username,
password
});
Toast.show('注册成功');
} catch (error) {
Toast.show('系统错误');
console.log(error);
}
};
return <div className={s.auth}>
<div className={s.head} />
<div className={s.tab}>
<span>登陆</span>
<span>注册</span>
</div>
<div className={s.form}>
<Cell icon={<CustomIcon type="zhanghao" />}>
<Input
clearable
type="text"
placeholder="请输入账号"
onChange={(value) => setUsername(value)}
/>
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="password"
placeholder="请输入密码"
onChange={(value) => setPassword(value)}
/>
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>
<Captcha charNum={4} onChange={handleChange} />
</Cell>
</div>
<div className={s.operation}>
<div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div>
<Button block theme="primary" onClick={onSubmit}>{'登录'}</Button>
</div>
</div>
}
export default Login;
记得要修改两个地方:
vim src/utils/axios.js
import axios from 'axios'
import { Toast } from 'zarm'
const MODE = import.meta.env.MODE
axios.defaults.baseURL = MODE == 'development' ? '/api' : 'http://dualseason.com:7001'
axios.defaults.withCredentials = true
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.interceptors.response.use(res => {
if (typeof res.data !== 'object') {
Toast.show('服务端异常!')
return Promise.reject(res)
}
if (res.data.code != 200) {
if (res.data.msg) Toast.show(res.data.msg)
if (res.data.code == 401) {
window.location.href = '/login'
}
return Promise.reject(res.data)
}
return res.data
})
export default axios
vim vite.config.js
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import styleImport from 'vite-plugin-style-import'
import path from 'path'
export default defineConfig({
plugins: [
reactRefresh(),
styleImport(
{
libs: [
{
libraryName: 'zarm',
esModule: true,
resolveStyle: (name) => {
return `zarm/es/${name}/style/css`;
}
}
]
}
)
],
css: {
modules: {
localsConvention: 'dashesOnly'
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'utils': path.resolve(__dirname, 'src/utils')
}
},
server: {
proxy: {
'/api': {
target: 'http://dualseason.com:7001',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
上述代码中,因为我们使用的是 async await 做异步处理,所以需要通过 try catch 来捕获异步处理过程中出现的错误,如果使用 Promise 的回调函数,则无需使用 try catch ,改动如下:
post('/api/user/register', {
username,
password
}).then(res => {
})
我们可以使用&& 来替代三元表达式中的选择显示功能:
{
type === 'register' &&
<div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div>
}
{
type === 'register' ?
<div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div>
:null
}
接下来把所有代码补上去:
import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input, Toast } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'
import { post } from '@/utils'
import cx from 'classnames'
const Login = () => {
const [captcha, setCaptcha] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [verify, setVerify] = useState('');
const [type, setType] = useState('login');
const handleChange = useCallback((captcha) => {
setCaptcha(captcha)
}, []);
const onSubmit = async () => {
if (!username) {
Toast.show('请输入账号')
return
}
if (!password) {
Toast.show('请输入密码')
return
}
try {
if (type == 'login') {
const { data } = await post('/api/user/login', {
username,
password
});
localStorage.setItem('token', data.token);
} else {
if (!verify) {
Toast.show('请输入验证码')
return
};
if (verify != captcha) {
Toast.show('验证码错误')
return
};
const { data } = await post('/api/user/register', {
username,
password
});
Toast.show('注册成功');
setType('login');
}
} catch (error) {
Toast.show('系统错误');
}
};
return <div className={s.auth}>
<div className={s.head} />
<div className={s.tab}>
<span className={cx({ [s.avtive]: type == 'login' })} onClick={() => setType('login')}>登录</span>
<span className={cx({ [s.avtive]: type == 'register' })} onClick={() => setType('register')}>注册</span>
</div>
<div className={s.form}>
<Cell icon={<CustomIcon type="zhanghao" />}>
<Input
clearable
type="text"
placeholder="请输入账号"
onChange={(value) => setUsername(value)}
/>
</Cell>
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="password"
placeholder="请输入密码"
onChange={(value) => setPassword(value)}
/>
</Cell>
{
type === 'register' &&
<Cell icon={<CustomIcon type="mima" />}>
<Input
clearable
type="text"
placeholder="请输入验证码"
onChange={(value) => setVerify(value)}
/>
<Captcha charNum={4} onChange={handleChange} />
</Cell>
}
</div>
<div className={s.operation}>
{
type === 'register' &&
<div className={s.agree}>
<Checkbox />
<label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label>
</div>
}
<Button block theme="primary" onClick={onSubmit}>{type === 'register' ? '注册' : '登陆'}</Button>
</div>
</div>
}
export default Login;
登陆成功则会填入token 到localStorage 里面。
useEffect(() => {
document.title = type == 'login' ? '登录' : '注册';
}, [type])
我们再完善一下代码,让它更像项目一点,我们让登录成功后跳转到首页。
import {useHistory} from 'react-router-dom'
const history = useHistory();
history.push('/')
window.location.href = '/';
?代码
第四章 账单列表页
上一章节介绍的登录注册是整个项目的根基,没有拿到 token ,将无法进行后续的各种操作,如账单的增删改查。所以务必将上一章节好好地阅读与揣摩,为后面的学习做好铺垫。我们直接进入本次前端实战项目的主题,账单的增删改查之列表页。
- 列表页编写
按照正常的开发流程,我们先将静态页面切出来,再填入数据使其动态化。在此之前,我们已经新建好了 Home 目录,该目录便是用于放置账单列表,所以我们直接在 Home/index.jsx 新增代码。
头部统计实现
import React from 'react'
import { Icon } from 'zarm'
import s from './style.module.less'
const Home = () => {
return <div className={s.home}>
<div className={s.header}>
<div className={s.dataWrap}>
<span className={s.expense}>总支出:<b>¥ 200</b></span>
<span className={s.income}>总收入:<b>¥ 500</b></span>
</div>
<div className={s.typeWrap}>
<div className={s.left}>
<span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
<div className={s.right}>
<span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
</div>
</div>
</div>
}
export default Home;
.home {
height: 100%;
display: flex;
flex-direction: column;
padding-top: 80px;
.header {
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 80px;
background-color: #007fff;
color: #fff;
font-size: 14px;
z-index: 100;
padding: 10px;
.data-wrap {
font-size: 14px;
>span {
font-size: 12px;
>b {
font-size: 26px;
font-family: DINCondensed-Bold, DINCondensed;
margin-left: 4px;
}
}
.income {
margin-left: 10px;
}
}
.type-wrap {
display: flex;
justify-content: flex-end;
align-items: flex-end;
>div {
align-self: flex-start;
background: rgba(0, 0, 0, 0.1);
border-radius: 30px;
padding: 3px 8px;
font-size: 12px;
}
.left {
margin-right: 6px;
}
.arrow {
font-size: 12px;
margin-left: 4px;
}
}
}
}
本次项目全程采用的是 Flex 弹性布局,这种布局形式在当下的开发生产环境已经非常成熟,同学们如果还有不熟悉的,请实现对 Flex 布局做一个简单的学习,这边推荐一个学习网站。
列表页面实现
列表页面会用到 Zarm 组件库为我们提供的 Pull 组件,来实现下拉刷新以及无限滚动,我们先来将基础布局实现,如下所示:
import React,{useState} from 'react'
import { Icon } from 'zarm'
import s from './style.module.less'
const Home = () => {
const [list, setList] = useState([
{
bills: [
{
amount: "25.00",
date: "1623390740000",
id: 911,
pay_type: 1,
remark: "",
type_id: 1,
type_name: "餐饮"
}
],
date: '2021-06-11'
}
]);
return <div className={s.home}>
<div className={s.header}>
<div className={s.dataWrap}>
<span className={s.expense}>总支出:<b>¥ 200</b></span>
<span className={s.income}>总收入:<b>¥ 500</b></span>
</div>
<div className={s.typeWrap}>
<div className={s.left}>
<span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
<div className={s.right}>
<span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
</div>
</div>
<div className={s.contentWrap}>
{
list.map((item, index) => <div>BillItem</div>)
}
</div>
</div>
}
export default Home;
我们看到BillItem 已经被渲染到页面上了,接下来我们来对BillItem 进行渲染即可,我们来回顾一下效果。
{
bills: [
{
amount: "25.00",
date: "1623390740000",
id: 911,
pay_type: 1,
remark: "",
type_id: 1,
type_name: "餐饮"
}
],
date: '2021-06-11'
}
这是一个BillItem 的数据结构,上面代码使用变量list 来存储,也就是说list.map(item => console.log(item)) 就会打印出这样的结构出来:
import React, { useState } from 'react'
import { Cell, Icon } from 'zarm'
import s from './style.module.less'
const Home = () => {
const [list, setList] = useState([
{
bills: [
{
amount: "25.00",
date: "1623390740000",
id: 911,
pay_type: 1,
remark: "wod",
type_id: 1,
type_name: "餐饮"
},
{
amount: "26.00",
date: "1623390740000",
id: 912,
pay_type: 1,
remark: "",
type_id: 1,
type_name: "餐饮"
}
],
date: '2021-06-11'
},
{
bills: [
{
amount: "25.00",
date: "1623390740000",
id: 913,
pay_type: 1,
remark: "",
type_id: 1,
type_name: "餐饮"
},
{
amount: "26.00",
date: "1623390740000",
id: 914,
pay_type: 2,
remark: "",
type_id: 1,
type_name: "餐饮"
}
],
date: '2021-06-12'
}
]);
return <div className={s.home}>
<div className={s.header}>
<div className={s.dataWrap}>
<span className={s.expense}>总支出:<b>¥ 200</b></span>
<span className={s.income}>总收入:<b>¥ 500</b></span>
</div>
<div className={s.typeWrap}>
<div className={s.left}>
<span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
<div className={s.right}>
<span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span>
</div>
</div>
</div>
<div className={s.contentWrap}>
{
list.map((item, index) => <div key={item.date}>
<div className={s.headerDate}>
<div className={s.date}>{item.date}</div>
<div className={s.money}>
<span>
<img src="//s.yezgea02.com/1615953405599/zhi%402x.png" alt='支' />
<span>¥1</span>
</span>
<span>
<img src="//s.yezgea02.com/1615953405599/shou%402x.png" alt="收" />
<span>¥2</span>
</span>
</div>
</div>
{
item.bills?.map(bill => <Cell
className={s.bill}
key={bill.id}
description={
<span style={{ color: bill.pay_type == 2 ? 'red' : '#39be77' }}>
{`${bill.pay_type == 1 ? '-' : '+'}${bill.amount}`}
</span>}
>
</Cell>)
}
</div>)
}
</div>
</div>
}
export default Home;
我们现在要把每个账单项的时间显示出来,我们用到dayjs 这个库。
yarn add dayjs -S
{
item.bills?.map(bill =>
<Cell className={s.item} key={bill.id}
description={
<span style={{ color: bill.pay_type == 2 ? 'red' : '#39be77' }}>
{`${bill.pay_type == 1 ? '-' : '+'}${bill.amount}`}
</span>}
help={
<div>{dayjs(Number(bill.date)).format('HH:mm')}
{bill.remark ? `| ${bill.remark}` : ''}</div>}
></Cell>)
}
加上上面这段代码就是这个效果了,接下来我们把项目标题给补上。
vim utils/index.js
export const typeMap = {
1: {
icon: 'canyin'
},
2: {
icon: 'fushi'
},
3: {
icon: 'jiaotong'
},
4: {
icon: 'riyong'
},
5: {
icon: 'gouwu'
},
6: {
icon: 'xuexi'
},
7: {
icon: 'yiliao'
},
8: {
icon: 'lvxing'
},
9: {
icon: 'renqing'
},
10: {
icon: 'qita'
},
11: {
icon: 'gongzi'
},
12: {
icon: 'jiangjin'
},
13: {
icon: 'zhuanzhang'
},
14: {
icon: 'licai'
},
15: {
icon: 'tuikuang'
},
16: {
icon: 'qita'
}
}
title={
<>
<CustomIcon
className={s.itemIcon}
type={item.type_id ? typeMap[item.type_id].icon : 1}
/>
<span>{ item.type_name }</span>
</>
}
现在没差了,只需要补上一点样式即可很漂亮哒啦,不过先不急,我们给他抽离出来形成独立的组件先。
|