@kaa413/shared

v0.0.1

Published

Shared utilities, components, hooks, and tools for React + Ant Design + UmiJS projects

Readme

@aspect/shared

React + Ant Design + UmiJS 项目共享工具库。提供工具函数、加密、请求层、组件、Hooks、样式、配置、类型和常量,统一多项目技术标准。

目录


功能概览

| 模块 | 导入路径 | 功能 | |------|---------|------| | Utils | @aspect/shared/utils | 格式化、验证、存储(带 TTL)、日志、剪贴板、HTML 过滤、权限、认证管理、树操作、地区数据、水印 | | Crypto | @aspect/shared/crypto | AES-256-CBC + HMAC-SHA256 端到端加密(Web Crypto API 原生实现) | | Request | @aspect/shared/request | UmiJS 请求配置工厂(认证拦截、加密拦截、错误处理)、CRUD Service 工厂、文件服务工厂 | | Components | @aspect/shared/components | StatusTag、ErrorBoundary、ActionColumn、RecycleBinModal、StatCard、DynamicFormModal、PageContainer 等 14 个 Ant Design 组件 | | Hooks | @aspect/shared/hooks | useTableData、useCrudList、useDetailPage、useFormModal、useFormSubmit、useModal、useFileUpload、useDownload 等 16 个 React Hooks | | Styles | @aspect/shared/styles | antd-style 样式工厂(通用样式、页面样式、响应式样式) | | Config | @aspect/shared/config | UmiJS 基础配置工厂、App Runtime 工厂(getInitialState + layout)、Biome/TSConfig 预设 | | Types | @aspect/shared/types | ApiResponse、PaginatedData、BaseUser、AuthToken、StatusMap 等通用类型定义 | | Constants | @aspect/shared/constants | BREAKPOINTS、TABLE_DEFAULTS、DEBOUNCE_DELAY、ErrorShowType 等 UI 常量 |


目录结构

keel-web-shared/
├── src/
│   ├── components/          # React + Ant Design 组件
│   │   ├── ActionColumn/    #   操作列(查看/编辑/删除/切换状态)
│   │   ├── DataTableCard/   #   数据表格卡片
│   │   ├── DetailPageLayout/#   详情页布局
│   │   ├── DynamicFormModal/#   动态表单弹窗
│   │   ├── ErrorBoundary/   #   错误边界
│   │   ├── GlobalAntdHolder/#   Antd 静态方法持有器
│   │   ├── PageContainer/   #   页面容器(带 Tab)
│   │   ├── RecycleBinModal/ #   回收站弹窗
│   │   ├── RouteGuard/      #   认证/访客路由守卫
│   │   ├── SearchFilterBar/ #   搜索筛选栏
│   │   ├── StatCard/        #   统计卡片 + 网格
│   │   ├── StatusShow/      #   状态展示(空/加载/错误)
│   │   ├── StatusTag/       #   状态标签
│   │   ├── UserAvatar/      #   用户头像
│   │   └── index.ts
│   ├── config/              # 配置工厂
│   │   ├── app-runtime.ts   #   App Runtime(getInitialState + layout)
│   │   ├── umi-base.ts      #   UmiJS 基础配置
│   │   ├── biome.json       #   Biome 代码检查预设
│   │   └── tsconfig.base.json  # TypeScript 基础配置
│   ├── constants/           # 统一常量
│   │   └── ui.ts            #   BREAKPOINTS, TABLE_DEFAULTS, ErrorShowType 等
│   ├── crypto/              # 加密模块
│   │   ├── factory.ts       #   createEncryption 工厂
│   │   ├── aes-cbc-hmac.ts  #   AES-CBC + HMAC 策略(环境自动检测)
│   │   ├── crypto-native.ts #   Web Crypto API 原生实现
│   │   ├── crypto-provider-factory.ts  # 加密器工厂(环境自动检测)
│   │   ├── hash.ts          #   SHA-256 哈希
│   │   ├── path-matcher.ts  #   路径白名单匹配
│   │   └── replay-guard.ts  #   重放防护
│   ├── hooks/               # React Hooks
│   │   ├── useCrudList.ts   #   CRUD 列表管理
│   │   ├── useDetailPage.ts #   详情页数据加载
│   │   ├── useDetailPageMode.ts # 详情页模式(查看/新建/编辑)
│   │   ├── useDownload.ts   #   文件下载
│   │   ├── useFileSelector.ts  # 文件选择
│   │   ├── useFileUpload.ts #   文件上传
│   │   ├── useFormModal.ts  #   表单弹窗
│   │   ├── useFormSubmit.ts #   表单提交
│   │   ├── useIntlText.ts   #   国际化文本
│   │   ├── useListData.ts   #   列表数据管理
│   │   ├── useModal.ts      #   弹窗状态
│   │   ├── useResponsive.ts #   响应式断点
│   │   ├── useTableConfig.tsx  # 表格配置(列/操作)
│   │   ├── useTableData.ts  #   ProTable 数据请求
│   │   ├── useUserPreference.ts # 用户偏好(主题/语言)
│   │   ├── shared-request.ts   # ProTable request 适配
│   │   └── index.ts
│   ├── request/             # 请求层
│   │   ├── create-request-config.ts  # 一站式请求配置工厂
│   │   ├── crud-service.ts  #   CRUD Service 工厂
│   │   ├── file-service.ts  #   文件服务工厂
│   │   ├── interceptors.ts  #   请求/响应拦截器
│   │   ├── error-handler.ts #   错误处理器
│   │   ├── constants.ts     #   HTTP_STATUS, API_SUCCESS_CODE
│   │   └── types.ts         #   请求层类型定义
│   ├── styles/              # antd-style 样式工厂
│   │   ├── common.ts        #   通用布局样式
│   │   ├── common-layout.ts #   布局辅助样式
│   │   ├── common-typography.ts # 排版样式
│   │   ├── common-utilities.ts  # 工具样式
│   │   ├── page.ts          #   页面级样式
│   │   └── responsive.ts    #   响应式样式
│   ├── types/               # 通用类型定义
│   │   ├── api.ts           #   ApiResponse, PaginatedData, ProTableResponse
│   │   ├── auth.ts          #   BaseUser, AuthToken, LoginParams
│   │   └── common.ts        #   StatusMapItem, StatusMap, SelectOption, TreeNode
│   ├── utils/               # 工具函数
│   │   ├── api-error.ts     #   错误信息提取
│   │   ├── api-helpers.ts   #   isApiSuccess, executeApiAction
│   │   ├── auth.ts          #   createAuthManager, emitLoginRequired
│   │   ├── clipboard.ts     #   copyToClipboard
│   │   ├── formatters.ts    #   日期/数字/货币/文件大小格式化
│   │   ├── logger.ts        #   createLogger 日志工厂
│   │   ├── navigation.ts    #   openInNewTab
│   │   ├── performance.ts   #   性能监控工具
│   │   ├── permission.ts    #   权限检查
│   │   ├── region.ts        #   地区数据(省市区)
│   │   ├── sanitize.ts      #   HTML 过滤(基于 DOMPurify)
│   │   ├── storage.ts       #   localStorage 安全封装(带 TTL)
│   │   ├── theme.ts         #   主题工具
│   │   ├── tree.ts          #   树结构转换
│   │   ├── url.ts           #   URL 工具
│   │   ├── validators.ts    #   手机号/邮箱/文件类型验证
│   │   ├── value-enum.ts    #   createValueEnum 工厂
│   │   └── watermark.ts     #   用户水印
│   └── index.ts             # 主入口(聚合所有模块)
├── package.json
├── tsup.config.ts
└── tsconfig.json

