npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

smh-web-uikit

v1.1.2

Published

SMH Web UIKit — 基于 React 的云盘文件管理组件库

Readme

SMH Web UIKit 接入文档

简介

smh-web-uikit 是一个基于 React 的云盘文件管理 UIKit,提供完整的文件浏览、上传、下载、删除、重命名、移动、搜索、分享等功能。内置两种 UI 模式compact 简洁 / full 完整),接入方只需提供必要的配置参数和一个 accessToken 获取函数,即可快速嵌入云盘能力。

项目地址


安装

npm install smh-web-uikit

宿主项目 peerDependencies 要求

| 依赖 | 版本要求 | |------|---------| | react | ^18.0.0 | | react-dom | ^18.0.0 |

宿主项目需自行安装上述 peerDependencies。UIKit 内部不依赖任何第三方 UI 组件库,所有 UI 均为原生实现。


SDK 依赖说明

smh-web-uikit UIKit 的完整运行依赖腾讯云 SMH 官方提供的两个 SDK,分别用于前端和后端:

smh-js-sdk(前端 SDK)

| 项目 | 说明 | |------|------| | 使用方 | smh-web-uikit(UIKit 内部已集成) | | 版本要求 | ^1.0.5 | | 安装 | UIKit 内部已包含,接入方无需单独安装 | | 用途 | 前端直连 SMH API,完成文件浏览、上传、下载、删除等操作 |

核心能力:

  • 目录操作client.directory.listDirectoryByPage() / createDirectory() / deleteDirectory():文件列表分页查询、创建/删除目录
  • 文件操作client.file.deleteFile() / infoFile():文件删除、获取文件详情
  • 文件上传client.createUploadTask():支持分片上传、断点续传、秒传检测,提供进度回调和状态变更通知
  • 使用量统计client.usage.getUsage():获取当前空间的配额和已用容量

UIKit 内部使用示例(接入方无需关心):

import { SMHClient, TaskStatus } from 'smh-js-sdk'

const client = new SMHClient({
  basePath: 'https://api.tencentsmh.cn',
  libraryId: 'smhxxx-xxxxx',
  spaceId: 'spaceyyy',
  accessToken: 'your_access_token',
})

// 获取文件列表
const res = await client.directory.listDirectoryByPage({
  filePath: '',
  byPage: 1,
  page: 1,
  pageSize: 100,
})

// 上传文件(支持分片、断点续传)
const uploader = client.createUploadTask({
  filePath: 'docs/hello.txt',
  file: fileObject,
  conflictResolutionStrategy: 'overwrite',
  onStateChange: (checkpoint, state, error) => {
    if (state === TaskStatus.SUCCESS) console.log('上传成功')
  },
  onProgress: (info) => console.log(`进度: ${Math.floor(info.progress * 100)}%`),
})
uploader.start()

smh-node-sdk(后端 SDK)

| 项目 | 说明 | |------|------| | 使用方 | 接入方的后端服务 | | 版本要求 | ^1.0.0 | | 安装 | npm install smh-node-sdk | | 用途 | 后端调用 SMH 管理 API,签发 accessToken、管理空间和配额 |

核心能力:

  • Token 签发smh.token.createToken():签发 admin / space_admin 级别的 accessToken(需要 library_secret,仅后端持有)
  • 空间管理smh.space.listSpace() / createSpace() / deleteSpace():租户空间的增删查
  • 配额管理smh.quota.getQuota() / createQuota() / updateQuota():存储配额的查询与设置
  • 使用量统计smh.usage.getUsage():获取空间容量使用情况

后端使用示例:

const { SMHClient } = require('smh-node-sdk')

const smh = new SMHClient({ basePath: 'https://api.tencentsmh.cn' })
smh.setDefaultLibraryId('smhxxx-xxxxx')

// 签发 space_admin 级别的 token(供前端 UIKit 使用)
const tokenData = await smh.token.createToken({
  libraryId: 'smhxxx-xxxxx',
  librarySecret: 'your_secret',  // ⚠️ 仅后端持有
  spaceId: 'spaceyyy',
  grant: 'space_admin',
  period: 1800,  // 30分钟
})

// 创建租户空间
const space = await smh.space.createSpace({
  libraryId: 'smhxxx-xxxxx',
  accessToken: adminToken,
  userId: 'user123',
  createSpaceRequest: {
    allowPhoto: true,
    isPublicRead: true,
    allowVideo: true,
  },
})

// 查询空间使用量
const usage = await smh.usage.getUsage({
  libraryId: 'smhxxx-xxxxx',
  spaceIds: 'spaceyyy',
  accessToken: adminToken,
  userId: 'user123',
})

两个 SDK 的分工关系

┌──────────────────────────────────────────────────────────┐
│                      浏览器 (前端)                        │
│                                                          │
│  smh-web-uikit 内部使用 smh-js-sdk 直连 SMH API        │
│  ├── 文件浏览、上传、下载、删除、重命名、移动              │
│  └── 前端持有 accessToken(由后端签发,不含 secret)       │
│                                                          │
└──────────────────────────┬───────────────────────────────┘
                           │ 调用 getAccessToken()
                           │ 获取 accessToken
                           ▼
┌──────────────────────────────────────────────────────────┐
│                    Node.js 服务端 (后端)                   │
│                                                          │
│  接入方后端使用 smh-node-sdk 调用 SMH 管理 API            │
│  ├── 签发 accessToken(需要 library_secret,仅后端持有)   │
│  ├── 空间管理(创建/删除租户空间)                         │
│  ├── 配额管理(查询/修改存储上限)                         │
│  └── 使用量统计                                           │
│                                                          │
└──────────────────────────────────────────────────────────┘

⚠️ 安全原则library_secret 仅存在于服务端,前端通过后端签发的 accessToken 访问 SMH API,永远不会接触到密钥。UIKit 内部的 smh-js-sdk 只需要 accessToken 即可工作。


快速开始

import { SpaceDrive } from 'smh-web-uikit'
import 'smh-web-uikit/dist/style.css'

function App() {
  // accessToken 提供函数(详见下方说明)
  const getAccessToken = async () => {
    const res = await fetch('/api/your-backend/get-smh-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ spaceId: 'your-space-id' }),
    })
    const data = await res.json()
    return {
      accessToken: data.accessToken,
      expiresAt: data.expiresAt, // 支持:毫秒时间戳、秒级时间戳、ISO 8601 字符串(含纳秒精度)
    }
  }

  return (
    <SpaceDrive
      basePath="https://api.tencentsmh.cn"
      libraryId="smhxxx-xxxxx"
      spaceId="space-xxxxx"
      getAccessToken={getAccessToken}
      uiMode="full"           // 'full'(完整模式)或 'compact'(简洁模式)
    />
  )
}

