从零开始-文件资源管理器-06-呈现数据格式化

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}
...
    />
  )
}
...

数据格式化

  1. 文件大小按 "KiB、MiB、GiB ..." 呈现(KiB 中间带 i 以 1024 为除数、不带 i 的 KB 以 1000 为除数)参考链接-中文 wiki
  2. 时间按 "年/月/日 时:分:秒"

文件路径: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

效果

git-repo

yangWs29/share-explorer

相关推荐
Bigger17 分钟前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang45334 分钟前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4531 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174461 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css
用户2136610035721 小时前
Vue2脚手架工程化与Axios集成
前端·vue.js
张元清1 小时前
React useDebounce Hook:给状态和回调做防抖(2026)
javascript·react.js
我不是外星人1 小时前
我把 Claude Code 搬到网页!自研高颜值 Web 交互工作台
前端·ai编程·claude
mixuecoding1 小时前
零成本搭建全球科技热点情报站:12 个平台,6 小时,0 元
前端
用户059540174461 小时前
用了3年Mock,才发现Redis记忆存储的测试一直漏掉了60%的边界场景
前端·css
石小石Orz1 小时前
AI具身交互:实现一个会说话的3D虚拟伴侣
前端·人工智能·后端