G
YouTube TikTok Games
Skip to content
mrdulinPublic

About

A dozen of utils for Front-End Development

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

@d-matrix/utils

codecov NPM Downloads npm bundle size NPM version

@d-matrix/utils 是一个面向前端开发的 TypeScript 工具函数集合,按命名空间导出浏览器、React、日期、数组、数值、文件与图表等常用能力。

文档同时保留了详细 API 说明、测试入口和发布命令,既适合作为包使用说明,也适合作为仓库维护入口。

特性

  • 按命名空间导出,导入路径稳定,适合在业务项目中按模块组织调用。
  • 覆盖前端常见场景,包括剪贴板、DOM、React Hook、文件处理、ECharts、数组与数值计算。
  • 提供 Cypress 组件测试、类型测试和示例页面,便于验证行为与查看用法。

快速开始

安装

npm install @d-matrix/utils

如果你会使用 react 或 echarts 相关能力,请根据项目实际情况安装对应的 peer dependencies。

导入方式

包的公开入口与 src/index.ts 保持一致,推荐按命名空间导入:

import { array, date, number, react } from '@d-matrix/utils';

基本示例

import { date, number } from '@d-matrix/utils';

const years = date.getYears({ type: date.YearOptionKind.Numbers, recentYears: 3 });
const ratio = number.safeDivide('12', '4');

console.log(years); // [2026, 2025, 2024]
console.log(ratio); // 3
import React from 'react';
import { react } from '@d-matrix/utils';

export function ResponsivePanel() {
  const isDesktopViewport = react.useMediaQuery('(min-width: 1024px)');
  const isMobileViewport = react.useMediaQuery('(max-width: 767px)');

  return <div>{isDesktopViewport ? 'desktop' : isMobileViewport ? 'mobile' : 'tablet'}</div>;
}

API 导航

每个 API 默认收起,点击展开可查看说明、示例和测试入口。

  • clipboard:剪贴板读写。
  • react:React 渲染、Hook 与类型辅助。
  • dom:DOM 操作与 HTML 字符串处理。
  • date:年份与星期相关工具。
  • types:TypeScript 类型工具。
  • algorithm:树结构与二分相关算法。
  • file:图片、下载与文件辅助。
  • support:运行环境能力检测。
  • timer:异步等待工具。
  • operator:运行时类型判断。
  • decimal:十进制格式化。
  • object:对象清理与键名辅助。
  • array:数组移动、统计与排名。
  • number:安全数值运算。
  • echarts:ECharts 配置与数据处理。
  • color:颜色转换。
  • scene:业务场景选择态与排序工具。

API 详情

clipboard

提供剪贴板写入能力,支持文本和图片两类常见前端交互场景。

相关测试:clipboard.cy.tsx

writeImage(element: HTMLImageElement | null | string): Promise<void>

复制图片到剪贴板。

writeText(text: string): Promise<void>

复制文本到剪贴板。

react

提供 React 渲染、常用 Hook、类组件增强能力与类型辅助工具。

相关测试:tests/react

render<P>(element: ReactElement<P>): Promise<string>

渲染 React 组件并返回 HTML 字符串。

cleanup(): void

清理函数,需要在调用 render() 后执行。

useDisableContextMenu(target: ContextMenuTarget = defaultContextMenuTarget): void

在 target 函数返回的元素上禁用右键菜单,默认 target 为 () => document。

例 1:在 id 是 test 的元素上禁用右键菜单。

import { react } from '@d-matrix/utils';

const TestComp = () => {
  react.useDisableContextMenu(() => document.getElementById('test'));

  return (
    <div>
      <div id="test">此元素的右键菜单被禁用</div>
    </div>
  );
};

例 2:在 document 上禁用右键菜单。

const TestComp = () => {
  react.useDisableContextMenu();

  return <div>内容</div>;
};
useStateCallback<T>(initialState: T): [T, (state: T, cb?: (state: T) => void) => void]

返回值中的 setState() 类似类组件里的 setState(updater[, callback]),可在 callback 中获取更新后的 state。

useIsMounted(): () => boolean

获取当前组件是否已挂载的 Hook。

const Test = () => {
  const isMounted = useIsMounted();

  useEffect(() => {
    if (isMounted()) {
      console.log('component mounted');
    }
  }, [isMounted]);

  return null;
};
useCopyToClipboard(props?: UseCopyToClipboardProps)

复制文本到剪贴板。更多用法见测试。

EnhancedComponent.prototype.setStateAsync(state)

setState() 方法的同步版本。

import { react } from '@d-matrix/utils';

class TestComponent extends EnhancedComponent<unknown, { pageIndex: number }> {
  state = {
    pageIndex: 1,
  };

  async onClick() {
    await this.setStateAsync({ pageIndex: 2 });
    console.log(this.state.pageIndex); // 2
  }

  render() {
    return (
      <button data-cy="test-button" onClick={() => this.onClick()}>
        click
      </button>
    );
  }
}
useDeepCompareRef(deps: DependencyList): React.MutableRefObject<number>