新项目接入

1. 安装依赖

# 本地链接(monorepo 开发)
pnpm add @aspect/shared@link:../../keel-web-shared

# 或从 npm 安装
pnpm add @aspect/shared

2. TypeScript 路径映射

tsconfig.json 中配置路径别名(本地链接时需要):

{
  "extends": "@aspect/shared/config/tsconfig.base.json",
  "compilerOptions": {
    "paths": {
      "@aspect/shared": ["../../keel-web-shared/src/index"],
      "@aspect/shared/*": ["../../keel-web-shared/src/*/index"]
    }
  }
}

3. Biome 代码检查

biome.json 中继承预设:

{
  "extends": ["@aspect/shared/config/biome.json"]
}

4. UmiJS 配置

// config/config.ts
import { createUmiBaseConfig } from '@aspect/shared/config';
import routes from './routes';

export default createUmiBaseConfig({
  routes,
  locale: { default: 'zh-CN', antd: true },
  title: '我的应用',
  proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } },
});

5. 请求配置

// src/requestErrorConfig.ts
import { createRequestConfig } from '@aspect/shared/request';
import { createEncryption } from '@aspect/shared/crypto';
import { getMessageApi } from '@aspect/shared/components';
import { history } from '@umijs/max';

export const request = createRequestConfig({
  timeout: 10000,
  auth: {
    tokenKey: 'my_auth_token',
    headerName: 'Authorization',
    headerPrefix: 'Bearer ',
  },
  encryption: {
    enabled: true,
    strategy: createEncryption({
      algorithm: 'aes-cbc-hmac',
      keys: {
        requestKeyHex: import.meta.env.VITE_AES_REQUEST_KEY_HEX,
        responseKeyHex: import.meta.env.VITE_AES_RESPONSE_KEY_HEX,
      },
    }),
    methods: ['POST', 'PUT'],
    excludePaths: ['/api/upload', '/api/download'],
  },
  errorConfig: {
    onUnauthorized: () => history.push('/login'),
    onError: (msg) => getMessageApi()?.error(msg),
  },
});

6. App Runtime

// src/app.tsx
import { createAppRuntime, createPageChangeGuard } from '@aspect/shared/config';
import { GlobalAntdHolder } from '@aspect/shared/components';
import { initSanitizer, initFormatters, createUserWatermark } from '@aspect/shared/utils';
import { history } from '@umijs/max';

