React七案例下
代码下载
登录模块
用户登录
页面结构
新建 Login
组件,对应结构:
export default function Login() {return (<div className={styles.root}><NavHeader className={styles.header}>账号登录</NavHeader><form className={styles.form}><input placeholder="请输入账号" className={styles.account}></input><input type="password" placeholder="请输入密码" className={styles.password}></input><Button color='success' className={styles.login} type="submit">登 录</Button></form><Link className={styles.backHome} to='/registe'>还没有账号,去注册~</Link></div>)}
功能实现
1、添加状态 username和password,获取表单元素值:
const [username, setUsername] = useState('')const [password, setPassword] = useState('')……<input placeholder="请输入账号" className={styles.account} onChange={(v) => setUsername(v)}>{username}</input><input type="password" placeholder="请输入密码" className={styles.password} onChange={(v) => setPassword(v)}>{password}</input>
2、在form表单的 onSubmit 方法中实现表单提交,通过username和password获取到账号和密码,使用API调用登录接口,将 username 和 password 作为参数,登录成功后,将token保存到本地存储中(hkzf_token)并返回登录前的页面:
const navigate = useNavigate()……<form className={styles.form} onSubmit={(e) => {// 阻止默认行为e.preventDefault()// 请求登录接口instance.post('/user/login', {username, password}).then((data) => {console.log('login data: ', data);const {status, description, body} = dataif (status !== 200) {// 登录成功localStorage.setItem('hkzf_token', body.token)navigate(-1)} else {// 登录失败Toast.show({content: description})}})}}>
表单验证说明
表单提交前,需要先进性表单验证,验证通过后再提交表单:
- 方式一:antd-mobile 组件库的方式(需要
Form.Item
文本输入组件) - 方法二(推荐):使用更通用的 formik,React中专门用来进行表单处理和表单校验的库
formik
- Github地址:formik文档
- 场景:表单处理,表单验证
- 优势:轻松处理React中的复杂表单,包括:获取表单元素的值,表单验证和错误信息,处理表单提交,并且将这些内容放在一起统一处理,有利于代码阅读,重构,测试等
- formik 具体使用
使用 formik 实现表单校验
- 安装
npm i formik
或yarn add formik
- 导入 Formik 组件,根据 render props 模式,使用 Formik 组件包裹Login组件
- 为 Formik 组件提供配置属性 initialValues,这是初始数据;onSubmit 表单提交的执行函数,通过values获取到表单元素值,完成登录逻辑
- Formik 的 children 属性是一个函数,通过函数参数获取到values(表单元素值对象),handleSubmit,handleChange
- 使用 values 提供的值设置为表单元素的 value,使用 handleChange 设置为表单元素的 onChange,使用handleSubmit设置为表单的onSubmit
<Formik initialValues={{'username': '', password: ''}}onSubmit={(values) => {console.log('login values: ', values);// 请求登录接口instance.post('/user/login', {'username': values.username, 'password': values.password}).then((data) => {console.log('login data: ', data);const {status, description, body} = dataif (data.status === 200) {// 登录成功localStorage.setItem('hkzf_token', body.token)navigate(-1)} else {// 登录失败Toast.show({content: description})}})}}>{({values, handleSubmit, handleChange}) => {return <form className={styles.form} onSubmit={handleSubmit}><input name="username" placeholder="请输入账号" className={styles.account} onChange={handleChange} value={values.username}></input><input name="password" type="password" placeholder="请输入密码" className={styles.password} onChange={handleChange} value={values.password}></input><Button color='success' className={styles.login} type="submit">登 录</Button></form>}}</Formik>
两种表单验证方式:
- 通过给 Formik 组件 配置 validate 属性手动校验
- 通过给 Formik 组件 validationSchema 属性配合Yup来校验(推荐)
给登录功能添加表单验证
1、安装npm i yup
或 yarn add yup
(Yup 文档),导入Yup
2、在 Formik 组件中添加配置项 validationSchema,使用 Yup 添加表单校验规则
validationSchema={Yup.object().shape({username: Yup.string().required('账号为必填项').matches(/^\w{5,8}/, '5~8位的数字、字母、下划线'),password: Yup.string().required('密码为必填项').matches(/^\w{5,12}/, '5~8位的数字、字母、下划线')})}
3、在 Formik 组件中,通过 children 函数属性获取到 errors(错误信息)和 touched(是否访问过,注意:需要给表单元素添加 handleBlur 处理失焦点事件才生效!),在表单元素中通过这两个对象展示表单校验错误信
{({values, handleSubmit, handleChange, errors, touched}) => {return <form className={styles.form} onSubmit={handleSubmit}><input name="username" placeholder="请输入账号" className={styles.account} onChange={handleChange} value={values.username}></input>{errors.username && touched.username && <div className={styles.error}>{errors.username}</div>}<input name="password" type="password" placeholder="请输入密码" className={styles.password} onChange={handleChange} value={values.password}></input>{errors.password && touched.password && <div className={styles.error}>{errors.password}</div>}<Button color='success' className={styles.login} type="submit">登 录</Button></form>}}
其他处理:
- 导入 Form 组件,替换 form 元素,去掉onSubmit
- 导入 Field 组件,替换 input 表单元素,去掉onChange,onBlur,value
- 导入 ErrorMessage 组件,替换原来的错误消息逻辑代码
{({values, handleSubmit, handleChange, errors, touched}) => {return <Form className={styles.form}><Field name="username" placeholder="请输入账号" className={styles.account}></Field><ErrorMessage name="username" className={styles.error} component='div'></ErrorMessage><Field name="password" type="password" placeholder="请输入密码" className={styles.password}></Field><ErrorMessage name="password" className={styles.error} component='div'></ErrorMessage><Button color='success' className={styles.login} type="submit">登 录</Button></Form>}}
我的页面
登录判断
查询本地缓存中是否有 token 信息来判断是否登录,新建 utils/auth.js
文件,为方便使用封装如下内容:
// localStorage 中存储 token 的键
const token_key = 'hkzf_token'// 获取 token
const getToken = () => localStorage.getItem(token_key)// 设置 token
const setToken = (token) => localStorage.setItem(token_key, token)// 删除 token
const removeToken = () => localStorage.removeItem(token_key)// 判断是否登录
const isAuth = !!getToken()export { getToken, setToken, removeToken, isAuth }
页面结构
我的页面根据是否登录展示有所不同:
import styles from "./Profile.module.css";
import { baseUrl } from "../utils/constValue";
import { useState } from "react";
import { isAuth } from "../utils/auth";
import { Grid } from "antd-mobile";
import { useNavigate } from "react-router-dom";// 默认头像
const DEFAULT_AVATAR = baseUrl + '/img/profile/avatar.png'
// 菜单数据
const menus = [{ id: 1, name: '我的收藏', iconfont: 'icon-coll', to: '/favorate' },{ id: 2, name: '我的出租', iconfont: 'icon-ind', to: '/rent' },{ id: 3, name: '看房记录', iconfont: 'icon-record' },{ id: 4, name: '成为房主', iconfont: 'icon-identity'},{ id: 5, name: '个人资料', iconfont: 'icon-myinfo' },{ id: 6, name: '联系我们', iconfont: 'icon-cust' }
]export default function Profile() {const [isLogin, setIsLogin] = useState(isAuth)const [userInfo, setUserInfo] = useState({avatar: '',nickname: ''})const {avatar, nickname} = userInfoconst navigate = useNavigate()return (<div className={styles.root}>{/* 个人信息 */}<div className={styles.title}><img className={styles.bg} src={baseUrl + '/img/profile/bg.png'}></img><div className={styles.info}><div className={styles.myIcon}><img className={styles.avater} src={avatar || DEFAULT_AVATAR} alt="头像"></img></div><div className={styles.user}><div className={styles.name}>{nickname || '游客'}</div><span className={isLogin ? styles.logout : styles.login}>{isLogin ? '退出' : '去登录'}</span></div><div className={styles.edit}>编辑个人资料<span className={styles.arrow}><i className="iconfont icon-arrow" /></span></div></div></div>{/* 九宫格菜单 */}<Grid columns={3} gap={8} className={styles.grid}>{menus.map((item) => {console.log('item: ', item);return <Grid.Item key={item.id} onClick={() => {if (item.to) navigate(item.to)}}><div className={styles.menusItem}><span className={'iconfont ' + item.iconfont}></span><span>{item.name}</span></div></Grid.Item>})}</Grid>{/* 加入我们 */}<div className={styles.add}><img src={baseUrl + '/img/profile/join.png'} alt=""></img></div></div>)
}
添加两个状态 isLogin(是否登录)和 userInfo(用户信息),从 utils/auth
中导入 isAuth 来判断是否登录。如果没有登录,渲染未登录信息;如果已登录,就渲染个人资料数据。
去登录与退出
- 给去登录、退出按钮绑定点击事件
- 点击去登录按钮,直接导航到登录页面
- 点击退出按钮,弹出 Modal 对话框,提示是否确定退出。先调用退出接口(让服务器端退出),再移除本地token(本地退出)、把登录状态 isLogin 设置为 false、清空用户状态对象。
<span className={isLogin ? styles.logout : styles.login} onClick={() => {if (isLogin) {Modal.show({title: '提示',content: '是否确定退出?',closeOnAction: true,actions: [{key: 'cancel',text: '取消'},{key: 'confirm',text: '退出',primary: true,onClick: () => {instance.post('/user/logout').finally((data) => {console.log('logout data: ', data);// 移除 tokenremoveToken()setIsLogin(isAuth)// 清除用户信息setUserInfo({avatar: '',nickname: ''})})}}]})} else {navigate('/login')}}}>{isLogin ? '退出' : '去登录'}</span>
获取用户信息
如果已登录,就使用 useEffect 根据接口发送请求,获取用户个人资料
useEffect(() => {let ignore = falseif (isLogin) {instance.get('/user', {headers: {authorization: getToken()}}).then((data) => {if (!ignore) {if (data.status === 200) {setUserInfo(data.body)}}})}return () => ignore = true}, [isLogin])
登录访问控制
项目中的两种类型的功能和两种类型的页面:
两种功能:
- 登录后才能进行操作(比如:获取个人资料)
- 不需要登录就可以操作(比如:获取房屋列表)
两种页面:
- 需要登录才能访问(比如:发布房源页)
- 不需要登录即可访问(比如:首页)
对于需要登录才能操作的功能使用 axios 拦截器 进行处理(比如:统一添加请求头 authorization等);对于需要登录才能访问的页面使用 路由控制
功能处理-使用axios拦截器统一处理token
在 api.js
中,添加请求拦截器 instance.interceptors.request.user()
,获取到当前请求的接口路径(url),判断接口路径,是否是以/user 开头,并且不是登录或注册接口(只给需要的接口添加请求头),如果是,就添加请求头Authorization:
// 请求拦截器
instance.interceptors.request.use((config) => {// 统一打印接口请求参数日志console.log('request url: ', config.baseURL + config.url);console.log('request params: ', config.params);console.log('request headers: ', config.headers);console.log('request data: ', config.data);// 统一显示加载提示const {url} = configToast.show({icon: 'loading', duration: 0, content: '加载中…', maskClickable: false})// 统一判断请求url路径添加请求头if (url.startsWith('/user') && !url.startsWith('/user/login') && !url.startsWith('/user/registered')) {config.headers.Authorization = getToken()}return config
})
添加响应拦截器 instance.interceptors.response.use()
,判断返回值中的状态码如果是 400 表示 token 失效,如果 data 中的状态码是 400 表示接口没有传递 token,则直接移除 token 并更新 isLogin 状态:
// 响应拦截器
instance.interceptors.response.use((res) => {// 统一打印接口响应数据日志console.log('response data: ', res);// 清除加载提示Toast.clear()// 统一判断 token 是否失效或者被清除const {status} = res.dataif (status === 400 || res.data.status === 400) {if (isAuth) {removeToken()}if (instance.setIsLogin) {instance.setIsLogin(isAuth())}}return res.data
}, (error) => {// 统一打印接口响应错误日志console.log('response error: ', error);// 清除加载提示Toast.clear()return Promise.reject(error)
})
页面处理-路由鉴权
限制某个页面只能在登陆的情况下访问,但是在React路由中并没有直接提供该该功能,需要手动封装来实现登陆访问控制(类似与Vue路由的导航守卫)。
react-router-dom的文档,实际上就是通过 Context 对原来路由系统做了一次包装,来实现一些额外的功能。
1、在 utils/auth.js
中添加 是否登录 和 设置是否登录 的 Context,并将它们导出:
// 是否登录 Context
const isLoginContext = createContext(isAuth())// 设置是否登录 Context
const setIsLoginContext = createContext(null)
2、在components目录中创建 AuthRoute.js
文件,创建组件AuthRoute并导出,添加状态 isLogin 并将 setIsLogin 函数传递给网络层,让后将原来的路由系统包裹在 isLoginContext 和 setIsLoginContext 中:
export default function AuthRoute() {const [isLogin, setIsLogin] = useState(useContext(isLoginContext))// 将修改登录状态函数传递给网络层instance.setIsLogin = setIsLoginreturn <isLoginContext.Provider value={isLogin}><setIsLoginContext.Provider value={setIsLogin}><Router><Routes>{/* 路由重定向 */}<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>{/* 父路由 */}<Route path='/' element={<App></App>}>{/* 子路由 */}<Route path='/home' element={<Home></Home>}></Route><Route path='/house' element={<House></House>}></Route><Route path='/news' element={<News></News>}></Route><Route path='/profile' element={<Profile></Profile>}></Route></Route><Route path='/cityList' element={<CityList></CityList>}></Route><Route path='/map' element={<Map></Map>}></Route><Route path='/detail/:id' element={<HouseDetail></HouseDetail>}></Route><Route path='/login' element={<Login></Login>}></Route></Routes></Router></setIsLoginContext.Provider></isLoginContext.Provider>
}
3、在components目录中创建 AuthRoute.js
文件中定义一个 loginRoute 函数,根据是否登录来返回对应的 Rute 组件(如果没有登陆,就重定向到登陆页面,并指定登陆成功后腰跳转的页面路径,并使用 replace 模式):
function loginRoute(isLogin) { return (route) => {if (isLogin) {return route}return <Route path={route.props.path} element={<Navigate to='/login' state={{from: {pathname: route.props.path}}} replace></Navigate>}></Route>}}
4、将启动文件 index.js
中的路由系统删除,由新的 <AuthRoute></AuthRoute>
组件代替:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode><AuthRoute></AuthRoute></React.StrictMode>
);
修改登录成功跳转
- 登陆成功后使用 setToken 清除 token并使用 Context 更新登录状态,判断是否需要跳转到用户想要访问的页面(使用 useLocation 获取路由的 state 参数, 判断 state.form 是否有值)
- 如果不需要,则直接调用history.go(-1) 返回上一页
- 如果需要,就跳转到
from.pathname
指定的页面(推荐使用replace方法模式)
const {state} = useLocation()const setIsLogin = useContext(setIsLoginContext)……// 请求登录接口instance.post('/user/login', {'username': values.username, 'password': values.password}).then((data) => {console.log('login data: ', data);const {description, body} = dataif (data.status === 200) {// 登录成功setToken(body.token)setIsLogin(isAuth())if (state && state.form) {navigate(state.form.pathname, {replace: true})} else {navigate(-1)}} else {// 登录失败Toast.show({content: description})}})
修改我的页面的登录状态
用 Context 将原来的 isLogin 状态替换:
// const [isLogin, setIsLogin] = useState(isAuth())const isLogin = useContext(isLoginContext)const setIsLogin = useContext(setIsLoginContext)
收藏模块
检查房源是否收藏
- 在 HouseDetail 页面中添加状态 isFavorite(表示是否收藏),默认值是false
- 使用 useEffect,在进入房源详情页面时执行
- 先调用isAuth方法,来判断是否登陆
- 如果未登录,直接return,不再检查是否收藏
- 如果已登陆,从路由参数中,获取当前房屋id
- 使用API调用接口,查询该房源是否收藏
- 如果返回状态码为200,就更新isFavorite;否则,不做任何处理
// 收藏const [isFavorite, setIsFavorite] = useState(false)useEffect(() => {let ignore = false// 登录才获取收藏数据if (isAuth()) {instance.get('/user/favorites/' + routerParams.id).then((data) => {if (!ignore) {console.log('favorite data: ', data);if (data.status === 200) {setIsFavorite(data.body.isFavorite)}}})}return () => ignore = true}, [])
在 HouseDetail 页面结构中,通过状态isFavorite修改收藏按钮的文字和图片内容:
{/* 底部栏工具栏 */}<div className={styles.footer}><div className={styles.favorite}><imgsrc={baseUrl + (isFavorite ? '/img/star.png' : '/img/unstar.png')}className={styles.favoriteImg}alt="收藏"/><span className={styles.favoriteTxt}>{isFavorite ? '已收藏' : '收藏'}</span></div><div className={styles.consult}>在线咨询</div><div className={styles.telephone}><a href="tel:400-618-4000" className={styles.telephoneTxt}>电话预约</a></div></div>
收藏房源
- 给收藏按钮绑定点击事件,调用isAuth方法,判断是否登陆
- 如果未登录,则使用
Toast.show
提示用户是否去登陆;如果点击取消,则不做任何操作;如果点击去登陆,就跳转到登陆页面,同时传递state(登陆后,再回到房源收藏页面) - 如果已登录则根据 isFavorite 判断当前房源是否收藏,如果未收藏,就调用添加收藏接口,添加收藏;如果收藏了,就调用删除接口,删除收藏
<div className={styles.favorite} onClick={async () => {if (isAuth()) {// 已登录if (isFavorite) {// 已收藏const deleteData = await instance.delete('/user/favorites/' + routerParams.id)console.log('delete data: ', deleteData);if (deleteData.status === 200) {Toast.show('已取消收藏')setIsFavorite(false)} else {Toast.show('登录超时,请重新登录')}} else {// 未收藏const postData = await instance.post('/user/favorites/' + routerParams.id)console.log('post data: ', postData);if (postData.status === 200) {Toast.show('已收藏')setIsFavorite(true)} else {Toast.show('登录超时,请重新登录')}}} else {// 未登录Modal.show({title: '提示',content: '登录后才能收藏房源,是否去登录?',closeOnAction: true,actions: [{key: 'cancel',text: '取消'},{key: 'confirm',text: '去登录',primary: true,onClick: async () => navigate('/login', {state: {from: 'location'}})}]})}}}><imgsrc={baseUrl + (isFavorite ? '/img/star.png' : '/img/unstar.png')}className={styles.favoriteImg}alt="收藏"/><span className={styles.favoriteTxt}>{isFavorite ? '已收藏' : '收藏'}</span></div>
房源发布模块
主要功能为获取房源的小区信息,房源图片上传,房源发布等。
前期准备
1、将 House 页面中没有找到房源时显示的内容封装成一个公共组件 NoHouse,并将其children属性校验为设置为 node(任何可以渲染的内容):
import styles from "./NoHouse.module.css";
import PropTypes from "prop-types";export function NoHouse(props) {return <div className={styles.noData}><img className={styles.img} src={baseUrl + '/img/not-found.png'} alt="暂无数据"/><p className={styles.msg}>{props.children}</p></div>
}NoHouse.propTypes = {// node(任何可以渲染的内容)children: PropTypes.node.isRequired
}
2、将 HouseDetail 页面中房屋配置的内容封装成一个公共组件 HousePackage,并为 onSelect 属性设置默认值:
export default function HousePackage({supporting = [], onSelect = () => {}}) {// 选中的配套名称console.log('supporting: ', supporting);const [selectedNames, setSelectedNames] = useState(supporting)return (<>{/* 房屋配套 */}<div className={styles.aboutList}>{HOUSE_PACKAGE.map((item, i) => {const si = selectedNames.indexOf(item.name)return <div className={styles.aboutItem + (si > -1 ? ' ' + styles.aboutActive : '')} key={item.id} onClick={() => {console.log('si: ', si);const newNames = [...selectedNames]if (si > -1) {newNames.splice(si, 1)} else {newNames.push(item.name)}// 修改选中setSelectedNames(newNames)// 将值传递给父组件onSelect(newNames)}}><p className={styles.aboutValue}><i className={`iconfont ${item.icon} ${styles.icon}`} /></p><div>{item.name}</div></div>})}</div></>)
}HousePackage.propTypes = {supporting: PropTypes.string,onSelect: PropTypes.func
}
3、创建了三个页面组件 Rent(已发布房源列表)、RentAdd(发布房源)、RentSearch(关键词搜索校区信息),并为 Rent 组件构建如下布局:
function renderNoHouse() {return <NoHouse>您还没有房源,<Link to='/rent/add' className={styles.link}>去发布房源</Link>吧~</NoHouse>
}
function renderList(list, navigate) {return list.map((value) => {return <HouseItem key={value.houseCode} item={value} onClick={() => {navigate('/detail/' + value.houseCode)}}></HouseItem>})
}
export default function Rent() {// 获取已发布房源数据const {data: rentData} = useData.get('/user/houses')console.log('rent data: ', rentData);const list = rentData && rentData.body ? rentData.body : []const navigate = useNavigate()return (<div className={styles.root}><NavHeader className={styles.navigate}>房屋管理</NavHeader>{list.length > 0 ? renderList(list, navigate) : renderNoHouse()}</div>)
}
4、在 AuthRoute 中导入 Rent、RentAdd、RentSearch 3个页面组件,使用 loginRoute 函数配置3个对应的路由规则:
{loginRoute(isLogin)(<Route path='/rent' element={<Rent></Rent>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/add' element={<RentAdd></RentAdd>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/search' element={<RentSearch></RentSearch>}></Route>)}
搜索小区
关键词搜索小区信息分析:
- 给 SearchBar 组件添加 onChange 事件获取文本框的值,判断当前文本框的值是否为空(如果为空,清空列表,然后return,不再发送请求;如果不为空,使用API发送请求,获取小区数据),使用定时器来延迟搜索,提升性能
- 给搜索列表项添加点击事件,使用 useNavigate 跳转到发布房源页面,将被点击的校区信息作为数据一起传递过去
防抖:搜索栏中每输入一个值,就发一次请求,这样对服务器压力比较大,用户体验不好。解决方式:使用定时器来进行延迟执行(关键词:JS文本框输入 防抖)
import { SearchBar } from "antd-mobile";
import styles from "./RentSearch.module.css";
import { useState } from "react";
import { instance } from "../utils/api";
import useCurrentCity from "../utils/useCurrentCity";
import { replace, useNavigate } from "react-router-dom";let timer = nullexport function RentSearch() {const [searchText, setSearchText] = useState('')const {currentCity} = useCurrentCity()const [list, setList] = useState([])console.log('current city: ', currentCity);const navigate = useNavigate()return <div className={styles.root}>{/* 搜索栏 */}<SearchBar className={styles.searchBar} placeholder="请输入小区名称" showCancelButton value={searchText} onChange={(value) => {console.log('searchText: ', value);if (value) {console.log('timer: ', timer);if (timer) {clearTimeout(timer)timer = null}timer = setTimeout(() => {instance.get('/area/community', {params: {name: value, id: currentCity.value}}).then((data) => {console.log('search data: ', data);setList(data.body)})}, 500);} else {setList([])}setSearchText(value)}}></SearchBar>{/* 搜索项 */}<ul className={styles.list}>{list.map((v) => <li key={v.community} className={styles.item} onClick={() => {navigate('/rent/add', {replace: true, state: {community: v}})}}>{v.communityName}</li>)}</ul></div>
}
发布房源
使用 ImageUploader, Input, List, Modal, Picker, TextArea 等组件搭建页面结构并实现功能:
// 房屋类型
const roomTypeData = [{ label: '一室', value: 'ROOM|d4a692e4-a177-37fd' },{ label: '二室', value: 'ROOM|d1a00384-5801-d5cd' },{ label: '三室', value: 'ROOM|20903ae0-c7bc-f2e2' },{ label: '四室', value: 'ROOM|ce2a5daa-811d-2f49' },{ label: '四室+', value: 'ROOM|2731c38c-5b19-ff7f' }
]// 朝向:
const orientedData = [{ label: '东', value: 'ORIEN|141b98bf-1ad0-11e3' },{ label: '西', value: 'ORIEN|103fb3aa-e8b4-de0e' },{ label: '南', value: 'ORIEN|61e99445-e95e-7f37' },{ label: '北', value: 'ORIEN|caa6f80b-b764-c2df' },{ label: '东南', value: 'ORIEN|dfb1b36b-e0d1-0977' },{ label: '东北', value: 'ORIEN|67ac2205-7e0f-c057' },{ label: '西南', value: 'ORIEN|2354e89e-3918-9cef' },{ label: '西北', value: 'ORIEN|80795f1a-e32f-feb9' }
]// 楼层
const floorData = [{ label: '高楼层', value: 'FLOOR|1' },{ label: '中楼层', value: 'FLOOR|2' },{ label: '低楼层', value: 'FLOOR|3' }
]// 获取数据列表中 value 对应的 label 值
const labelForValue = (data, value) => {for (let index = 0; index < data.length; index++) {const element = data[index];if (element.value === value) {return element.label}}return null
}export default function RentAdd() {const {state} = useLocation()const navigate = useNavigate()const [info, setInfo] = useState({community: state ? state.community : {}})return <div className={styles.root}><NavHeader className={styles.navHeader}>发布房源</NavHeader><div className={styles.content}><List className={styles.header} header='房源信息'><List.Item prefix={<label>小区名称</label>} extra={info.community.communityName || '请选择'} clickable onClick={() => navigate('/rent/search')}></List.Item><List.Item prefix={<label htmlFor='price'>租 金</label>} extra='¥/月'><Input id="price" placeholder="请输入租金/月" value={info.price} onChange={(v) => {const newInfo = {...info}newInfo.price = vsetInfo(newInfo)}}></Input></List.Item><List.Item prefix={<label htmlFor='size'>建筑面积</label>} extra='m²'><Input id="size" placeholder="请输入建筑面积" value={info.size} onChange={(v) => {const newInfo = {...info}newInfo.size = vsetInfo(newInfo)}}></Input></List.Item><Picker columns={[roomTypeData]} value={[info.roomType]} onConfirm={(v) => {console.log('room type value: ', v)const newInfo = {...info}newInfo.roomType = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>户 型</label>} extra={labelForValue(roomTypeData, info.roomType) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker><Picker columns={[floorData]} value={[info.floor]} onConfirm={(v) => {console.log('floor value: ', v)const newInfo = {...info}newInfo.floor = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>所在楼层</label>} extra={labelForValue(floorData, info.floor) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker><Picker columns={[orientedData]} value={[info.oriented]} onConfirm={(v) => {console.log('oriented value: ', v)const newInfo = {...info}newInfo.oriented = v[0]setInfo(newInfo)}}>{(_, actions) => {return <List.Item prefix={<label>朝 向</label>} extra={labelForValue(orientedData, info.oriented) || '请选择'} clickable onClick={actions.open}></List.Item>}}</Picker></List><List header='房屋标题'><List.Item><Input placeholder="请输入标题(例如:整租 小区名 2室 5000元)" value={info.title} onChange={(v) => {const newInfo = {...info}newInfo.title = vsetInfo(newInfo)}}></Input></List.Item></List><List header='房屋图像'><List.Item><ImageUploader value={info.houseImg} multiple maxCount={9} showUpload={info.houseImg ? info.houseImg.length < 9 : true} onCountExceed={(exceed) => Toast.show(`最多选择 9 张图片,您多选了 ${exceed} 张`)} onChange={(v) => {console.log('temp slides value: ', v);const newInfo = {...info}newInfo.houseImg = vconsole.log('new info: ', newInfo);setInfo(newInfo)}} upload={async (file) => {console.log('file: ', file);const fd = new FormData()fd.append('file', file)const data = await instance.post('/houses/image', fd, {headers: {'Content-Type': 'multipart/form-data'}})console.log('image data: ', data);if (data.status === 200) {const url = data.body[0]return {url: baseUrl + data.body[0]}}}}></ImageUploader></List.Item></List><List header='房屋配置'><List.Item><HousePackage onSelect={(names) => {const newInfo = {...info}newInfo.supporting = names.join('|')setInfo(newInfo)}}></HousePackage></List.Item></List><List header='房屋描述'><List.Item><TextArea placeholder="请输入房屋描述信息" value={info.description} rows={5} onChange={(v) => {const newInfo = {...info}newInfo.description = vsetInfo(newInfo)}}></TextArea></List.Item></List><div className={styles.bottom}><div className={styles.cancel} onClick={() => {Modal.show({title: '提示',content: '放弃发布房源?',closeOnAction: true,actions: [{key: 'cancel',text: '放弃',onClick: () => {navigate(-1)}},{key: 'edit',text: '继续编辑',primary: true}]})}}>取消</div><div className={styles.confirm} onClick={() => {console.log('confirm info: ', info);const params = {...info}if (info.community) {params.community = info.community.community}if (info.houseImg) {const imgs = info.houseImg.map((v) => v.url.replace(baseUrl, ''))params.houseImg = imgs.join('|')}console.log('confirm params: ', params);instance.post('/user/houses', params).then((data) => {console.log('add houses data: ', data);if (data.status === 200) {navigate('/rent')} else {Toast.show('服务开小差,请稍后再试!')}})}}>提交</div></div></div></div>
}
说明:
- 上传房屋图片,创建 FormData 对象,调用图片上传接口传递 form 参数,并设置请求头 Content-Type 为 multipart/form-data,通过接口返回值获取到图片路径
- 发布房源,从 state 里面获取到所有的房屋数据,调用发布房源接口传递所有房屋数据。
项目打包
create-react-app 脚手架的 打包文档说明。
简易打包
1、在根目录创建 .env.production 文件,配置生产环境变量:
REACT_APP_URL = http://localhost:8080REACT_APP_TIME_OUT = 10000
2、打开终端进入项目根目录,输入命令 npm run build
或 yarn build
进行项目打包,生成build文件夹(打包好的项目内容),将build目录中的文件内容,部署到都服务器中即可。
出现以下提示,就代表打包成功,在根目录中就会生成一个build文件夹:
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.File sizes after gzip:209.02 kB build/static/js/main.b6b1c41b.js10.47 kB build/static/css/main.ef78ebb8.css1.79 kB build/static/js/453.b9229bd0.chunk.jsThe project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.The build folder is ready to be deployed.
You may serve it with a static server:npm install -g serveserve -s buildFind out more about deployment here:https://cra.link/deployment
3、也可以通过终端中的提示,使用 serve-s build
来本地查看(需要全局安装工具包 serve)
脚手架的配置说明
create-react-app 中隐藏了 webpack的配置,隐藏在react-scripts包中,两种方式来修改:
- 运行命令
npm run eject
释放 webpack配置(注意:不可逆) - 通过第三方包重写 webpack配置(比如:react-app-rewired 等)
eject 说明:
如果对构建工具和配置选择不满意可以eject
随时进行。此命令将从项目中删除单个构建依赖项。
相反,它会将所有配置文件和传递依赖项(Webpack,Babel,ESLint等)作为依赖项复制到项目中package.json
。从技术上讲,依赖关系和开发依赖关系之间的区别对于生成静态包的前端应用程序来说是非常随意的。此外,它曾经导致某些托管平台出现问题,这些托管平台没有安装开发依赖项(因此无法在服务器上构建项目或在部署之前对其进行测试)。可以根据需要自由重新排列依赖项package.json
。
除了eject
仍然可以使用所有命令,但它们将指向复制的脚本,以便可以调整它们。在这一点上是独立的。
不必使用eject
,策划的功能集适用于中小型部署,不应觉得有义务使用此功能。但是我们知道如果准备好它时无法自定义此工具将无用。
antd-mobile 按需加载
1、安装 npm install react-app-rewired customize-cra --save-dev
用于脚手架重写配置
2、修改package.json 中的 scripts:
"scripts": {"start": "react-app-rewired start","build": "react-app-rewired build","test": "react-scripts test","eject": "react-scripts eject"}
3、安装 npm install babel-plugin-import --save-dev
插件,用于按需加载组件代码和样式
4、在项目根目录创建文件 config-overrides.js
用于覆盖脚手架默认配置:
const { override, fixBabelImports } = require('customize-cra')module.exports = override(fixBabelImports('import', {libraryName: 'antd-moble',style: 'css'})
)
5、重新打包,发现两次打包的体积并没有变化
打开 Ant Design 按需加载文档,会发现
antd
默认支持基于 ES modules 的 tree shaking,直接引入import { Button } from 'antd';
就会有按需加载的效果。
基于路由代码分割(路由懒加载)
将代码按照路由进行分割,只在访问该路由的时候才加载该组件内容,提高首屏加载速度。通过 React.lazy() 方法 + import() 方法、Suspense组件来实现,(React Code-Splitting文档)。
- React.lazy() 作用: 处理动态导入的组件,让其像普通组件一样使用
- import(‘组件路径’),作用:告诉webpack,这是一个代码分割点,进行代码分割
- Suspense组件:用来在动态组件加载完成之前,显示一些loading内容,需要包裹动态组件内容
在 AuthRoute.js
文件中做如下调整:
import App from '../App.js'
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import { isLoginContext, setIsLoginContext } from "../utils/auth.js";
import React, { useContext, useState } from "react";
import { instance } from "../utils/api.js";// import Home from '../pages/Home';
// import House from '../pages/House.js';
// import News from '../pages/News.js';
// import Profile from '../pages/Profile.js';
// import CityList from '../pages/CityList';
// import Map from '../pages/Map.js';
// import HouseDetail from "../pages/HouseDetail.js";
// import Login from '../pages/Login.js';
// import Rent from "../pages/Rent.js";
// import RentAdd from "../pages/RentAdd.js";
// import RentSearch from "../pages/RentSearch.js";
// import FormikLearn from "../pages/FormikLearn.js";const Home = React.lazy(() => import('../pages/Home'))
const House = React.lazy(() => import('../pages/House.js'))
const News = React.lazy(() => import('../pages/News.js'))
const Profile = React.lazy(() => import('../pages/Profile.js'))
const CityList = React.lazy(() => import('../pages/CityList'))
const Map = React.lazy(() => import('../pages/Map.js'))
const HouseDetail = React.lazy(() => import('../pages/HouseDetail.js'))
const Login = React.lazy(() => import('../pages/Login.js'))
const Rent = React.lazy(() => import('../pages/Rent.js'))
const RentAdd = React.lazy(() => import('../pages/RentAdd.js'))
const RentSearch = React.lazy(() => import('../pages/RentSearch.js'))
const FormikLearn = React.lazy(() => import('../pages/FormikLearn.js'))function loginRoute(isLogin) { return (route) => {console.log('AuthRoute isLogin: ', isLogin);if (isLogin) {return route}return <Route path={route.props.path} element={<Navigate to='/login' state={{from: {pathname: route.props.path}}} replace></Navigate>}></Route>}
}export default function AuthRoute() {const [isLogin, setIsLogin] = useState(useContext(isLoginContext))// 将修改登录状态函数传递给网络层instance.setIsLogin = setIsLoginreturn <isLoginContext.Provider value={isLogin}><setIsLoginContext.Provider value={setIsLogin}><React.Suspense><Router><Routes>{/* 路由重定向 */}<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>{/* 父路由 */}<Route path='/' element={<App></App>}>{/* 子路由 */}<Route path='/home' element={<Home></Home>}></Route><Route path='/house' element={<House></House>}></Route><Route path='/news' element={<News></News>}></Route><Route path='/profile' element={<Profile></Profile>}></Route></Route><Route path='/cityList' element={<CityList></CityList>}></Route><Route path='/map' element={<Map></Map>}></Route><Route path='/detail/:id' element={<HouseDetail></HouseDetail>}></Route><Route path='/login' element={<Login></Login>}></Route>{loginRoute(isLogin)(<Route path='/rent' element={<Rent></Rent>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/add' element={<RentAdd></RentAdd>}></Route>)}{loginRoute(isLogin)(<Route path='/rent/search' element={<RentSearch></RentSearch>}></Route>)}<Route path='/formik' element={<FormikLearn></FormikLearn>}></Route></Routes></Router></React.Suspense></setIsLoginContext.Provider></isLoginContext.Provider>
}
其他性能优化
1、react-virtualized只加载用到的组件 文档,如果只使用少量的组件并增加应用程序的包大小,可以直接导入需要的组件,像这样:
// import { List, AutoSizer, InfiniteLoader } from "react-virtualized";
import AutoSizer from "react-virtualized/dist/commonjs/AutoSizer";
import List from 'react-virtualized/dist/commonjs/List';
import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader';
2、脚手架配置解决跨域问题,代理 API 请求 文档,首先,使用 npm 或 Yarn 安装 http-proxy-middleware:
npm install http-proxy-middleware --save
$ # or
$ yarn add http-proxy-middleware
接下来,创建 src/setupProxy.js
现在可以根据需要注册代理:
const { createProxyMiddleware } = require('http-proxy-middleware');module.exports = function (app) {app.use('/api',createProxyMiddleware({target: 'http://localhost:5000',changeOrigin: true,}));
};
注意:不需要将此文件导入到任何地方。当启动开发服务器时,它会自动注册。该文件仅支持 Node 的 JavaScript 语法。确保仅使用受支持的语言功能(即不支持 Flow、ES 模块等)。将路径传递给代理函数允许在路径上使用通配符和/或模式匹配,这比快速路由匹配更灵活。
相关文章:
React七案例下
代码下载 登录模块 用户登录 页面结构 新建 Login 组件,对应结构: export default function Login() {return (<div className{styles.root}><NavHeader className{styles.header}>账号登录</NavHeader><form className{styles.form}>&…...
Rust包管理与错误处理
文章目录 包管理箱(Crate)包(Package)模块(Module)访问权限use关键字 错误处理不可恢复错误可恢复错误错误传递kind方法 包管理 Rust的包管理有三个重要的概念,分别是箱、包、模块 箱…...
关于gitee的readme文档中的图片问题
使用markdown编辑readme文档,需要注意 添加图片,但是不显示问题 1.记得连图片一起上传到仓库中,不能只是在本地markdown文件中复制就结束了,因为存储的是本地图片地址,上传后找不到的 2.注意使用网络地址࿰…...
记录vscode连接不上wsl子系统下ubuntu18.04问题解决方法
记录vscode连接不上wsl子系统下ubuntu18.04问题解决方法 报错内容尝试第一次解决方法尝试第二次解决方法注意事项参考连接 报错内容 Unable to download server on client side: Error: Request downloadRequest failed unexpectedly without providing any details… Will tr…...
aws平台练习
注册 AWS 账户 访问 AWS 官方网站,点击“免费注册”按钮,按照提示完成账户注册: 提供电子邮件地址、密码和电话号码。 验证身份(可能需要手机验证码)。 设置 billing 信息。 2. 登录 AWS 管理控制台 使用注册的邮箱和…...
实战篇-梳理时钟树
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据 总结 前言 这是B站傅里叶的猫视频的笔记 一、建立工程 以Vivado的wave_gen为例子。为了引入异…...
【Hadoop入门】Hadoop生态之Hive简介
1 什么是Hive? 1.1 Hive概述 在大数据时代,如何让传统的数据分析师和SQL开发人员也能轻松处理海量数据?Hive应运而生。Hive是基于Hadoop构建的一套数据仓库分析系统,它提供了一种类似SQL的查询语言(HQL)&a…...
DSP复习【3章】
F2812提供了XINTF用于扩展并行接口的外设芯片。 XINTF(外部接口)所需的时钟是 SYSCLKOUT 和 XTIMCLK。 所以正确答案是: ✅ SYSCLKOUT 和 XTIMCLK 什么是XREADY信号,如何使用? 章节例题: 1. 如何通过软件判…...
Hadoop案例——流量统计
Hadoop案例——流量统计 在大数据时代,流量统计是许多企业和组织的关键需求之一。通过分析网络流量数据,企业可以优化网络资源分配、提升用户体验、制定精准的营销策略等。本文将介绍如何使用 Hadoop 框架实现一个简单的流量统计案例,包括数…...
Linux管道 有名管道(FIFO)工作机制全解:从理论到实践
有名管道(重要) 有名管道/命名管道,主要用于没有血缘关系进程间的通信 当然也支持有血缘关系的情况,只是如果有血缘关系,没有必要使用有名管道,无名管道效果更佳 引入 好了,现在使用条件有了…...
java基础-修饰符
java修饰符 修饰符分类访问修饰符的作用域代码说明访问修饰符总览 非访问修饰符staticfinalabstractsynchronizedvolatiletransientnativestrictfp非访问修饰符总览表 非访问修饰符组合与冲突规则 修饰符分类 分类:访问修饰符 和 非访问修饰符 1.访问修饰符 公共…...
解锁基因密码之重测序(从测序到分析)
在生命科学的奇妙世界中,基因恰似一本记录着生命奥秘的“天书”,它承载着生物体生长、发育、衰老乃至疾病等一切生命现象的关键信息。而重测序技术,则是开启基因“天书”奥秘的一把神奇钥匙。 试想,你手中有一本经典书籍的通用版…...
当使用 Docker Desktop 启动 Tomcat 镜像时时间不对
当使用 Docker Desktop 启动 Tomcat 镜像时时间不对,可能由以下原因导致并可采取相应解决方法: 宿主机时间设置问题:Docker 容器的时间是由宿主机提供的,如果宿主机的时间不正确,那么容器的时间也会不正确。需确保宿主…...
golang gmp模型分析
思维导图: 1. 发展过程 思维导图: 在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行 单进程的缺点有: 单一执行流程、计算机只能一个任务一个任务进行处理进程阻塞所带来的CPU时间的浪费 处于对CPU资源的利用&…...
Redisson的RedLock与联锁(MultiLock)的区别
Redisson提供了两种分布式锁机制:RedLock(红锁)和MultiLock(联锁),它们在实现分布式锁时有重要区别。 1. RedLock (红锁) 设计原理: 基于Redis官方提出的Redlock算法实现目的是在Redis集群环境下提供更可靠的分布式锁需要至少3个独立的Redi…...
图灵逆向——题十-魔改算法
目录列表 过程分析JS代码还原代码实现运行结果 本题属于魔改标准加密算法,所以无法使用JS或Python中的标准库来进行模拟加密了,只能一步一步的还原它的环境咯。。。 过程分析 打开控制台发现有个无限debugger,直接过掉~[doge]。。。 OK过掉…...
K8S学习之基础七十九:关闭istio功能
关闭istio功能 kubectl get ns --show-labels kubectl label ns default istio-injection-有istio-injectionenabled的命名空间,pod都会开启istio功能 反之,如果要开启istio,在对应命名空间打上该标签即可...
AI大模型课程系列汇总-某客时间篇
某客时间系列篇章💥 NO.文章(点击可跳转)1【课程系列11】某客时间AI 大模型应用开发实战营_ai大模型应用开发实战营 百度云-CSDN博客2【课程系列12】某客时间AI大模型微调训练营_极客时间 大模型rag 网盘-CSDN博客3【课程系列15】2024某客时…...
【人工智能】大语言模型多义词解析技术揭秘——以“项目“歧义消解为例
今天田辛老师和小伙伴探讨了一个有趣的多义词问题, 在人工智能技术日新月异的今天,大语言模型(LLM)对自然语言的理解能力已经达到令人惊叹的水平。大模型到底是如何去区分多义词的? 比如:当用户提到"…...
Coze平台 发布AI测试Agent的完整实现方案
以下是基于 Coze平台 发布AI测试Agent的完整实现方案,包含架构设计、核心功能模块、集成方式和落地步骤: 一、方案架构图 #mermaid-svg-kzS7cltxrbetjROl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-kzS7cltxr…...
OpenHarmony子系统开发 - 调测工具(二)
OpenHarmony子系统开发 - 调测工具(二) 三、hiperf使用指导 hiperf是为开发人员提供性能采样分析的工具,基于内核perf机制进行的用户态能力的扩展,可以对指定的程序或者整个系统进行性能采样。 hiperf支持的命令有:l…...
keil ERROR: L6220E 如何解决
最近调试MK8000方案,增加code后遇到: keil ERROR: L6220E: Execution region RW_RAM1 size (24592 bytes) execeeds limit (24568 bytes) .region contains 93 bytes of padding and 0 bytes of veneers (total 93 bytes of linker generated content) …...
c++比较器——priority_queue用 ; unordered_map 自定义哈希函数
文章目录 priority_queue自定义比较方法对 比较对象结构体 重载 <仿函数为什么是传一个类std::less<T> 和 std::greater<T> lambda unordered_map自定义哈希函数仿函数lambda priority_queue template<class T,class Container std::vector<T>,class…...
centos-LLM-生物信息-BioGPT安装
参考: GitHub - microsoft/BioGPT https://github.com/microsoft/BioGPT BioGPT:用于生物医学文本生成和挖掘的生成式预训练转换器 |生物信息学简报 |牛津学术 — BioGPT: generative pre-trained transformer for biomedical text generation and mini…...
esp32cam远程图传:AI Thinker ESP32-CAM -》 服务器公网 | 服务器 -》 电脑显示
用AI Thinker ESP32-CAM板子访问公网ip的5112端口并上传你的摄像头拍摄的图像视频数据,并写一段python程序打开弹窗接受图像实现超远程图像传输教程免费 1. 首先你要有一个公网ip也就是去买一台拥有公网的服务器电脑,我买的是腾讯云1年38元的服务器还可…...
今日踩坑之@Autowired与@Resource区别
案例说明 今天算是体验到了这俩注解的区别了,真所谓不报错就记不住这两注解区别,其实之前本人也是知道这俩注解的区别的只是我们往往项目上用习惯了Autowired就忘了,看来只有真正体会到报错才能记忆犹新o(╥﹏╥)o。 说明一下本人的踩坑情况&…...
在 M1 芯片的 Mac 电脑上安装 Redis 可以通过 Homebrew 快速完成
以下是详细步骤: 1. 安装 Homebrew(若未安装) 打开 终端,执行以下命令安装 Homebrew(已安装可跳过): /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/…...
【从零开始学习JVM | 第一篇】快速认识JVM
什么是JVM? JVM--Java虚拟机,它是Java实现平台无关性的基石。 Java程序运行的时候,编译器将Java代码编译为平台无关的Java字节码文件(.class),接下来对应平台的JVM对字节码进行运行解释,翻译成…...
linux 里的创建了一个tomcat用户 怎么禁止该用户使用crontab 功能
在 Linux 系统中,可以通过以下方法禁止 tomcat 用户使用 crontab 功能: 方法 1:通过 /etc/cron.deny 文件限制 这是 最推荐的标准方法,适用于所有主流 Linux 发行版(CentOS、Ubuntu 等)。 操作步骤&#…...
Elasticsearch入门指南(二)
一、RestClient操作索引库 什么是RestClient? ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址: https://www.elastic.co/guide/en/elasticsearch/client/in…...
Mac监控新风尚:酷炫界面,性能监控更直观!
你是否曾经希望自己的Mac能够像Windows那样,轻松一按就能查看任务管理器来监控性能状态呢? 对于Mac用户来说,系统性能监控一直是个挑战——苹果公司并没有提供一个直观的性能监控工具,用户往往需要通过复杂的活动监视器来获取这些…...
碰一碰发视频源码开发深度解析,定制化开发
在移动应用开发领域,便捷的数据分享功能始终备受关注。碰一碰发视频这一创新功能,借助近场通信(NFC)技术,为用户提供了一种快速、直观的视频分享体验。本文将深入剖析碰一碰发视频功能的源码开发过程,涵盖从…...
基于 AI智能体、大模型、RAG、Agent 等技术构建公司内部闭环智能问答系统的详细方案,结合 Spring Boot + Vue 管理系统 的改造思路
以下是基于 AI智能体、大模型、RAG、Agent 等技术构建公司内部闭环智能问答系统的详细方案,结合 Spring Boot Vue 管理系统 的改造思路: 1. 系统架构设计 核心组件 数据源层 内部文档库:公司知识库(如操作手册、FAQ、流程文档&a…...
红宝书第三十四讲:零基础学会单元测试框架:Jest、Mocha、QUnit
红宝书第三十四讲:零基础学会单元测试框架:Jest、Mocha、QUnit 资料取自《JavaScript高级程序设计(第5版)》。 查看总目录:红宝书学习大纲 一、单元测试是什么? 就像给代码做“体检”,帮你检查…...
视觉分析AI赋能智慧水务多场景应用
利用视觉分析的方式智慧水务的AI算法方案 一、背景 在传统水务监管中,面临着诸多痛点。例如,对于河道污染、水面异常、河湖四乱问题以及水生态环境差等情况,传统监管效率低下,预警不及时,资源分散,监控功…...
网络2 IP与MAC IP地址
IP地址与MAC地址 一 1.关系: 网卡有唯一的物理地址:MAC地址,IP是配置在网卡上的逻辑地址。 IP地址、MAC地址是进行网络通信必不可少的 2.MAC地址是物理地址,不可更改,通常与IP地址绑定 3.MAC地址:48位 IP地…...
spark-Core
运行架构 核心组件 由上图可以看出,对于 Spark 框架有两个核心组件: Driver,Spark 驱动器节点 其中Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。 Driver 在 Spark 作业执行时主要…...
前端开发中的问题排查与定位:HTML、CSS、JavaScript(报错的解决方式)
目录 1.html 1. 结构错误调试:标签未正确嵌套 2. 语法问题调试:缺失引号 3. 断点调试:动态生成内容时的 JavaScript 错误 4. 网络调试:资源加载错误 5. 性能调试:页面加载性能 总结: 2.CSS 1. 定位…...
VMware Fusion Pro 13 for Mac虚拟机
VMware Fusion Pro 13 for Mac虚拟机 文章目录 VMware Fusion Pro 13 for Mac虚拟机一、介绍二、效果下载 一、介绍 VMware Fusion Pro for Mac,是一款mac虚拟机软件,跟Parallels Desktop一样,都可以让你的 Mac 同时运行一个或多个不同的操作…...
使用cline(VSCode插件)、continue(IDEA插件)、cherry-studio玩转MCP
安装环境 uv(python) 为什么不用pip? 使用 uv 时无需进行特定安装。使用 uvx 直接运行。 ⚡️ 比pip快10-100x https://github.com/pypa/pip https://ossinsight.io/analyze/pypa/pip?vsastral-sh%2Fuv#overview 安装 https://github…...
Kotlin FragmentTransaction多容器管理多个fragment
在Activity中管理五个Fragment的切换显示和隐藏,并且希望将这部分逻辑进行封装。之前已经教过他们如何在Kotlin中使用FragmentTransaction进行基本的添加、隐藏、显示和替换操作,现在需要进一步封装这些操作,提高代码的可维护性和复用性。 管…...
PyCharm显示主菜单和工具栏
显示主菜单 新版 PyCharm 是不显示主菜单的,要想显示主菜单和工具栏,则通过 “视图” → “外观” ,勾选 “在单独的工具栏中显示主菜单” 和 “工具栏” 即可。 设置工具栏 此时工具栏里并没有什么工具,因此我们需要自定义工具…...
WebView2最低支持.NET frame4.5,win7系统
WebView2最低支持.NET frame什么版本 WebView2 对 .NET Framework 的最低版本要求 基础支持范围 WebView2 官方支持的 .NET Framework 最低版本为 4.5,同时兼容 .NET Core 3.0 及以上版本18。对于 WPF、WinForms 等桌面应用开发,需确…...
ClickOnce 部署
1、在远程服务器172.16.9.252共享文件文件夹Bluetooth. 2、设置版本自动更新. 3、设置部署 4、设置创建桌面菜单 二、远程发布IIS即可...
Kotlin 中 集合 Collection 的扩展方法完全指南
Kotlin 中 Collection 的扩展方法完全指南 “代码是最美的诗篇”——本文将带你进入 Kotlin 集合扩展函数的世界,帮助你写出既高效又优雅的代码 🚀 一、引言 🤔 在 Android 开发中,集合(Collection)的操作…...
STM32F407使用ESP8266连接阿里云并上传数据
文章目录 前言一、ESP01S介绍1.ESP01S2.MQTT固件烧录3.WIFI连接 二、阿里云平台介绍1.创建产品及添加设备2.连接云平台 三、数据上报四、命令下发五、完整工程 前言 在实现OTA功能我们必须学会如何连接云平台,本文会仔细介绍使用STM32F407和ESP8266连接阿里云平台&…...
TorchServe部署模型-index_to_name.json
在TorchServe部署模型时,若要将模型输出结果映射到指定标签(如分类任务的类别名称),需通过index_to_name.json文件定义索引与标签的映射关系,并在打包模型时将其作为额外文件包含。以下是完整流程和命令示例࿱…...
每日一题——BMN3 小红炸砖块
“落下”操作只会对y轴有影响,所以注意y轴的变化即可 只要给出的坐标有砖块,就遍历查找他的上面是否有砖块,每一层都是这样,直到到没有砖块的那一层; 注意:定义矩阵时要注意,给出的坐标都是大…...
AWS服务器 磁盘空间升级到100G后,怎么使其生效?
在AWS(Amazon Web Services)上扩展EBS(Elastic Block Store)卷的大小后,服务器操作系统并不会自动识别新增的空间。要使操作系统识别并使用新增的磁盘空间,您需要进行一些额外的步骤。以下是详细的指导和说…...
AWS弹性容器服务(AWS Elastic Container Service,ECS)概述
李升伟 编译 标签:AWS | ECS | 容器 | Docker AWS弹性容器服务(AWS Elastic Container Service,ECS)简介 AWS弹性容器服务(ECS)是一项完全托管的容器编排服务,支持运行、管理和扩展容器化应用…...