card 视图 column 可变
目前列数是由
yaml
grid={{ gutter: 0, xs: 3, md: 4, lg: 5, xl: 6, xxl: 7 }}
进行动态控制。这部分逻辑在服务端渲染时,服务端是不知道当前设备的 width,所以首次进入或主动刷新浏览器时会出现布局混乱。需要等待客户端重新渲染后才会正常显示。
方案
由于客户端使用的终端为浏览器,默认情况下不会将设备的 width 与 height 主动传递。为让服务端知道 width 值,可将当前设备的 width 、height 写入 Cookie 内。浏览器每次再请求服务端时,都会将 Cookie 传递回服务器。之后服务器每次执行渲染时从 Cookie 读取 width 与 height 值。这样就可以保证客户端与服务端渲染 html 一致。
这里使用 ahooks 内的 useCookieState 将设备的 width、height 写入 Cookie。
useSessionStorageState 本地管理客户端 column 值
实现
文件树
lua
explorer/src/app/layout.tsx
explorer/src/app/path/[[...path]]/card-display.tsx
explorer/src/app/path/[[...path]]/change-column.tsx
explorer/src/app/path/[[...path]]/layout-footer.tsx
explorer/src/app/path/context.tsx
explorer/src/components/viewport/context.tsx
explorer/src/components/viewport/index.tsx
文件路径:explorer/src/app/path/context.tsx
上下文组件加入 column,changeColumn 属性与方法
默认 column 的大小为 width / 280 向上取整
typescript
...
import { useViewport } from '@/components/viewport/context'
import { useMount, useSessionStorageState } from 'ahooks'
type PathContextType = {
...
column: number
changeColumn: React.Dispatch<React.SetStateAction<number>>
}
...
export const PathContextProvider: React.FC<React.ProviderProps<ReaddirListType>> = ({ value, children }) => {
const { width } = useViewport()
const def_column = Math.ceil(width / 280)
...
const [column, changeColumn] = useState<number>(def_column)
const [session_column, changeSessionColumn] = useSessionStorageState('card-column')
useEffect(() => {
changeSessionColumn(column)
}, [column])
useMount(() => {
changeColumn(Number(session_column))
})
return (
<PathContext.Provider
value={{
...
column: column || def_column,
changeColumn: changeColumn,
}}
>
{children}
</PathContext.Provider>
)
}
这里需要多一步 useEffect 与 useMount。
当初始化时,将 sessionStorage 的值写入 useState 内,使用 useState 控制实际的 column。当 column 改变时使用 useSessionStorageState 改变 sessionStorage 内的值。
如果直接使用 useSessionStorageState。会触发服务端与客户端渲染不一致的错误。导致客户端的渲染失败。
文件路径:explorer/src/components/viewport/context.tsx
创建视窗上下文,在视窗大小发生改变时将最新的 width 与 height 重新写入 Cookie
typescript
'use client'
import React, { createContext, useCallback, useContext } from 'react'
import { useCookieState, useMount } from 'ahooks'
export type ViewportType = {
width: number
height: number
}
export const getWindowSize = () => ({
width: window.innerWidth,
height: window.innerHeight,
})
export const viewportContext = createContext<ViewportType>(null!)
export const useViewport = () => {
return useContext(viewportContext)
}
export const ViewportProvider: React.FC<React.ProviderProps<ViewportType>> = ({ value, children }) => {
const [cookie_viewport, changeCookieViewport] = useCookieState('viewport-size')
const handleResize = useCallback(() => {
const { width, height } = getWindowSize()
changeCookieViewport(JSON.stringify({ width, height }))
}, [])
useMount(() => {
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
})
const viewport = cookie_viewport ? JSON.parse(cookie_viewport) : value
return <viewportContext.Provider value={viewport}>{children}</viewportContext.Provider>
}
文件路径:explorer/src/components/viewport/index.tsx
使用 Next.js 的 cookies 读取 cookie,写入视窗上下文中
javascript
import React from 'react'
import { ViewportProvider } from '@/components/viewport/context'
import { cookies } from 'next/headers'
const Viewport: React.FC<React.PropsWithChildren> = ({ children }) => {
const { width, height } = JSON.parse(
decodeURIComponent(cookies().get('viewport-size')?.value || JSON.stringify({ width: 0, height: 0 })),
)
return <ViewportProvider value={{ width, height }}>{children}</ViewportProvider>
}
export default Viewport
由于 cookies 方法与 'use client' 客户端组件冲突所以需要将写入上下文的组件提出到服务端组件内。
文件路径:explorer/src/app/layout.tsx
将视窗组件插入顶层的 layout 组件内。让所有的子组件都能通过上下文读取到视窗上下文数据
javascript
...
import Viewport from '@/components/viewport'
...
const RootLayout: React.FC<React.PropsWithChildren> = ({ children }) => (
<html lang="en">
<body className={inter.className}>
<AntdStyledComponentsRegistry>
<Viewport>{children}</Viewport>
</AntdStyledComponentsRegistry>
</body>
</html>
)
...
文件路径:explorer/src/app/path/[[...path]]/change-column.tsx
使用 antd 的 Slider 滑条与 InputNumber 控制 column 值。
内置一个重置按钮,点击将会使用 width / 280 向上取整重置 column 值
javascript
'use client'
import React from 'react'
import { Button, Flex, InputNumber, Slider, Space } from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import { usePathContext } from '@/app/path/context'
const SliderChangeColumn: React.FC = () => {
const { column, changeColumn } = usePathContext()
return (
<Flex>
<Space>
<Slider
max={14}
min={1}
style={{ width: '10em' }}
defaultValue={column}
value={column}
onChange={(value) => {
changeColumn(value)
}}
/>
<Space.Compact>
<InputNumber
controls={false}
style={{ width: '2em' }}
max={14}
min={1}
value={column}
onChange={(number) => number && changeColumn(number)}
/>
<Button icon={<ReloadOutlined />} onClick={() => changeColumn(Math.ceil(width / 280))} />
</Space.Compact>
</Space>
</Flex>
)
}
const ChangeColumn: React.FC = () => {
const { display_type } = usePathContext()
return display_type === 'card' && <SliderChangeColumn />
}
export default ChangeColumn
文件路径:explorer/src/app/path/[[...path]]/layout-footer.tsx
将控制 column 组件插入页尾功能组件内
javascript
...
import ChangeColumn from '@/app/path/[[...path]]/change-column'
const LayoutFooter: React.FC = () => {
return (
<Layout.Footer style={{ padding: '0px 20px' }}>
...
<Flex justify="flex-end" flex={1}>
<Space>
<ChangeColumn />
<DisplayType />
</Space>
</Flex>
...
</Layout.Footer>
)
}
...
文件路径:explorer/src/app/path/[[...path]]/card-display.tsx
将之前的根据宽度自适应改为视窗组件控制
ini
...
const CardDisplay: React.FC = () => {
const pathname = usePathname()
const { readdir, column } = usePathContext()
return (
<List
grid={{ gutter: 0, column: column }}
dataSource={readdir}
...
/>
)
}
...
数据格式化
- 文件大小按 "KiB、MiB、GiB ..." 呈现(KiB 中间带 i 以 1024 为除数、不带 i 的 KB 以 1000 为除数)参考链接-中文 wiki
- 时间按 "年/月/日 时:分:秒"
文件路径:explorer/src/components/bit.tsx
使用 while 循环除 1024 直到小于 1024 跳出循环。目前仅设置到 TiB 级别。
typescript
import React from 'react'
import { Space } from 'antd'
import { FileExclamationOutlined } from '@ant-design/icons'
const unit_type_list = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
const Bit: React.FC<{ title?: React.ReactNode; icon?: boolean; children: React.ReactNode }> = ({
title,
children,
icon = false,
}) => {
let size = Number(children)
let run = true
let unit_level = 0
while (run) {
if (size > 1024) {
size /= 1024
unit_level += 1
} else {
run = false
}
}
return (
<Space>
{icon ? <FileExclamationOutlined /> : title && <span>{title}</span>}
{size ? (
<span>
{size.toFixed(unit_level === 0 ? 0 : 2)} {unit_type_list[unit_level]}
</span>
) : (
'-'
)}
</Space>
)
}
export default Bit
文件路径:explorer/src/components/date-format.tsx
这里使用 toLocaleString 方法对时间进行格式化,并指定时区
javascript
import React from 'react'
import { Space } from 'antd'
const DateFormat: React.FC<React.PropsWithChildren & { title?: React.ReactNode }> = ({ title, children: time }) => {
return (
<Space>
{title && <span>{title}</span>}
<span>{time ? new Date(Number(time)).toLocaleString('zh-Hans-CN') : '-'}</span>
</Space>
)
}
export default DateFormat