Last updated on October 18, 2024 am
本节实现:DarkMode、DashBoard建立、Error Bundary、项目部署
一、实现Dark Mode (1)useLocalStorage 这个自定义钩子类似useState的效果,不同的是它是从LocalStorage中来读取值。首先value从LocalStorage通过key值读取,如果没有就返回初始状态。每次value或者key改变,useEffect
会被触发,将当前的 value
保存到 localStorage
中。这确保了状态的任何更新都会同步到 localStorage
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { useState, useEffect } from 'react' ;export function useLocalStorageState (initialState, key ) { const [value, setValue] = useState (function ( ) { const storedValue = localStorage .getItem (key); return storedValue ? JSON .parse (storedValue) : initialState; }); useEffect ( function ( ) { localStorage .setItem (key, JSON .stringify (value)); }, [value, key] ); return [value, setValue]; }
使用 useLocalStorageState
的优势
持久化存储 :通过 localStorage
,状态在用户刷新页面或关闭浏览器后仍然保留。
用户偏好 :能够保存用户的界面偏好设置(如 dark mode
或 light mode
),提升用户体验。
易于管理 :与 localStorage
同步的状态,使得跨页面的状态管理变得简单。
(2)contextAPI darkmode的状态被设置为全球状态,以加强页面的一致性。因此用到了contextAPI,复习。
使用createContext新建context
建立Provider函数,返回context.provider,传递value
建立useContext()钩子,方便读取context内容。
导出provider和useContext
用Provider整个包裹住App
这里的Provider放置一个useEffect钩子,使得每次isDarkMode切换的时候,都相应地添加或者移除light或者dark类,实现改变亮暗风格的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const DarkModeContext = createContext ();function DarkModeProvider ({ children } ) { const [isDarkMode, setIsDarkMode] = useLocalStorageState (false , 'isDarkMode' ); useEffect ( function ( ) { if (isDarkMode) { document .documentElement .classList .add ('dark-mode' ); document .documentElement .classList .remove ('light-mode' ); } else { document .documentElement .classList .add ('light-mode' ); document .documentElement .classList .remove ('dark-mode' ); } }, [isDarkMode] ); function toggleDarkMode ( ) { setIsDarkMode ((isDark ) => !isDark); } return ( <DarkModeContext.Provider value ={{ isDarkMode , toggleDarkMode }}> {children} </DarkModeContext.Provider > ); }function useDarkMode ( ) { const context = useContext (DarkModeContext ); if (context === undefined ) throw new Error ('DarkModeContext was used outside of DarkModeProvider' ); return context; }export { DarkModeProvider , useDarkMode };
二、构建Dashboard (1)apiBooking 1、获得指定日期后的预定数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 export async function getBookingsAfterDate (date ) { const { data, error } = await supabase .from ('bookings' ) .select ('created_at, totalPrice, extrasPrice' ) .gte ('created_at' , date) .lte ('created_at' , getToday ({ end : true })); if (error) { console .error (error); throw new Error ('Bookings could not get loaded' ); } return data; }
2、获得指定日期后的入住数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export async function getStaysAfterDate (date ) { const { data, error } = await supabase .from ('bookings' ) .select ('*, guests(fullName)' ) .gte ('startDate' , date) .lte ('startDate' , getToday ()); if (error) { console .error (error); throw new Error ('Bookings could not get loaded' ); } return data; }
3、获取当天的活动
从 bookings
表中查询数据。
选择所有字段(*
)以及关联的 guests
表中的特定字段(fullName
、nationality
和 countryFlag
)。
通过 or
条件筛选出符合以下任一条件的预订:
状态为 unconfirmed
且 startDate
等于今天的日期。
状态为 checked-in
且 endDate
等于今天的日期。
根据 created_at
字段对查询结果进行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export async function getStaysTodayActivity ( ) { const { data, error } = await supabase .from ('bookings' ) .select ('*, guests(fullName, nationality, countryFlag)' ) .or ( `and(status.eq.unconfirmed,startDate.eq.${getToday()} ),and(status.eq.checked-in,endDate.eq.${getToday()} )` ) .order ('created_at' ); if (error) { console .error (error); throw new Error ('Bookings could not get loaded' ); } return data; }
(2)使用api中的函数 useRecentStays 通过params得到filter中的数据,并计算实际入住的数量。
subDays函数计算指定日期之前的日期,toISOString()将其转化为supabase能接受的IOS形式。
useRecentBookings和它很像,不赘述;useTodayActivity较为简单,也不赘述
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { useQuery } from '@tanstack/react-query' ;import { subDays } from 'date-fns' ;import { useSearchParams } from 'react-router-dom' ;import { getStaysAfterDate } from '../../services/apiBookings' ;export function useRecentStays ( ) { const [searchParams] = useSearchParams (); const numDays = !searchParams.get ('last' ) ? 7 : Number (searchParams.get ('last' )); const queryDate = subDays (new Date (), numDays).toISOString (); const { isLoading, data : stays } = useQuery ({ queryFn : () => getStaysAfterDate (queryDate), queryKey : ['stays' , `last-${numDays} ` ], }); const confirmedStays = stays?.filter ( (stay ) => stay.status === 'checked-in' || stay.status === 'checked-out' ); return { isLoading, stays, confirmedStays }; }
(3)构建stats
用到了可重用的组件stat。四个放置在stats组件中,较为简单不赘述。
(4)使用Recharts Library构建图表 ⭕npm i recharts@2
1、销售量线状图(可切换明暗模式) (太多参数了,看得我眼花缭乱😵💫😵💫😵💫)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 function SalesChart ({ bookings, numDays } ) { const { isDarkMode } = useDarkMode (); const allDates = eachDayOfInterval ({ start : subDays (new Date (), numDays - 1 ), end : new Date (), }); const data = allDates.map ((date ) => { return { label : format (date, 'MMM dd' ), totalSales : bookings .filter ((booking ) => isSameDay (date, new Date (booking.created_at ))) .reduce ((acc, cur ) => acc + cur.totalPrice , 0 ), extrasSales : bookings .filter ((booking ) => isSameDay (date, new Date (booking.created_at ))) .reduce ((acc, cur ) => acc + cur.extrasPrice , 0 ), }; }); const colors = isDarkMode ? { totalSales : { stroke : '#4f46e5' , fill : '#4f46e5' }, extrasSales : { stroke : '#22c55e' , fill : '#22c55e' }, text : '#e5e7eb' , background : '#18212f' , } : { totalSales : { stroke : '#4f46e5' , fill : '#c7d2fe' }, extrasSales : { stroke : '#16a34a' , fill : '#dcfce7' }, text : '#374151' , background : '#fff' , }; return ( <StyledSalesChart > <Heading as ="h2" > Sales from {format(allDates.at(0), 'MMM dd yyyy')} — {' '} {format(allDates.at(-1), 'MMM dd yyyy')}{' '} </Heading > <ResponsiveContainer height ={300} width ="100%" > <AreaChart data ={data} > <XAxis dataKey ="label" tick ={{ fill: colors.text }} tickLine ={{ stroke: colors.text }} /> <YAxis unit ="$" tick ={{ fill: colors.text }} tickLine ={{ stroke: colors.text }} /> <CartesianGrid strokeDasharray ="4" /> <Tooltip contentStyle ={{ backgroundColor: colors.background }} /> <Area dataKey ="totalSales" type ="monotone" stroke ={colors.totalSales.stroke} fill ={colors.totalSales.fill} strokeWidth ={2} name ="Total sales" unit ="$" /> <Area dataKey ="extrasSales" type ="monotone" stroke ={colors.extrasSales.stroke} fill ={colors.extrasSales.fill} strokeWidth ={2} name ="Extras sales" unit ="$" /> </AreaChart > </ResponsiveContainer > </StyledSalesChart > ); }
2、构建入住时间统计饼图
(5)构建今日活动
1、整体布局组件 加载状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function TodayActivity ( ) { const { isLoading, activities } = useTodayActivity (); return ( <StyledToday > <Row type ="horizontal" > <Heading as ="h2" > Today</Heading > </Row > {!isLoading ? ( activities?.length > 0 ? ( <TodayList > {activities.map((activity) => ( <TodayItem activity ={activity} key ={activity.id} /> ))} </TodayList > ) : ( <NoActivity > No activity today...</NoActivity > ) ) : ( <Spinner /> )} </StyledToday > ); }
2、每个活动的条目组件
alt
属性是图像的替代文本。在图像无法显示时,替代文本会被显示出来,此外,它对使用屏幕阅读器的用户也很重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function TodayItem ({ activity } ) { const { id, status, guests, numNights } = activity; return ( <StyledTodayItem > {status === 'unconfirmed' && <Tag type ="green" > Arriving</Tag > } {status === 'checked-in' && <Tag type ="blue" > Departing</Tag > } <Flag src ={guests.countryFlag} alt ={ `Flag of ${guests.country }`} /> <Guest > {guests.fullName}</Guest > <div > {numNights} nights</div > {status === 'unconfirmed' && ( <Button size ="small" variation ="primary" as ={Link} to ={ `/checkin /${id }`} > Check in </Button > )} {status === 'checked-in' && <CheckoutButton bookingId ={id} /> } </StyledTodayItem > ); }
三、Error Boundaries ⭕npm i react-error-boundary
(1)为App提供错误边界处理 1 2 3 4 5 6 7 8 9 10 ReactDOM .createRoot (document .getElementById ('root' )).render ( <React.StrictMode > <ErrorBoundary FallbackComponent ={ErrorFallback} onReset ={() => window.location.replace('/')} > <App /> </ErrorBoundary > </React.StrictMode > );
ErrorBoundary
是一个错误边界 组件,它捕获渲染树中的 JavaScript 错误,防止整个应用崩溃。错误边界只捕获渲染 、生命周期 和构造函数 中的错误,不捕获事件处理器中的错误。
**FallbackComponent={ErrorFallback}
**:
FallbackComponent
指定了当应用中发生错误时要显示的组件。ErrorFallback
是一个自定义组件,用于在错误发生时为用户展示友好的信息。
**onReset={() => window.location.replace('/')}
**:
onReset
是 ErrorBoundary
组件的一个回调函数,当用户尝试重置错误时触发。在这个例子中,当 ErrorFallback
中的“重试”按钮被点击时,页面将通过 window.location.replace('/')
重新加载到应用的根路径(/
)。
(2)构建错误发生后的组件界面(ErrorFallBack) 由于这个组件是脱离组件树的,所以要放置GlobalStyle,否则样式不起作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function ErrorFallback ({ error, resetErrorBoundary } ) { return ( <> <GlogalStyles /> <StyledErrorFallback > <Box > <Heading as ="h1" > Someting went wrong 😢</Heading > <p > {error.message}</p > <Button size ="large" onClick ={resetErrorBoundary} > Try again </Button > </Box > </StyledErrorFallback > </> ); }
四、BUG修整 (1)toggle的矛盾 之前我们把检测是否在窗体外点击的默认情况下,将 listenCapturing
设置为 true
,表示事件是在捕获阶段被处理的。但是这会造成一个问题是,点击某个窗体的toggle,再次点击无法实现关闭的功能。一下是两点需要修改的地方。
(2)不存在的booking订单 当我们将bookingId随便修改一个值之后,会到达ErrorFallBack界面,显示booking是undifined,但是觉得这个错误描述的不好,于是修改一下错误界面:
if (!booking) return <Empty resourceName="booking" />;
(3)明暗模式的默认值 在我们的电脑的控制台输入这行代码,会显示true or false。true代表电脑默认是使用的dark模式,false代表light,比如我的电脑就是。于是我们期望可以按照不同用户的电脑需求来使得他们刚进入APP时显示相应的mode。
window.matchMedia('(prefers-color-scheme: dark)').matches
所以可以把isDarkMode的默认值就设为上述代码。
1 2 3 4 const [isDarkMode, setIsDarkMode] = useLocalStorageState ( window .matchMedia ('(prefers-color-scheme: dark)' ).matches , 'isDarkMode' );
(4)功能展望
创建booking、编辑booking,创建餐厅,结账时到发票界面、可以选择打印。
五、部署APP ⭕npm run build
会生成一个dist包,这就是我们需要上传的包
(1)上传到Netlify 1、完善软件包 首先,我们需要在dist文件夹中新建一个文件:Netlify.toml
,内容是:
1 2 3 4 [[redirects]]from = "/*" to = "/index.html" status = 200
然后再把它添加到项目的根目录下,这样如果需要修改项目再部署,这时候这个文件会自动添加到dist文件夹中,就不需要手动创建了。这是重定向配置,确保当用户直接访问某个页面(例如 /dashboard
或 /about
)时,Netlify 会加载 index.html
,从而让前端路由器处理这些路径。
2、登陆上传项目 首先注册登陆,在主页选择manually部署,将整个dist文件夹上传。
上传后点击下图按钮,可以进入我们的APP界面,其中的URL如果分享给别人,他们就可以打开并看到我们的项目。
其中URL里的项目名可以自己修改。
(2)部署到Github仓库 首先创建一个仓库,最好设为私有。生成一个token(但是我部署项目的时候没用到,不知道为什么)
在我们的项目中,先输入git init
然后会看到所有的文件都变成了绿色,表示已经被git控制,其中除开public和src文件夹外的其他文件在最右边都有一个字母U,表示未被跟踪,终端输入git status
也可以查看这一点。
第一次上传/每次修改文件后上传
输入:git add -A
将所有的变更(包括新增、修改和删除的文件)添加到暂存区
输入:git commit -m "your message"
提交变更,确保所有工作都被保存。
输入:git remote add origin "你的github仓库地址".git
将本地仓库与远程 GitHub 仓库关联起来(只有第一次上传的时候需要)
输入:git push -u origin main
将本地的 main
分支推送到远程的 origin
仓库,并设置上游(upstream)跟踪关系。这意味着以后你可以只使用 git push
或 git pull
来更新该分支,而不需要每次都指定远程仓库和分支。
(3)部署到Vercel
不知道为啥报错了,不管了!
六、完结撒花 🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