深比较 deps。返回的 ref.current 是自增数字,每次 deps 变化时加 1。更多用法见测试。

InferRef<T>

推导子组件的 ref 类型,适用于组件没有导出 ref 类型的场景。更多用法见测试。

interface ChildRefProps {
  prop1: () => void;
  prop2: () => void;
}

interface ChildProps {
  otherProp: string;
}

const Child = React.forwardRef<ChildRefProps, ChildProps>((props, ref) => {
  React.useImperativeHandle(
    ref,
    () => ({
      prop1() {},
      prop2() {},
    }),
    [],
  );

  return null;
});

type InferredChildRef = InferRef<typeof Child>; // 等价于 ChildRefProps

const Parent = () => {
  const childRef = React.useRef<InferredChildRef>(null);

  return <Child ref={childRef} otherProp="a" />;
};
useForwardRef = <T>(ref: ForwardedRef<T>, initialValue: any = null): React.MutableRefObject<T>

解决 React.forwardRef 场景下调用 ref.current.someMethod() 时出现 Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void' 的 TypeScript 类型错误。问题背景见这里。

const Input = React.forwardRef<HTMLInputElement, React.ComponentPropsWithRef<'input'>>((props, ref) => {
  const forwardRef = useForwardRef<HTMLInputElement>(ref);
  useEffect(() => {
    forwardRef.current.focus();
  });
  return <input type="text" ref={forwardRef} value={props.value} />;
});
useMediaQuery(query, options?): boolean

使用 Match Media API 检测当前 document 是否匹配 media query。

import { useMediaQuery } from '@d-matrix/utils/react';

export default function Component() {
  const isViewportAtLeast768PixelsWide = useMediaQuery('(min-width: 768px)');
  const isViewportAtMost767PixelsWide = useMediaQuery('(max-width: 767px)');

  return (
    <div>
      <div>{`The viewport is at least 768 pixels wide: ${isViewportAtLeast768PixelsWide}`}</div>
      <div>{`The viewport is at most 767 pixels wide: ${isViewportAtMost767PixelsWide}`}</div>
    </div>
  );
}
useIsFirstRender(): boolean

用于判断当前渲染是否为组件的第一次渲染,适合在初始渲染与后续渲染之间做逻辑区分。

useVersionGuard(deps: DependencyList): { captureVersion: () => number; isCurrentVersion: (version: number) => boolean }

基于依赖列表维护一个自增版本号,用来解决「异步结果晚于上下文变化返回」的问题。典型场景包括:搜索关键字连续变化、路由参数切换、详情页资源 ID 切换、定时器或延迟任务返回时组件上下文已变化。

使用步骤:

  1. 将能代表当前业务上下文的值作为依赖数组传入,例如 [query]、[routeId]、[userId, filter]。
  2. 在发起异步任务前调用 captureVersion(),记录当前版本快照。
  3. 在异步回调返回后调用 isCurrentVersion(version),只在返回 true 时写入状态。

搜索请求示例

当用户快速输入多个搜索关键字时,旧请求可能比新请求更晚返回。useVersionGuard() 可以避免旧请求覆盖新结果。

import React from 'react';
import { react } from '@d-matrix/utils';

export function SearchResult({ query }: { query: string }) {
  const [result, setResult] = React.useState('');
  const { captureVersion, isCurrentVersion } = react.useVersionGuard([query]);

  React.useEffect(() => {
    const version = captureVersion();

    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then((response) => response.text())
      .then((text) => {
        if (isCurrentVersion(version)) {
          setResult(text);
        }
      });
  }, [captureVersion, isCurrentVersion, query]);

  return <div>{result}</div>;
}

路由资源示例

当详情页从 userId=1 切到 userId=2 时,userId=1 的请求结果如果晚返回,不应该覆盖 userId=2 的页面状态。

import React from 'react';
import { react } from '@d-matrix/utils';

interface UserProfileProps {
  userId: string;
}

export function UserProfile({ userId }: UserProfileProps) {
  const [name, setName] = React.useState('');
  const { captureVersion, isCurrentVersion } = react.useVersionGuard([userId]);

  React.useEffect(() => {
    const version = captureVersion();

    fetch(`/api/users/${userId}`)
      .then((response) => response.json())
      .then((user: { name: string }) => {
        if (isCurrentVersion(version)) {
          setName(user.name);
        }
      });
  }, [captureVersion, isCurrentVersion, userId]);

  return <div>{name}</div>;
}

注意事项

  • 依赖数组应该包含能代表当前上下文的值,不需要在外部手动拼接字符串 key。
  • 依赖值会使用深比较判断是否变化;复杂对象建议保持不可变更新,避免原地修改导致历史依赖快照同时被改写。
  • useVersionGuard() 只负责判断结果是否过期,不会取消已经发出的请求;如果需要真正取消网络请求,可以结合 AbortController 使用。
  • captureVersion() 和 isCurrentVersion() 引用稳定,可安全放入 useEffect 依赖数组。更多行为验证见测试。

