diff --git a/docs/zh-CN/components/shape.md b/docs/zh-CN/components/shape.md
new file mode 100644
index 00000000000..e1be6ea38f1
--- /dev/null
+++ b/docs/zh-CN/components/shape.md
@@ -0,0 +1,179 @@
+---
+title: Shape 形状
+description:
+type: 0
+group: ⚙ 组件
+menuName: Tabs
+icon:
+---
+
+用于展示形状
+
+## 基本用法
+
+```schema
+{
+ type: "page",
+ body: [
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'triangle'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'square'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'pentagon'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'star'
+ }
+ ]
+}
+```
+
+## 配置大小
+
+```schema
+{
+ type: "page",
+ body: [
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'triangle',
+ size: 100
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'square',
+ size: 100
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'pentagon',
+ size: 100
+ }
+ ]
+}
+```
+
+## 配置圆角
+
+```schema
+{
+ type: "page",
+ body: [
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'triangle',
+ radius: 4
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'square',
+ radius: 4
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'pentagon',
+ radius: 4
+ }
+ ]
+}
+```
+
+## 更多图形
+
+```schema
+{
+ type: "page",
+ body: [
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'triangle',
+ size: 50
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'square',
+ size: 50
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'convex-arc-rectangle',
+ size: 50
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'convex-arc-rectangle',
+ size: 50,
+ radius: 4
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'concave-arc-rectangle',
+ size: 50,
+ radius: 4
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ shapeType: 'double-arc-rectangle',
+ size: 50,
+ radius: 4
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ size: 50,
+ shapeType: 'pentagon'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ size: 50,
+ shapeType: 'hexagon'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ size: 50,
+ shapeType: 'star'
+ },
+ {
+ type: 'shape',
+ className: 'm-2',
+ size: 50,
+ shapeType: 'heart'
+ }
+ ]
+}
+```
+
+## 属性表
+
+| 属性名 | 类型 | 默认值 | 说明 |
+| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------- |
+| type | `'string'` | `'shape'` | 指定为图形渲染器 |
+| shapeType | `'square'` 正方形
`'triangle'` 三角形
`'convex-arc-rectangle'` 上凸矩形
`'concave-arc-rectangle'` 上凹矩形
`'double-arc-rectangle'` 双凸矩形
`'pentagon'` 五边形
`'hexagon'` 六边形
`'star'` 五角星
`'heart'` 心形 | `'-'` | 图形类型 |
+| className | `string` | | 自定义 CSS 样式类名 |
+| size | `number` | `200` | 图形大小 |
+| radius | `number` | `0` | 圆角大小(1-10) |
diff --git a/examples/components/Components.tsx b/examples/components/Components.tsx
index 459b047241f..d75b322246f 100644
--- a/examples/components/Components.tsx
+++ b/examples/components/Components.tsx
@@ -1114,6 +1114,13 @@ export const components = [
component: React.lazy(() =>
import('../../docs/zh-CN/components/timeline.md').then(wrapDoc)
)
+ },
+ {
+ label: 'Shape 形状',
+ path: '/zh-CN/components/shape',
+ component: React.lazy(() =>
+ import('../../docs/zh-CN/components/shape.md').then(wrapDoc)
+ )
}
]
},
diff --git a/packages/amis-ui/scss/components/_shape.scss b/packages/amis-ui/scss/components/_shape.scss
new file mode 100644
index 00000000000..21fce5b23ba
--- /dev/null
+++ b/packages/amis-ui/scss/components/_shape.scss
@@ -0,0 +1,12 @@
+/**
+ * @file _shape.scss
+ *
+ * @author allenve(yupeng12@baidu.com)
+ * @created: 2024/12/12
+ */
+
+.#{$ns}Shape {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/packages/amis-ui/scss/themes/_common.scss b/packages/amis-ui/scss/themes/_common.scss
index 81c0629aaaf..7364fccf12b 100644
--- a/packages/amis-ui/scss/themes/_common.scss
+++ b/packages/amis-ui/scss/themes/_common.scss
@@ -81,6 +81,7 @@
@import '../components/steps';
@import '../components/portlet';
@import '../components/grid-nav';
+@import '../components/shape';
@import '../components/form/fieldset';
@import '../components/form/group';
diff --git a/packages/amis-ui/src/components/Shape.tsx b/packages/amis-ui/src/components/Shape.tsx
new file mode 100644
index 00000000000..fe2db8924e4
--- /dev/null
+++ b/packages/amis-ui/src/components/Shape.tsx
@@ -0,0 +1,291 @@
+/**
+ * @file Shape.tsx 图形组件
+ *
+ * @author allenve(yupeng12@baidu.com)
+ * @created: 2024/12/12
+ */
+
+import React from 'react';
+import {themeable, ThemeProps} from 'amis-core';
+
+export type IShapeType =
+ | 'square'
+ | 'triangle'
+ | 'convex-arc-rectangle'
+ | 'concave-arc-rectangle'
+ | 'double-arc-rectangle'
+ | 'pentagon' // 五边形
+ | 'hexagon' // 六边形
+ | 'star'
+ | 'heart';
+
+export interface IShapeProps extends ThemeProps {
+ shapeType: IShapeType;
+ radius: number;
+ size?: number;
+}
+
+class SvgPathGenerator {
+ constructor(readonly size: number) {}
+ toRadians(degrees: number) {
+ return degrees * (Math.PI / 180);
+ }
+
+ getGenerage(type: IShapeType) {
+ const ShapeConfig: {
+ [key in IShapeType]: (radius: number) => string[];
+ } = {
+ 'square': this.getSquarePath.bind(this),
+ 'triangle': this.getTrianglePath.bind(this),
+ 'convex-arc-rectangle': this.getConvexArcRectangle.bind(this),
+ 'concave-arc-rectangle': this.getConcaveArcRectangle.bind(this),
+ 'double-arc-rectangle': this.getDoubleArcRectangle.bind(this),
+ 'pentagon': this.getPentagon.bind(this),
+ 'hexagon': this.getHexagon.bind(this),
+ 'star': this.getStar.bind(this),
+ 'heart': this.getHeart.bind(this)
+ };
+
+ return ShapeConfig[type];
+ }
+
+ getSquarePath(radius: number) {
+ const sideLength = this.size;
+ const path1 = `
+ M ${sideLength / 2} 0
+ L ${sideLength - radius} 0
+ Q ${sideLength} 0 ${sideLength} ${radius}
+ L ${sideLength} ${sideLength - radius}
+ Q ${sideLength} ${sideLength} ${sideLength - radius} ${sideLength}
+ L ${radius} ${sideLength}
+ Q 0 ${sideLength} 0 ${sideLength - radius}
+ L 0 ${radius}
+ Q 0 0 ${radius} 0
+ Z
+ `;
+ const path2 = `
+ M ${sideLength / 2} ${sideLength}
+ L ${sideLength - radius} ${sideLength}
+ Q ${sideLength} ${sideLength} ${sideLength} ${sideLength - radius}
+ L ${sideLength} ${radius}
+ Q ${sideLength} 0 ${sideLength - radius} 0
+ L ${radius} 0
+ Q 0 0 0 ${radius}
+ L 0 ${sideLength - radius}
+ Q 0 ${sideLength} ${radius} ${sideLength}
+ Z
+ `;
+ // 用两个path重叠 避免曲线的间隙
+ return [path1, path2];
+ }
+
+ getTrianglePath(radius: number) {
+ const S = this.size;
+ const height = Math.sin(this.toRadians(60)) * S;
+ const x = ((2 - Math.sqrt(3)) / 4) * S - (Math.sqrt(3) / 8) * radius;
+ const dy = Math.sin(this.toRadians(60)) * radius;
+
+ const path1 = `
+ M ${S / 2} ${height + x}
+ L ${radius} ${height + x}
+ Q 0 ${height + x} ${radius / 2} ${height + x - dy}
+ L ${S / 2 - radius / 2} ${dy + x}
+ Q ${S / 2} ${x} ${S / 2 + radius / 2} ${dy + x}
+ L ${S - radius / 2} ${height + x - dy}
+ Q ${S} ${height + x} ${S - radius} ${height + x}
+ Z
+ `;
+ // 反向画一个三角形
+ const path2 = `
+ M ${S / 2} ${height + x}
+ L ${S - radius} ${height + x}
+ Q ${S} ${height + x} ${S - radius / 2} ${height + x - dy}
+ L ${S / 2 + radius / 2} ${dy + x}
+ Q ${S / 2} ${x} ${S / 2 - radius / 2} ${dy + x}
+ L ${radius / 2} ${height + x - dy}
+ Q 0 ${height + x} ${radius} ${height + x}
+ Z
+ `;
+ return [path1, path2];
+ }
+
+ getConvexArcRectangle(radius: number) {
+ const S = this.size;
+ const w = S;
+ const h = (S / 3) * 2;
+ const x = (S - h) / 2 - radius / 4;
+ const path1 = `
+ M 0 ${h + x}
+ L 0 ${radius + x}
+ Q ${w / 2} ${x} ${w} ${radius + x}
+ L ${w} ${h + x}
+ Z
+ `;
+ const path2 = `
+ M 0 ${radius + x}
+ Q ${w / 2} ${x} ${w} ${radius + x}
+ L ${w} ${h + x}
+ L 0 ${h + x}
+ Z
+ `;
+
+ return [path1, path2];
+ }
+
+ getConcaveArcRectangle(radius: number) {
+ const S = this.size;
+ const w = S;
+ const h = S / 2;
+ const x = (S - h) / 2;
+ const path1 = `
+ M 0 ${h + x}
+ L 0 ${x}
+ Q ${w / 2} ${radius + x} ${w} ${x}
+ L ${w} ${h + x}
+ Z
+ `;
+ const path2 = `
+ M 0 ${x}
+ Q ${w / 2} ${radius + x} ${w} ${x}
+ L ${w} ${h + x}
+ L 0 ${h + x}
+ Z
+ `;
+
+ return [path1, path2];
+ }
+
+ getDoubleArcRectangle(radius: number) {
+ const S = this.size;
+ const w = S;
+ const h = S;
+ const x = (S - h) / 2;
+ const path = `
+ M 0 ${h + x}
+ L 0 ${radius + x}
+ Q ${w / 2} ${x} ${w} ${radius + x}
+ L ${w} ${h + x - radius}
+ Q ${w / 2} ${h + x} 0 ${h + x - radius}
+ Z
+ `;
+ return [path];
+ }
+
+ getStar() {
+ const path = `
+ M100,11.1297757
+ L129.05723,70.0061542
+ L194.031171,79.4474205
+ L147.015586,125.27629
+ L158.11446,189.987692
+ L100,159.435112
+ L41.8855403,189.987692
+ L52.9844145,125.27629
+ L5.96882894,79.4474205
+ L70.9427701,70.0061542
+ L100,11.1297757
+ Z`;
+
+ return [path];
+ }
+
+ getHeart() {
+ const path = `
+ M143.526375,12
+ C132.21418,12 124.346417,16.1423074 111.598905,27.0093876
+ C111.007635,27.5125184 107.668026,30.3835001 106.701139,31.2037806
+ C103.410893,33.9868624 98.5891073,33.9868624 95.2988609,31.2037806
+ C94.331974,30.3807731 90.9923649,27.5125184 90.4010952,27.0093876
+ C77.653583,16.1423074 69.78582,12 58.4736246,12
+ C24.0713382,12 2,39.4823959 2,79.1438299
+ C2,109.386491 32.9900653,146.921686 95.9859458,190.440184
+ C99.0044944,192.519939 102.995506,192.519939 106.014054,190.440184
+ C169.009935,146.924413 200,109.386491 200,79.1438299
+ C200,39.4823959 177.928662,12 143.526375,12
+ Z`;
+ return [path];
+ }
+
+ getPentagon(radius: number) {
+ const S = this.size;
+ const a = S / (Math.tan(this.toRadians(54)) * 2);
+ const b = S / 2;
+ const c = Math.sqrt(a * a + b * b);
+ const R = (c / 200) * radius;
+ const ra = Math.sin(this.toRadians(36)) * R;
+ const rb = Math.cos(this.toRadians(36)) * R;
+ const x1 = Math.sin(this.toRadians(18)) * c;
+ const rx1 = Math.sin(this.toRadians(18)) * R;
+ const x2 = Math.cos(this.toRadians(18)) * c;
+ const rx2 = Math.cos(this.toRadians(18)) * R;
+ const dx = (S - (a + x2)) / 2;
+ const path1 = `
+ M ${b + rb} ${dx + ra}
+ L ${S - rb} ${a + dx - ra}
+ Q ${S} ${a + dx} ${S - rx1} ${a + dx + rx2}
+ L ${S - x1 + rx1} ${a + x2 + dx - rx2}
+ Q ${S - x1} ${a + x2 + dx} ${S - x1 - R} ${a + x2 + dx}
+ L ${x1 + R} ${a + x2 + dx}
+ Q ${x1} ${a + x2 + dx} ${x1 - rx1} ${a + x2 + dx - rx2}
+ L ${rx1} ${a + dx + rx2}
+ Q 0 ${a + dx} ${rb} ${a + dx - ra}
+ L ${b - rb} ${dx + ra}
+ Q ${b} ${dx} ${b + rb} ${dx + ra}
+ Z
+ `;
+ const path2 = `
+ M ${b - rb} ${dx + ra}
+ L ${rb} ${a + dx - ra}
+ Q 0 ${a + dx} ${rx1} ${a + dx + rx2}
+ L ${x1 - rx1} ${a + x2 + dx - rx2}
+ Q ${x1} ${a + x2 + dx} ${x1 + R} ${a + x2 + dx}
+ L ${S - x1 - R} ${a + x2 + dx}
+ Q ${S - x1} ${a + x2 + dx} ${S - x1 + rx1} ${a + x2 + dx - rx2}
+ L ${S - rx1} ${a + dx + rx2}
+ Q ${S} ${a + dx} ${S - rb} ${a + dx - ra}
+ L ${b + rb} ${dx + ra}
+ Q ${b} ${dx} ${b - rb} ${dx + ra}
+ Z
+ `;
+ return [path1, path2];
+ }
+
+ getHexagon() {
+ const path = `
+ M149.380343,14.4707367
+ L142.764632,75.3098284
+ L198.760686,100
+ L142.764632,124.690172
+ L149.380343,185.529263
+ L100,149.380343
+ L50.6196568,185.529263
+ L57.2353684,124.690172
+ L1.23931367,100
+ L57.2353684,75.3098284
+ L50.6196568,14.4707367
+ L100,50.6196568
+ L149.380343,14.4707367
+ Z`;
+ return [path];
+ }
+}
+
+const Shape: React.FC = props => {
+ const {classnames: cx, className, shapeType, radius, size = 200} = props;
+ const radiusValue = Math.floor(Math.max(0, Math.min(10, radius))) || 0;
+ const generator = new SvgPathGenerator(200);
+ const genFun = generator.getGenerage(shapeType);
+ const paths = genFun(radiusValue * 10);
+
+ return (
+
+
+
+ );
+};
+
+export default themeable(Shape);
diff --git a/packages/amis-ui/src/components/index.tsx b/packages/amis-ui/src/components/index.tsx
index 9f13d5b954e..e57f4e38f88 100644
--- a/packages/amis-ui/src/components/index.tsx
+++ b/packages/amis-ui/src/components/index.tsx
@@ -141,6 +141,8 @@ import OverflowTpl from './OverflowTpl';
import Signature from './Signature';
import VerificationCode from './VerificationCode';
+import Shape from './Shape';
+import type {IShapeType} from './Shape';
import MobileDevTool from './MobileDevTool';
export {
@@ -283,5 +285,7 @@ export {
OverflowTpl,
Signature,
VerificationCode,
+ Shape,
+ IShapeType,
MobileDevTool
};
diff --git a/packages/amis-ui/tsconfig.json b/packages/amis-ui/tsconfig.json
index fd8d3a87142..a03fd7b97f6 100644
--- a/packages/amis-ui/tsconfig.json
+++ b/packages/amis-ui/tsconfig.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"rootDir": "./",
"outDir": "./lib",
+ "sourceMap": true,
"sourceRoot": "./",
"typeRoots": [
"../../types",
diff --git a/packages/amis/src/Schema.ts b/packages/amis/src/Schema.ts
index 6efc0ae8f39..85b4d3f6ed8 100644
--- a/packages/amis/src/Schema.ts
+++ b/packages/amis/src/Schema.ts
@@ -257,6 +257,7 @@ export type SchemaType =
| 'pdf-viewer'
| 'input-signature'
| 'input-verification-code'
+ | 'shape'
// editor 系列
| 'editor'
diff --git a/packages/amis/src/minimal.ts b/packages/amis/src/minimal.ts
index d25366d1083..e28407e43ea 100644
--- a/packages/amis/src/minimal.ts
+++ b/packages/amis/src/minimal.ts
@@ -157,6 +157,10 @@ registerRenderer({
type: 'flex',
getComponent: () => import('./renderers/Flex')
});
+registerRenderer({
+ type: 'shape',
+ getComponent: () => import('./renderers/Shape')
+});
// import './renderers/Form/ButtonGroupSelect';
registerRenderer({
type: 'button-group-select',
diff --git a/packages/amis/src/renderers/Shape.tsx b/packages/amis/src/renderers/Shape.tsx
new file mode 100644
index 00000000000..d51ac020729
--- /dev/null
+++ b/packages/amis/src/renderers/Shape.tsx
@@ -0,0 +1,51 @@
+/**
+ * @file Shape.tsx 图形组件
+ *
+ * @author allenve(yupeng12@baidu.com)
+ * @created: 2024/12/12
+ */
+
+import React from 'react';
+import {Renderer, RendererProps} from 'amis-core';
+import {Shape, IShapeType} from 'amis-ui';
+import cx from 'classnames';
+import {BaseSchema} from '../Schema';
+
+export interface IShapeSchema extends BaseSchema {
+ type: 'shape';
+ /**
+ * 图形类型
+ */
+ shapeType: IShapeType;
+
+ /**
+ * 图形大小
+ */
+ size?: number;
+ /**
+ * 圆角大小 1~10
+ */
+ radius: number;
+}
+
+interface IShapeRenderProps
+ extends RendererProps,
+ Omit {}
+
+@Renderer({
+ type: 'shape'
+})
+export class ShapeRenderer extends React.Component {
+ render() {
+ const {className, shapeType, radius, size} = this.props;
+
+ return (
+
+ );
+ }
+}