react课程21-完善table的显示功能以及booking的状态更改

Last updated on September 27, 2024 pm

本节实现在cabin、booking界面的过滤、排序、分页的功能,并实现修改booking状态。

一、在客户端实现过滤cabin(可重用,排序功能)

添加一个按钮,实现查看全部/有折扣的/没有折扣的cabin。并且在选择后需要相应地改变url。

新建了CabinTableOperation组件,把它代替之前的文本,放在标题的右边。这个组件包含Filter组件,这就是我们要实现的过滤功能。

(1)创建Filter

🔥《10.28》修改bug:每次过滤后要把page设置为1,否则会访问不存在的页数的数据


1、Filter.jsx

在这个组件里,我们要用到useParams Hook。(刚好之前不太懂)

Filter接收两个prop,一个是filterField,一个是options数组。

  • filterField负责被设置成URL中查询参数的键,并将其值设置为value,随后setSearchParams函数更新查询参数对象,使得URL反映出修改后的查询参数
  • options是一个对象数组,含有value和label两个属性。value就是传递给handleClick参数的键值,label是过滤器显示在屏幕上的文本。

根据传递的options数组来map出FilterButton,使得代码更具有重用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Filter({ filterField, options }) {
const [searchParams, setSearchParams] = useSearchParams();
const currentFilter = searchParams.get(filterField) || options.at(0).value;

function handleClick(value) {
searchParams.set(filterField, value);
setSearchParams(searchParams);
}

return (
<StyledFilter>
{options.map((option) => (
<FilterButton
onClick={() => handleClick(option.value)}
key={option.value}
active={option.value === currentFilter}
disabled={option.value === currentFilter}
>
{option.label}
</FilterButton>
))}
</StyledFilter>
);
}
2、CabinTableOperations.jsx

这个组件的目的就是给Filter一个造型上的作用,并且把相应的参数传递

3、CabinTable.jsx

这个组件之前的作用就是呈现出cabin table的header和下面的列表。但是由于我们想要加入过滤的功能,所以显示的cabin列表就要随着过滤器而改变。而方法就是通过读取URL上的查询参数的值,来条件更新应该展示的cabins。

1
2
3
4
5
6
7
8
9
10
const [searchParams] = useSearchParams();

const filterValue = searchParams.get('discount') || 'all';

let filteredCabins;
if (filterValue === 'all') filteredCabins = cabins;
if (filterValue === 'no-discount')
filteredCabins = cabins.filter((cabin) => cabin.discount === 0);
if (filterValue === 'with-discount')
filteredCabins = cabins.filter((cabin) => cabin.discount > 0);

这里的要点有一个是:当初次进入cabin界面的时候,我们想要展示所有的cabin(all),但是此时查询参数是空的,所以就可以通过||短路运算符,使得默认值是“all”。

(2)实现排序功能

1、CabinTableOperations传递给sortBy的参数
1
2
3
4
5
6
7
8
options={[
{ value: 'name-asc', label: 'Sort by name (A-Z)' },
{ value: 'name-desc', label: 'Sort by name (Z-A)' },
{ value: 'regularPrice-asc', label: 'Sort by Price (low first)' },
{ value: 'regularPrice-desc', label: 'Sort by Price (high first)' },
{ value: 'maxCapacity-asc', label: 'Sort by capacity (low first)' },
{ value: 'maxCapacity-desc', label: 'Sort by capacity (high first)' },
]}

这就是排序选项中的value和label数组,注意value的前值需要和cabin的属性保持一致。asc表示正序,desc表示倒序。

2、sortBy(可重用)

这个组件的功能和刚刚的Filter很像,也是把相应的value设置到查询参数中,使得CabinTable可以读取并获得相应的参数。这个不用传递键的名字,因为不管谁用,键的名字都是“sortBy”。在这个函数中得到键值并传递给Select组件,是为了传递正在被选中的value的参数。然后把onChange函数传递过去,使得改变被选元素时改变查询参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SortBy({ options }) {
const [searchParams, setSearchParams] = useSearchParams();
const sortBy = searchParams.get('sortBy') || '';

function handleChange(e) {
searchParams.set('sortBy', e.target.value);
setSearchParams(searchParams);
}

return (
<Select
options={options}
type="white"
value={sortBy}
onChange={handleChange}
/>
);
}
3、Select.jsx

"...props"接收所有剩余prop。

1
2
3
4
5
6
7
8
9
10
11
function Select({ options, value, onChange, ...props }) {
return (
<StyledSelect value={value} onChange={onChange} {...props}>
{options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
))}
</StyledSelect>
);
}
4、CabinTable