dom

提供 DOM 滚动、纯文本提取、HTML 颜色值转换和文本尺寸测量等浏览器侧工具。

相关测试:dom.cy.tsx

scrollToTop(element: Element | null | undefined): void

将元素滚动条滚动到顶部,并兼容老旧浏览器。浏览器兼容性见MDN。

strip(html: string): string

从字符串中去除 HTML 标签并返回纯文本内容。

import { dom } from '@d-matrix/utils';

dom.strip('测试<em>高亮</em>测试'); // '测试高亮测试'
convertRgbToHexInHtml(htmlStr: string): string

将 HTML 字符串中的 RGB / RGBA 颜色值转换为十六进制颜色值。

const html = '<div style="color: rgb(255, 0, 0)">Red text</div>';
dom.convertRgbToHexInHtml(html); // <div style="color: #ff0000">Red text</div>
createTextMeasurer(): TextMeasurer

创建文本测量器。测量器内部会复用一个隐藏的 DOM 容器,适合在表格列宽、图表标签或浮层布局中反复测量文本尺寸。

使用完成后调用 dispose(),释放内部缓存的测量容器。

import { dom } from '@d-matrix/utils';

const measurer = dom.createTextMeasurer();

const size = measurer.measure({
  text: '测试文本',
  fontSize: '14px',
  fontFamily: 'Arial',
  fontWeight: 'normal',
  maxWidth: 120,
  minWidth: 0,
  lineHeight: '20px',
});

console.log(size.width, size.height);

measurer.dispose();

React 函数组件中也可以这样使用:

import React from 'react';
import { dom } from '@d-matrix/utils';

export function TextWidthPreview() {
  const measurer = React.useMemo(() => dom.createTextMeasurer(), []);
  const [width, setWidth] = React.useState(0);

  React.useEffect(() => {
    const size = measurer.measure({
      text: '示例文本',
      fontSize: '14px',
      fontFamily: 'Arial',
      maxWidth: 120,
      lineHeight: '20px',
    });

    setWidth(size.width);

    return () => {
      measurer.dispose();
    };
  }, [measurer]);

  return <div>{`width: ${width}px`}</div>;
}

date

提供年份选项构造和星期展示等日期辅助能力。

相关测试:date.cy.ts

rangeOfYears(start: number, end: number = new Date().getFullYear()): number[]

创建 start 到 end 之间的年份数组。

getYears()

获取最近若干年。type 传 YearOptionKind.Numbers 时返回数字数组;传 YearOptionKind.Objects 时返回对象数组。

export interface YearOption {
  label: string;
  value: number;
}

export enum YearOptionKind {
  Numbers,
  Objects,
}

export type GetYearsOptions = {
  startYear?: number;
  recentYears?: number;
  endYear?: number;
  suffix?: string;
};

export function getYears(options: GetYearsOptions & { type: YearOptionKind.Numbers }): number[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind.Objects }): YearOption[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind }): number[] | YearOption[];
[
  { value: 2023, label: '2023年' },
  { value: 2022, label: '2022年' },
  { value: 2021, label: '2021年' },
]

更多用法见测试用例。

dayOfWeek(num: number, lang: keyof typeof i18n = 'zh'): string

返回星期几。lang 仅支持 zh 和 en,num 需要是整数,内部按 7 取模。

dayOfWeek(0); // '日'

types

提供常用的 TypeScript 类型变换工具,适合在业务类型声明中复用。

相关测试:types.ts

WithOptional<T, K extends keyof T>

将指定属性设为可选。

type A = { a: number; b: number; c: number };
type T0 = WithOptional<A, 'b' | 'c'>; // { a: number; b?: number; c?: number }
WithUndefinable<T, K extends keyof T>

将指定属性的值类型扩展为 undefined,并保留原属性的必选或可选修饰。

type A = { a: number; b: string; c?: boolean };
type T0 = WithUndefinable<A, 'b' | 'c'>; // { a: number; b: string | undefined; c?: boolean | undefined }
Nullishable<T>

为任意类型 T 扩展 undefined | null。

type T0 = Nullishable<string>; // string | undefined | null
Optional<T>

为任意类型 T 扩展 undefined。

type T0 = Optional<number>; // number | undefined
NullableValue<T>

为任意类型 T 扩展 null。

type T0 = NullableValue<boolean>; // boolean | null
FunctionPropertyNames<T>

获取对象中的方法名称,返回 union type。

class A {
  add() {}
  minus() {}
  div() {}
  public result: number = 0;
}
type T0 = FunctionPropertyNames<A>; // 'add' | 'minus' | 'div'

const t1 = {
  add() {},
  minus() {},
  div() {},
  result: 0,
};
type T1 = FunctionPropertyNames<typeof t1>; // 'add' | 'minus' | 'div'
NonFunctionPropertyNames<T>