// 初始化工具
initSanitizer();
initFormatters({ locale: 'zh-CN' });

const appRuntime = createAppRuntime({
  auth: {
    fetchUserInfo: async () => {
      const res = await fetchCurrentUser();
      return isApiSuccess(res) ? res.data : undefined;
    },
    loginPath: '/login',
    guestPaths: ['/login', '/register'],
    isAuthenticated: () => !!localStorage.getItem('my_auth_token'),
  },
  layout: {
    title: '我的应用',
    logo: '/logo.svg',
    showAvatarName: true,
    avatarMenuItems: [
      { key: 'settings', label: '个人设置', onClick: () => history.push('/settings') },
      { key: 'logout', label: '退出登录', danger: true, onClick: handleLogout },
    ],
    onPageChange: createPageChangeGuard({
      loginPath: '/login',
      guestPaths: ['/login', '/register'],
    }),
    childrenRender: (children, initialState) => (
      <>
        {children}
        <GlobalAntdHolder />
      </>
    ),
  },
  navigate: (path) => history.push(path),
});

export const getInitialState = appRuntime.getInitialState;
export const layout = appRuntime.layout;

模块使用指南

1. Utils 工具函数

@aspect/shared/utils 导入。包含 19 个子模块:

认证管理

import { createAuthManager, emitLoginRequired, LOGIN_REQUIRED_EVENT } from '@aspect/shared/utils';

// 创建认证管理器(每个项目配置不同的存储键和过期策略)
const auth = createAuthManager({
  tokenKey: 'my_token',
  expireKey: 'my_token_expire',
  ttlMs: 30 * 24 * 60 * 60 * 1000, // 30 天
  extraStorageKeys: ['userRoles', 'userMenus'],
  onUnauthorized: () => history.push('/login'),
});

auth.setToken('abc123');
auth.getToken();       // 'abc123'(自动检查过期)
auth.isAuthenticated(); // true
auth.clearAuth();       // 清除 token + 所有 extraStorageKeys

格式化

import { formatDate, formatDateTime, formatRelativeTime, formatNumber, formatPercentage } from '@aspect/shared/utils';

formatDate('2024-01-15');           // '2024-01-15'
formatDateTime('2024-01-15 10:30'); // '2024-01-15 10:30:00'
formatRelativeTime('2024-01-14');   // '1 天前'
formatNumber(12345.6);              // '12,345.6'
formatPercentage(0.856);            // '85.6%'

存储

import { safeGetStorage, safeSetStorage, safeRemoveStorage, clearStorage } from '@aspect/shared/utils';

safeSetStorage('key', { foo: 'bar' });     // 安全写入 (自动 JSON.stringify)
const val = safeGetStorage<MyType>('key');  // 安全读取 (自动 JSON.parse)
safeRemoveStorage('key');                   // 安全删除
clearStorage();                             // 清除所有

API 辅助

import { isApiSuccess, executeApiAction } from '@aspect/shared/utils';

// 判断 API 是否成功
if (isApiSuccess(response)) {
  console.log(response.data);
}

// 安全执行 API 调用 + 自动错误处理
const data = await executeApiAction(
  () => userService.remove(id),
  {
    successMsg: '删除成功',
    onSuccess: (data, msg) => {
      message.success(msg);
      actionRef.current?.reload();
    },
    onError: (msg) => message.error(msg),
  },
);

其他工具

import { createLogger } from '@aspect/shared/utils';
import { copyToClipboard } from '@aspect/shared/utils';
import { sanitizeHtml, initSanitizer } from '@aspect/shared/utils';
import { openInNewTab } from '@aspect/shared/utils';
import { validatePhone, validateEmail, validateFileSize, validateFileType } from '@aspect/shared/utils';
import { createValueEnum, createSelectOptionsFromMap } from '@aspect/shared/utils';
import { setRegionDataSource, getRegionData, getCityData } from '@aspect/shared/utils';
import { createUserWatermark } from '@aspect/shared/utils';
import { extractErrorMessage } from '@aspect/shared/utils';
import { initPerformanceMonitoring } from '@aspect/shared/utils';

// 日志
const logger = createLogger({ prefix: 'MyApp' });
logger.info('应用启动');
logger.warn('配置缺失');

// 剪贴板
await copyToClipboard('复制内容');

// HTML 过滤
initSanitizer(); // 全局初始化一次
const clean = sanitizeHtml('<script>alert(1)</script><p>安全内容</p>');

// 值枚举
const statusEnum = createValueEnum([
  { value: 'active', label: '启用', status: 'Success' },
  { value: 'inactive', label: '禁用', status: 'Error' },
]);

2. Crypto 加密

@aspect/shared/crypto 导入。当前内置算法为 aes-cbc-hmac(Web Crypto API 原生实现)。

import {
  createEncryption,
  getProviderType,
  hashPassword,
  isUsingNativeProvider,
} from '@aspect/shared/crypto';