Props 参数说明

<SpaceDrive /> 组件

| 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | basePath | string | ✅ | - | SMH API 基础路径,如 https://api.tencentsmh.cn | | libraryId | string | ✅ | - | SMH 媒体库 ID,由腾讯云 SMH 控制台创建后获取 | | spaceId | string | ✅ | - | SMH 空间 ID,标识一个独立的存储空间 | | getAccessToken | Function | ✅ | - | accessToken 提供函数,详见下方 | | enableSearch | boolean | ❌ | false | 是否启用搜索功能(顶部工具栏搜索框) | | enableRecycleBin | boolean | ❌ | false | 是否启用回收站功能(顶部工具栏回收站按钮 + 回收站面板) | | uiMode | 'compact' \| 'full' | ❌ | 'full' | UI 模式切换,详见下方说明 | | title | string | ❌ | '云盘' | 顶部标题文字,完整模式(full)下显示在左上角 |

uiMode — UI 模式切换

SpaceDrive 组件内置两种 UI 风格,通过 uiMode 参数切换:

| 模式 | 值 | 说明 | |------|-----|------| | 完整模式 | 'full'(默认) | 「流动蓝图」设计风格,功能齐全,适合独立云盘页面 | | 简洁模式 | 'compact' | 传统列表设计,轻量紧凑,适合嵌入 Drawer/侧栏等有限空间 |

功能对比:

| 特性 | compact 简洁 | full 完整 | |------|:-:|:-:| | 文件列表(表格) | ✅ | ✅ | | 网格视图切换 | — | ✅ | | 网格拖拽框选 + Shift 多选 | — | ✅ | | 底部浮动批量操作栏(下载/删除) | — | ✅ | | 排序控件(时间/名称/大小) | — | ✅ | | 分享功能(临时链接) | — | ✅ | | 视频/音频预览 | — | ✅ | | Markdown 渲染预览 | — | ✅ | | 批量上传(数量/大小限制) | — | ✅ | | 上传详情面板(队列/速度/耗时/进度) | — | ✅ | | 回收站(恢复/永久删除/清空) | — | ✅(需 enableRecycleBin) | | 可收起侧边栏 | ✅ | — | | 行内重命名(点击即编辑) | ✅ | 弹窗式 | | 配额进度条 | 侧边栏 | 顶部信息栏 | | 搜索结果按文件夹分组 | ✅ | 平铺列表 | | 上传速度/秒传状态展示 | ✅ | 上传详情面板 |

示例:

// 完整模式(默认)— 适合独立页面
<SpaceDrive
  basePath="https://api.tencentsmh.cn"
  libraryId="smhxxx-xxxxx"
  spaceId="space-xxxxx"
  getAccessToken={getAccessToken}
  uiMode="full"
  enableRecycleBin      // 启用回收站功能(默认关闭)
/>

// 简洁模式 — 适合嵌入 Drawer、侧栏
<SpaceDrive
  basePath="https://api.tencentsmh.cn"
  libraryId="smhxxx-xxxxx"
  spaceId="space-xxxxx"
  getAccessToken={getAccessToken}
  uiMode="compact"
/>

💡 两种 UI 组件通过 import() 懒加载,切换模式不会增加另一种模式的打包体积。

组件目录结构

两种 UI 模式的代码按目录隔离,互不依赖:

src/components/cloudFile/
├── compact/                          # compact 简洁模式
│   ├── file.jsx                      # 主组件(含内联预览、侧边栏、缩略图等)
│   └── file.css                      # 样式
│
└── full/                             # full 完整模式(流动蓝图)
    ├── FileSpace.jsx                 # 主组件(表格/网格视图、框选、批量操作等)
    ├── FileActions.jsx               # 文件操作下拉菜单
    ├── DocPreviewOverlay.jsx         # 文档预览全屏遮罩
    ├── ImagePreviewOverlay.jsx       # 图片/视频/音频预览遮罩
    ├── RecycleBin.jsx                # 回收站面板
    ├── UploadPanel.jsx               # 上传详情面板(队列/速度/耗时)
    ├── helpers.jsx                   # 工具函数(文件图标、格式化、路径处理等)
    └── FileSpace.css                 # 样式

维护时只需关注对应模式的子目录,修改一种模式不会影响另一种。


核心概念

参数获取方式

1. basePath — SMH API 基础路径

SMH 服务的 API 域名地址,由腾讯云 SMH 服务提供。格式如:

https://api.tencentsmh.cn

该值通常由后端配置提供,不同环境(测试/生产)可能不同。

2. libraryId — 媒体库 ID

腾讯云 SMH 控制台 创建媒体库后获得的唯一标识。格式如:

smhxxx-xxxxx

一个 libraryId 下可以包含多个 spaceId(空间)。

3. spaceId — 空间 ID

每个用户/租户对应一个独立的存储空间,由后端通过 SMH API 创建。格式如:

spaceyyyyyy

不同用户应使用不同的 spaceId,以实现数据隔离。

4. getAccessToken — Token 提供函数(重点)

这是接入方必须实现的核心函数。UIKit 内部会在以下时机调用此函数:

  • 初始化时:组件挂载后首次获取 token
  • token 即将过期时:UIKit 内部每 30 秒检查一次,当 token 距过期不足 5 分钟时自动调用续期
  • 请求发现 token 失效时:API 请求返回 token 无效错误时自动重试

函数签名:

type GetAccessToken = () => Promise<{
  accessToken: string           // SMH 访问令牌
  expiresAt: number | string    // 过期时间,支持以下格式:
                                //   - 毫秒级时间戳(如 1711468800000)
                                //   - 秒级时间戳(如 1711468800,自动 ×1000)
                                //   - ISO 8601 字符串(如 "2026-04-04T02:23:20.923Z")
                                //   - 带纳秒精度的字符串(如 "2026-04-04T02:23:20.923548596Z",自动截断)
                                //   - Date 对象
                                // SDK 内部会自动统一转为毫秒时间戳,无效值会使用默认有效期(2小时)兜底
}>

实现示例:

const getAccessToken = async () => {
  const res = await fetch('/api/your-backend/generate-space-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ spaceId: 'your-space-id' }),
  })

  if (!res.ok) {
    throw new Error('获取访问令牌失败')
  }

  const data = await res.json()
  return {
    accessToken: data.accessToken,  // SMH accessToken
    expiresAt: data.expiresAt,      // 毫秒级过期时间戳
  }
}