获取对象中的非函数属性名称,返回 union type。

class A {
  add() {}
  minus() {}
  div() {}
  public result: number = 0;
}
type T0 = NonFunctionPropertyNames<A>; // 'result'

const t1 = {
  add() {},
  minus() {},
  div() {},
  result: 0,
};
type T1 = NonFunctionPropertyNames<typeof t1>; // 'result'
ValueOf<T>

获取对象中所有值组成的 union type。

const map = {
  0: '0m',
  1: '1m',
  2: '2m',
  3: '3m',
  4: '4m',
  5: '5m',
  6: '6m',
} as const;

type T0 = ValueOf<typeof map>; // '0m' | '1m' | '2m' | '3m' | '4m' | '5m' | '6m'
WithRequired<T, K extends keyof T>

将指定属性变为必选。

type Input = {
  a: number;
  b?: string;
};
type Output = WithRequired<Input, 'b'>; // { a: number; b: string }
Simplify<T>

将交叉类型或映射类型展开为单个更易读的对象类型,便于编辑器展示解析后的结构。

type A = { name: string };
type B = { age: number };
type User = Simplify<A & B>; // { name: string; age: number }
Mutable<T>

移除对象类型中所有属性的 readonly 修饰。

type ReadonlyUser = {
  readonly id: number;
  readonly name: string;
};

type User = Mutable<ReadonlyUser>; // { id: number; name: string }
DeepStringLeafValues<T>

递归提取对象中所有字符串属性值类型,并组成联合类型;非字符串叶子节点会被忽略。

type Bond = {
  code: '240215';
  issuer: {
    name: '国家开发银行';
    type: '政策性银行';
  };
  duration: number;
};

type BondText = DeepStringLeafValues<Bond>; // '240215' | '国家开发银行' | '政策性银行'

algorithm

收录树结构与二分相关的算法辅助能力。

相关测试:tree.cy.ts、binary.cy.ts

nodeCountAtDepth(root: Record<string, any>, depth: number, childrenKey: string = 'children'): number

计算指定层级的节点数量。

const root = {
  id: 1,
  children: [
    { id: 2, children: [{ id: 21 }, { id: 22 }, { id: 23 }] },
    { id: 3, children: [{ id: 31 }, { id: 32 }, { id: 33 }] },
  ],
};
expect(tree.nodeCountAtDepth(root, 0)).to.be.equal(1);
expect(tree.nodeCountAtDepth(root, 1)).to.be.equal(2);
expect(tree.nodeCountAtDepth(root, 2)).to.be.equal(6);
findNode<T extends Record<string, any>>(tree: T[], predicate: (node: T) => boolean, childrenKey = 'children'): T | null

找到符合条件的节点。

const root = {
  id: 1,
  children: [
    { id: 2, children: [{ id: 21 }, { id: 22 }, { id: 23 }] },
    { id: 3, children: [{ id: 31 }, { id: 32 }, { id: 33 }] },
  ],
};
const actual = tree.findNode([root], (node) => node.id === 3);
expect(actual).to.be.deep.equal(root.children[1]);

const actual2 = tree.findNode([root], (node) => node.id === 33);
expect(actual2).to.be.deep.equal(root.children[1].children[2]);
findParent(tree, child, indentityKey, childrenKey)

根据子节点查找父节点。

const treeData = {
  code: 1,
  subs: [
    { code: 2, subs: [{ code: 21 }, { code: 22 }, { code: 23 }] },
    { code: 3, subs: [{ code: 31 }, { code: 32 }, { code: 33 }] },
  ],
};

const actual = tree.findParent(treeData, treeData.subs[1].subs[2], 'code', 'subs');
expect(actual).to.be.deep.equal(treeData.subs[1]);
findPath<T extends Record<string, any>>(tree: T[], func: (node: T) => boolean, childrenKey = 'children'): T[] | null

查找节点路径,返回节点数组或 null。

const root = {
  id: 1,
  children: [
    { id: 2, children: [{ id: 21 }, { id: 22 }, { id: 23 }] },
    { id: 3, children: [{ id: 31 }, { id: 32 }, { id: 33 }] },
  ],
};

const actual = tree.findPath([root], (node) => node.id === 33);
expect(actual).to.be.deep.equal([root, root.children[1], root.children[1].children[2]]);
flatten(tree, childrenKey = 'children')

扁平化树结构,返回的每个节点都不包含 children 属性。

file

提供图片加载、尺寸校验、文件下载和响应头文件名解析等工具。

相关测试:file.cy.ts

toImage(file: BlobPart | FileURL, options?: BlobPropertyBag): Promise<HTMLImageElement>

将 BlobPart 或文件地址转换为图片对象。

validateImageSize(file: BlobPart | FileURL, limitSize: { width: number; height: number }, options?: BlobPropertyBag): Promise<ImageSizeValidationResult>

图片宽高校验。