第一步当然是得到查询参数的值。

但是由于它是由“-”连接,且两部分值都需要,所以可以用split函数分开。

sort函数复习:a-b是正序,b-a是倒序。因此这里用了一个巧妙的小技巧,就是使用modifier符号,如果是asc就是1,否则为-1,乘以相应逻辑,实现不同排序功能。

这里name是string,它的排序需要换一种方式,否则无法实现。

1
2
3
4
5
6
7
8
9
10
11
12
const sortBy = searchParams.get('sortBy') || 'name-asc';
const [field, direction] = sortBy.split('-');

const modifier = direction === 'asc' ? 1 : -1;

const SortedCabins = filteredCabins.sort((a, b) => {
if (typeof a[field] === 'string') {
return a[field].localeCompare(b[field]) * modifier;
} else {
return (a[field] - b[field]) * modifier;
}
});

(怎么现在每个视频都是20分钟了😨😨😨😨😨😨要疯了我)

此处穿插一条喜报,我发现了一条Jonas都没发现的bug,就是regularPrice 和 discount的值为300 80的时候竟然会报错说discount应该小于regularPrice。百思不得其解后,感觉应该是它们俩被当作字符串比较了。但是我还是很疑惑,于是GPT这样说:

即使在表单输入中将 type 设置为 number,在 JavaScript 中获取到的值仍然是字符串。这是因为 HTML 表单元素在提交时会将所有输入的值以字符串的形式传递给 JavaScript。

二、建立Booking Table

✅fetching data的时候,和getCabins不同,因为bookings连接了cabins和guests,所以也可以获取这两个表单的信息:(括号内可以写进任何想要获取的信息,此处挑选需要的)

1
2
3
const { data, error } = await supabase
.from('bookings')
.select('*,cabins(name),guests(fullName,email)');

把data文件夹中的UpLoader组件包含在SideBar中就会出现这样一个可以上传样本信息到表单中,这样就不用手动创建。(记得修改Policy)(浅浅看了一下,这个函数逻辑算是比较简单)

image-20240925171213178

(1)实现过滤booking

1、实现

换一种方式:不在客户端过滤,而是在服务端

operation组件已经写好,过滤是通过status的状态,而排序是根据预定的开始日期和总价格。

