从零开始-文件资源管理器-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

相关推荐
道不尽世间的沧桑1 小时前
第17篇:网络请求与Axios集成
开发语言·前端·javascript
diemeng11192 小时前
AI前端开发技能变革时代:效率与创新的新范式
前端·人工智能
bin91535 小时前
DeepSeek 助力 Vue 开发:打造丝滑的复制到剪贴板(Copy to Clipboard)
前端·javascript·vue.js·ecmascript·deepseek
晴空万里藏片云6 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一6 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球6 小时前
el-button按钮的loading状态设置
前端·javascript
kidding7236 小时前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
Σίσυφος19008 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端8 小时前
0基础学前端-----CSS DAY13
前端·css
css趣多多9 小时前
案例自定义tabBar
前端