返回值:

interface ImageSizeValidationResult {
  isOk: boolean;
  width: number;
  height: number;
}
isImageExists(src: string, img: HTMLImageElement = new Image()): Promise<boolean>

检测图片地址是否可用。

import { file } from '@d-matrix/utils';

const url = 'https://picsum.photos/200/300';
const res = await file.isImageExists(url);

传入 HTML 中已存在的 img 元素:

import { file } from '@d-matrix/utils';

const $img = document.getElementById('img');
const res = await file.isImageExists(url, $img as HTMLImageElement);
getFilenameFromContentDispositionHeader(header: { ['content-disposition']: string }): string

从 Content-Disposition response header 中获取 filename。

import { file } from '@d-matrix/utils';

const header = {
  'content-disposition': 'attachment;filename=%E5%A4%A7%E8%A1%8C%E6%8C%87%E5%AF%BC2024-06-27-2024-06-28.xlsx',
};
const filename = file.getFilenameFromContentDispositionHeader(header);
// '大行指导2024-06-27-2024-06-28.xlsx'
download(source: string | Blob, fileName = '', target?: HyperLinkTarget): void

文件下载,source 可以是文件地址或 blob 对象。

type HyperLinkTarget = '_self' | '_blank' | '_parent' | '_top';
downloadByIframe(source: string): boolean

通过创建 iframe 进行文件下载。

support

提供浏览器、WebSocket、SharedWorker 等运行环境能力检测函数。

相关测试:暂无专用测试

isBrowserEnv(): boolean

判断当前是否为浏览器环境。

isWebSocket(): boolean

判断当前环境是否支持 WebSocket。

isSharedWorker(): boolean

判断当前环境是否支持 SharedWorker。

timer

提供基于 Promise 的异步等待工具。

相关测试:暂无专用测试

sleep(ms?: number): Promise<unknown>

使用 setTimeout 与 Promise 实现暂停执行若干毫秒。

await sleep(3000); // 暂停 3 秒
console.log('continue'); // 继续执行

operator

提供运行时值类型判断工具。

相关测试:operator.cy.ts

trueTypeOf(obj: unknown): string

检查数据类型。

trueTypeOf([]); // array
trueTypeOf({}); // object
trueTypeOf(''); // string
trueTypeOf(new Date()); // date
trueTypeOf(1); // number
trueTypeOf(function () {}); // function
trueTypeOf(/test/i); // regexp
trueTypeOf(true); // boolean
trueTypeOf(null); // null
trueTypeOf(undefined); // undefined

decimal

提供基于 decimal.js-light 的数值格式化能力。

相关测试:decimal.cy.ts

format(value: number | string | undefined | null, options?: FormatOptions): string

格式化数字,默认保留 3 位小数,可添加前缀、后缀,默认值为 '--'。更多用法见测试。

type FormatOptions = {
  decimalPlaces?: number | false;
  suffix?: string;
  prefix?: string;
  defaultValue?: string;
  operation?: {
    operator: 'add' | 'sub' | 'mul' | 'div' | 'toDecimalPlaces';
    value: number;
  }[];
};

object

提供对象零值清理与类型安全键名访问辅助函数。

相关测试:object.cy.ts

removeZeroValueKeys = <T extends Record<string, any>>(obj: T, zeroValues = ZeroValues): T

移除零值的键。默认零值包括 undefined、null、''、NaN、[]、{}。

removeZeroValueKeys({ a: '', b: 'abc', c: undefined, d: null, e: NaN, f: -1, g: [], h: {} });
// { b: 'abc', f: -1 }
typedKeys(obj: T): Array<keyof T>

返回 tuple 类型,而不是 string[]。

const obj = { a: 1, b: '2' };
Object.keys(obj); // string[]
object.typedKeys({ a: 1, b: '2' }); // ('a' | 'b')[]

array

提供数组移动、序号计算、百分位排名和标准差等实用函数。

相关测试:array.cy.ts

moveImmutable<T>(array: T[], fromIndex: number, toIndex: number): T[]

移动数组元素位置,返回新数组,不修改原数组。

import { array } from '@d-matrix/utils';

const input = ['a', 'b', 'c'];

const array1 = array.moveImmutable(input, 1, 2);
console.log(array1);
//=> ['a', 'c', 'b']

const array2 = array.moveImmutable(input, -1, 0);
console.log(array2);
//=> ['c', 'a', 'b']

const array3 = array.moveImmutable(input, -2, -3);
console.log(array3);
//=> ['b', 'a', 'c']
moveMutable<T>(array: T[], fromIndex: number, toIndex: number): void

移动数组元素位置,直接修改原数组。

moveToStart<T>(array: T[], predicate: (item: T) => boolean): T[]

将满足条件的元素移动到数组首位,不修改原数组。

import { array } from '@d-matrix/utils';

const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newList = array.moveToStart(list, (item) => item.id === 4);