⚠️ 重要getAccessToken 函数的引用应保持稳定(使用 useCallback 或定义在组件外部),避免因引用变化导致组件重复初始化。


后端需要提供的接口

接入方的后端需要实现一个 生成 SMH Space Token 的接口,供前端 getAccessToken 函数调用。

接口职责

  1. 验证当前用户的身份和权限
  2. 调用 SMH API 生成指定 spaceIdaccessToken
  3. 返回 accessToken 和过期时间

SMH 生成 Token 的 API

GET {basePath}/api/v1/token?library_id={libraryId}&library_secret={librarySecret}&Grant=space_admin&Period=7200&spaceId={spaceId}

| 参数 | 说明 | |------|------------------------------| | library_id | 媒体库 ID | | library_secret | 媒体库密钥(仅后端持有,切勿暴露给前端) | | Grant | 授权级别,推荐 space_admin | | Period | token 有效期(秒),推荐 1800(30分钟) | | spaceId | 目标空间 ID |

后端接口返回格式(建议)

{
  "accessToken": "xxx-access-token-xxx",
  "expiresAt": 1711468800000,
  "libraryId": "smhxxx-xxxxx",
  "spaceId": "spaceyyyyyy",
  "basePath": "https://api.tencentsmh.cn"
}

其中 expiresAt毫秒级时间戳,计算方式:Date.now() + Period * 1000

Node.js 后端示例

app.post('/api/generate-space-token', async (req, res) => {
  const { spaceId } = req.body

  // 1. 验证用户权限(根据业务逻辑)
  // ...

  // 2. 调用 SMH API 获取 token
  const params = new URLSearchParams({
    library_id: process.env.SMH_LIBRARY_ID,
    library_secret: process.env.SMH_LIBRARY_SECRET,
    Grant: 'space_admin',
    Period: '1800', // 30分钟
    spaceId,
  })

  const response = await fetch(`${process.env.SMH_BASE_PATH}/api/v1/token?${params}`)
  const tokenData = await response.json()

  // 3. 计算过期时间并返回
  const expiresAt = Date.now() + 30 * 60 * 1000

  res.json({
    accessToken: tokenData.accessToken,
    expiresAt,
    libraryId: process.env.SMH_LIBRARY_ID,
    spaceId,
    basePath: process.env.SMH_BASE_PATH,
  })
})

完整接入示例

以下是一个在管理后台中嵌入云盘的完整示例:

import { useState, useCallback, useRef } from 'react'
import { Drawer, Button } from 'tdesign-react' // 示例使用 TDesign,宿主项目可使用任意 UI 库
import { SpaceDrive, clearConfig as clearDriveConfig } from 'smh-web-uikit'
import 'smh-web-uikit/dist/style.css'

export default function MyPage() {
  const [drawerVisible, setDrawerVisible] = useState(false)
  const [driveConfig, setDriveConfig] = useState(null)
  const spaceIdRef = useRef('')
  const cachedTokenRef = useRef(null)

  // accessToken 提供函数(引用稳定)
  const getAccessToken = useCallback(async () => {
    // 如果有缓存的 token(首次进入时预获取),直接返回
    if (cachedTokenRef.current) {
      const cached = cachedTokenRef.current
      cachedTokenRef.current = null
      return cached
    }

    const res = await fetch('/api/generate-space-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ spaceId: spaceIdRef.current }),
    })
    if (!res.ok) throw new Error('获取访问令牌失败')
    const data = await res.json()
    return {
      accessToken: data.accessToken,
      expiresAt: data.expiresAt,
    }
  }, [])

  // 打开云盘
  const handleOpenDrive = async (spaceId) => {
    spaceIdRef.current = spaceId

    // 预获取 token 和配置信息
    const res = await fetch('/api/generate-space-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ spaceId }),
    })
    const data = await res.json()

    // 缓存首次 token,避免 UIKit 初始化时重复请求
    cachedTokenRef.current = {
      accessToken: data.accessToken,
      expiresAt: data.expiresAt,
    }

    setDriveConfig({
      basePath: data.basePath,
      libraryId: data.libraryId,
      spaceId,
      getAccessToken,
    })
    setDrawerVisible(true)
  }

  // 关闭云盘
  const handleCloseDrive = () => {
    setDrawerVisible(false)
    setTimeout(() => {
      setDriveConfig(null)
      cachedTokenRef.current = null
      clearDriveConfig() // 清理 UIKit 内部状态
    }, 300)
  }

  return (
    <div>
      <Button onClick={() => handleOpenDrive('space-xxxxx')}>
        打开云盘
      </Button>

      <Drawer
        visible={drawerVisible}
        onClose={handleCloseDrive}
        size="85%"
        destroyOnClose
      >
        {driveConfig && (
          <SpaceDrive
            basePath={driveConfig.basePath}
            libraryId={driveConfig.libraryId}
            spaceId={driveConfig.spaceId}
            getAccessToken={driveConfig.getAccessToken}
            enableSearch={true}  // 启用搜索功能(默认关闭)
            uiMode="compact"     // 嵌入 Drawer 时推荐使用简洁模式
          />
        )}
      </Drawer>
    </div>
  )
}

导出的工具函数(高级用法)

除了 <SpaceDrive /> 组件,UIKit 还导出了以下工具函数,供高级场景使用。所有 API 均从 smh-web-uikit 统一导入,只需一行 import 即可:

import {
  // ---- 核心组件 ----
  SpaceDrive,                // 核心组件(自动根据 uiMode 切换 UI)
  FilePage,                  // 简洁模式文件管理组件(底层组件,高级用法)
  FileSpace,                 // 完整模式文件管理组件(流动蓝图,高级用法)

  // ---- 配置与 Token 管理 ----
  setSmhConfig,              // 手动设置 SMH 配置
  getAccessToken,            // 获取当前 accessToken
  getLibraryId,              // 获取当前 libraryId
  getSpaceId,                // 获取当前 spaceId
  getBasePath,               // 获取当前 basePath
  getTokenExpireInfo,        // 获取 token 过期时间信息
  isTokenExpiringSoon,       // 检查 token 是否即将过期
  isTokenExpired,            // 检查 token 是否已过期
  ensureValidToken,          // 确保 token 有效(自动续期)
  initToken,                 // 初始化 token
  clearConfig,               // 清除所有配置和 token(包括所有 space 的缓存)
  clearSpaceCache,           // 清除指定 space 的 token 缓存

  // ---- 文件操作 API ----
  getFileList,               // 获取文件列表(分页)
  uploadFile,                // 上传文件(分片/断点续传/秒传)
  delFile,                   // 删除文件
  delDirectory,              // 删除目录
  createDirectory,           // 创建文件夹
  moveFile,                  // 移动文件
  moveDirectory,             // 移动目录
  renameFile,                // 重命名文件
  renameDirectory,           // 重命名目录
  getFileInfo,               // 获取文件详情
  getPreview,                // 获取预览地址或内容
  getDocPreviewUrl,          // 获取文档在线预览 URL
  downloadFile,              // 下载文件(触发浏览器下载)
  getFilePreviewUrlOrContent,// 自动判断类型获取预览
  getSpaceUsage,             // 获取空间配额和使用量
  searchFiles,               // 搜索与过滤文件/目录
  copyFile,                  // 复制文件
  copyDirectory,             // 复制目录
  resetClient,               // 重置 SDK Client 实例(支持按 spaceId 重置)
  renewTokenViaSdk,          // 通过 SDK 续期 Token(降级方案)

  // ---- 回收站 API ----
  getRecycleList,            // 获取回收站列表(分页)
  recycleRestore,            // 恢复回收站项目
  recycleRestoreBatch,       // 批量恢复回收站项目
  recyclePurge,              // 永久删除回收站项目
  recyclePurgeBatch,         // 批量永久删除回收站项目
  recycleEmpty,              // 清空回收站
} from 'smh-web-uikit'

仅使用服务层(自定义 UI)

如果你希望完全自定义 UI 界面,只使用 smh-web-uikit 提供的服务层能力(Token 管理 + 文件操作 API),可以跳过 <SpaceDrive /> 组件,直接调用底层服务函数。

服务层不依赖任何 UI 组件(无 React、无 TDesign 依赖),可以在任意前端框架中使用。

接入步骤

1. 初始化配置 & Token

在应用启动时,调用 setSmhConfig 设置 SMH 连接参数,然后调用 initToken 完成首次 Token 获取:

import {
  setSmhConfig,
  initToken,
  clearConfig,
} from 'smh-web-uikit'

// 1. 设置配置
setSmhConfig({
  basePath: 'https://api.tencentsmh.cn',
  libraryId: 'smhxxx-xxxxx',
  spaceId: 'spaceyyyyyy',
  getAccessToken: async () => {
    const res = await fetch('/api/your-backend/generate-space-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ spaceId: 'spaceyyyyyy' }),
    })
    const data = await res.json()
    return { accessToken: data.accessToken, expiresAt: data.expiresAt }
  },
  // 可选:自定义错误提示(不传则静默,仅 throw Error)
  onError: ({ message }) => {
    // 替换为你自己的 UI 提示方式,如 antd message、Element Plus ElMessage 等
    console.error('[SMH Error]', message)
  },
})

// 2. 初始化 Token(必须在调用任何文件操作 API 之前完成)
await initToken()

// 3. 应用卸载时清理
// clearConfig()

2. 调用文件操作 API

初始化完成后,直接 import 服务层的文件操作函数即可使用。所有 API 内部会自动管理 Token 续期,调用方无需关心 Token 生命周期。

import {
  getFileList,
  uploadFile,
  delFile,
  delDirectory,
  createDirectory,
  moveFile,
  moveDirectory,
  renameFile,
  renameDirectory,
  getPreview,
  getDocPreviewUrl,
  downloadFile,
  getFileInfo,
  getFilePreviewUrlOrContent,
  getSpaceUsage,
  searchFiles,
  resetClient,
  // 回收站
  getRecycleList,
  recycleRestore,
  recycleRestoreBatch,
  recyclePurge,
  recyclePurgeBatch,
  recycleEmpty,
} from 'smh-web-uikit'

所有 API 均从 smh-web-uikit 统一导入。

服务层 API 一览