// 检测当前加密器类型
const providerType = getProviderType(); // 'native'
if (isUsingNativeProvider()) {
  console.log('使用 Web Crypto API 原生加密(性能最优)');
}

// 创建加密实例
const crypto = createEncryption({
  algorithm: 'aes-cbc-hmac',
  keys: {
    requestKeyHex: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
    responseKeyHex: 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
  },
});

// 加密/解密
const timestamp = Date.now();
const encrypted = await crypto.encrypt({ username: 'test' }, timestamp);
const decrypted = await crypto.decrypt(encrypted, timestamp);

// 密码哈希(SHA-256)
const hashed = await hashPassword('password123');

底层能力也可单独使用:

import {
  AESCBCHMACStrategy,
  encryptRequestData,
  decryptResponseData,
  isExcludedPath,
  validateTimestamp,
  generateRandomHex,
  encryptAESOptimized,
  decryptAESOptimized,
} from '@aspect/shared/crypto';

加密方案说明

SDK 默认使用 Web Crypto API 原生实现:

| 运行条件 | 使用的实现 | 性能 | |------|-----------|------| | 支持 Web Crypto API | Web Crypto API 原生 | ⚡ 性能最优 |


3. Request 请求层

@aspect/shared/request 导入。基于 UmiJS request 插件。

一站式配置(createRequestConfig)

新项目接入 > 请求配置

CRUD Service 工厂

消除手写重复 service 代码。为业务实体自动生成 list / detail / create / update / remove / toggleStatus / save 方法。

// src/services/user.ts
import { createCrudService } from '@aspect/shared/request';
import { request } from '@umijs/max';

// 完整配置
const userService = createCrudService<User, CreateUserDTO, UpdateUserDTO>('/api/users', {
  request,
  list:   { method: 'POST', path: '/page' },
  detail: { method: 'GET',  path: '/:id' },
  create: { method: 'POST', path: '' },
  update: { method: 'PUT',  path: '' },
  delete: { method: 'DELETE', path: '/:id' },
  toggleStatus: { method: 'PUT', path: '/:id/status' },
});

// 默认配置已覆盖常见 RESTful 约定,可简写:
const userService = createCrudService<User>('/api/users', { request });

// 禁用不需要的操作
const readonlyService = createCrudService<Article>('/api/articles', {
  request,
  create: false,
  update: false,
  delete: false,
  toggleStatus: false,
});

// 使用
const res = await userService.list({ pageNo: 1, pageSize: 10, keyword: '张三' });
const user = await userService.detail(1);
await userService.create({ name: 'test' });
await userService.save({ id: 1, name: 'updated' }); // 有 id 自动走 update
await userService.remove(1);
await userService.toggleStatus(1, 'inactive');

默认操作路径约定:

| 操作 | 方法 | 路径 | |------|------|------| | list | POST | {basePath}/page | | detail | GET | {basePath}/:id | | create | POST | {basePath} | | update | PUT | {basePath} | | delete | DELETE | {basePath}/:id | | toggleStatus | PUT | {basePath}/:id/status |

文件服务工厂

import { createFileService } from '@aspect/shared/request';
import { request } from '@umijs/max';

const fileService = createFileService('/api/files', {
  request,
  upload:   { path: '/upload', requestOptions: { skipEncrypt: true } },
  delete:   { path: '/', paramName: 'fileUrl' },
  download: { path: '/download/:id' },
});

// 上传
const res = await fileService.upload({
  file: selectedFile,
  moduleType: 'avatar',
  businessId: userId,
});

// 删除
await fileService.remove('https://cdn.example.com/files/abc.jpg');

// 下载
const blob = await fileService.download('file-123');

请求常量和错误类型

import { API_SUCCESS_CODE, HTTP_STATUS, ErrorShowType } from '@aspect/shared/request';

// ErrorShowType 枚举
ErrorShowType.SILENT;        // 0 - 不显示
ErrorShowType.WARN_MESSAGE;  // 1 - 警告
ErrorShowType.ERROR_MESSAGE; // 2 - 错误
ErrorShowType.NOTIFICATION;  // 3 - 通知框
ErrorShowType.REDIRECT;      // 9 - 跳转

4. Components 组件

@aspect/shared/components 导入。基于 Ant Design + antd-style。

组件清单

| 组件 | 用途 | 关键 Props | |------|------|-----------| | StatusTag | 状态标签,根据 statusMap 显示颜色和文本 | status, statusMap | | ErrorBoundary | React 错误边界 | fallback, onError | | ActionColumn | ProTable 操作列(查看/编辑/删除/切换状态) | actions | | RecycleBinModal | 回收站弹窗(已删除数据恢复/彻底删除) | api, columns | | StatCard / StatCardGrid | 统计卡片,支持网格布局 | items, colorPresets | | StatusShow | 状态展示(空、加载、错误) | type | | UserAvatar | 用户头像 | src, name, size | | AuthGuard / GuestGuard | 认证/访客路由守卫 | fallback | | DataTableCard | 数据表格 + 卡片包装 | title, columns | | DetailPageLayout | 详情页面板布局 | mode, breadcrumbs | | SearchFilterBar | 搜索筛选栏 | fields | | DynamicFormModal | 动态表单弹窗(字段配置驱动) | fields, mode | | PageContainer | 带 Tab 的页面容器 | tabs | | GlobalAntdHolder | Antd 静态 API 持有器(message/notification/modal) | — |

基本用法

import { StatusTag, ErrorBoundary, ActionColumn, GlobalAntdHolder, getMessageApi } from '@aspect/shared/components';
import type { StatusMap, ActionItem } from '@aspect/shared/types';

// StatusTag
const STATUS_MAP: StatusMap<'active' | 'inactive'> = {
  active:   { text: '启用', color: 'green' },
  inactive: { text: '禁用', color: 'red' },
};
<StatusTag status="active" statusMap={STATUS_MAP} />

// ActionColumn
const actions: ActionItem[] = [
  createViewAction(() => handleView(record)),
  createEditAction(() => handleEdit(record)),
  createDeleteAction(() => handleDelete(record.id)),
];
<ActionColumn actions={actions} />

// ErrorBoundary
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

// GlobalAntdHolder(在 app.tsx 或根组件中挂载一次)
<GlobalAntdHolder />
// 之后在任意位置使用静态 API
getMessageApi()?.success('操作成功');
getNotificationApi()?.info({ message: '通知', description: '详情' });
getModalApi()?.confirm({ title: '确认删除?' });

StatCardGrid

import { StatCardGrid } from '@aspect/shared/components';
import type { StatCardGridItem } from '@aspect/shared/components';

const items: StatCardGridItem[] = [
  { title: '总用户', value: 1234, color: 'blue' },
  { title: '活跃用户', value: 567, color: 'green', trend: 12.5 },
  { title: '收入', value: '¥89,000', color: 'gold' },
];
<StatCardGrid items={items} />

DynamicFormModal

import { DynamicFormModal } from '@aspect/shared/components';
import type { DynamicFormField } from '@aspect/shared/components';

const fields: DynamicFormField[] = [
  { name: 'name', label: '名称', type: 'text', required: true },
  { name: 'status', label: '状态', type: 'select', options: [
    { label: '启用', value: 'active' },
    { label: '禁用', value: 'inactive' },
  ]},
  { name: 'description', label: '描述', type: 'textarea' },
];

<DynamicFormModal
  open={open}
  mode="create"
  fields={fields}
  onSubmit={handleSubmit}
  onCancel={() => setOpen(false)}
/>

5. Hooks

@aspect/shared/hooks 导入。共 16 个 React Hooks。

useTableData — ProTable 数据请求

import { useTableData } from '@aspect/shared/hooks';

const { request, actionRef } = useTableData<User>({
  fetchApi: (params) => userService.list(params),
});

<ProTable request={request} actionRef={actionRef} />

useCrudList — 完整 CRUD 列表管理

import { useCrudList } from '@aspect/shared/hooks';
import { Modal, message } from 'antd';
import { history } from '@umijs/max';

const {
  request,
  actionRef,
  goCreate,
  goEdit,
  goDetail,
  handleDelete,
  handleBatchDelete,
} = useCrudList<User>({
  fetchApi: userService.list,
  deleteApi: userService.remove,
  batchDeleteApi: userService.batchRemove,
  basePath: '/system/user',
  navigate: history.push,
  showMessage: (content, type) => message[type](content),
  showConfirm: ({ title, content, onOk }) =>
    Modal.confirm({ title, content, onOk }),
});

useFormModal — 表单弹窗

import { useFormModal } from '@aspect/shared/hooks';
import { Form, Modal } from 'antd';

const [form] = Form.useForm<UserForm>();

const { modalProps, openCreate, openEdit, submit } = useFormModal<
  UserForm,
  UserRecord
>({
  form,
  createApi: (data) => userService.create(data),
  updateApi: (data) => userService.update(data),
  messages: { createSuccess: '创建成功', updateSuccess: '更新成功' },
  onSuccess: () => actionRef.current?.reload(),
});

<Modal {...modalProps}>
  <Form form={form} layout="vertical" onFinish={submit}>
    {/* ... */}
  </Form>
</Modal>

useDetailPage — 详情页

import { useDetailPage } from '@aspect/shared/hooks';
import { useParams } from '@umijs/max';

const params = useParams<{ id: string }>();
const { data, loading, error, refresh } = useDetailPage<User>({
  fetchApi: (id) => userService.detail(id),
  id: params.id,
});

useModal — 弹窗状态

import { useModal } from '@aspect/shared/hooks';

const modal = useModal<User>();  // { open, data, show(data), close() }
modal.show(user);    // 打开并传入数据
modal.close();       // 关闭

useDownload — 文件下载

import { useDownload } from '@aspect/shared/hooks';

const { downloading, downloadFromUrl, downloadJSON } = useDownload();
await downloadFromUrl('/api/export', '报表.xlsx');
downloadJSON(data, '数据.json');