// [{ id: 4 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }]
moveMulti<T extends unknown>(arr: T[], indexes: number[], start: number): T[]

将多个元素移动到数组中的指定位置。更多用法见测试用例。

getArrayOrUndefined<T>(array?: T[] | undefined | null): T[] | undefined

如果 array 是非空数组则返回该数组,否则返回 undefined。更多用法见测试用例。

calcUnusedMinSerialNumber = <T extends Record<string, unknown>>(list: T[], options: { fieldName: keyof T; prefix: string; defaultNo?: number }): number

从指定列表中提取“前缀 + 数字”格式的值,找到未被使用的最小正整数序号。更多用法见测试用例。

percentRank(arr: number[], value: number | undefined | null): number | undefined

计算给定值在数组中的百分位排名。函数内部会先对数组做升序排序,再计算结果。

const arr = [10, 20, 40, 50];

array.percentRank(arr, 30); // 0.5
array.percentRank(arr, 10); // 0
array.percentRank(arr, 50); // 1
percentRankOfSorted(sorted: number[], value: number | undefined | null): number | undefined

计算给定值在已排序数组中的百分位排名,适用于输入已经是升序数组的场景,避免重复排序。支持精确命中和区间插值,超出范围时返回 undefined。更多用法见测试用例。

const sorted = [10, 20, 40, 50];

array.percentRankOfSorted(sorted, 30); // 0.5
array.percentRankOfSorted(sorted, 10); // 0
array.percentRankOfSorted(sorted, 50); // 1
standardDeviation(arr: number[]): number | undefined

计算数字数组的总体标准差,空数组时返回 undefined。更多用法见测试用例。

array.standardDeviation([1, 2, 3, 4, 5]); // 1.4142135623730951
array.standardDeviation([5]); // 0
array.standardDeviation([]); // undefined

number

提供随机整数、字符串转数值与安全四则运算函数。

相关测试:number.cy.ts

randomInt(min: number, max: number): number

返回 min 到 max 之间的随机整数。

safeMinus(a: number | string | undefined | null, b: number | string | undefined | null): number | undefined

安全减法。支持数字和可转为数字的字符串。任一参数为 undefined、null、空字符串、空白字符串或非法数字字符串时返回 undefined。

number.safeMinus(5, 2); // 3
number.safeMinus('5', '2'); // 3
number.safeMinus(undefined, 2); // undefined
number.safeMinus('abc', 2); // undefined
safePlus(a: number | string | undefined | null, b: number | string | undefined | null): number | undefined

安全加法。支持数字和可转为数字的字符串。任一参数为 undefined、null、空字符串、空白字符串或非法数字字符串时返回 undefined。

number.safePlus(5, 2); // 7
number.safePlus('5', '2'); // 7
number.safePlus(null, 2); // undefined
number.safePlus('', 2); // undefined
safeMultiply(a: number | string | undefined | null, b: number | string | undefined | null): number | undefined

安全乘法。支持数字和可转为数字的字符串。任一参数为 undefined、null、空字符串、空白字符串或非法数字字符串时返回 undefined。

number.safeMultiply(5, 2); // 10
number.safeMultiply('5', '2'); // 10
number.safeMultiply(5, undefined); // undefined
number.safeMultiply('   ', 2); // undefined
safeDivide(a: number | string | undefined | null, b: number | string | undefined | null): number | undefined

安全除法。支持数字和可转为数字的字符串。任一参数为 undefined、null、空字符串、空白字符串、非法数字字符串,或者除数为 0(包括字符串 '0')时返回 undefined。

number.safeDivide(6, 2); // 3
number.safeDivide('6', '2'); // 3
number.safeDivide(1, 0); // undefined
number.safeDivide(1, '0'); // undefined
number.safeDivide(6, null); // undefined

echarts

提供 ECharts 配置合并、时间序列补点和 Y 轴范围计算工具。

相关测试:mergeOption.cy.ts、fill.cy.ts、calcYAxisRange.cy.ts

mergeOption(defaults: EChartsOption, overrides: EChartsOption, option?: deepmerge.Options): EChartsOption

deep merge ECharts 配置。更多用法见测试用例。

fill<T extends Record<string, any>, XAxisField extends keyof T, YAxisField extends keyof T>(dataSource: T[], xAxisField: XAxisField, yAxisField: YAxisField): T[]

适用于后端接口只返回部分时间点数据时,按 5 分钟粒度补点。填充点的 Y 轴值沿用前一个点的值。

时间示例:[9:23, 9:27] => [9:23, 9:25, 9:27, 9:30]

更多用法见测试用例,效果图见折线图。

calcYAxisRange<T extends Record<string, any>, Key extends keyof T>(data: T[], key: Key, decimalPlaces = 2, splitNumber = 5): { max: number; min: number }

计算 ECharts YAxis 的 max 和 min,使折线图能够根据实际数据动态调整波动范围,并让第一个点始终位于 Y 轴中间位置。效果图见这里。