配置与 Token 管理(smh-web-uikit

| 函数 | 说明 | 调用时机 | |------|------|----------| | setSmhConfig(config) | 设置 SMH 连接配置 | 应用启动时,必须最先调用 | | initToken() | 初始化 Token(首次获取) | 应用启动时setSmhConfig 之后调用 | | ensureValidToken() | 确保 Token 有效,自动续期 | 一般无需手动调用,文件操作 API 内部已自动调用 | | isTokenExpiringSoon() | 检查 Token 是否即将过期(5 分钟内) | 需要自行实现定时续期逻辑时使用 | | isTokenExpired() | 检查 Token 是否已过期 | 需要判断 Token 状态时使用 | | getAccessToken() | 获取当前 accessToken 字符串 | 需要手动拼接请求时使用 | | getTokenExpireInfo() | 获取 Token 过期时间信息 | 需要展示 Token 状态时使用 | | getLibraryId() | 获取当前 libraryId | 需要手动拼接请求时使用 | | getSpaceId() | 获取当前 spaceId | 需要手动拼接请求时使用 | | getBasePath() | 获取当前 basePath | 需要手动拼接请求时使用 | | clearConfig() | 清除所有配置和 Token(包括所有 space 的缓存) | 应用卸载 / 切换用户时调用 | | clearSpaceCache(spaceId?) | 清除指定 space 的 Token 缓存,不传则清除当前 space | 需要强制刷新某个 space 的 Token 时使用 |

文件操作 API(smh-web-uikit

| 函数 | 说明 | 典型场景 | |------|------|----------| | getFileList(dirPath, { page, pageSize }) | 获取目录下的文件列表(分页) | 文件列表页、目录浏览 | | uploadFile(file, filePath, callbacks) | 上传文件(支持分片、断点续传、秒传) | 文件上传功能 | | delFile(filePath) | 删除单个文件 | 文件删除操作 | | delDirectory(dirPath) | 删除目录 | 目录删除操作 | | createDirectory(dirPath) | 创建文件夹 | 新建文件夹功能 | | moveFile(fromPath, toPath) | 移动文件到目标路径 | 文件移动 / 整理 | | moveDirectory(fromPath, toPath) | 移动目录到目标路径 | 目录移动 / 整理 | | renameFile(oldPath, newPath) | 重命名文件 | 文件重命名 | | renameDirectory(oldPath, newPath) | 重命名目录 | 目录重命名 | | getFileInfo(filePath) | 获取文件详情信息 | 文件详情页、属性面板 | | getPreview(filePath, isDoc?) | 获取文件预览地址或内容 | 图片/视频预览、文档内容读取 | | getDocPreviewUrl(filePath) | 获取文档在线预览 URL | iframe 嵌入预览 Office 文档 | | downloadFile(filePath, fileName?) | 下载文件(触发浏览器原生下载) | 文件下载功能 | | getFilePreviewUrlOrContent(file) | 根据文件类型自动获取预览链接或内容 | 通用预览(自动判断图片/视频/文档) | | getSpaceUsage() | 获取空间配额和已用容量 | 存储用量展示、容量告警 | | searchFiles(options) | 搜索与过滤文件/目录(支持关键字、类型、后缀、大小、时间等) | 文件搜索、高级过滤 | | resetClient(spaceId?) | 重置内部 SDK Client 实例,传 spaceId 则重置指定 space,不传则重置所有 | 配置变更后调用(切换空间时无需手动调用,内部自动缓存复用) | | renewTokenViaSdk() | 通过 SDK API 续期当前 Token(降级方案,不依赖 getAccessToken) | Token 续期降级场景 | | getRecycleList(options) | 获取回收站列表(分页),支持按删除时间、名称、大小排序 | 回收站页面展示 | | recycleRestore(recycledItemId) | 恢复单个回收站项目到原始路径(路径不存在回退到根目录) | 单个文件恢复 | | recycleRestoreBatch(ids) | 批量恢复回收站项目 | 批量恢复 | | recyclePurge(recycledItemId) | 永久删除单个回收站项目 | 单个永久删除 | | recyclePurgeBatch(ids) | 批量永久删除回收站项目 | 批量永久删除 | | recycleEmpty() | 清空回收站所有项目 | 一键清空回收站 |

完整示例:Vue 3 中使用服务层

以下示例展示在 Vue 3 项目中,仅使用服务层构建自定义文件管理界面:

<template>
  <div class="file-manager">
    <div class="toolbar">
      <button @click="handleCreateFolder">新建文件夹</button>
      <input type="file" @change="handleUpload" />
      <span>已用: {{ formatSize(usage.used) }} / {{ formatSize(usage.total) }}</span>
    </div>

    <ul class="file-list">
      <li v-for="file in files" :key="file.name" @click="handleOpen(file)">
        <span>{{ file.type === 'dir' ? '📁' : '📄' }} {{ file.name }}</span>
        <button @click.stop="handleDelete(file)">删除</button>
        <button @click.stop="handleDownload(file)" v-if="file.type !== 'dir'">下载</button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import {
  setSmhConfig, initToken, clearConfig,
  isTokenExpiringSoon, ensureValidToken,
  getFileList, uploadFile, delFile, delDirectory,
  createDirectory, downloadFile, getSpaceUsage,
} from 'smh-web-uikit'

const files = ref([])
const currentPath = ref('')
const usage = ref({ used: 0, total: 0 })

// 初始化
onMounted(async () => {
  setSmhConfig({
    basePath: 'https://api.tencentsmh.cn',
    libraryId: 'smhxxx-xxxxx',
    spaceId: 'spaceyyyyyy',
    getAccessToken: async () => {
      const res = await fetch('/api/generate-space-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ spaceId: 'spaceyyyyyy' }),
      })
      const data = await res.json()
      return { accessToken: data.accessToken, expiresAt: data.expiresAt }
    },
    onError: ({ message }) => ElMessage.error(message), // Element Plus 提示
  })

  await initToken()
  await loadFiles()

  const usageData = await getSpaceUsage()
  if (usageData) usage.value = usageData
})

// Token 定时续期
const timer = setInterval(() => {
  if (isTokenExpiringSoon()) ensureValidToken().catch(() => {})
}, 30 * 1000)

onUnmounted(() => {
  clearInterval(timer)
  clearConfig()
})

// 加载文件列表
async function loadFiles() {
  const data = await getFileList(currentPath.value, { page: 1, pageSize: 100 })
  files.value = data.contents || []
}

// 新建文件夹
async function handleCreateFolder() {
  const name = prompt('请输入文件夹名称')
  if (!name) return
  const path = currentPath.value ? `${currentPath.value}/${name}` : name
  await createDirectory(path)
  await loadFiles()
}

// 上传文件
async function handleUpload(e) {
  const file = e.target.files[0]
  if (!file) return
  const filePath = currentPath.value ? `${currentPath.value}/${file.name}` : file.name
  await uploadFile(file, filePath, {
    onProgressCallback: (percent) => console.log(`上传进度: ${percent}%`),
    onSuccessCallback: () => loadFiles(),
    onErrorCallback: () => console.error('上传失败'),
  })
}

// 删除
async function handleDelete(file) {
  if (file.type === 'dir') {
    await delDirectory(currentPath.value ? `${currentPath.value}/${file.name}` : file.name)
  } else {
    await delFile(currentPath.value ? `${currentPath.value}/${file.name}` : file.name)
  }
  await loadFiles()
}

// 下载
async function handleDownload(file) {
  const filePath = currentPath.value ? `${currentPath.value}/${file.name}` : file.name
  await downloadFile(filePath, file.name)
}

function formatSize(bytes) {
  if (!bytes) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  const i = Math.floor(Math.log(bytes) / Math.log(1024))
  return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i]
}
</script>

自行管理 Token 续期

当你不使用 <SpaceDrive /> 组件时,需要自行实现 Token 定时续期。推荐方式:

import { isTokenExpiringSoon, ensureValidToken } from 'smh-web-uikit'

// 每 30 秒检查一次,即将过期时自动续期
const timer = setInterval(() => {
  if (isTokenExpiringSoon()) {
    ensureValidToken().catch(err => console.error('Token 续期失败:', err))
  }
}, 30 * 1000)

// 应用卸载时清理
clearInterval(timer)

<SpaceDrive /> 组件内部已内置此逻辑,使用组件时无需手动处理。

服务层 API 详细说明

以下是每个文件操作 API 的详细参数签名、返回值和使用示例。

getFileList(dirPath, options) — 获取文件列表

function getFileList(
  dirPath?: string,                    // 目录路径,空字符串表示根目录
  options?: { page?: number, pageSize?: number }  // 分页参数,默认 page=1, pageSize=100
): Promise<{
  contents: Array<{                    // 文件/目录列表
    name: string                       // 文件名
    type: 'file' | 'dir'               // 类型
    size?: number                      // 文件大小(字节),目录无此字段
    creationTime?: string              // 创建时间
    modificationTime?: string          // 修改时间
    // ...其他 SMH 返回字段
  }>
  totalNum: number                     // 总条数
  hasMore: boolean                     // 是否有更多
}>

示例:

// 获取根目录第一页
const data = await getFileList('', { page: 1, pageSize: 50 })
console.log(data.contents)  // 文件列表
console.log(data.totalNum)  // 总数

// 获取子目录
const subData = await getFileList('docs/reports', { page: 1, pageSize: 100 })

uploadFile(file, filePath, callbacks) — 上传文件

支持分片上传、断点续传、秒传检测。如果目标目录不存在,会自动创建。

function uploadFile(
  file: File,                          // 浏览器 File 对象
  filePath: string,                    // 上传目标路径,如 'docs/report.pdf'
  callbacks?: {
    onProgressCallback?: (percent: number, speed: number) => void  // 进度回调,percent 为 0-100,speed 单位 B/s
    onSuccessCallback?: (result: { id: string, name: string }) => void  // 上传成功回调
    onErrorCallback?: (error: any) => void                        // 上传失败回调
    onStateChangeCallback?: (state: string) => void               // 状态变更回调(如 'computing_hash' 秒传校验中)
  }
): Promise<object>                     // 返回上传 checkpoint 信息

示例:

// 基础上传
await uploadFile(fileObject, 'docs/hello.pdf')

// 带进度回调的上传
await uploadFile(fileObject, 'images/photo.jpg', {
  onProgressCallback: (percent, speed) => {
    console.log(`进度: ${percent}%, 速度: ${(speed / 1024).toFixed(1)} KB/s`)
  },
  onSuccessCallback: ({ id, name }) => {
    console.log(`上传成功: ${name}`)
  },
  onErrorCallback: (err) => {
    console.error('上传失败:', err)
  },
  onStateChangeCallback: (state) => {
    if (state === 'computing_hash') console.log('正在校验秒传...')
  },
})

delFile(filePath) — 删除文件

function delFile(filePath: string | string[]): Promise<boolean>  // 返回是否删除成功

示例:

const success = await delFile('docs/old-report.pdf')
// 也支持数组形式的路径
const success2 = await delFile(['docs', 'old-report.pdf'])

delDirectory(dirPath) — 删除目录

function delDirectory(dirPath: string | string[]): Promise<boolean>  // 返回是否删除成功

示例:

const success = await delDirectory('docs/archive')

createDirectory(dirPath) — 创建文件夹

如果同名目录已存在,会自动重命名(追加后缀)。

function createDirectory(dirPath: string | string[]): Promise<object>  // 返回创建结果

示例:

await createDirectory('docs/new-folder')

moveFile(fromPath, toPath) — 移动文件

将文件从一个路径移动到另一个路径。如果目标路径已存在同名文件,会自动重命名。

function moveFile(
  fromPath: string | string[],         // 原文件路径
  toPath: string | string[]            // 目标文件路径
): Promise<object>

示例:

// 将文件从根目录移动到 docs 目录
await moveFile('report.pdf', 'docs/report.pdf')

// 数组形式
await moveFile(['uploads', 'photo.jpg'], ['images', 'photo.jpg'])

moveDirectory(fromPath, toPath) — 移动目录

将目录从一个路径移动到另一个路径。如果目标路径已存在同名目录,会自动重命名。

function moveDirectory(
  fromPath: string | string[],         // 原目录路径
  toPath: string | string[]            // 目标目录路径
): Promise<object>

示例:

await moveDirectory('temp/drafts', 'docs/drafts')

renameFile(oldPath, newPath) — 重命名文件

通过移动操作实现重命名。如果目标路径已存在同名文件,会提示冲突(conflictResolutionStrategy: 'ask')。

function renameFile(
  oldPath: string | string[],          // 原文件路径
  newPath: string | string[]           // 新文件路径
): Promise<object>

示例:

await renameFile('docs/old-name.pdf', 'docs/new-name.pdf')

renameDirectory(oldPath, newPath) — 重命名目录

function renameDirectory(
  oldPath: string | string[],          // 原目录路径
  newPath: string | string[]           // 新目录路径
): Promise<object>

示例:

await renameDirectory('docs/old-folder', 'docs/new-folder')

getFileInfo(filePath) — 获取文件详情

function getFileInfo(filePath: string | string[]): Promise<{
  name: string
  type: 'file' | 'dir'
  size?: number
  creationTime?: string
  modificationTime?: string
  // ...其他 SMH 返回字段
}>

示例:

const info = await getFileInfo('docs/report.pdf')
console.log(`文件大小: ${info.size} 字节`)

getPreview(filePath, isDoc?) — 获取预览地址或内容

根据文件类型返回不同结果:

  • 图片/视频:返回带 accessToken 的直连 URL(字符串)
  • 文档isDoc=true):获取文件的 cosUrl 并返回文本内容
function getPreview(
  filePath: string | string[],         // 文件路径
  isDoc?: boolean                      // 是否为文档类型,默认 false
): Promise<string | object>            // 图片/视频返回 URL 字符串,文档返回文本内容

示例:

// 图片预览 URL
const imgUrl = await getPreview('images/photo.jpg')
img.src = imgUrl

// 文档内容
const textContent = await getPreview('docs/readme.md', true)
console.log(textContent)

getDocPreviewUrl(filePath) — 获取文档在线预览 URL

返回文档的 cosUrl,可用于 iframe 嵌入在线预览 Office 文档。

function getDocPreviewUrl(filePath: string | string[]): Promise<string>  // cosUrl

示例:

const previewUrl = await getDocPreviewUrl('docs/report.docx')
// 在 iframe 中预览
iframe.src = previewUrl

downloadFile(filePath, fileName?) — 下载文件

通过 SDK 的 downloadByUrl 方法获取文件的 cosUrl,并通过 <a> 标签触发浏览器原生下载。

function downloadFile(
  filePath: string | string[],         // 文件路径
  fileName?: string                    // 自定义保存文件名,不传则使用远端文件名
): Promise<void>

示例:

// 使用原始文件名下载
await downloadFile('docs/report.pdf')

// 自定义下载文件名
await downloadFile('docs/report.pdf', '2024年度报告.pdf')

getFilePreviewUrlOrContent(file) — 自动获取预览

根据文件扩展名自动判断类型,调用对应的预览方法:

  • 图片/视频(jpg、png、gif、mp4 等)→ 返回预览 URL
  • 文档/文本(json、txt、md、log、docx)→ 返回文本内容
  • 其他类型 → 返回预览 URL
function getFilePreviewUrlOrContent(file: {
  name: string                         // 文件名(用于判断扩展名)
  path?: string[]                      // 文件路径数组
}): Promise<string>

示例:

const result = await getFilePreviewUrlOrContent({
  name: 'photo.jpg',
  path: ['images', 'photo.jpg'],
})

getSpaceUsage() — 获取空间使用量

function getSpaceUsage(): Promise<{
  used: number   // 已使用容量(字节)
  total: number  // 总配额(字节)
} | null>        // 获取失败返回 null

示例:

