fix: 新增message、popover、slider、card、calendar组件

This commit is contained in:
NICE CODE BY DEV 2024-02-19 15:23:34 +08:00
parent 315d075c96
commit 43741393f7
431 changed files with 38460 additions and 376 deletions

View File

@ -1,13 +1,32 @@
#root .dumi-default-doc-layout .dumi-default-features-item {
text-align: center;
}
/* stylelint-disable-next-line rule-empty-line-before */
#root .dumi-default-doc-layout > main {
max-width: none;
}
#root .dumi-default-header {
box-shadow: 2px 1px 16px #dfdfdf;
}
#root .dumi-default-sidebar {
width: 188px;
overflow-y: hidden;
/* stylelint-disable-next-line rule-empty-line-before */
&:hover {
overflow-y: auto;
}
/* stylelint-disable-next-line rule-empty-line-before */
dd a {
font-size: 14px;
}
}
#root .dumi-default-navbar li {
font-size: 14px;
}
#root .dumi-default-footer {
display: none;
}

View File

@ -2,7 +2,6 @@ import { defineConfig } from 'father';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: { output: 'dist' },
extraBabelPlugins: [
[
'import',

View File

@ -2,13 +2,16 @@
"cSpell.words": [
"ahooks",
"antd",
"commitlint",
"COMPATER",
"constract",
"dumi",
"favicons",
"flvjs",
"indicatorsize",
"lambo",
"remuxer",
"stylelint",
"transmuxer",
"zhst"
]

View File

@ -13,7 +13,7 @@
"dist"
],
"scripts": {
"add": "pnpm run build && pnpm changeset add",
"add": "pnpm changeset add",
"build": "pnpm --filter=@zhst/* run build",
"build:watch": "father dev",
"dev": "dumi dev",

View File

@ -1,5 +1,14 @@
# @zhst/biz
## 0.5.1
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.6.0
- @zhst/func@0.6.0
- @zhst/meta@0.7.0
## 0.5.0
### Minor Changes

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/biz",
"version": "0.5.0",
"version": "0.5.1",
"description": "业务库",
"keywords": [
"business",

View File

@ -1,5 +1,11 @@
# @zhst/biz
## 0.6.0
### Minor Changes
- feat: 变更
## 0.5.0
### Minor Changes

View File

@ -1,6 +1,12 @@
# @zhst/constants
:::info{title=开发中}
稍等...
:::
## 介绍
业务库
静态变量
## 安装
@ -11,5 +17,4 @@
```js
import React from 'react';
import { TYPE } from '@zhst/constants'
```

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/constants",
"version": "0.5.0",
"version": "0.6.0",
"description": "常量库",
"keywords": [
"constants",

View File

@ -1,5 +1,16 @@
# @zhst/utils
## 0.6.0
### Minor Changes
- feat: 变更
### Patch Changes
- Updated dependencies
- @zhst/request@0.6.0
## 0.5.0
### Minor Changes

View File

@ -325,7 +325,6 @@ export var generateImg = function generateImg(_imgKey) {
if (/(http|https):\/\/([\w.]+\/?)\S*/ig.test(imgKey)) {
return imgKey;
}
console.log('imgKey', imgKey);
try {
if (matchS3Prefix(imgKey)) {
imgKey = base64DecodeImageKey(imgKey);

View File

@ -7,4 +7,7 @@ export * from "./number";
export * from "./time";
export * from "./utils";
export * from "./camera";
export * from "./math";
export * from "./math";
export var a = function a(data) {
console.log('data', data);
};

View File

@ -1,3 +1,4 @@
var _ALGORITHM_VERSION;
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@ -8,8 +9,8 @@ function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symb
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
import { cloneDeep, get, isNull } from 'lodash-es';
import { dataURLToBlob } from "../file";
var proto = {
@ -21,7 +22,7 @@ var proto = {
}
}
};
export var ALGORITHM_VERSION = _defineProperty(_defineProperty(_defineProperty({}, '7', '形体'), '4', '人脸'), '6', '非机动车');
export var ALGORITHM_VERSION = (_ALGORITHM_VERSION = {}, _defineProperty(_ALGORITHM_VERSION, '7', '形体'), _defineProperty(_ALGORITHM_VERSION, '4', '人脸'), _defineProperty(_ALGORITHM_VERSION, '6', '非机动车'), _ALGORITHM_VERSION);
export var algorithmVersions = _toConsumableArray(Object.keys(ALGORITHM_VERSION));
export var getBikeExtendRect = function getBikeExtendRect(rect, maxW) {
var newRect = _objectSpread({}, rect);

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/func",
"version": "0.5.0",
"version": "0.6.0",
"description": "函数合集",
"keywords": [
"hooks"

View File

@ -281,8 +281,6 @@ export const generateImg: (imgKey: string, host?: string) => string = (_imgKey,
return imgKey;
}
console.log('imgKey', imgKey)
try {
if (matchS3Prefix(imgKey)) {
imgKey = base64DecodeImageKey(imgKey);

View File

@ -8,3 +8,7 @@ export * from './time'
export * from './utils'
export * from './camera'
export * from './math'
export const a = (data: string) => {
console.log('data', data)
}

View File

@ -1,5 +1,16 @@
# @zhst/hooks
## 0.6.0
### Minor Changes
- feat: 变更
### Patch Changes
- Updated dependencies
- @zhst/func@0.6.0
## 0.5.0
### Minor Changes

View File

@ -1,4 +0,0 @@
export declare const useUnActivateWrapper: (cb: () => void) => void;
export declare const useActivateWrapper: (cb: any) => null;
export declare const useActivateState: () => boolean;
export default useActivateWrapper;

View File

@ -6,6 +6,7 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
import { useState, useRef, useCallback } from 'react';
import { useActivate, useUnactivate } from 'react-activation';
// @ts-ignore
import { noop, isFunction } from '@zhst/func';
import { useLatest, useMount, useUnmount } from 'ahooks';
export var useUnActivateWrapper = function useUnActivateWrapper(cb) {

View File

@ -1,4 +0,0 @@
export declare const useUnActivateWrapper: (cb: () => void) => void;
export declare const useActivateWrapper: (cb: any) => null;
export declare const useActivateState: () => boolean;
export default useActivateWrapper;

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/hooks",
"version": "0.5.0",
"version": "0.6.0",
"description": "hooks合集",
"keywords": [
"hooks"

View File

@ -1,5 +1,6 @@
import { useState, useRef, useCallback } from 'react';
import { useActivate, useUnactivate } from 'react-activation';
// @ts-ignore
import { noop, isFunction } from '@zhst/func';
import { useLatest, useMount, useUnmount } from 'ahooks';

View File

@ -1,5 +1,15 @@
# @zhst/biz
## 0.5.1
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.6.0
- @zhst/func@0.6.0
- @zhst/meta@0.7.0
- @zhst/biz@0.5.1
## 0.5.0
### Minor Changes

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/material",
"version": "0.5.0",
"version": "0.5.1",
"description": "物料库",
"keywords": [
"business",

View File

@ -1,5 +1,18 @@
# @zhst/utils
## 0.7.0
### Minor Changes
- feat: 变更
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.6.0
- @zhst/func@0.6.0
- @zhst/meta@0.7.0
## 0.6.0
### Minor Changes

View File

@ -10,12 +10,11 @@
```jsx
import React from 'react';
import { Button } from 'antd'
import { doubleClick } from '@zhst/meta'
import { Button } from '@zhst/meta'
export default () => {
return (
<Button type="primary" onClick={() => doubleClick()} >按我啊!</Button>
<Button type="primary" onClick={() => console.log('ok')} >按我啊!</Button>
)
}
```

View File

@ -24,9 +24,11 @@ getExtendRect,
// @ts-ignore
getTransformRect,
// @ts-ignore
getRotateImg, getTransforms, addEventListenerWrapper, getFileByRect } from '@zhst/func';
getRotateImg, getTransforms, addEventListenerWrapper, getFileByRect
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';
import { Button, Empty } from 'antd';
import { Button, Empty } from '..';
import Icon from "../iconfont";
import { Cropper, Viewer, EVENT_VIEWER_TRANSFORM_CHANGE, EVENT_VIEWER_READY, EVENT_CROP_START, EVENT_CROP_END } from "../ImageEditor";
import BtnGroup from "./components/BtnGroup";

View File

@ -5,6 +5,7 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
import React, { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
// @ts-ignore
import { generateImg, get, addEventListenerWrapper } from '@zhst/func';
import { useUpdateEffect } from '@zhst/hooks';
import Button from "../button";

View File

@ -14,12 +14,14 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { noop, get, addEventListenerWrapper, dataURLToBlob, nextTick, toRealNumber, getTransforms, formatDurationTime } from '@zhst/func';
import { noop, get, addEventListenerWrapper, dataURLToBlob, nextTick, toRealNumber, getTransforms, formatDurationTime
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';
import { useLatest, useUpdateEffect, useFullscreen, useUnmount } from '@zhst/hooks';
import classNames from 'classnames';
import download from 'downloadjs';
import { Button, message } from 'antd';
import { Button, message } from '..';
import Icon from "../iconfont";
import { Cropper, EVENT_CROP_START, EVENT_CROP_END } from "../ImageEditor";
import FlvPlayer, { FLV_EVENT } from "./components/FlvPlayer";

View File

@ -19,6 +19,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import React, { Component } from 'react';
import flvjs from 'flv.js';
// @ts-ignore
import { isEqual } from '@zhst/func';
export var FLV_EVENT = flvjs.Events;
var VideoPlayer = /*#__PURE__*/function (_Component) {

View File

@ -4,14 +4,16 @@ export { default as BigImagePreview } from "./BigImagePreview";
export { default as VideoPlayer } from "./VideoPlayer";
export { default as Tabs } from "./tabs";
export { default as Button } from "./button";
export { default as message } from "./message";
export { default as Space } from "./space";
export { default as Slider } from "./slider";
export { default as Switch } from "./switch";
export { default as Grid } from "./grid";
export { default as Row } from "./row";
export { default as Col } from "./col";
export { default as TimePicker } from "./time-picker";
export { default as DatePicker } from "./date-picker";
export { default as Calender } from "./calendar";
export { default as Calendar } from "./calendar";
export { default as Empty } from "./empty";
export { default as Form } from "./form";
export { default as Select } from "./select";
@ -29,4 +31,14 @@ export { default as Divider } from "./divider";
export { default as Descriptions } from "./descriptions";
export { default as Flex } from "./flex";
export { default as Score } from "./score";
export { default as Progress } from "./progress";
export { default as Progress } from "./progress";
export { default as theme } from "./theme";
export { default as Badge } from "./badge";
export { default as Alert } from "./alert";
export { default as Popover } from "./popover";
export { default as Avatar } from "./avatar";
export { default as Card } from "./card";
export { default as Skeleton } from "./skeleton";
export { default as Tooltip } from "./tooltip";
export { default as Tour } from "./tour";
export { default as Segmented } from "./segmented";

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/meta",
"version": "0.6.0",
"version": "0.7.0",
"description": "原子组件",
"keywords": [
"meta",
@ -81,6 +81,7 @@
"@ant-design/icons": "^5.2.6",
"@ctrl/tinycolor": "^4.0.2",
"@rc-component/mutate-observer": "^1.1.0",
"@rc-component/tour": "^1.12.3",
"@rc-component/trigger": "^1.18.2",
"@turf/boolean-point-in-polygon": "^6.5.0",
"@turf/turf": "^6.5.0",
@ -96,6 +97,7 @@
"dayjs": "^1.11.10",
"downloadjs": "^1.4.7",
"flv.js": "^1.6.2",
"lunar-typescript": "^1.7.3",
"rc-align": "^4.0.15",
"rc-cascader": "~3.20.0",
"rc-checkbox": "~3.1.0",

View File

@ -18,9 +18,10 @@ import {
getTransforms,
addEventListenerWrapper,
getFileByRect
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';
import { Button, Empty } from 'antd';
import { Button, Empty } from '..';
import { type Rect, type IScreenshotButtonProp, type AlignType, type IOdRectOrigin } from '@zhst/types'
import Icon from '../iconfont';
import {

View File

@ -0,0 +1,68 @@
import React, { useRef } from 'react';
import { Button, Space } from 'antd'
import { BigImagePreview } from '@zhst/meta'
const props = {
heigth: '500px',
data: {
imageKey: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
odRect:{
"x":0.553125,"y":0.29722223,"w":0.048958335,"h":0.2462963
},
attachImg: [
{
"url": "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png","label": "形体"
},{
"url": "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png",
"label": "人脸"
}
],
score: 0.815207, // 人脸质量分
showScore: true, // 人脸质量分
cameraPosition: 'string', // 摄像头位置
time: '2022-01-01', // 摄像头拍摄时间
objects: [
{
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.5519352,
"y": 0.2965385,
"w": 0.05185461,
"h": 0.24698898
},
},
},
},
{
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.58543766,
"y": 0.3203356,
"w": 0.052037954,
"h": 0.2664015
},
},
},
},
]
}
}
export default () => {
const imgRef = useRef(null)
return (
<Space size={[8, 16]} direction="vertical">
<BigImagePreview {...props} ref={imgRef} />
<Space>
<Button type="primary" onClick={() => imgRef.current?.setShowCrop(true)}></Button>
<Button onClick={() => imgRef.current?.handleChangeIndex(1)}></Button>
<Button onClick={() => imgRef.current?.setShowCrop(false)}></Button>
</Space>
</Space>
)
}

View File

@ -1,5 +1,5 @@
---
group: 通用
group: 媒体
category: Components
subtitle: 大图预览组件
title: BigImagePreview 大图预览组件
@ -7,180 +7,7 @@ title: BigImagePreview 大图预览组件
# BigImagePreview 大图预览组件
```jsx
import React, { useState, useRef } from 'react';
import { Button, Space } from 'antd'
import { BigImagePreview } from '@zhst/meta'
const props = {
heigth: '500px',
dataSource: [
{
imageKey: 'http://10.0.0.120:30003/file/singer-20240110/1/5/1744894622934503424.jpg',
odRect:{
"x":0.5445312,
"y":0.19166666,
"w":0.08671875,
"h":0.40138888
},
attachImg: [
{
"url": "http://10.0.0.120:30003/file/singer-20240110/1/5/1744894622695428096.jpg","label": "形体"
},{
"url": "http://10.0.0.120:30003/file/singer-20240110/1/5/1744894588427964418.jpg",
"label": "人脸"
}
],
score: '0.6', // 人脸质量分
showScore: true, // 人脸质量分
cameraPosition: 'string', // 摄像头位置
time: '2022-01-01', // 摄像头拍摄时间
objects: [
{
"objectIndex": {
"objectId": "1746832189053474816",
"solutionId": "0",
"deviceId": "0",
"fragmentId": "0"
},
"objectType": "OBJECT_TYPE_PEDESTRAIN",
"sourceObjectId": "0",
"level": 0,
"confidence": 0.881164,
"frameInfo": {
"frameId": "0",
"frameTimestamp": "1705312223057",
"width": 0,
"height": 0,
"originWidth": 0,
"originHeight": 0,
"offsetTime": "0",
"skipNumber": "0"
},
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.61418945,
"y": 0.34309354,
"w": 0.067661405,
"h": 0.34659258
},
},
"countInSource": 0,
"indexInSource": 0
},
"qualityScore": 0,
}
]
},
{
imageKey: 'http://10.0.0.120:30003/file/singer-20240115/1/9/1746795581994436608.jpg',
odRect:{
"x":0.553125,"y":0.29722223,"w":0.048958335,"h":0.2462963
},
attachImg: [
{
"url": "http://10.0.0.120:30003/file/singer-20240115/1/9/1746795581163964416.jpg","label": "形体"
},{
"url": "http://10.0.0.120:30003/file/singer-20240115/1/9/1746795546867140608.jpg",
"label": "人脸"
}
],
score: 0.815207, // 人脸质量分
showScore: true, // 人脸质量分
cameraPosition: 'string', // 摄像头位置
time: '2022-01-01', // 摄像头拍摄时间
objects: [
{
"objectIndex": {
"objectId": "1746816737430472704",
"solutionId": "0",
"deviceId": "0",
"fragmentId": "0"
},
"objectType": "OBJECT_TYPE_PEDESTRAIN",
"sourceObjectId": "0",
"frameInfo": {
"frameId": "0",
"frameTimestamp": "1705308539109",
"width": 0,
"height": 0,
"originWidth": 0,
"originHeight": 0,
"offsetTime": "0",
"skipNumber": "0"
},
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.5519352,
"y": 0.2965385,
"w": 0.05185461,
"h": 0.24698898
},
},
"countInSource": 0,
"indexInSource": 0
},
"qualityScore": 0,
},
{
"objectIndex": {
"objectId": "1746816737430472705",
"solutionId": "0",
"deviceId": "0",
"fragmentId": "0"
},
"objectType": "OBJECT_TYPE_PEDESTRAIN",
"sourceObjectId": "0",
"level": 0,
"confidence": 0.9310699,
"frameInfo": {
"frameId": "0",
"frameTimestamp": "1705308539109",
"width": 0,
"height": 0,
"originWidth": 0,
"originHeight": 0,
"offsetTime": "0",
"skipNumber": "0"
},
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.58543766,
"y": 0.3203356,
"w": 0.052037954,
"h": 0.2664015
},
},
"countInSource": 0,
"indexInSource": 0
},
"qualityScore": 0,
}
]
}
],
}
export default () => {
const imgRef = useRef(null)
return (
<Space size={[8, 16]} direction="vertical">
<BigImagePreview {...props} ref={imgRef} />
<Space>
<Button type="primary" onClick={() => imgRef.current?.setShowCrop(true)}>编辑</Button>
<Button onClick={() => imgRef.current?.handleChangeIndex(1)}>下一张</Button>
<Button onClick={() => imgRef.current?.setShowCrop(false)}>取消</Button>
</Space>
</Space>
)
}
```
<code src="./demo/base.tsx">基本</code>
## API

View File

@ -1,4 +1,5 @@
import React, { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'
// @ts-ignore
import { generateImg, get, addEventListenerWrapper } from '@zhst/func';
import { useUpdateEffect } from '@zhst/hooks';
import Button from '../button';

View File

@ -0,0 +1,30 @@
import React, { useRef } from 'react';
import { CompareImage, Space, Button } from '@zhst/meta'
const props = {
label: "目标图",
openRoll:true,
dataSource:[
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
score: '0.5'
},
{
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
score: '0.8'
},
]
}
export default () => {
const ref = useRef(null)
return (
<Space>
<Button onClick={() => ref.current?.handleIndexChange(-1)}></Button>
<Button onClick={() => ref.current?.handleIndexChange(1)}></Button>
<CompareImage {...props} ref={ref} />
</Space>
)
}

View File

@ -1,5 +1,5 @@
---
group: 通用
group: 媒体
category: Components
subtitle: 图片预览
title: CompareImage 图片预览
@ -7,37 +7,7 @@ title: CompareImage 图片预览
# CompareImage 图片预览
```jsx
import React, { useRef } from 'react';
import { CompareImage, Space, Button } from '@zhst/meta'
const props = {
label: "目标图",
openRoll:true,
dataSource:[
{
url: 'http://10.0.0.120:30003/file/public/public_20240110/file/60f26318-19fc-40b3-b249-91bd2a4da3f3.jpg',
score: '0.5'
},
{
url: 'http://10.0.0.120:30003/file/public/public_20240110/file/7c9b33a8-5106-4982-9d58-a593ad8acbf6.jpg',
score: '0.8'
},
]
}
export default () => {
const ref = useRef(null)
return (
<Space>
<Button onClick={() => ref.current?.handleIndexChange(-1)}>上一张</Button>
<Button onClick={() => ref.current?.handleIndexChange(1)}>下一张</Button>
<CompareImage {...props} ref={ref} />
</Space>
)
}
```
<code src="./demo/basic.tsx">基本</code>
## API

View File

@ -8,13 +8,14 @@ import {
toRealNumber,
getTransforms,
formatDurationTime
// @ts-ignore
} from '@zhst/func';
import Align from 'rc-align';
import { Rect, IScreenshotButtonProp, AlignType } from '@zhst/types'
import { useLatest, useUpdateEffect, useFullscreen, useUnmount } from '@zhst/hooks';
import classNames from 'classnames';
import download from 'downloadjs';
import { Button, message } from 'antd';
import { Button, message } from '..';
import Icon from '../iconfont';
import {
Cropper,

View File

@ -1,5 +1,6 @@
import React, { Component, CSSProperties } from 'react';
import flvjs from 'flv.js';
// @ts-ignore
import { isEqual } from '@zhst/func';
export const FLV_EVENT = flvjs.Events;

View File

@ -0,0 +1,18 @@
import React, { useRef, useState } from 'react';
import { VideoPlayer, Space, Button } from '@zhst/meta'
export default () => {
const videoRef = useRef(null)
const [url, setUrl] = useState(null)
return (
<Space>
<Space>
<Button onClick={() => setUrl('ws://10.0.0.7:9033/flv/File/test/test_h264_1.mp4.flv?ip=127.0.0.1')}></Button>
<Button onClick={() => videoRef.current?.setShowCrop(true)}></Button>
<Button onClick={() => videoRef.current?.setShowCrop(false)}></Button>
</Space>
{url && <VideoPlayer ref={videoRef} url={url} />}
</Space>
)
}

View File

@ -1,5 +1,5 @@
---
group: 通用
group: 媒体
category: Components
subtitle: 视频播放
title: VideoPlayer 视频播放
@ -13,24 +13,7 @@ demo:
播放进度条操作还未完善,在播放结束和拖拽的过程中都有一定程度卡顿,目前逻辑待优化
:::
```jsx
import React, { useRef, useState } from 'react';
import { VideoPlayer, Space, Button } from '@zhst/meta'
export default () => {
const videoRef = useRef(null)
const [url, setUrl] = useState(null)
return (
<Space>
<Button onClick={() => setUrl('ws://10.0.0.7:9033/flv/File/test/test_h264_1.mp4.flv?ip=127.0.0.1')}>播放</Button>
<Button onClick={() => videoRef.current?.setShowCrop(true)}>截图</Button>
<Button onClick={() => videoRef.current?.setShowCrop(false)}>取消</Button>
<VideoPlayer ref={videoRef} url={url} />
</Space>
)
}
```
<code src="./demo/basic.tsx">基础</code>
<code src="./demo/multiple.tsx">测试视频播放压力</code>

View File

@ -0,0 +1,228 @@
import type { ReactElement } from 'react';
import * as React from 'react';
import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
import InfoCircleFilled from '@ant-design/icons/InfoCircleFilled';
import classNames from 'classnames';
import CSSMotion from 'rc-motion';
import pickAttrs from 'rc-util/lib/pickAttrs';
import { replaceElement } from '../_util/reactNode';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import useStyle from './style';
export interface AlertProps {
/** Type of Alert styles, options:`success`, `info`, `warning`, `error` */
type?: 'success' | 'info' | 'warning' | 'error';
/** Whether Alert can be closed */
closable?: boolean;
/**
* @deprecated please use `closeIcon` instead.
* Close text to show
*/
closeText?: React.ReactNode;
/** Content of Alert */
message?: React.ReactNode;
/** Additional content of Alert */
description?: React.ReactNode;
/** Callback when close Alert */
onClose?: React.MouseEventHandler<HTMLButtonElement>;
/** Trigger when animation ending of Alert */
afterClose?: () => void;
/** Whether to show icon */
showIcon?: boolean;
/** https://www.w3.org/TR/2014/REC-html5-20141028/dom.html#aria-role-attribute */
role?: string;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
rootClassName?: string;
banner?: boolean;
icon?: React.ReactNode;
/** Custom closeIcon */
closeIcon?: boolean | React.ReactNode;
action?: React.ReactNode;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
const iconMapFilled = {
success: CheckCircleFilled,
info: InfoCircleFilled,
error: CloseCircleFilled,
warning: ExclamationCircleFilled,
};
interface IconNodeProps {
type: AlertProps['type'];
icon: AlertProps['icon'];
prefixCls: AlertProps['prefixCls'];
description: AlertProps['description'];
}
const IconNode: React.FC<IconNodeProps> = (props) => {
const { icon, prefixCls, type } = props;
const iconType = iconMapFilled[type!] || null;
if (icon) {
return replaceElement(icon, <span className={`${prefixCls}-icon`}>{icon}</span>, () => ({
className: classNames(`${prefixCls}-icon`, {
[(icon as ReactElement).props.className]: (icon as ReactElement).props.className,
}),
})) as ReactElement;
}
return React.createElement(iconType, { className: `${prefixCls}-icon` });
};
interface CloseIconProps {
isClosable: boolean;
prefixCls: AlertProps['prefixCls'];
closeIcon: AlertProps['closeIcon'];
handleClose: AlertProps['onClose'];
}
const CloseIcon: React.FC<CloseIconProps> = (props) => {
const { isClosable, prefixCls, closeIcon, handleClose } = props;
const mergedCloseIcon =
closeIcon === true || closeIcon === undefined ? <CloseOutlined /> : closeIcon;
return isClosable ? (
<button type="button" onClick={handleClose} className={`${prefixCls}-close-icon`} tabIndex={0}>
{mergedCloseIcon}
</button>
) : null;
};
const Alert: React.FC<AlertProps> = (props) => {
const {
description,
prefixCls: customizePrefixCls,
message,
banner,
className,
rootClassName,
style,
onMouseEnter,
onMouseLeave,
onClick,
afterClose,
showIcon,
closable,
closeText,
closeIcon,
action,
...otherProps
} = props;
const [closed, setClosed] = React.useState(false);
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Alert');
warning.deprecated(!closeText, 'closeText', 'closeIcon');
}
const ref = React.useRef<HTMLDivElement>(null);
const { getPrefixCls, direction, alert } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('alert', customizePrefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
setClosed(true);
props.onClose?.(e);
};
const type = React.useMemo<AlertProps['type']>(() => {
if (props.type !== undefined) {
return props.type;
}
// banner mode defaults to 'warning'
return banner ? 'warning' : 'info';
}, [props.type, banner]);
// closeable when closeText or closeIcon is assigned
const isClosable = React.useMemo(() => {
if (closeText) {
return true;
}
if (typeof closable === 'boolean') {
return closable;
}
// should be true when closeIcon is 0 or ''
return closeIcon !== false && closeIcon !== null && closeIcon !== undefined;
}, [closeText, closeIcon, closable]);
// banner mode defaults to Icon
const isShowIcon = banner && showIcon === undefined ? true : showIcon;
const alertCls = classNames(
prefixCls,
`${prefixCls}-${type}`,
{
[`${prefixCls}-with-description`]: !!description,
[`${prefixCls}-no-icon`]: !isShowIcon,
[`${prefixCls}-banner`]: !!banner,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
alert?.className,
className,
rootClassName,
cssVarCls,
hashId,
);
const restProps = pickAttrs(otherProps, { aria: true, data: true });
return wrapCSSVar(
<CSSMotion
visible={!closed}
motionName={`${prefixCls}-motion`}
motionAppear={false}
motionEnter={false}
onLeaveStart={(node) => ({ maxHeight: node.offsetHeight })}
onLeaveEnd={afterClose}
>
{({ className: motionClassName, style: motionStyle }) => (
<div
ref={ref}
data-show={!closed}
className={classNames(alertCls, motionClassName)}
style={{ ...alert?.style, ...style, ...motionStyle }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick}
role="alert"
{...restProps}
>
{isShowIcon ? (
<IconNode
description={description}
icon={props.icon}
prefixCls={prefixCls}
type={type}
/>
) : null}
<div className={`${prefixCls}-content`}>
{message ? <div className={`${prefixCls}-message`}>{message}</div> : null}
{description ? <div className={`${prefixCls}-description`}>{description}</div> : null}
</div>
{action ? <div className={`${prefixCls}-action`}>{action}</div> : null}
<CloseIcon
isClosable={isClosable}
prefixCls={prefixCls}
closeIcon={closeText || closeIcon}
handleClose={handleClose}
/>
</div>
)}
</CSSMotion>,
);
};
if (process.env.NODE_ENV !== 'production') {
Alert.displayName = 'Alert';
}
export default Alert;

View File

@ -0,0 +1,50 @@
import * as React from 'react';
import Alert from './Alert';
interface ErrorBoundaryProps {
message?: React.ReactNode;
description?: React.ReactNode;
children?: React.ReactNode;
}
interface ErrorBoundaryStates {
error?: Error | null;
info?: {
componentStack?: string;
};
}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryStates> {
state = {
error: undefined,
info: {
componentStack: '',
},
};
componentDidCatch(error: Error | null, info: object) {
this.setState({ error, info });
}
render() {
const { message, description, children } = this.props;
const { error, info } = this.state;
const componentStack = info && info.componentStack ? info.componentStack : null;
const errorMessage = typeof message === 'undefined' ? (error || '').toString() : message;
const errorDescription = typeof description === 'undefined' ? componentStack : description;
if (error) {
return (
<Alert
type="error"
message={errorMessage}
description={
<pre style={{ fontSize: '0.9em', overflowX: 'auto' }}>{errorDescription}</pre>
}
/>
);
}
return children;
}
}
export default ErrorBoundary;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert custom action 1`] = `
<div
class="ant-alert ant-alert-success"
data-show="true"
role="alert"
>
<span
aria-label="check-circle"
class="anticon anticon-check-circle ant-alert-icon"
role="img"
>
<svg
aria-hidden="true"
data-icon="check-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
/>
</svg>
</span>
<div
class="ant-alert-content"
>
<div
class="ant-alert-message"
>
Success Tips
</div>
</div>
<div
class="ant-alert-action"
>
<button
class="ant-btn ant-btn-text ant-btn-sm"
type="button"
>
<span>
UNDO
</span>
</button>
</div>
<button
class="ant-alert-close-icon"
tabindex="0"
type="button"
>
<span
aria-label="close"
class="anticon anticon-close"
role="img"
>
<svg
aria-hidden="true"
data-icon="close"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
/>
</svg>
</span>
</button>
</div>
`;
exports[`Alert rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-alert ant-alert-info ant-alert-no-icon ant-alert-rtl"
data-show="true"
role="alert"
>
<div
class="ant-alert-content"
/>
</div>
`;

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('alert', { skip: ['loop-banner.tsx', 'component-token.tsx'] });

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('alert', { skip: ['loop-banner.tsx', 'component-token.tsx'] });

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Alert image', () => {
imageDemoTest('alert');
});

View File

@ -0,0 +1,171 @@
import userEvent from '@testing-library/user-event';
import { resetWarned } from 'rc-util/lib/warning';
import React from 'react';
import Alert from '..';
import accessibilityTest from '../../../tests/shared/accessibilityTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, render, screen } from '../../../tests/utils';
import Button from '../../button';
import Popconfirm from '../../popconfirm';
import Tooltip from '../../tooltip';
const { ErrorBoundary } = Alert;
describe('Alert', () => {
rtlTest(Alert);
accessibilityTest(Alert);
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('should show close button and could be closed', async () => {
const onClose = jest.fn();
render(
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
closable
onClose={onClose}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /close/i }));
act(() => {
jest.runAllTimers();
});
expect(onClose).toHaveBeenCalledTimes(1);
});
it('custom action', () => {
const { container } = render(
<Alert
message="Success Tips"
type="success"
showIcon
action={
<Button size="small" type="text">
UNDO
</Button>
}
closable
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it('should sets data attributes on alert when pass attributes to props', () => {
render(
<Alert data-test="test-id" data-id="12345" aria-describedby="some-label" message={null} />,
);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('data-test', 'test-id');
expect(alert).toHaveAttribute('data-id', '12345');
expect(alert).toHaveAttribute('aria-describedby', 'some-label');
});
it('sets role attribute on input', () => {
render(<Alert role="status" message={null} />);
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('should show error as ErrorBoundary when children have error', () => {
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
expect(warnSpy).toHaveBeenCalledTimes(0);
// @ts-expect-error
// eslint-disable-next-line react/jsx-no-undef
const ThrowError = () => <NotExisted />;
render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
expect(screen.getByRole('alert')).toHaveTextContent(
'ReferenceError: NotExisted is not defined',
);
warnSpy.mockRestore();
});
it('could be used with Tooltip', async () => {
render(
<Tooltip title="xxx" mouseEnterDelay={0}>
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
/>
</Tooltip>,
);
await userEvent.hover(screen.getByRole('alert'));
act(() => {
jest.runAllTimers();
});
expect(screen.getByRole('tooltip')).toBeInTheDocument();
});
it('could be used with Popconfirm', async () => {
render(
<Popconfirm title="xxx">
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
/>
</Popconfirm>,
);
await userEvent.click(screen.getByRole('alert'));
act(() => {
jest.runAllTimers();
});
expect(screen.getByRole('tooltip')).toBeInTheDocument();
});
it('could accept none react element icon', () => {
render(<Alert message="Success Tips" type="success" showIcon icon="icon" />);
expect(screen.getByRole('alert')).toHaveTextContent(/success tips/i);
expect(screen.getByRole('alert')).toHaveTextContent(/icon/i);
});
it('should not render message div when no message', () => {
const { container } = render(<Alert description="description" />);
expect(!!container.querySelector('.ant-alert-message')).toBe(false);
});
it('close button should be hidden when closeIcon setting to null or false', () => {
const { container, rerender } = render(<Alert closeIcon={null} />);
expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy();
rerender(<Alert closeIcon={false} />);
expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy();
rerender(<Alert closeIcon />);
expect(container.querySelector('.ant-alert-close-icon')).toBeTruthy();
rerender(<Alert />);
expect(container.querySelector('.ant-alert-close-icon')).toBeFalsy();
});
it('should warning when using closeText', () => {
resetWarned();
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { container } = render(<Alert closeText="close" />);
expect(warnSpy).toHaveBeenCalledWith(
`Warning: [antd: Alert] \`closeText\` is deprecated. Please use \`closeIcon\` instead.`,
);
expect(container.querySelector('.ant-alert-close-icon')?.textContent).toBe('close');
warnSpy.mockRestore();
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
可以在右上角自定义操作项。
## en-US
Custom action.

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Alert, Button, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
message="Success Tips"
type="success"
showIcon
action={
<Button size="small" type="text">
UNDO
</Button>
}
closable
/>
<Alert
message="Error Text"
showIcon
description="Error Description Error Description Error Description Error Description"
type="error"
action={
<Button size="small" danger>
Detail
</Button>
}
/>
<Alert
message="Warning Text"
type="warning"
action={
<Space>
<Button type="text" size="small" ghost>
Done
</Button>
</Space>
}
closable
/>
<Alert
message="Info Text"
description="Info Description Info Description Info Description Info Description"
type="info"
action={
<Space direction="vertical">
<Button size="small" type="primary">
Accept
</Button>
<Button size="small" danger ghost>
Decline
</Button>
</Space>
}
closable
/>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
页面顶部通告形式,默认有图标且 `type` 为 'warning'。
## en-US
Display Alert as a banner at top of page.

View File

@ -0,0 +1,17 @@
import React from 'react';
import { Alert, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert message="Warning text" banner />
<Alert
message="Very long warning text warning text text text text text text text"
banner
closable
/>
<Alert showIcon={false} message="Warning text without icon" banner />
<Alert type="error" message="Error text" banner />
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
最简单的用法,适用于简短的警告提示。
## en-US
The simplest usage for short messages.

View File

@ -0,0 +1,6 @@
import React from 'react';
import { Alert } from '@zhst/meta';
const App: React.FC = () => <Alert message="Success Text" type="success" />;
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
显示关闭按钮,点击可关闭警告提示。
## en-US
To show close button.

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Alert, Space } from '@zhst/meta';
const onClose = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
console.log(e, 'I was closed.');
};
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
closable
onClose={onClose}
/>
<Alert
message="Error Text"
description="Error Description Error Description Error Description Error Description Error Description Error Description"
type="error"
closable
onClose={onClose}
/>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义组件 Token。
## en-US
Custom component token.

View File

@ -0,0 +1,28 @@
import { SmileOutlined } from '@ant-design/icons';
import React from 'react';
import { Alert, ConfigProvider } from '@zhst/meta';
const icon = <SmileOutlined />;
const App: React.FC = () => (
<ConfigProvider
theme={{
components: {
Alert: {
withDescriptionIconSize: 32,
withDescriptionPadding: 16,
},
},
}}
>
<Alert
icon={icon}
message="Success Tips"
description="Detailed description and advices about successful copywriting."
type="success"
showIcon
/>
</ConfigProvider>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
可口的图标让信息类型更加醒目。
## en-US
A relevant icon makes information clearer and more friendly.

View File

@ -0,0 +1,45 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import { Alert, Space } from '@zhst/meta';
const icon = <SmileOutlined />;
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert icon={icon} message="showIcon = false" type="success" />
<Alert icon={icon} message="Success Tips" type="success" showIcon />
<Alert icon={icon} message="Informational Notes" type="info" showIcon />
<Alert icon={icon} message="Warning" type="warning" showIcon />
<Alert icon={icon} message="Error" type="error" showIcon />
<Alert
icon={icon}
message="Success Tips"
description="Detailed description and advices about successful copywriting."
type="success"
showIcon
/>
<Alert
icon={icon}
message="Informational Notes"
description="Additional description and informations about copywriting."
type="info"
showIcon
/>
<Alert
icon={icon}
message="Warning"
description="This is a warning notice about copywriting."
type="warning"
showIcon
/>
<Alert
icon={icon}
message="Error"
description="This is an error message about copywriting."
type="error"
showIcon
/>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
含有辅助性文字介绍的警告提示。
## en-US
Additional description for alert message.

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Alert, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
message="Success Text"
description="Success Description Success Description Success Description"
type="success"
/>
<Alert
message="Info Text"
description="Info Description Info Description Info Description Info Description"
type="info"
/>
<Alert
message="Warning Text"
description="Warning Description Warning Description Warning Description Warning Description"
type="warning"
/>
<Alert
message="Error Text"
description="Error Description Error Description Error Description Error Description"
type="error"
/>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
友好的 [React 错误处理](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) 包裹组件。
## en-US
ErrorBoundary Component for making error handling easier in [React](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html).

View File

@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { Alert, Button } from '@zhst/meta';
const { ErrorBoundary } = Alert;
const ThrowError: React.FC = () => {
const [error, setError] = useState<Error>();
const onClick = () => {
setError(new Error('An Uncaught Error'));
};
if (error) {
throw error;
}
return (
<Button danger onClick={onClick}>
Click me to throw a error
</Button>
);
};
const App: React.FC = () => (
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
可口的图标让信息类型更加醒目。
## en-US
A relevant icon will make information clearer and more friendly.

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Alert, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert message="Success Tips" type="success" showIcon />
<Alert message="Informational Notes" type="info" showIcon />
<Alert message="Warning" type="warning" showIcon closable />
<Alert message="Error" type="error" showIcon />
<Alert
message="Success Tips"
description="Detailed description and advice about successful copywriting."
type="success"
showIcon
/>
<Alert
message="Informational Notes"
description="Additional description and information about copywriting."
type="info"
showIcon
/>
<Alert
message="Warning"
description="This is a warning notice about copywriting."
type="warning"
showIcon
closable
/>
<Alert
message="Error"
description="This is an error message about copywriting."
type="error"
showIcon
/>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
配合 [react-text-loop-next](https://npmjs.com/package/react-text-loop-next) 或 [react-fast-marquee](https://npmjs.com/package/react-fast-marquee) 实现消息轮播通知栏。
## en-US
Show a loop banner by using with [react-text-loop-next](https://npmjs.com/package/react-text-loop-next) or [react-fast-marquee](https://npmjs.com/package/react-fast-marquee).

View File

@ -0,0 +1,16 @@
import React from 'react';
import Marquee from 'react-fast-marquee';
import { Alert } from '@zhst/meta';
const App: React.FC = () => (
<Alert
banner
message={
<Marquee pauseOnHover gradient={false}>
I can be a React component, multiple React components, or just some text.
</Marquee>
}
/>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
平滑、自然的卸载提示。
## en-US
Smoothly unmount Alert upon close.

View File

@ -0,0 +1,22 @@
import React, { useState } from 'react';
import { Alert, Switch, Space } from '@zhst/meta';
const App: React.FC = () => {
const [visible, setVisible] = useState(true);
const handleClose = () => {
setVisible(false);
};
return (
<Space direction="vertical" style={{ width: '100%' }}>
{visible && (
<Alert message="Alert Message Text" type="success" closable afterClose={handleClose} />
)}
<p>click the close button to see the effect</p>
<Switch onChange={setVisible} checked={visible} disabled={visible} />
</Space>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
共有四种样式 `success`、`info`、`warning`、`error`。
## en-US
There are 4 types of Alert: `success`, `info`, `warning`, `error`.

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Alert, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert message="Success Text" type="success" />
<Alert message="Info Text" type="info" />
<Alert message="Warning Text" type="warning" />
<Alert message="Error Text" type="error" />
</Space>
);
export default App;

View File

@ -0,0 +1,16 @@
import type React from 'react';
import type { AlertProps } from './Alert';
import InternalAlert from './Alert';
import ErrorBoundary from './ErrorBoundary';
export type { AlertProps } from './Alert';
type CompoundedComponent = React.FC<AlertProps> & {
ErrorBoundary: typeof ErrorBoundary;
};
const Alert = InternalAlert as CompoundedComponent;
Alert.ErrorBoundary = ErrorBoundary;
export default Alert;

View File

@ -0,0 +1,57 @@
---
category: Components
subtitle: 警告提示
title: Alert 警告提示
demo:
cols: 2
group:
title: 反馈
order: 6
---
警告提示,展现需要关注的信息。
## 何时使用
- 当某个页面需要向用户显示警告的信息时。
- 非浮层的静态展现形式,始终展现,不会自动消失,用户可以点击关闭。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">基本</code>
<code src="./demo/style.tsx">四种样式</code>
<code src="./demo/closable.tsx">可关闭的警告提示</code>
<code src="./demo/description.tsx">含有辅助性文字介绍</code>
<code src="./demo/icon.tsx">图标</code>
<code src="./demo/banner.tsx" iframe="250">顶部公告</code>
<!-- <code src="./demo/loop-banner.tsx">轮播的公告</code> -->
<code src="./demo/smooth-closed.tsx">平滑地卸载</code>
<code src="./demo/error-boundary.tsx">React 错误处理</code>
<code src="./demo/custom-icon.tsx" debug>自定义图标</code>
<code src="./demo/action.tsx">操作</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
## API
通用属性参考:[通用属性](/docs/react/common-props)
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| action | 自定义操作项 | ReactNode | - | 4.9.0 |
| afterClose | 关闭动画结束后触发的回调函数 | () => void | - | |
| banner | 是否用作顶部公告 | boolean | false | |
| closeIcon | 自定义关闭 Icon>=5.7.0: 设置为 `null``false` 时隐藏关闭按钮 | boolean \| ReactNode | `<CloseOutlined />` | |
| description | 警告提示的辅助性文字介绍 | ReactNode | - | |
| icon | 自定义图标,`showIcon` 为 true 时有效 | ReactNode | - | |
| message | 警告提示内容 | ReactNode | - | |
| showIcon | 是否显示辅助图标 | boolean | false`banner` 模式下默认值为 true | |
| type | 指定警告提示的样式,有四种选择 `success`、`info`、`warning`、`error` | string | `info``banner` 模式下默认值为 `warning` | |
| onClose | 关闭时触发的回调函数 | (e: MouseEvent) => void | - | |
### Alert.ErrorBoundary
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| description | 自定义错误内容,如果未指定会展示报错堆栈 | ReactNode | {{ error stack }} | |
| message | 自定义错误标题,如果未指定会展示原生报错信息 | ReactNode | {{ error }} | |

View File

@ -0,0 +1,260 @@
import type { CSSProperties } from 'react';
import type { CSSObject } from '@ant-design/cssinjs';
import { unit } from '@ant-design/cssinjs';
import { resetComponent } from '../../style';
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import { genStyleHooks } from '../../theme/internal';
export interface ComponentToken {
// Component token here
/**
* @desc
* @descEN Default padding
*/
defaultPadding: CSSProperties['padding'];
/**
* @desc
* @descEN Padding with description
*/
withDescriptionPadding: CSSProperties['padding'];
/**
* @desc
* @descEN Icon size with description
*/
withDescriptionIconSize: number;
}
// @ts-ignore
type AlertToken = FullToken<'Alert'> & {
// Custom token here
};
const genAlertTypeStyle = (
bgColor: string,
borderColor: string,
iconColor: string,
token: AlertToken,
alertCls: string,
): CSSObject => ({
background: bgColor,
border: `${unit(token.lineWidth)} ${token.lineType} ${borderColor}`,
[`${alertCls}-icon`]: {
color: iconColor,
},
});
export const genBaseStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSObject => {
const {
componentCls,
motionDurationSlow: duration,
marginXS,
marginSM,
fontSize,
fontSizeLG,
lineHeight,
borderRadiusLG: borderRadius,
motionEaseInOutCirc,
// @ts-ignore
withDescriptionIconSize,
colorText,
colorTextHeading,
// @ts-ignore
withDescriptionPadding,
// @ts-ignore
defaultPadding,
} = token;
return {
[componentCls]: {
...resetComponent(token),
position: 'relative',
display: 'flex',
alignItems: 'center',
padding: defaultPadding,
wordWrap: 'break-word',
borderRadius,
[`&${componentCls}-rtl`]: {
direction: 'rtl',
},
[`${componentCls}-content`]: {
flex: 1,
minWidth: 0,
},
[`${componentCls}-icon`]: {
marginInlineEnd: marginXS,
lineHeight: 0,
},
[`&-description`]: {
display: 'none',
fontSize,
lineHeight,
},
'&-message': {
color: colorTextHeading,
},
[`&${componentCls}-motion-leave`]: {
overflow: 'hidden',
opacity: 1,
transition: `max-height ${duration} ${motionEaseInOutCirc}, opacity ${duration} ${motionEaseInOutCirc},
padding-top ${duration} ${motionEaseInOutCirc}, padding-bottom ${duration} ${motionEaseInOutCirc},
margin-bottom ${duration} ${motionEaseInOutCirc}`,
},
[`&${componentCls}-motion-leave-active`]: {
maxHeight: 0,
marginBottom: '0 !important',
paddingTop: 0,
paddingBottom: 0,
opacity: 0,
},
},
[`${componentCls}-with-description`]: {
alignItems: 'flex-start',
padding: withDescriptionPadding,
[`${componentCls}-icon`]: {
marginInlineEnd: marginSM,
fontSize: withDescriptionIconSize,
lineHeight: 0,
},
[`${componentCls}-message`]: {
display: 'block',
marginBottom: marginXS,
color: colorTextHeading,
fontSize: fontSizeLG,
},
[`${componentCls}-description`]: {
display: 'block',
color: colorText,
},
},
[`${componentCls}-banner`]: {
marginBottom: 0,
border: '0 !important',
borderRadius: 0,
},
};
};
export const genTypeStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSObject => {
const {
componentCls,
colorSuccess,
colorSuccessBorder,
colorSuccessBg,
colorWarning,
colorWarningBorder,
colorWarningBg,
colorError,
colorErrorBorder,
colorErrorBg,
colorInfo,
colorInfoBorder,
colorInfoBg,
} = token;
return {
[componentCls]: {
'&-success': genAlertTypeStyle(
colorSuccessBg,
colorSuccessBorder,
colorSuccess,
token,
componentCls,
),
'&-info': genAlertTypeStyle(colorInfoBg, colorInfoBorder, colorInfo, token, componentCls),
'&-warning': genAlertTypeStyle(
colorWarningBg,
colorWarningBorder,
colorWarning,
token,
componentCls,
),
'&-error': {
...genAlertTypeStyle(colorErrorBg, colorErrorBorder, colorError, token, componentCls),
[`${componentCls}-description > pre`]: {
margin: 0,
padding: 0,
},
},
},
};
};
export const genActionStyle: GenerateStyle<AlertToken> = (token: AlertToken): CSSObject => {
const {
componentCls,
iconCls,
motionDurationMid,
marginXS,
fontSizeIcon,
colorIcon,
colorIconHover,
} = token;
return {
[componentCls]: {
[`&-action`]: {
marginInlineStart: marginXS,
},
[`${componentCls}-close-icon`]: {
marginInlineStart: marginXS,
padding: 0,
overflow: 'hidden',
fontSize: fontSizeIcon,
lineHeight: unit(fontSizeIcon),
backgroundColor: 'transparent',
border: 'none',
outline: 'none',
cursor: 'pointer',
[`${iconCls}-close`]: {
color: colorIcon,
transition: `color ${motionDurationMid}`,
'&:hover': {
color: colorIconHover,
},
},
},
'&-close-text': {
color: colorIcon,
transition: `color ${motionDurationMid}`,
'&:hover': {
color: colorIconHover,
},
},
},
};
};
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Alert'> = (token) => {
const paddingHorizontal = 12; // Fixed value here.
return {
withDescriptionIconSize: token.fontSizeHeading3,
defaultPadding: `${token.paddingContentVerticalSM}px ${paddingHorizontal}px`,
withDescriptionPadding: `${token.paddingMD}px ${token.paddingContentHorizontalLG}px`,
};
};
export default genStyleHooks(
// @ts-ignore
'Alert',
(token) => [genBaseStyle(token), genTypeStyle(token), genActionStyle(token)],
prepareComponentToken,
);

View File

@ -0,0 +1,13 @@
import * as React from 'react';
import type { ScreenSizeMap } from '../_util/responsiveObserver';
export type AvatarSize = 'large' | 'small' | 'default' | number | ScreenSizeMap;
export interface AvatarContextType {
size?: AvatarSize;
shape?: 'circle' | 'square';
}
const AvatarContext = React.createContext<AvatarContextType>({});
export default AvatarContext;

View File

@ -0,0 +1,224 @@
import React, { useState } from 'react';
import Avatar from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render } from '../../../tests/utils';
import useBreakpoint from '../../grid/hooks/useBreakpoint';
import ConfigProvider from '../../config-provider';
jest.mock('../../grid/hooks/useBreakpoint');
describe('Avatar Render', () => {
mountTest(Avatar);
rtlTest(Avatar);
const sizes = { xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 };
let originOffsetWidth: PropertyDescriptor['get'];
beforeAll(() => {
// Mock offsetHeight
originOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')?.get;
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get() {
if (this.className === 'ant-avatar-string') {
return 100;
}
return 80;
},
});
});
afterAll(() => {
// Restore Mock offsetHeight
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get: originOffsetWidth,
});
});
it('Render long string correctly', () => {
const { container } = render(<Avatar>TestString</Avatar>);
expect(container.querySelectorAll('.ant-avatar-string').length).toBe(1);
});
it('should render fallback string correctly', () => {
const div = global.document.createElement('div');
global.document.body.appendChild(div);
const { container } = render(<Avatar src="http://error.url">Fallback</Avatar>);
fireEvent.error(container.querySelector('img')!);
const children = container.querySelectorAll('.ant-avatar-string');
expect(children.length).toBe(1);
expect(children[0].innerHTML).toBe('Fallback');
global.document.body.removeChild(div);
});
it('should handle onError correctly', () => {
const LOAD_FAILURE_SRC = 'http://error.url/';
const LOAD_SUCCESS_SRC = 'https://api.dicebear.com/7.x/pixel-art/svg';
const Foo: React.FC = () => {
const [avatarSrc, setAvatarSrc] = useState<typeof LOAD_FAILURE_SRC | typeof LOAD_SUCCESS_SRC>(
LOAD_FAILURE_SRC,
);
const onError = (): boolean => {
setAvatarSrc(LOAD_SUCCESS_SRC);
return false;
};
return <Avatar src={avatarSrc} onError={onError} />;
};
const { container } = render(<Foo />);
expect(container.querySelector('img')?.src).toBe(LOAD_FAILURE_SRC);
// mock img load Error, since jsdom do not load resource by default
// https://github.com/jsdom/jsdom/issues/1816
fireEvent.error(container.querySelector('img')!);
expect(container.querySelector('img')?.src).toBe(LOAD_SUCCESS_SRC);
expect(container.firstChild).toMatchSnapshot();
});
it('should show image on success after a failure state', () => {
const LOAD_FAILURE_SRC = 'http://error.url';
const LOAD_SUCCESS_SRC = 'https://api.dicebear.com/7.x/pixel-art/svg';
const div = global.document.createElement('div');
global.document.body.appendChild(div);
// simulate error src url
const { container, rerender } = render(<Avatar src={LOAD_FAILURE_SRC}>Fallback</Avatar>);
fireEvent.error(container.querySelector('img')!);
expect(container.firstChild).toMatchSnapshot();
expect(container.querySelectorAll('.ant-avatar-string').length).toBe(1);
// children should show, when image load error without onError return false
expect(container.querySelector<HTMLDivElement>('.ant-avatar-string')?.style).not.toHaveProperty(
'opacity',
0,
);
// simulate successful src url
rerender(<Avatar src={LOAD_SUCCESS_SRC}>Fallback</Avatar>);
expect(container.firstChild).toMatchSnapshot();
expect(container.querySelectorAll('.ant-avatar-image').length).toBe(1);
global.document.body.removeChild(div);
});
it('should calculate scale of avatar children correctly', () => {
const { container, rerender } = render(<Avatar>Avatar</Avatar>);
expect(container.querySelector('.ant-avatar-string')).toMatchSnapshot();
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
get() {
return this.className === 'ant-avatar-string' ? 100 : 40;
},
});
rerender(<Avatar>xx</Avatar>);
expect(container.querySelector('.ant-avatar-string')).toMatchSnapshot();
});
it('should calculate scale of avatar children correctly with gap', () => {
const { container } = render(<Avatar gap={2}>Avatar</Avatar>);
expect(container.querySelector('.ant-avatar-string')).toMatchSnapshot();
});
it('should warning when pass a string as icon props', () => {
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Avatar size={64} icon="aa" />);
expect(warnSpy).not.toHaveBeenCalled();
render(<Avatar size={64} icon="user" />);
expect(warnSpy).toHaveBeenCalledWith(
`Warning: [antd: Avatar] \`icon\` is using ReactNode instead of string naming in v4. Please check \`user\` at https://ant.design/components/icon`,
);
warnSpy.mockRestore();
});
it('support size is number', () => {
const { container } = render(<Avatar size={100}>TestString</Avatar>);
expect(container.firstChild).toMatchSnapshot();
});
Object.entries(sizes).forEach(([key, value]) => {
it(`adjusts component size to ${value} when window size is ${key}`, () => {
(useBreakpoint as any).mockReturnValue({ [key]: true });
const { container } = render(<Avatar size={sizes} />);
expect(container).toMatchSnapshot();
});
});
it('support onMouseEnter', () => {
const onMouseEnter = jest.fn();
const { container } = render(<Avatar {...{ onMouseEnter }}>TestString</Avatar>);
fireEvent.mouseEnter(container.firstChild!);
expect(onMouseEnter).toHaveBeenCalled();
});
it('fallback', () => {
const div = global.document.createElement('div');
global.document.body.appendChild(div);
const { container } = render(
<Avatar shape="circle" src="http://error.url">
A
</Avatar>,
);
fireEvent.error(container.querySelector('img')!);
expect(container.firstChild).toMatchSnapshot();
global.document.body.removeChild(div);
});
it('should exist crossorigin attribute', () => {
const LOAD_SUCCESS_SRC = 'https://api.dicebear.com/7.x/pixel-art/svg';
const crossOrigin = 'anonymous';
const { container } = render(
<Avatar src={LOAD_SUCCESS_SRC} crossOrigin={crossOrigin}>
crossorigin
</Avatar>,
);
expect(container.querySelector('img')?.crossOrigin).toBeTruthy();
expect(container.querySelector('img')?.crossOrigin).toEqual(crossOrigin);
});
it('should not exist crossorigin attribute', () => {
const LOAD_SUCCESS_SRC = 'https://api.dicebear.com/7.x/pixel-art/svg';
const { container } = render(<Avatar src={LOAD_SUCCESS_SRC}>crossorigin</Avatar>);
expect(container.querySelector('img')?.crossOrigin).toBeFalsy();
expect(container.querySelector('img')?.crossOrigin).toEqual('');
});
it('clickable', () => {
const onClick = jest.fn();
const { container } = render(<Avatar onClick={onClick}>TestString</Avatar>);
fireEvent.click(container.querySelector('.ant-avatar-string')!);
expect(onClick).toHaveBeenCalled();
});
it('Avatar.Group support shape props', () => {
const { container } = render(
<Avatar.Group shape="square">
<Avatar>A</Avatar>
<Avatar shape="circle">B</Avatar>
<Avatar>C</Avatar>
<Avatar shape="circle">D</Avatar>
</Avatar.Group>,
);
const avatars = container?.querySelectorAll<HTMLSpanElement>('.ant-avatar-group .ant-avatar');
expect(avatars?.[0]).toHaveClass('ant-avatar-square');
expect(avatars?.[1]).toHaveClass('ant-avatar-circle');
expect(avatars?.[2]).toHaveClass('ant-avatar-square');
expect(avatars?.[3]).toHaveClass('ant-avatar-circle');
});
it('should apply the componentSize of CP', () => {
const { container } = render(
<>
<ConfigProvider componentSize="small">
<Avatar>test</Avatar>
</ConfigProvider>
<ConfigProvider componentSize="large">
<Avatar>test</Avatar>
</ConfigProvider>
</>,
);
expect(container.querySelector('.ant-avatar-sm')).toBeTruthy();
expect(container.querySelector('.ant-avatar-lg')).toBeTruthy();
});
});

View File

@ -0,0 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Avatar Render adjusts component size to 24 when window size is xs 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 24px; height: 24px; line-height: 24px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 32 when window size is sm 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 32px; height: 32px; line-height: 32px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 40 when window size is md 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 40px; height: 40px; line-height: 40px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 64 when window size is lg 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 64px; height: 64px; line-height: 64px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 80 when window size is xl 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 80px; height: 80px; line-height: 80px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render adjusts component size to 100 when window size is xxl 1`] = `
<div>
<span
class="ant-avatar ant-avatar-circle"
style="width: 100px; height: 100px; line-height: 100px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="transform: scale(0.32) translateX(-50%);"
/>
</span>
</div>
`;
exports[`Avatar Render fallback 1`] = `
<span
class="ant-avatar ant-avatar-circle"
>
<span
class="ant-avatar-string"
style="transform: scale(1) translateX(-50%);"
>
A
</span>
</span>
`;
exports[`Avatar Render rtl render component should be rendered correctly in RTL direction 1`] = `
<span
class="ant-avatar ant-avatar-circle"
>
<span
class="ant-avatar-string"
style="transform: scale(0.72) translateX(-50%);"
/>
</span>
`;
exports[`Avatar Render should calculate scale of avatar children correctly 1`] = `
<span
class="ant-avatar-string"
style="transform: scale(0.72) translateX(-50%);"
>
Avatar
</span>
`;
exports[`Avatar Render should calculate scale of avatar children correctly 2`] = `
<span
class="ant-avatar-string"
style="transform: scale(0.72) translateX(-50%);"
>
xx
</span>
`;
exports[`Avatar Render should calculate scale of avatar children correctly with gap 1`] = `
<span
class="ant-avatar-string"
style="transform: scale(0.36) translateX(-50%);"
>
Avatar
</span>
`;
exports[`Avatar Render should handle onError correctly 1`] = `
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="https://api.dicebear.com/7.x/pixel-art/svg"
/>
</span>
`;
exports[`Avatar Render should show image on success after a failure state 1`] = `
<span
class="ant-avatar ant-avatar-circle"
>
<span
class="ant-avatar-string"
style="transform: scale(1) translateX(-50%);"
>
Fallback
</span>
</span>
`;
exports[`Avatar Render should show image on success after a failure state 2`] = `
<span
class="ant-avatar ant-avatar-circle ant-avatar-image"
>
<img
src="https://api.dicebear.com/7.x/pixel-art/svg"
/>
</span>
`;
exports[`Avatar Render support size is number 1`] = `
<span
class="ant-avatar ant-avatar-circle"
style="width: 100px; height: 100px; line-height: 100px; font-size: 18px;"
>
<span
class="ant-avatar-string"
style="line-height: 100px; transform: scale(0.32) translateX(-50%);"
>
TestString
</span>
</span>
`;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('avatar');

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest';
demoTest('avatar');
rootPropsTest(
'avatar',
(Avatar, props) => (
<Avatar.Group {...props} maxCount={1}>
<Avatar>Bamboo</Avatar>
<Avatar>Light</Avatar>
</Avatar.Group>
),
{
name: 'Avatar.Group',
},
);

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Avatar image', () => {
imageDemoTest('avatar');
});

View File

@ -0,0 +1,259 @@
import * as React from 'react';
import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer';
import { composeRef } from 'rc-util/lib/ref';
import type { Breakpoint } from '../_util/responsiveObserver';
import { responsiveArray } from '../_util/responsiveObserver';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import useSize from '../config-provider/hooks/useSize';
import useBreakpoint from '../grid/hooks/useBreakpoint';
import type { AvatarContextType, AvatarSize } from './AvatarContext';
import AvatarContext from './AvatarContext';
import useStyle from './style';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
export interface AvatarProps {
/** Shape of avatar, options: `circle`, `square` */
shape?: 'circle' | 'square';
/*
* Size of avatar, options: `large`, `small`, `default`
* or a custom number size
* */
size?: AvatarSize;
gap?: number;
/** Src of image avatar */
src?: React.ReactNode;
/** Srcset of image avatar */
srcSet?: string;
draggable?: boolean | 'true' | 'false';
/** Icon to be used in avatar */
icon?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
rootClassName?: string;
children?: React.ReactNode;
alt?: string;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
onClick?: (e?: React.MouseEvent<HTMLElement>) => void;
/* callback when img load error */
/* return false to prevent Avatar show default fallback behavior, then you can do fallback by yourself */
onError?: () => boolean;
}
const InternalAvatar: React.ForwardRefRenderFunction<HTMLSpanElement, AvatarProps> = (
props,
ref,
) => {
const [scale, setScale] = React.useState(1);
const [mounted, setMounted] = React.useState(false);
const [isImgExist, setIsImgExist] = React.useState(true);
const avatarNodeRef = React.useRef<HTMLSpanElement>(null);
const avatarChildrenRef = React.useRef<HTMLSpanElement>(null);
const avatarNodeMergeRef = composeRef<HTMLSpanElement>(ref, avatarNodeRef);
const { getPrefixCls, avatar } = React.useContext(ConfigContext);
const avatarCtx = React.useContext<AvatarContextType>(AvatarContext);
const setScaleParam = () => {
if (!avatarChildrenRef.current || !avatarNodeRef.current) {
return;
}
const childrenWidth = avatarChildrenRef.current.offsetWidth; // offsetWidth avoid affecting be transform scale
const nodeWidth = avatarNodeRef.current.offsetWidth;
// denominator is 0 is no meaning
if (childrenWidth !== 0 && nodeWidth !== 0) {
const { gap = 4 } = props;
if (gap * 2 < nodeWidth) {
setScale(nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1);
}
}
};
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
setIsImgExist(true);
setScale(1);
}, [props.src]);
React.useEffect(setScaleParam, [props.gap]);
const handleImgLoadError = () => {
const { onError } = props;
const errorFlag = onError?.();
if (errorFlag !== false) {
setIsImgExist(false);
}
};
const {
prefixCls: customizePrefixCls,
shape,
size: customSize,
src,
srcSet,
icon,
className,
rootClassName,
alt,
draggable,
children,
crossOrigin,
...others
} = props;
const size = useSize((ctxSize) => customSize ?? avatarCtx?.size ?? ctxSize ?? 'default');
const needResponsive = Object.keys(typeof size === 'object' ? size || {} : {}).some((key) =>
['xs', 'sm', 'md', 'lg', 'xl', 'xxl'].includes(key),
);
const screens = useBreakpoint(needResponsive);
const responsiveSizeStyle = React.useMemo<React.CSSProperties>(() => {
if (typeof size !== 'object') {
return {};
}
const currentBreakpoint: Breakpoint = responsiveArray.find((screen) => screens[screen])!;
const currentSize = size[currentBreakpoint];
return currentSize
? {
width: currentSize,
height: currentSize,
lineHeight: `${currentSize}px`,
fontSize: currentSize && (icon || children) ? currentSize / 2 : 18,
}
: {};
}, [screens, size]);
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Avatar');
warning(
!(typeof icon === 'string' && icon.length > 2),
'breaking',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
}
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
const rootCls = useCSSVarCls(prefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const hasImageElement = React.isValidElement(src);
const mergedShape = shape || avatarCtx?.shape || 'circle';
const classString = classNames(
prefixCls,
sizeCls,
avatar?.className,
`${prefixCls}-${mergedShape}`,
{
[`${prefixCls}-image`]: hasImageElement || (src && isImgExist),
[`${prefixCls}-icon`]: !!icon,
},
cssVarCls,
rootCls,
className,
rootClassName,
hashId,
);
const sizeStyle: React.CSSProperties =
typeof size === 'number'
? {
width: size,
height: size,
lineHeight: `${size}px`,
fontSize: icon ? size / 2 : 18,
}
: {};
let childrenToRender: React.ReactNode;
if (typeof src === 'string' && isImgExist) {
childrenToRender = (
<img
src={src}
draggable={draggable}
srcSet={srcSet}
onError={handleImgLoadError}
alt={alt}
crossOrigin={crossOrigin}
/>
);
} else if (hasImageElement) {
childrenToRender = src;
} else if (icon) {
childrenToRender = icon;
} else if (mounted || scale !== 1) {
const transformString = `scale(${scale}) translateX(-50%)`;
const childrenStyle: React.CSSProperties = {
msTransform: transformString,
WebkitTransform: transformString,
transform: transformString,
};
const sizeChildrenStyle: React.CSSProperties =
typeof size === 'number'
? {
lineHeight: `${size}px`,
}
: {};
childrenToRender = (
<ResizeObserver onResize={setScaleParam}>
<span
className={`${prefixCls}-string`}
ref={avatarChildrenRef}
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
</ResizeObserver>
);
} else {
childrenToRender = (
<span className={`${prefixCls}-string`} style={{ opacity: 0 }} ref={avatarChildrenRef}>
{children}
</span>
);
}
// The event is triggered twice from bubbling up the DOM tree.
// see https://codesandbox.io/s/kind-snow-9lidz
delete others.onError;
delete others.gap;
return wrapCSSVar(
<span
{...others}
style={{ ...sizeStyle, ...responsiveSizeStyle, ...avatar?.style, ...others.style }}
className={classString}
ref={avatarNodeMergeRef}
>
{childrenToRender}
</span>,
);
};
const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(InternalAvatar);
if (process.env.NODE_ENV !== 'production') {
Avatar.displayName = 'Avatar';
}
export default Avatar;

View File

@ -0,0 +1,7 @@
## zh-CN
通常用于消息提示。
## en-US
Usually used for reminders and notifications.

View File

@ -0,0 +1,16 @@
import { UserOutlined } from '@ant-design/icons';
import React from 'react';
import { Avatar, Badge, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space size={24}>
<Badge count={1}>
<Avatar shape="square" icon={<UserOutlined />} />
</Badge>
<Badge dot>
<Avatar shape="square" icon={<UserOutlined />} />
</Badge>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
头像有三种尺寸,两种形状可选。
## en-US
Three sizes and two shapes are available.

View File

@ -0,0 +1,22 @@
import { UserOutlined } from '@ant-design/icons';
import React from 'react';
import { Avatar, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space direction="vertical" size={16}>
<Space wrap size={16}>
<Avatar size={64} icon={<UserOutlined />} />
<Avatar size="large" icon={<UserOutlined />} />
<Avatar icon={<UserOutlined />} />
<Avatar size="small" icon={<UserOutlined />} />
</Space>
<Space wrap size={16}>
<Avatar shape="square" size={64} icon={<UserOutlined />} />
<Avatar shape="square" size="large" icon={<UserOutlined />} />
<Avatar shape="square" icon={<UserOutlined />} />
<Avatar shape="square" size="small" icon={<UserOutlined />} />
</Space>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
Component Token Debug.
## en-US
Component Token Debug

View File

@ -0,0 +1,51 @@
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
import React from 'react';
import { Avatar, Badge, ConfigProvider, Space, Tooltip } from '@zhst/meta';
const App: React.FC = () => (
<ConfigProvider
theme={{
components: {
Avatar: {
containerSize: 60,
containerSizeLG: 30,
containerSizeSM: 16,
textFontSize: 18,
textFontSizeLG: 28,
textFontSizeSM: 12,
borderRadius: 10,
groupOverlapping: -10,
groupBorderColor: '#eee',
},
},
}}
>
<Space>
<Avatar shape="circle" src="http://abc.com/not-exist.jpg">
A
</Avatar>
</Space>
<Space>
<Avatar.Group maxCount={2} maxStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}>
<Avatar src="https://api.dicebear.com/7.x/miniavs/svg?seed=2" />
<Avatar style={{ backgroundColor: '#f56a00' }}>K</Avatar>
<Tooltip title="Ant User" placement="top">
<Avatar style={{ backgroundColor: '#87d068' }} icon={<UserOutlined />} />
</Tooltip>
<Avatar style={{ backgroundColor: '#1890ff' }} icon={<AntDesignOutlined />} />
</Avatar.Group>
</Space>
<Space>
<Badge count={1}>
<Avatar shape="square" icon={<UserOutlined />} />
</Badge>
<Badge dot>
<Avatar shape="square" icon={<UserOutlined />} />
</Badge>
</Space>
</ConfigProvider>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
对于字符型的头像,当字符串较长时,字体大小可以根据头像宽度自动调整。也可使用 `gap` 来设置字符距离左右两侧边界单位像素。
## en-US
For letter type Avatar, when the letters are too long to display, the font size can be automatically adjusted according to the width of the Avatar. You can also use `gap` to set the unit distance between left and right sides.

View File

@ -0,0 +1,43 @@
import React, { useState } from 'react';
import { Avatar, Button } from '@zhst/meta';
const UserList = ['U', 'Lucy', 'Tom', 'Edward'];
const ColorList = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae'];
const GapList = [4, 3, 2, 1];
const App: React.FC = () => {
const [user, setUser] = useState(UserList[0]);
const [color, setColor] = useState(ColorList[0]);
const [gap, setGap] = useState(GapList[0]);
const changeUser = () => {
const index = UserList.indexOf(user);
setUser(index < UserList.length - 1 ? UserList[index + 1] : UserList[0]);
setColor(index < ColorList.length - 1 ? ColorList[index + 1] : ColorList[0]);
};
const changeGap = () => {
const index = GapList.indexOf(gap);
setGap(index < GapList.length - 1 ? GapList[index + 1] : GapList[0]);
};
return (
<>
<Avatar style={{ backgroundColor: color, verticalAlign: 'middle' }} size="large" gap={gap}>
{user}
</Avatar>
<Button
size="small"
style={{ margin: '0 16px', verticalAlign: 'middle' }}
onClick={changeUser}
>
ChangeUser
</Button>
<Button size="small" style={{ verticalAlign: 'middle' }} onClick={changeGap}>
changeGap
</Button>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
图片不存在时,如果 `src` 本身是个 ReactElement会尝试回退到 `src`,否则尝试回退到 `icon`,最后回退到显示 `children`
## en-US
When the image does not exist, if `src` is a ReactElement, it will try to fall back to `src`, otherwise it will try to fall back to `icon`, and finally fall back to displaying `children`.

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Avatar, Space } from '@zhst/meta';
const App: React.FC = () => (
<Space>
<Avatar shape="circle" src="http://abc.com/not-exist.jpg">
A
</Avatar>
<Avatar shape="circle" src="http://abc.com/not-exist.jpg">
ABC
</Avatar>
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
头像组合展现。
## en-US
Avatar group display.

Some files were not shown because too many files have changed in this diff Show More