confinePosition: TooltipComponentPositionCallback(已弃用)

confinePosition 会根据鼠标位置与 tooltip 尺寸计算提示框左上角坐标,避免提示框超出图表视图右侧和底部。默认偏移量为右下方各 20px;空间不足时会尝试显示在鼠标左侧或上方。

在线示例:CodeSandbox。

推荐直接使用 ECharts 原生的 tooltip.confine: true 配置。它会将 tooltip 限制在图表容器中,无需维护自定义位置回调。

import type { EChartsOption } from 'echarts';

const option: EChartsOption = {
  tooltip: {
    trigger: 'axis',
    confine: true,
  },
};

在迁移完成前,仍可通过深路径导入已弃用的回调:

import type { EChartsOption } from 'echarts';
import { confinePosition } from '@d-matrix/utils/dist/echarts/tooltip';

const option: EChartsOption = {
  tooltip: {
    trigger: 'axis',
    position: confinePosition,
  },
};

自定义图标

自定义 ECharts symbol 定义位于 src/echarts/symbols/index.ts,可通过深路径导入单个图标或 symbols 集合。

import { ChevronLeftOutline, symbols } from '@d-matrix/utils/dist/echarts/symbols';

const option = {
  series: [
    {
      type: 'scatter',
      symbol: ChevronLeftOutline,
      symbolSize: 24,
      data: [[0, 0]],
    },
  ],
};

symbols.StarFilled;

ECharts 图标预览

中国地图 GeoJSON

中国地图数据位于 src/echarts/geo-json/china.json。在创建图表前使用 registerMap() 注册数据;geo.map 与地图系列的 map 使用相同名称。

import * as echarts from 'echarts';
import chinaJson from './src/echarts/geo-json/china.json';

echarts.registerMap('china', chinaJson);

const option: echarts.EChartsOption = {
  geo: {
    map: 'china',
  },
  series: [
    {
      type: 'map',
      map: 'china',
    },
  ],
};

更多参数见 ECharts registerMap API。

color

提供十六进制颜色与 RGBA 之间的转换能力。

相关测试:color.cy.ts

hexToRGBA(hex: string, alpha: number | string = 1): string

将十六进制颜色转换为 RGBA 颜色。更多用法见测试用例。

scene

提供多选默认项切换、带空值兜底的排序和按键防抖调度等场景工具。

相关测试:toggleSelectionValue.cy.ts、sortRecordsBySortStateNilLast.cy.ts、keyedDebounceScheduler.cy.ts

相关源码:keyedDebounceScheduler.ts、d3LeanLabeler.ts

toggleSelectionValue<T extends string | number>(params: { selectedValues: T[] | undefined; toggledValue: T; selected: boolean; defaultValue: T; isDefaultSelected: (value: T[] | undefined) => boolean; }): T[]

切换多选值,并在取消最后一个普通项时回退到默认值。适用于“全部”与普通选项互斥的场景。

import { scene } from '@d-matrix/utils';

const defaultValue = 'all';
const isDefaultSelected = (value: string[] | undefined) => scene.hasSameSelectionValues(value, [defaultValue]);

scene.toggleSelectionValue({
  selectedValues: [defaultValue],
  toggledValue: 'A',
  selected: true,
  defaultValue,
  isDefaultSelected,
}); // ['A']

scene.toggleSelectionValue({
  selectedValues: ['A'],
  toggledValue: 'A',
  selected: false,
  defaultValue,
  isDefaultSelected,
}); // ['all']
createKeyedDebounceScheduler<TKey>(delay: number): KeyedDebounceScheduler<TKey>

创建按键管理的防抖调度器。同一个键重复调度时只执行最后一次回调,不同键的回调互不影响。

import { scene } from '@d-matrix/utils';

const scheduler = scene.createKeyedDebounceScheduler<string>(300);
const fetchContacts = (groupId: string) => {
  console.log(`获取分组 ${groupId} 下的联系人`);
};

scheduler.schedule('record-1', () => {
  fetchContacts('record-1');
});

scheduler.cancel('record-1');
scheduler.dispose();

dispose() 用于在组件卸载或页面离开时结束调度器的生命周期:

  • 会取消所有 key 对应的待执行定时器,因此这些回调不会再执行;
  • 已经执行完成的回调不受影响;
  • 调用后调度器不可恢复,再次调用 schedule() 只会直接忽略,不会创建新的定时器;
  • 重复调用 dispose() 是安全的,不会产生额外副作用。

如果只是想取消某个分组当前尚未执行的任务,应使用 cancel(key);取消后调度器仍可继续调用 schedule():

scheduler.cancel('record-1');
scheduler.schedule('record-1', () => {
  fetchContacts('record-1');
});

如果已经调用 dispose(),原调度器就不能再次复用。后续仍需要调度任务时,必须重新创建实例:

scheduler.dispose();