useFileUpload — 文件上传

import { useFileUpload } from '@aspect/shared/hooks';

const { uploading, uploadFile, fileList } = useFileUpload({
  uploadApi: (file) => fileService.upload({ file }),
  maxSize: 10 * 1024 * 1024, // 10MB
  accept: '.jpg,.png,.pdf',
});

其他 Hooks

import { useFormSubmit } from '@aspect/shared/hooks';      // 表单提交(防重复)
import { useDetailPageMode } from '@aspect/shared/hooks';  // 详情页模式(查看/新建/编辑)
import { useListData } from '@aspect/shared/hooks';        // 通用列表数据
import { useResponsive } from '@aspect/shared/hooks';      // 响应式断点检测
import { useIntlText } from '@aspect/shared/hooks';        // 国际化文本
import { useTableConfig } from '@aspect/shared/hooks';     // 表格列/操作配置
import { useFileSelector } from '@aspect/shared/hooks';    // 文件选择
import { useUserPreference } from '@aspect/shared/hooks';  // 用户偏好(主题/语言)

Hook 开发约定(贡献者)

  • 不稳定回调/配置统一通过 useLatestRef 读取,避免闭包陈旧与依赖震荡。
  • 可取消异步请求统一复用 src/hooks/shared-async.tsstartAbortableRequest / runAbortableTask / handleAbortableRequestError)。
  • ProTable 分页请求统一复用 src/hooks/shared-request.ts,避免重复实现参数转换与响应适配。
  • Hook 的 useCallback 依赖仅放稳定引用,避免 .current 直接进入依赖数组导致误触发。

6. Styles 样式

@aspect/shared/styles 导入。基于 antd-style 的 CSS-in-JS 样式工厂。

import { createCommonStyles, createPageStyles, createResponsiveStyles } from '@aspect/shared/styles';

// 通用样式
const useCommonStyles = createCommonStyles();
const { styles } = useCommonStyles();
<div className={styles.flexCenter}>居中内容</div>

// 页面级样式
const usePageStyles = createPageStyles();
const { styles } = usePageStyles();
<div className={styles.pageContainer}>页面内容</div>

// 响应式样式
const useResponsiveStyles = createResponsiveStyles();

7. Config 配置

@aspect/shared/config 导入。

UmiJS 基础配置

import { createUmiBaseConfig } from '@aspect/shared/config';

export default createUmiBaseConfig({
  routes,
  locale: { default: 'zh-CN', antd: true },
  title: '应用标题',
  publicPath: '/',
  alias: { '@': './src' },
  define: { 'process.env.API_BASE_URL': '/api' },
  proxy: { '/api': { target: 'http://localhost:8080' } },
  useProLayout: true,           // 启用 ProLayout
  codeSplitting: true,          // 启用代码分割
  antdTheme: { token: { colorPrimary: '#1890ff' } },
});

内置默认值:

  • hash 路由
  • model + initialState + access 插件
  • mako 构建
  • fastRefresh 热更新
  • moment → dayjs
  • antd CSS 变量模式
  • esbuildMinifyIIFE

App Runtime 配置

新项目接入 > App Runtime

共享预设文件

// tsconfig.json — 继承 TypeScript 基础配置
{ "extends": "@aspect/shared/config/tsconfig.base.json" }

// biome.json — 继承 Biome 检查规则
{ "extends": ["@aspect/shared/config/biome.json"] }

8. Types 类型

@aspect/shared/types 导入。仅类型定义,零运行时代码。

import type {
  // API 相关
  ApiResponse,          // { code, msg, data }
  PaginatedData,        // { list, total, pageNo?, pageNum?, pageSize? }
  PaginatedResponse,    // ApiResponse<PaginatedData<T>>
  PaginationParams,     // { current?, pageSize? }
  ProTableRequestParams,// PaginationParams + sort + filter
  ProTableResponse,     // { data, success, total }

  // 认证相关
  BaseUser,             // { id, username?, nickname?, avatar?, roles?, permissions? }
  AuthToken,            // { token, tokenType?, expiresAt?, refreshToken? }
  SessionConfig,        // { tokenKey, ttlDays?, expireKey? }
  LoginParams,          // { username, password, rememberMe?, captcha? }
  LoginResult,          // { success, token?, user?, message? }

  // 通用业务
  StatusMapItem,        // { text, color, icon? }
  StatusMap,            // Record<T, StatusMapItem>
  SelectOption,         // { label, value, disabled?, children? }
  TreeNode,             // { key, title, data?, children?, isLeaf? }
  KeyValue,             // { key, value }
  SortOrder,            // 'ascend' | 'descend' | null
  SortInfo,             // { field, order }
} from '@aspect/shared/types';

9. Constants 常量

@aspect/shared/constants 导入。

