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 ( +
+ + {paths.map((path, index) => ( + + ))} + +
+ ); +}; + +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 ( + + ); + } +}