const nextScheduler = scene.createKeyedDebounceScheduler<string>(300);
nextScheduler.schedule('record-1', () => {
  fetchContacts('record-1');
});
hasSameSelectionValues<T extends string | number>(selectedValues: T[] | undefined, nextSelectedValues: T[]): boolean

判断两次选中值是否一致。undefined 会按空数组处理。

scene.hasSameSelectionValues(undefined, []); // true
scene.hasSameSelectionValues(['A', 'B'], ['A', 'B']); // true
scene.hasSameSelectionValues(['A', 'B'], ['B', 'A']); // false
sortRecordsBySortStateNilLast<T extends object>(records: T[] | undefined, sortState: VirtualTableSort | undefined, options?: { getSortValue?: (record: T, field: keyof T) => unknown; }): T[] | undefined

根据排序状态对记录排序,并将 null / undefined 放到结果末尾。

import { scene } from '@d-matrix/utils';

const records = [{ value: 3 }, { value: undefined }, { value: 1 }];

scene.sortRecordsBySortStateNilLast(records, {
  field: 'value',
  direction: scene.VirtualTableSortDirection.ASC,
});
// [{ value: 1 }, { value: 3 }, { value: undefined }]
d3LeanLabeler(): D3LeanLabeler

基于模拟退火的标签避让器,用于减少标签重叠、标签与锚点重叠,以及引导线相交。

注意事项:

  • 这是 src/scene/d3LeanLabeler.ts 的命名导出函数,当前不会通过根入口的 scene 命名空间暴露。
  • label() 与 anchor() 传入的数组需要等长,且按索引一一对应。
  • start() 会原地修改 label 数组中的 x、y 坐标。
  • alt_schedule() 目前只保留兼容签名,实际仍使用内置的线性降温策略。
import { d3LeanLabeler, type Anchor, type Label } from '@d-matrix/utils/dist/scene/d3LeanLabeler';

const labels: Label[] = [
  { x: 120, y: 110, width: 80, height: 20, id: 'A' },
  { x: 160, y: 118, width: 80, height: 20, id: 'B' },
];

const anchors: Anchor[] = [
  { x: 100, y: 100, r: 4, id: 'A' },
  { x: 140, y: 108, r: 4, id: 'B' },
];

const labeler = d3LeanLabeler().width(800).height(600).label(labels).anchor(anchors);

labeler.start(200);

console.log(labels); // 坐标已被原地更新

链式 API:

  • width(width) / height(height):设置布局边界。
  • label(labels) / anchor(anchors):注入标签与锚点数据。
  • alt_energy(fn):覆盖默认能量函数。
  • alt_schedule(fn):兼容接口,当前不会真正生效。
  • start(nsweeps):执行退火布局。
VirtualTableSortDirection

排序方向常量。

scene.VirtualTableSortDirection.NONE; // 0
scene.VirtualTableSortDirection.ASC; // 1
scene.VirtualTableSortDirection.DESC; // -1
VirtualTableSort

排序状态类型。

type VirtualTableSort = {
  field: string;
  direction: ValueOf<typeof scene.VirtualTableSortDirection>;
};

测试与开发

测试

运行全部组件测试:

npm run cy:component:all

运行单个组件测试:

npm run cy:component -- tests/date.cy.ts

运行类型测试:

npm run test:tsd

运行 E2E 测试前,先将 src 通过 tsc 构建到 public/dist 目录:

npm run build:public

启动一个 Web 服务器访问 public/index.html,dist 目录脚本可通过 <script type="module" /> 引入:

npm run start

最后启动 Cypress GUI 客户端并选择 E2E 测试:

npm run cy:open

本地开发

构建 npm 包输出到 dist 目录:

npm run build

启动本地示例页服务:

npm run start

如果需要先为示例页准备 public/dist 下的构建产物,请先执行:

npm run build:public

发布说明

默认发布路径改为“打版本 tag 触发 GitHub Actions 自动发布”。发布前建议先在本地执行一次完整校验:

npm run release:verify

更新 package 版本:

npm version <minor> or <major>...

npm version 会更新版本、创建 git tag,并通过 postversion 自动 push commit 与 tag。tag 推送后,GitHub Actions 会执行:

npm ci
npm run build
npm run test:types
npm run cy:component:all
npm pack --dry-run
npm publish --access public
自动创建 GitHub Release

如果只想本地验证产物而不发布,可单独执行:

npm run pack:check

不再建议把 npm publish 作为常规发布入口;除非自动发布链路不可用且你明确要走人工兜底。

网络原因导致连接 registry 服务器超时时,可在人工兜底发布时指定 proxy:

npm --proxy http://127.0.0.1:7890 publish

镜像站查询版本与手动同步:

npm 镜像站

GitHub Release 默认由 Actions 自动创建,并启用 GitHub 的自动 release notes 分类;只有在需要补充说明时,才需要手动编辑 release 内容。

git log --oneline --decorate

补充链接

与发布和包维护相关的补充资料:

About

A dozen of utils for Front-End Development

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages