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

相关推荐
y先森1 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐4 小时前
前端图像处理(一)
前端