既然要在接收数据的时候就过滤,那么难道应该在apiBookings的函数中实现吗?不尽然。因为既然要过滤,我们就要从URL读取数据,要使用useParams,在异步fetching data阶段,是不允许使用这种钩子的。( Hooks 只能在函数组件或自定义 Hooks 中被调用

因此我们可以在useBookings组件,也就是调用getBookings函数的地方得到查询参数的键和值,再传递给getBookings函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const filterValue = searchParams.get('status');
const filter =
!filterValue || filterValue === 'all'
? null
: { field: 'status', value: filterValue, method: 'eq' };

const {
isLoading,
data: bookings,
error,
} = useQuery({
queryKey: ['bookings', filter],
queryFn: () => getBookings({ filter }),
});

然后getBookings函数中在接收数据的时候根据键和值进行过滤。

1
2
3
4
5
let query = supabase
.from('bookings')
.select('*,cabins(name),guests(fullName,email)');
if (filter !== null)
query = query[filter.method || 'eq'](filter.field, filter.value);
2、要点

传递的filter是一个object,除开键和值以外还有一个method,因为query是根据method、键、值来进行过滤处理的。eq是等于,还有gte是大于等于的意思。

这里queryKey变成了一个对象,除开关键字“bookings”以外还有filter。这是因为React Query并不知道会在改变过滤的内容时就重新获取数据,于是我们切换过滤选项,页面不变,但URL会相应改变,刷新后才会显示过滤后的项目。于是把filter填进queryKey,类似于依赖数组的作用,每当filter发生变化的时候,都会重新fetch一次data。

image-20240925190720412

(React Query的作用)

(2)实现Sort Booking

1、useBookings
1
2
3
const sortByRaw = searchParams.get('sortBy') || 'startDate-desc';
const [field, direction] = sortByRaw.split('-');
const sortBy = { field, direction };

比较简单

2、getBookings
1
2
3
4
if (sortBy)
query = query.order(sortBy.field, {
ascending: sortBy.direction === 'asc',
});

order函数是作用于特定库来排序的,包括supabase,因此很方便。第一个参数传递根据什么来排序,第二个参数传递是否是根据升序排列。asc就是ascending的简称。

(3)实现Pagination

需要先看下一大节:如何建立Pagination

1、从api中获取count

我们可以直接在bookingtable中得到bookings.length,但是有一个更好的办法,是在获取数据的时候加上count。,然后将它返回,这样就可以容易得到获得的结果数了。

1
.select('*,cabins(name),guests(fullName,email)', { count: 'exact' });
2、useBookings
1
const page = !searchParams.get('page') ? 1 : Number(searchParams.get('page'));

跟分页组件中的逻辑一样,不多言。记得传递page并将其加入queryKey。

3、getBookings
1
2
3
4
5
if (page) {
const from = (page - 1) * PAGE_SIZE;
const to = from + PAGE_SIZE - 1;
query = query.range(from, to);
}

range也是supabase的特殊函数,它会自动接收从from到to的数据。由于数组第一位是0位,所以from不必加1.这里的小技巧是to可以直接用from+PAGE_SIZE(记得-1),就算超过了count,也不会有错误,只会返回范围内的结果。

三、建立可重用的Pagination(分页)组件

组件接收count prop,表示结果数。

(1)组件部分

image-20240925195151800

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<StyledPagination>
<P>
Showing <span>{(currentPage - 1) * PAGE_SIZE + 1}</span> to{' '}
<span>
{currentPage === pageCount ? count : currentPage * PAGE_SIZE}
</span>{' '}
of <span>{count}</span> results
</P>

<Buttons>
<PaginationButton onClick={prevPage} disabled={currentPage === 1}>
<HiChevronLeft /> <span>Previous</span>
</PaginationButton>
<PaginationButton
onClick={nextPage}
disabled={currentPage === pageCount}
>
<span>Next</span>
<HiChevronRight />
</PaginationButton>
</Buttons>
</StyledPagination>

如果在最后一页的话,to后的数字就应该是count,防止出错。向前翻页和向后翻页的按钮条件禁用,原理比较易懂。

(2)变量和函数部分

首先定义在函数体外一个常量:每页的结果数。const PAGE_SIZE = 10;

✅根据上一大节的内容,我们知道这个常量还需要在apiBookings文件中使用,因此我们可以在utils文件夹中创建一个文件:Constants.jsx来存储这样的常量,每次使用只需导入即可。

然后是其他代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const [searchParams, setSearchParams] = useSearchParams();

if (count <= PAGE_SIZE) return null;

const currentPage = !searchParams.get('page')
? 1
: Number(searchParams.get('page'));

const pageCount = Math.ceil(count / PAGE_SIZE);

function nextPage() {
const next = currentPage === pageCount ? currentPage : currentPage + 1;

searchParams.set('page', next);
setSearchParams(searchParams);
}

function prevPage() {
const prev = currentPage === 1 ? currentPage : currentPage - 1;

searchParams.set('page', prev);
setSearchParams(searchParams);
}

仍旧是通过强大的Param来得到当前的页数。当然刚进入这个界面的时候不存在那个查询参数,于是把当前页数设置为1,否则可以根据查询参数得到。

如果总结果数还没一页的结果多,那么直接不返回这个组件。

pageCount保留最大的页数。

每次翻页的时候都相应地更新查询参数。

(3)Prefetching

Prefetching 是一种优化技术,用于在用户需要数据之前提前获取和加载数据,以加速应用程序的响应速度。它常用于提高用户体验,减少等待时间,特别是在网页应用和数据驱动的应用中。

现在要做的很简单,就是我们再切换页面的时候,不管是前翻还是后翻,总会出现一个加载页面。但是我们想要数据可以丝滑地出现在界面上,于是,在useBookings组件中:

首先获得:const queryClient = useQueryClient();

然后在useQuery查询数据之后,返回所有数据之前:(有点简单不多说了)

1
2
3
4
5
6
7
8
9
10
11
12
13
const pageCount = Math.ceil(count / PAGE_SIZE);

if (page < pageCount)
queryClient.prefetchQuery({
queryKey: ['bookings', filter, sortBy, page + 1],
queryFn: () => getBookings({ filter, sortBy, page: page + 1 }),
});

if (page > 1)
queryClient.prefetchQuery({
queryKey: ['bookings', filter, sortBy, page - 1],
queryFn: () => getBookings({ filter, sortBy, page: page - 1 }),
});

四、建立Booking的Detail界面

image-20240925232925950

首先在App.js的Router中加上这样一条:

<Route path="bookings/:bookingId" element={<Booking />} />

我们新创建了一个界面:Booking,里面直接返回BookingDetial组件,而这个组件需要接收相应的booking数据,这个数据是由我们新创建的钩子中获得。

(1)useBooking

要点有二:

  • 通过URL获得bookingId,使用了useParams钩子可直接获得
  • 把id传递给getBooking函数中

(2)getBooking

这个函数位于api中,作用是返回相应id的Booking的所有详细信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function getBooking(id) {
const { data, error } = await supabase
.from('bookings')
.select('*, cabins(*), guests(*)')
.eq('id', id) //提取等于id的booking内容
.single();

if (error) {
console.error(error);
throw new Error('Booking not found');
}

return data;
}

(3)BookingRow

用到了先前创建的Menu复合组件。点击按钮会导航界面到相应的detail界面,因此把相应的id放入URL中方便被提取。

1
2
3
4
5
6
7
8
9
10
11
<Menus.Menu>
<Menus.Toggle id={bookingId}></Menus.Toggle>
<Menus.List id={bookingId}>
<Menus.Button
icon={<HiEye />}
onClick={() => navigate(`/bookings/${bookingId}`)}
>
See details
</Menus.Button>
</Menus.List>
</Menus.Menu>

(4)BookingDetail

以下几行有一个要点:

1
2
3
4
5
6
const { booking, isLoading } = useBooking();
const moveBack = useMoveBack();

if (isLoading) return <Spinner />;

const { status, id: bookingId } = booking;

第一useMoveBack钩子不能放在if语句之后;

第二读取{ status, id: bookingId }必须放在判断是否在加载语句之后,否则我们会在数据还没有加载完成的时候就试图读取,就会报错。(还让人百思不得其解)

五、实现Checkin、Checkout、Delete的功能

(1)实现基本checkin功能

新建了checkin/:bookingId的界面。

在BookingRow和BookingDetial中都加入了“checkin”按钮,可以导航到checkin界面。这个界面中还有一个确认checkin的按钮,作用是需要修改booking的statu变成checkin,是否付钱变为Paid。(只有状态为unconfirmed的booking才能被checkin)

useCheckin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function useCheckin() {
const queryClient = useQueryClient();
const navigate = useNavigate();

const { mutate: checkin, isLoading: isCheckingIn } = useMutation({
mutationFn: (bookingId) =>
updateBooking(bookingId, { status: 'checked-in', isPaid: true }),

onSuccess: (data) => {
toast.success(`Booking #${data.id} successfully checked in`);
queryClient.invalidateQueries({
active: true,
});
navigate('/');
},

onError: () => toast.error('There was an error while checking in'),
});

return { checkin, isCheckingIn };
}

通过代码可以看到,onSuccess函数是可以接收到返回的data数据的。

queryClient.invalidateQueries({});之前是放置queryId,现在放置active设置为true

queryClient.invalidateQueries() 是 React Query 中用于使缓存失效并触发重新获取数据的方法。在这里使用了一个配置对象 { active: true },这表示使所有当前 “active”(即正在被观察或使用)的查询失效。

(2)实现添加早饭的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{!hasBreakfast && (
<Box>
<Checkbox
checked={addBreakfast}
onChange={() => {
setAddBreakfast((addBreakfast) => !addBreakfast);
setConfirmPaid(false);
}}
id="Breakfast"
>
Want to add breakfast for {formatCurrency(optionalBreakfastPrice)}?
</Checkbox>
</Box>
)}

除此之外确定付钱的checkbox也要把总钱数修改一下。

这个时候,在checkin的时候就要分情况讨论是否有添加早餐,传递给checkin函数的就不仅仅是bookingId了,而且需要是一个对象,包含:

1
2
3
4
5
6
7
checkin({
bookingId,
breakfast: {
hasBreakfast: true,
extrasPrice: optionalBreakfastPrice,
totalPrice: totalPrice + optionalBreakfastPrice,
},

useCheckin中也需要发生改变:

1
2
3
4
5
6
mutationFn: ({ bookingId, breakfast }) =>
updateBooking(bookingId, {
status: 'checked-in',
isPaid: true,
...breakfast,
}),

(3)checkout和delete

delete中只有一个地方,就是在detail界面想要删掉后就返回booking界面。所以在这里传递一个对象,包含想要发生的事情,这里传递的onSettled意思是不管成功或失败均执行。

1
2
3
4
5
6
7
8
9
<Modal.Window name="delete">
<ConfirmDelete
resourceName="booking"
disabled={isDeletingBooking}
onConfirm={() =>
deleteBooking(bookingId, { onSettled: () => navigate(-1) })
}
/>
</Modal.Window>

react课程21-完善table的显示功能以及booking的状态更改
http://example.com/2024/09/25/react课程21-完善table的显示功能以及booking的状态更改/
Author
Yaodeer
Posted on
September 25, 2024
Licensed under