const usage = await getSpaceUsage()
if (usage) {
  const usedGB = (usage.used / 1024 / 1024 / 1024).toFixed(2)
  const totalGB = (usage.total / 1024 / 1024 / 1024).toFixed(2)
  console.log(`已用 ${usedGB} GB / 共 ${totalGB} GB`)
}

searchFiles(options) — 搜索与过滤文件/目录

基于 smh-js-sdkclient.search.searchFs 接口,支持多维度搜索与过滤。

注意:该接口 QPS 上限为 10,不适用于高频操作场景(如首页列表查询)。

function searchFiles(options?: {
  keywords?: string[]              // 搜索关键字数组,如 ['身份证', '中国'](多个为或关系)
  extname?: string[]              // 文件后缀过滤(带点号),如 ['.png', '.jpg']
  excludeExtName?: string[]       // 排除的文件后缀(带点号),如 ['.xls', '.xlsx']
  fileType?: Array<'file' | 'dir' | 'symlink'>  // 文件类型数组
  minFileSize?: number            // 最小文件大小(Byte)
  maxFileSize?: number            // 最大文件大小(Byte)
  modificationTimeStart?: string  // 修改时间起始(ISO 8601)
  modificationTimeEnd?: string    // 修改时间截止(ISO 8601)
  orderBy?: 'name' | 'modificationTime' | 'size' | 'creationTime' | 'localCreationTime' | 'localModificationTime'  // 排序字段
  orderByType?: 'asc' | 'desc'   // 排序方式
  labels?: string[]               // 文件标签过滤
  categories?: string[]           // 文件分类过滤
  limit?: number                  // 每页数量,默认 50,范围 [1, 100]
  marker?: string                 // 分页标识(用于翻页)
  dirPath?: string                // 目录路径过滤,仅返回该目录下的文件,如 'folderA/subFolder'
}): Promise<{
  contents: Array<{               // 搜索结果列表
    type: string                  // 条目类型:dir / file / image / video / symlink
    inode: string                 // 文件目录 ID
    name: string                  // 文件名或目录名
    path: string[]                // 文件路径数组,如 ['folderA', '1.jpg'](通过 inode 接口自动补全)
    size: string                  // 文件大小(字符串格式,单位 Byte)
    creationTime: string          // 创建时间(ISO 8601)
    modificationTime: string      // 修改时间(ISO 8601)
    contentType: string           // 媒体类型
    // ... 更多字段见 SDK 文档
  }>
  nextMarker: string | undefined  // 下一页标识,为空表示已到最后一页
}>

说明:搜索接口原始返回结果中不包含文件夹路径信息,searchFiles 内部会自动通过 GET /api/v1/inode/{LibraryId}/{SpaceId}/{Inode} 接口为每个结果补全 path 数组,因此调用方无需额外处理。

目录过滤:传入 dirPath 后,会在补全 path 之后进行前缀匹配过滤,仅保留该目录下的文件。注意这是客户端过滤,不影响 API 请求本身,因此返回的 contents 数量可能少于 limit

示例 — 关键字搜索:

// 搜索包含 "报告" 的文件
const result = await searchFiles({ keywords: ['报告'] })
console.log(`找到 ${result.contents.length} 个结果`)
result.contents.forEach(item => {
  const folder = item.path.slice(0, -1).join('/') || '根目录'
  console.log(`${item.type === 'dir' ? '📁' : '📄'} ${item.name}  (位于: ${folder})`)
})

示例 — 多条件过滤:

// 搜索最近 7 天内修改的 PDF 和 DOCX 文件,按修改时间倒序
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString()
const result = await searchFiles({
  extname: ['.pdf', '.docx'],
  fileType: ['file'],
  modificationTimeStart: sevenDaysAgo,
  orderBy: 'modificationTime',
  orderByType: 'desc',
  limit: 20,
})

示例 — 文件大小过滤:

// 搜索大于 10MB 的文件
const result = await searchFiles({
  fileType: ['file'],
  minFileSize: 10 * 1024 * 1024,
  orderBy: 'size',
  orderByType: 'desc',
})

示例 — 目录过滤:

// 仅搜索 'documents/reports' 目录下包含 "季度" 的文件
const result = await searchFiles({
  keywords: ['季度'],
  dirPath: 'documents/reports',
})
console.log(`在 documents/reports 下找到 ${result.contents.length} 个结果`)

示例 — 分页加载:

// 首次搜索
let result = await searchFiles({ keywords: ['图片'], limit: 20 })
let allResults = [...result.contents]

// 翻页加载更多
while (result.nextMarker) {
  result = await searchFiles({
    keywords: ['图片'],
    limit: 20,
    marker: result.nextMarker,
  })
  allResults.push(...result.contents)
}
console.log(`共找到 ${allResults.length} 个结果`)

resetClient(spaceId?) — 重置 SDK Client

重置内部的 SMHClient 实例。传入 spaceId 则只重置指定 space 的 client,不传则重置所有 space 的 client。

注意:切换空间时无需手动调用 resetClient(),内部已自动按 spaceId 缓存和复用 client。仅在 basePathlibraryId 等全局配置变更时才需要调用。

function resetClient(spaceId?: string): void

示例:

// 重置指定 space 的 client
resetClient('space-xxxxx')

// 重置所有 space 的 client(全局配置变更时)
resetClient()

renewTokenViaSdk() — 通过 SDK 续期 Token

通过 SMH API POST /api/v1/token/{LibraryId}/{AccessToken} 续期当前 Token。这是一个降级方案,当未提供 getAccessToken 回调时,ensureValidToken 内部会自动尝试调用此方法续期。

⚠️ 该方法依赖当前 Token 尚未完全过期,且续期时长固定为创建 Token 时指定的 Period。推荐优先使用 getAccessToken 回调方式续期。

function renewTokenViaSdk(): Promise<{
  accessToken: string  // 续期后的新 Token
  expiresAt: number    // 新的过期时间(毫秒级时间戳)
}>

示例:

import { renewTokenViaSdk } from 'smh-web-uikit'

try {
  const { accessToken, expiresAt } = await renewTokenViaSdk()
  console.log('续期成功,新 Token 过期时间:', new Date(expiresAt))
} catch (err) {
  console.error('续期失败:', err.message)
}

getRecycleList(options) — 获取回收站列表

function getRecycleList(options?: {
  page?: number           // 分页码,默认 1
  pageSize?: number       // 分页大小,默认 20
  orderBy?: string        // 排序字段:'name' | 'modificationTime' | 'size' | 'removalTime' | 'remainingTime'
  orderByType?: string    // 排序方式:'asc' | 'desc',默认 'desc'
}): Promise<{
  contents: Array<{
    recycledItemId: number       // 回收站项目 ID(用于恢复/删除操作)
    name: string                 // 文件名
    type: string                 // 类型:'file' | 'dir'
    size?: number                // 文件大小(字节)
    path?: string[]              // 原始路径数组
    removalTime?: string         // 删除时间(ISO 8601)
    remainingTime?: number       // 剩余保留时间(秒)
  }>
  totalNum: number               // 总条数
}>