import {
  BREAKPOINTS,          // { xs: 480, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1600 }
  TABLE_DEFAULTS,       // { PAGE_SIZE: 10, PAGE_SIZE_OPTIONS, SCROLL_X, ROW_KEY, ... }
  DEBOUNCE_DELAY,       // { SEARCH: 300, RESIZE: 200, SCROLL: 100, VALIDATE: 500 }
  ANIMATION_DURATION,   // { FAST: 150, NORMAL: 300, SLOW: 500 }
  ICON_SIZE,            // { SMALL: 14, MEDIUM: 16, LARGE: 20, XLARGE: 24 }
  SPACING,              // { XS: 4, SM: 8, MD: 16, LG: 24, XL: 32, PAGE: 24 }
  Z_INDEX,              // { DROPDOWN, FIXED, MODAL, MESSAGE, TOOLTIP }
  ErrorShowType,        // enum { SILENT, WARN_MESSAGE, ERROR_MESSAGE, NOTIFICATION, REDIRECT }
} from '@aspect/shared/constants';

推荐使用模式

导入路径

始终使用子模块路径导入,而非主入口,以获得更好的 Tree-shaking:

// ✅ 推荐 — 按模块导入
import { formatDate, createLogger } from '@aspect/shared/utils';
import { useTableData, useModal } from '@aspect/shared/hooks';
import { StatusTag, ErrorBoundary } from '@aspect/shared/components';
import type { ApiResponse, StatusMap } from '@aspect/shared/types';
import { BREAKPOINTS, TABLE_DEFAULTS } from '@aspect/shared/constants';

// ❌ 不推荐 — 主入口导入
import { formatDate, useTableData, StatusTag } from '@aspect/shared';

项目级封装层

对于需要注入项目配置的工具(如 auth、logger、加密),建议创建项目级封装文件:

src/utils/
├── auth.ts       ← 基于 createAuthManager,注入项目存储键和过期策略
├── logger.ts     ← 基于 createLogger,注入项目前缀
├── encrypt.ts    ← 基于 createEncryption,注入项目密钥
└── index.ts      ← barrel 文件

封装文件模板:

// src/utils/auth.ts — 有业务封装,保留
import { createAuthManager } from '@aspect/shared/utils';

export const auth = createAuthManager({
  tokenKey: 'my_session_id',
  expireKey: 'my_session_expire',
  ttlMs: 30 * 24 * 60 * 60 * 1000,
  extraStorageKeys: ['userRoles'],
  onUnauthorized: () => history.push('/login'),
});

export const { getToken, setToken, clearAuth } = auth;
// src/utils/logger.ts — 极薄封装,注入前缀
import { createLogger } from '@aspect/shared/utils';
export const logger = createLogger({ prefix: 'MyApp' });

Hooks 重导出

推荐在项目中创建 hooks/index.ts barrel 文件,统一导出来自 shared 的 hooks 和项目特有的 hooks:

// src/hooks/index.ts
// ===== shared hooks =====
export {
  useCrudList,
  useDetailPage,
  useDetailPageMode,
  useDownload,
  useFileSelector,
  useFileUpload,
  useFormModal,
  useFormSubmit,
  useIntlText,
  useListData,
  useModal,
  useResponsive,
  useTableConfig,
  useTableData,
  useUserPreference,
} from '@aspect/shared/hooks';

// ===== 项目特有 hooks =====
export { usePermission } from './usePermission';
export { useTopicDrawer } from './useTopicDrawer';

消费方统一从 @/hooks 导入,后续如需在 shared 和项目级实现间切换,只需修改 barrel 文件。

组件重导出

推荐为每个 shared 组件创建本地代理文件,保持 @/components/XXX 的导入一致性:

// src/components/StatusTag/index.tsx
export { StatusTag } from '@aspect/shared/components';
export type { StatusTagProps } from '@aspect/shared/components';
export type { StatusMap, StatusMapItem } from '@aspect/shared/types';

如需扩展,可在同文件添加项目特有的状态映射常量:

// src/components/StatusTag/index.tsx
export { StatusTag } from '@aspect/shared/components';
export type { StatusTagProps } from '@aspect/shared/components';
export type { StatusMap, StatusMapItem } from '@aspect/shared/types';

// ===== 项目特有状态映射 =====
export const USER_STATUS_MAP = {
  active:   { text: '正常',   color: 'green' },
  disabled: { text: '已禁用', color: 'red' },
};

什么时候保留中间文件

| 场景 | 建议 | |------|------| | 仅转发、无任何业务逻辑 | 保留为 barrel/代理层,提供项目级 API 边界 | | 注入项目配置(存储键、前缀、密钥等) | 保留,这是必要的业务封装 | | 适配 API 签名(返回值转换、参数默认值) | 保留,确保项目内接口一致性 | | 扩展功能(添加业务方法、错误提示集成) | 保留,合理的功能增强 |


常规操作步骤

添加新的工具函数

  1. 判断功能是否通用(3 个项目都可能用到)

    • 通用 → 添加到 keel-web-shared/src/utils/ 对应文件
    • 项目特有 → 添加到项目本地 src/utils/
  2. keel-web-shared/src/utils/index.ts 中添加 export

  3. 运行 pnpm build 重新构建

  4. 消费项目通过 @aspect/shared/utils 导入

添加新的 Hook

  1. keel-web-shared/src/hooks/ 下创建 useXxx.ts

  2. keel-web-shared/src/hooks/index.ts 中添加导出

  3. 在消费项目的 hooks/index.ts barrel 文件中添加重导出

添加新的组件

  1. keel-web-shared/src/components/ 下创建 XxxComponent/index.tsx

  2. keel-web-shared/src/components/index.ts 中添加导出

  3. 在消费项目中创建代理文件 src/components/XxxComponent/index.tsx

添加新的 CRUD Service

// src/services/[module]/[entity].ts
import { createCrudService } from '@aspect/shared/request';
import { request } from '@umijs/max';

export interface MyEntity {
  id: number;
  name: string;
  status: string;
}

export const myEntityService = createCrudService<MyEntity>('/api/my-entity', {
  request,
  // 使用默认配置,或按需覆盖
  // list: { method: 'POST', path: '/list' },
});

修改共享库代码(联调开发)

消费项目默认使用 link: 本地引用,配合 watch 模式即可实时联调:

# 终端 1:启动 watch 模式
cd keel-web-shared
pnpm dev

# 终端 2:启动消费项目(改动自动热更新)
cd keelcoud/keel-cloud-web
pnpm dev

注意:不要手动将 link: 改为版本号,发布时由 deploy.py 自动处理。

添加新的类型

  1. 判断类型归属:

    • API 响应相关 → types/api.ts
    • 认证相关 → types/auth.ts
    • 通用业务类型 → types/common.ts
  2. 在对应文件中添加 export interface

  3. 消费项目通过 @aspect/shared/types 导入

添加新的常量

  1. keel-web-shared/src/constants/ui.ts 中添加

  2. 消费项目通过 @aspect/shared/constants 导入


发布与开发

依赖引用策略

本项目采用 link 开发 + 版本发布 双模式:

  • 日常开发:消费项目使用 "@aspect/shared": "link:../../keel-web-shared" 本地链接,配合 pnpm dev (watch) 实时生效
  • 生产发布:通过 deploy.py 脚本一键发布到 npm,脚本会自动处理 link ↔ 版本号的切换

一键发布(推荐)

cd keel-web-shared

# 自动 patch +1 并发布(包含 test + build + publish)
python deploy.py patch

# 自动 minor +1 并发布
python deploy.py minor

# 指定版本号发布
python deploy.py 1.0.0

# 模拟发布(不实际执行)
python deploy.py patch --dry-run

# 跳过构建(已有最新 dist)
python deploy.py patch --skip-build

# 跳过测试
python deploy.py patch --skip-test

# 发布 beta 版本
python deploy.py patch --tag beta

deploy.py 会自动:① 运行测试 → ② 构建 dist → ③ 将消费项目 link: 切为 ^x.y.z → ④ 发布到 npm → ⑤ 恢复消费项目为 link: 。无需手动操作。

本地开发联调

消费项目 package.json 中默认已配置 link:,配合 watch 模式即可:

# 终端 1:启动本包 watch 模式
cd keel-web-shared
pnpm dev

# 终端 2:启动消费项目(改动自动热更新)
cd keelcoud/keel-cloud-web
pnpm dev

消费此包的项目

| 项目 | 路径 | 依赖方式 | |------|------|---------| | keel-cloud-web | keelcoud/keel-cloud-web | link:../../keel-web-shared(开发) / ^x.y.z(生产) | | ruiying-web | ruiying/ruiying-web | link:../../keel-web-shared(开发) / ^x.y.z(生产) | | mc-revenue-guide-web | mc-guide/mc-revenue-guide-web | link:../../keel-web-shared(开发) / ^x.y.z(生产) |

可用脚本

| 命令 | 说明 | |------|------| | pnpm build | 使用 tsup 构建到 dist/(ESM + CJS + 类型声明) | | pnpm dev | watch 模式,源码改动自动重编译 | | pnpm test | 运行 vitest 测试 | | pnpm test:watch | watch 模式运行测试 | | pnpm lint | 使用 Biome 检查代码 | | pnpm typecheck | TypeScript 类型检查 | | python deploy.py | 一键发布到 npm(见上方说明) |


Peer Dependencies

| 依赖 | 版本 | 必需模块 | |------|------|---------| | dayjs | ^1.11.0 | 所有(日期格式化) | | react | >=18.0.0 | components, hooks, styles(可选) | | react-dom | >=18.0.0 | components(可选) | | antd | ^6.0.0 | components, styles(可选) | | antd-style | ^4.0.0 | styles(可选) | | @umijs/max | ^4.0.0 | config, request(可选) | | dompurify | ^3.0.0 | utils/sanitize(可选) | | china-division | ^2.0.0 | utils/region(可选) |

仅使用 utils/crypto 模块时,只需安装 dayjs,其余均为可选。


License

MIT