示例:

// 获取回收站列表,按删除时间倒序
const data = await getRecycleList({ page: 1, pageSize: 20 })
console.log(`回收站共 ${data.totalNum} 项`)
data.contents.forEach(item => {
  const days = Math.ceil(item.remainingTime / 86400)
  console.log(`${item.name} — 剩余 ${days} 天`)
})

recycleRestore(recycledItemId) — 恢复回收站项目

恢复到原始路径,路径不存在时回退到根目录。冲突时自动重命名。

function recycleRestore(recycledItemId: number): Promise<object>

示例:

await recycleRestore(12345)  // 恢复 ID 为 12345 的回收站项目

recycleRestoreBatch(ids) — 批量恢复回收站项目

function recycleRestoreBatch(ids: number[]): Promise<object>

示例:

await recycleRestoreBatch([12345, 12346, 12347])

recyclePurge(recycledItemId) — 永久删除回收站项目

永久删除后不可恢复

function recyclePurge(recycledItemId: number): Promise<void>

示例:

await recyclePurge(12345)

recyclePurgeBatch(ids) — 批量永久删除回收站项目

function recyclePurgeBatch(ids: number[]): Promise<void>

示例:

await recyclePurgeBatch([12345, 12346, 12347])

recycleEmpty() — 清空回收站

清空回收站内所有项目,操作不可恢复。清空操作异步执行,调用后回收站立即不可见。

function recycleEmpty(): Promise<void>

示例:

await recycleEmpty()

两种接入方式对比

| | 使用 <SpaceDrive /> 组件 | 仅使用服务层 | |---|---|---| | 适用场景 | 快速接入,使用现成 UI(可选简洁/完整两种风格) | 需要完全自定义 UI 风格 | | UI 框架 | 必须使用 React | 任意框架(Vue、Angular、Svelte 等) | | UI 模式 | 通过 uiMode 切换:'compact'(简洁)/ 'full'(完整) | 自行实现 | | Token 续期 | 自动(组件内置) | 需自行实现定时检查 | | 错误提示 | 自动(内置 Toast) | 通过 onError 回调自定义 | | 需要引入样式 | 是(style.css) | 否 | | 代码量 | 最少,几行即可 | 较多,需自行构建 UI |


Token 自动续期机制

UIKit 内部实现了完整的 token 自动续期机制,接入方无需手动管理 token 生命周期

时间线:
0h                              1h55m                    2h
|------- 正常使用 token ---------|--- 检测到即将过期 ---|--- 旧 token 过期 ---|
                                  ↓
                          自动调用 getAccessToken()
                          获取新 token,无缝续期
  • 检查频率:每 30 秒检查一次 token 状态
  • 提前续期:在 token 过期前 5 分钟触发续期
  • 并发保护:多个请求同时发现 token 过期时,只会触发一次续期(按 spaceId 隔离)
  • 请求级保护:每次 API 请求前都会检查 token 有效性
  • 缓存池机制:每个 space 的 token 独立缓存,切换 space 时自动保存和恢复,缓存中过期的 token 会自动触发续期
  • 过期安全:如果 getAccessToken 返回的 expiresAt 已过期或无法解析,自动使用默认有效期(2小时)兜底
  • 时间格式兼容expiresAt 支持毫秒/秒级时间戳、ISO 8601 字符串(含纳秒精度)、Date 对象,SDK 内部自动转换

切换空间

当需要切换到不同的 spaceId 时,UIKit 内部采用 Token 缓存池 + Client 缓存池 机制自动处理:

  1. 检测到 spaceId 变化后,先将当前 space 的 token 保存到缓存池
  2. 尝试从缓存池恢复目标 space 的 token 和 client(如果之前访问过)
  3. 如果缓存中没有目标 space 的 token,或缓存的 token 已过期,自动调用 getAccessToken 获取新 token
  4. 无需手动调用 resetClient(),client 按 spaceId 自动缓存和复用

性能优势:频繁在多个 space 之间切换时,已访问过的 space 可以直接从缓存恢复,无需重新获取 token 和重建 client,大幅减少网络请求。

接入方只需更新传入的 spaceId prop 即可:

<SpaceDrive
  basePath={basePath}
  libraryId={libraryId}
  spaceId={currentSpaceId}  // 切换此值即可,内部自动缓存和恢复
  getAccessToken={getAccessToken}
/>

💡 缓存安全性:即使缓存中的 token 已过期,ensureValidToken() 会在每次 API 请求前自动检测并触发续期,不会导致请求失败。

手动清除缓存

如果需要强制刷新某个 space 的 token(例如权限变更后),可以使用 clearSpaceCache

import { clearSpaceCache } from 'smh-web-uikit'

// 清除指定 space 的缓存
clearSpaceCache('space-xxxxx')

// 清除当前 space 的缓存
clearSpaceCache()

样式引入

UIKit 的样式需要在宿主项目中手动引入:

// 在入口文件中引入
import 'smh-web-uikit/dist/style.css'

UIKit 内部所有 UI 均为原生实现,不依赖第三方 UI 组件库。


注意事项

  1. librarySecret 绝不能暴露给前端,token 的生成必须在后端完成
  2. getAccessToken 函数引用需保持稳定,建议使用 useCallback 包裹或定义在组件外部,避免不必要的重新初始化
  3. 组件销毁时调用 clearConfig(),清理内部状态(包括所有 space 的 token 和 client 缓存),防止内存泄漏和状态残留
  4. expiresAt 支持多种时间格式:毫秒时间戳、秒级时间戳、ISO 8601 字符串(含纳秒精度)、Date 对象,SDK 内部会自动统一转换
  5. 组件默认占满父容器的 100% 高度,请确保父容器有合适的高度约束
  6. 切换空间无需手动 resetClient(),内部按 spaceId 自动缓存 token 和 client,切换时自动恢复。如需强制刷新某个 space 的 token,使用 clearSpaceCache(spaceId)

相关 SDK 文档

| SDK | npm 包名 | 说明 | |-----|---------|------| | 前端 SDK | smh-js-sdk | 前端直连 SMH API,UIKit 内部已集成 | | 后端 SDK | smh-node-sdk | 后端管理 API,接入方需在后端安装使用 |

更多 SMH 服务信息请参考 腾讯云智能媒体托管文档