From 1a92a3c7843854131e38b6b17fd33bba16ded3b1 Mon Sep 17 00:00:00 2001 From: kousum Date: Wed, 15 Jan 2025 16:01:27 +0800 Subject: [PATCH] feat: semi 2.73.0 --- docs/.vitepress/theme/layout/Layout.vue | 20 + docs/images/docIcons/doc-audioplayer.svg | 5 + docs/images/docIcons/doc-colorPlatteNew.svg | 2 +- docs/images/docIcons/doc-cropper.svg | 7 + docs/images/docIcons/doc-dragmove.svg | 10 + docs/images/docIcons/doc-jsonviewer.svg | 8 + docs/images/docIcons/doc-vchart.svg | 2 +- docs/src/public/designToken.json | 443 +++++++++++++++- docs/src/zh-CN/plus/audioPlayer/index.md | 175 +++++++ docs/src/zh-CN/plus/dragMove/index.md | 276 ++++++++++ docs/src/zh-CN/show/cropper/index.md | 308 +++++++++++ package.json | 9 +- packages/semi-animation-vue/package.json | 8 +- packages/semi-icons-lab-vue/package.json | 4 +- packages/semi-icons-vue/package.json | 4 +- packages/semi-illustrations-vue/package.json | 4 +- packages/semi-ui-vue/package.json | 6 +- packages/semi-ui-vue/src/App.tsx | 6 +- .../audioPlayer/__stories__/Demo.stories.tsx | 32 ++ .../audioPlayer/__test__/AudioPlayerDemo.tsx | 76 +++ .../components/audioPlayer/audioSlider.tsx | 187 +++++++ .../src/components/audioPlayer/index.tsx | 484 ++++++++++++++++++ .../src/components/audioPlayer/utils.ts | 5 + .../cropper/__stories__/Demo.stories.tsx | 32 ++ .../cropper/__test__/CropperDemo.tsx | 79 +++ .../components/cropper/__test__/test.spec.ts | 25 + .../src/components/cropper/index.tsx | 365 +++++++++++++ .../src/components/dragMove/index.ts | 18 +- .../src/components/dropdown/index.tsx | 4 + packages/semi-ui-vue/src/components/index.ts | 3 + .../src/components/jsonViewer/index.tsx | 27 +- .../@douyinfe__semi-foundation@2.73.0.patch | 77 +++ pnpm-lock.yaml | 111 ++-- 33 files changed, 2728 insertions(+), 94 deletions(-) create mode 100644 docs/images/docIcons/doc-audioplayer.svg create mode 100644 docs/images/docIcons/doc-cropper.svg create mode 100644 docs/images/docIcons/doc-dragmove.svg create mode 100644 docs/images/docIcons/doc-jsonviewer.svg create mode 100644 docs/src/zh-CN/plus/audioPlayer/index.md create mode 100644 docs/src/zh-CN/plus/dragMove/index.md create mode 100644 docs/src/zh-CN/show/cropper/index.md create mode 100644 packages/semi-ui-vue/src/components/audioPlayer/__stories__/Demo.stories.tsx create mode 100644 packages/semi-ui-vue/src/components/audioPlayer/__test__/AudioPlayerDemo.tsx create mode 100644 packages/semi-ui-vue/src/components/audioPlayer/audioSlider.tsx create mode 100644 packages/semi-ui-vue/src/components/audioPlayer/index.tsx create mode 100644 packages/semi-ui-vue/src/components/audioPlayer/utils.ts create mode 100644 packages/semi-ui-vue/src/components/cropper/__stories__/Demo.stories.tsx create mode 100644 packages/semi-ui-vue/src/components/cropper/__test__/CropperDemo.tsx create mode 100644 packages/semi-ui-vue/src/components/cropper/__test__/test.spec.ts create mode 100644 packages/semi-ui-vue/src/components/cropper/index.tsx create mode 100644 patches/@douyinfe__semi-foundation@2.73.0.patch diff --git a/docs/.vitepress/theme/layout/Layout.vue b/docs/.vitepress/theme/layout/Layout.vue index 763f76f1..b864cda6 100644 --- a/docs/.vitepress/theme/layout/Layout.vue +++ b/docs/.vitepress/theme/layout/Layout.vue @@ -129,6 +129,11 @@ const navItem = [ text: 'Chat 对话', icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('chat') })), }, + { + itemKey: '/plus/dragMove/', + text: 'DragMove 拖拽移动', + icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('dragmove') })), + }, { itemKey: '/plus/codehighlight/', text: 'Codehighlight 代码高亮', @@ -149,6 +154,11 @@ const navItem = [ text: 'Markdown 渲染', icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('markdown') })), }, + { + itemKey: '/plus/audioPlayer/', + text: 'AudioPlayer 播放器', + icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('audioplayer') })), + }, ], }, { @@ -371,6 +381,11 @@ const navItem = [ text: 'Image 图片', icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('image') })), }, + { + itemKey: '/show/cropper/', + text: 'Cropper 图片裁剪', + icon: h(Icon, {}, () => h(InlineSvg, { svg: getIcon('cropper') })), + }, { itemKey: '/show/list/', text: 'List 列表', @@ -594,6 +609,11 @@ provide('hero-image-slot-exists', null); :root{ --vp-icon-copy:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E") } +.msg.err{ + pre{ + width: 100%; + } +} .semi-overview-list { display: flex; flex-wrap: wrap; diff --git a/docs/images/docIcons/doc-audioplayer.svg b/docs/images/docIcons/doc-audioplayer.svg new file mode 100644 index 00000000..4ef947b2 --- /dev/null +++ b/docs/images/docIcons/doc-audioplayer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/docIcons/doc-colorPlatteNew.svg b/docs/images/docIcons/doc-colorPlatteNew.svg index 12bcf6a5..dc203c82 100644 --- a/docs/images/docIcons/doc-colorPlatteNew.svg +++ b/docs/images/docIcons/doc-colorPlatteNew.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/docs/images/docIcons/doc-cropper.svg b/docs/images/docIcons/doc-cropper.svg new file mode 100644 index 00000000..e1c238d8 --- /dev/null +++ b/docs/images/docIcons/doc-cropper.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/images/docIcons/doc-dragmove.svg b/docs/images/docIcons/doc-dragmove.svg new file mode 100644 index 00000000..2654d1c0 --- /dev/null +++ b/docs/images/docIcons/doc-dragmove.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/images/docIcons/doc-jsonviewer.svg b/docs/images/docIcons/doc-jsonviewer.svg new file mode 100644 index 00000000..acda2cb6 --- /dev/null +++ b/docs/images/docIcons/doc-jsonviewer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/images/docIcons/doc-vchart.svg b/docs/images/docIcons/doc-vchart.svg index 169752a1..f18b1f58 100644 --- a/docs/images/docIcons/doc-vchart.svg +++ b/docs/images/docIcons/doc-vchart.svg @@ -3,4 +3,4 @@ - + \ No newline at end of file diff --git a/docs/src/public/designToken.json b/docs/src/public/designToken.json index 005f6bd6..ebed1253 100644 --- a/docs/src/public/designToken.json +++ b/docs/src/public/designToken.json @@ -211,6 +211,248 @@ "raw": "$transform_scale-anchor_title-text: var(--semi-transform_scale-none);//锚点-放大" } ], + "audioplayer": [ + { + "key": "$color-audio-player-background", + "value": "rgba(var(--semi-grey-9), .8)", + "category": "color", + "raw": "$color-audio-player-background: rgba(var(--semi-grey-9), .8);" + }, + { + "key": "$color-audio-player-control-icon", + "value": "var(--semi-color-bg-0)", + "category": "color", + "raw": "$color-audio-player-control-icon: var(--semi-color-bg-0);" + }, + { + "key": "$color-audio-player-control-icon-play", + "value": "var(--semi-color-text-0)", + "category": "color", + "raw": "$color-audio-player-control-icon-play: var(--semi-color-text-0);" + }, + { + "key": "$color-audio-player-font-color", + "value": "var(--semi-color-bg-0)", + "category": "color", + "raw": "$color-audio-player-font-color: var(--semi-color-bg-0);" + }, + { + "key": "$color-audio-player-font-color-speed", + "value": "rgba(var(--semi-grey-8), 1)", + "category": "color", + "raw": "$color-audio-player-font-color-speed: rgba(var(--semi-grey-8), 1);" + }, + { + "key": "$color-audio-player-background-light", + "value": "var(--semi-color-bg-0)", + "category": "color", + "raw": "$color-audio-player-background-light: var(--semi-color-bg-0);" + }, + { + "key": "$color-audio-player-control-icon-light", + "value": "rgba(var(--semi-grey-9), 1)", + "category": "color", + "raw": "$color-audio-player-control-icon-light: rgba(var(--semi-grey-9), 1);" + }, + { + "key": "$color-audio-player-control-icon-play-light", + "value": "var(--semi-color-bg-0)", + "category": "color", + "raw": "$color-audio-player-control-icon-play-light: var(--semi-color-bg-0);" + }, + { + "key": "$color-audio-player-font-color-light", + "value": "rgba(var(--semi-grey-9), 1)", + "category": "color", + "raw": "$color-audio-player-font-color-light: rgba(var(--semi-grey-9), 1);" + }, + { + "key": "$font-size-audio-player-text", + "value": "14px", + "category": "font", + "raw": "$font-size-audio-player-text: 14px;" + }, + { + "key": "$gap-audio-player-small", + "value": "4px", + "category": "other", + "raw": "$gap-audio-player-small: 4px;" + }, + { + "key": "$gap-audio-player-medium", + "value": "16px", + "category": "other", + "raw": "$gap-audio-player-medium: 16px;" + }, + { + "key": "$gap-audio-player-large", + "value": "24px", + "category": "other", + "raw": "$gap-audio-player-large: 24px;" + }, + { + "key": "$width-audio-player-max", + "value": "1440px", + "category": "width", + "raw": "$width-audio-player-max: 1440px;" + }, + { + "key": "$height-audio-player", + "value": "78px", + "category": "height", + "raw": "$height-audio-player: 78px;" + }, + { + "key": "$width-audio-player-slider", + "value": "323px", + "category": "width", + "raw": "$width-audio-player-slider: 323px;" + }, + { + "key": "$width-audio-player-speed", + "value": "40px", + "category": "width", + "raw": "$width-audio-player-speed: 40px;" + }, + { + "key": "$height-audio-player-speed", + "value": "24px", + "category": "height", + "raw": "$height-audio-player-speed: 24px;" + }, + { + "key": "$width-audio-player-speed-menu", + "value": "65px", + "category": "width", + "raw": "$width-audio-player-speed-menu: 65px;" + }, + { + "key": "$width-audio-player-volume", + "value": "43px", + "category": "width", + "raw": "$width-audio-player-volume: 43px;" + }, + { + "key": "$height-audio-player-volume", + "value": "164px", + "category": "height", + "raw": "$height-audio-player-volume: 164px;" + }, + { + "key": "$height-audio-player-time", + "value": "22px", + "category": "height", + "raw": "$height-audio-player-time: 22px;" + }, + { + "key": "$border-radius-audio-player-speed", + "value": "3px", + "category": "other", + "raw": "$border-radius-audio-player-speed: 3px;" + }, + { + "key": "$border-radius-audio-player-volume", + "value": "4px", + "category": "other", + "raw": "$border-radius-audio-player-volume: 4px;" + }, + { + "key": "$border-radius-audio-player-slider", + "value": "9999px", + "category": "other", + "raw": "$border-radius-audio-player-slider: 9999px;" + }, + { + "key": "$font-size-audio-player-small", + "value": "12px", + "category": "font", + "raw": "$font-size-audio-player-small: 12px;" + }, + { + "key": "$line-height-audio-player-small", + "value": "16px", + "category": "other", + "raw": "$line-height-audio-player-small: 16px;" + }, + { + "key": "$width-audio-player-slider-bar", + "value": "4px", + "category": "width", + "raw": "$width-audio-player-slider-bar: 4px;" + }, + { + "key": "$size-audio-player-slider-dot", + "value": "16px", + "category": "other", + "raw": "$size-audio-player-slider-dot: 16px;" + }, + { + "key": "$color-audio-player-disabled-bg", + "value": "rgba(var(--semi-grey-0), .35)", + "category": "color", + "raw": "$color-audio-player-disabled-bg: rgba(var(--semi-grey-0), .35);" + }, + { + "key": "$color-audio-player-slider-bg", + "value": "rgba(var(--semi-grey-5), 1)", + "category": "color", + "raw": "$color-audio-player-slider-bg: rgba(var(--semi-grey-5), 1);" + }, + { + "key": "$color-audio-player-slider-bg-light", + "value": "rgba(var(--semi-grey-2), 1)", + "category": "color", + "raw": "$color-audio-player-slider-bg-light: rgba(var(--semi-grey-2), 1);" + }, + { + "key": "$color-audio-player-slider-progress", + "value": "rgba(var(--semi-blue-4), 1)", + "category": "color", + "raw": "$color-audio-player-slider-progress: rgba(var(--semi-blue-4), 1);" + }, + { + "key": "$color-audio-player-slider-dot-bg", + "value": "rgba(var(--semi-white), 1)", + "category": "color", + "raw": "$color-audio-player-slider-dot-bg: rgba(var(--semi-white), 1);" + }, + { + "key": "$color-audio-player-disabled-text", + "value": "var(--semi-color-grey-7)", + "category": "color", + "raw": "$color-audio-player-disabled-text: var(--semi-color-grey-7);" + }, + { + "key": "$color-audio-player-text-default", + "value": "var(--semi-color-default)", + "category": "color", + "raw": "$color-audio-player-text-default: var(--semi-color-default);" + }, + { + "key": "$color-audio-player-light-disabled-bg", + "value": "var(--semi-color-disabled-text)", + "category": "color", + "raw": "$color-audio-player-light-disabled-bg: var(--semi-color-disabled-text);" + }, + { + "key": "$color-audio-player-light-disabled-text", + "value": "rgba(var(--semi-white), 1)", + "category": "color", + "raw": "$color-audio-player-light-disabled-text: rgba(var(--semi-white), 1);" + }, + { + "key": "$color-audio-player-light-text", + "value": "rgba(var(--semi-grey-9), 1)", + "category": "color", + "raw": "$color-audio-player-light-text: rgba(var(--semi-grey-9), 1);" + }, + { + "key": "$color-audio-player-light-hover-bg", + "value": "rgba(var(--semi-grey-1), 1)", + "category": "color", + "raw": "$color-audio-player-light-hover-bg: rgba(var(--semi-grey-1), 1);" + } + ], "autocomplete": [ { "key": "$spacing-autoComplete_loading_wrapper-paddingTop", @@ -2583,6 +2825,48 @@ "category": "spacing", "raw": "$spacing-button_iconOnly_small-paddingBottom: $spacing-extra-tight; // 图标按钮底部内边距 - 小尺寸" }, + { + "key": "$height-button_iconOnly_small", + "value": "$height-control-small", + "comment": "图标按钮 height - 小尺寸", + "category": "height", + "raw": "$height-button_iconOnly_small: $height-control-small; // 图标按钮 height - 小尺寸" + }, + { + "key": "$width-button_iconOnly_small", + "value": "$height-control-small", + "comment": "图标按钮 width - 小尺寸", + "category": "width", + "raw": "$width-button_iconOnly_small: $height-control-small; // 图标按钮 width - 小尺寸" + }, + { + "key": "$height-button_iconOnly_default", + "value": "$height-control-default", + "comment": "图标按钮 height - 默认", + "category": "height", + "raw": "$height-button_iconOnly_default:$height-control-default; // 图标按钮 height - 默认" + }, + { + "key": "$width-button_iconOnly_default", + "value": "$height-control-default", + "comment": "图标按钮 width - 默认", + "category": "width", + "raw": "$width-button_iconOnly_default: $height-control-default; // 图标按钮 width - 默认" + }, + { + "key": "$height-button_iconOnly_large", + "value": "$height-control-large", + "comment": "图标按钮 height - 大尺寸", + "category": "height", + "raw": "$height-button_iconOnly_large: $height-control-large; // 图标按钮 height - 大尺寸" + }, + { + "key": "$width-button_iconOnly_large", + "value": "$height-control-large", + "comment": "图标按钮 width - 大尺寸", + "category": "width", + "raw": "$width-button_iconOnly_large: $height-control-large; // 图标按钮 width - 大尺寸" + }, { "key": "$spacing-button_iconOnly_content-marginLeft", "value": "$spacing-tight", @@ -6376,6 +6660,43 @@ "raw": "$font-colorPicker_inputNumberSuffix-fontSize:14px; // 颜色手动输入区域百分比字体大小" } ], + "cropper": [ + { + "key": "$color-cropper_mask-bg", + "value": "var(--semi-color-overlay-bg)", + "comment": "裁切框遮罩背景颜色", + "category": "color", + "raw": "$color-cropper_mask-bg: var(--semi-color-overlay-bg); // 裁切框遮罩背景颜色" + }, + { + "key": "$color-cropper_box-outline", + "value": "var(--semi-color-primary)", + "comment": "裁切框边框颜色", + "category": "color", + "raw": "$color-cropper_box-outline: var(--semi-color-primary); // 裁切框边框颜色" + }, + { + "key": "$color-cropper_box_corner-bg", + "value": "var(--semi-color-primary)", + "comment": "裁切框调整块背景色", + "category": "color", + "raw": "$color-cropper_box_corner-bg: var(--semi-color-primary); // 裁切框调整块背景色" + }, + { + "key": "$width-cropper_box-outline", + "value": "1px", + "comment": "裁切框边框宽度", + "category": "width", + "raw": "$width-cropper_box-outline: 1px; // 裁切框边框宽度" + }, + { + "key": "$width-cropper_box_corner", + "value": "10px", + "comment": "裁切框调整块宽高", + "category": "width", + "raw": "$width-cropper_box_corner: 10px; // 裁切框调整块宽高" + } + ], "datepicker": [ { "key": "$width-datepicker_day", @@ -10011,7 +10332,7 @@ { "key": "$color-inputNumber_button-bg-hover", "value": "var(--semi-color-fill-0)", - "comment": "步进器按钮图标颜色 - 悬浮", + "comment": "步进�按钮图标颜色 - 悬浮", "category": "color", "raw": "$color-inputNumber_button-bg-hover: var(--semi-color-fill-0); // 步进器按钮图标颜色 - 悬浮" }, @@ -10107,6 +10428,77 @@ "raw": "$transform_scale-inputNumber: var(--semi-transform_scale-none);//数字输入框-变大" } ], + "jsonviewer": [ + { + "key": "$color-json-viewer-background", + "value": "var(--semi-color-default)", + "comment": "JSON背景颜色", + "category": "color", + "raw": "$color-json-viewer-background: var(--semi-color-default); // JSON背景颜色" + }, + { + "key": "$color-json-viewer-key", + "value": "rgba(var(--semi-red-5), 1)", + "comment": "JSON key 颜色", + "category": "color", + "raw": "$color-json-viewer-key: rgba(var(--semi-red-5), 1); // JSON key 颜色" + }, + { + "key": "$color-json-viewer-value", + "value": "rgba(var(--semi-blue-5), 1)", + "category": "color", + "raw": "$color-json-viewer-value: rgba(var(--semi-blue-5), 1); " + }, + { + "key": "$color-json-viewer-number", + "value": "rgba(var(--semi-green-5), 1)", + "comment": "JSON number 颜色", + "category": "color", + "raw": "$color-json-viewer-number: rgba(var(--semi-green-5), 1); // JSON number 颜色" + }, + { + "key": "$color-json-viewer-keyword", + "value": "rgba(var(--semi-blue-5), 1)", + "comment": "JSON keyword 颜色", + "category": "color", + "raw": "$color-json-viewer-keyword: rgba(var(--semi-blue-5), 1); // JSON keyword 颜色" + }, + { + "key": "$color-json-viewer-delimiter-comma", + "value": "rgba(var(--semi-blue-6), 1)", + "comment": "JSON delimiter comma 颜色", + "category": "color", + "raw": "$color-json-viewer-delimiter-comma: rgba(var(--semi-blue-6), 1); // JSON delimiter comma 颜色" + }, + { + "key": "$color-json-viewer-search-result-background", + "value": "rgba(var(--semi-green-2), 1)", + "comment": "JSON search result background 颜色", + "category": "color", + "raw": "$color-json-viewer-search-result-background: rgba(var(--semi-green-2), 1); // JSON search result background 颜色" + }, + { + "key": "$color-json-viewer-current-search-result-background", + "value": "rgba(var(--semi-yellow-4), 1)", + "comment": "JSON current search result background 颜色", + "category": "color", + "raw": "$color-json-viewer-current-search-result-background: rgba(var(--semi-yellow-4), 1); // JSON current search result background 颜色" + }, + { + "key": "$color-json-viewer-folding-icon", + "value": "rgba(var(--semi-blue-7), 1)", + "comment": "JSON folding icon 颜色", + "category": "color", + "raw": "$color-json-viewer-folding-icon: rgba(var(--semi-blue-7), 1); // JSON folding icon 颜色" + }, + { + "key": "$color-json-viewer-line-number", + "value": "rgba(var(--semi-grey-5), 1)", + "comment": "JSON line number 颜色", + "category": "color", + "raw": "$color-json-viewer-line-number: rgba(var(--semi-grey-5), 1); // JSON line number 颜色" + } + ], "list": [ { "key": "$color-list_default-border-default", @@ -13724,6 +14116,55 @@ "comment": "伸缩框组件中handler的z-index", "category": "other", "raw": "$z-resizable_handler: 2000 !default; // 伸缩框组件中handler的z-index" + }, + { + "key": "$z-resizable_background", + "value": "2010", + "comment": "伸缩框组件中背景的z-index", + "category": "other", + "raw": "$z-resizable_background: 2010; // 伸缩框组件中背景的z-index" + }, + { + "key": "$height-row-handler", + "value": "10px", + "comment": "单个伸缩框中上下handler的高度", + "category": "height", + "raw": "$height-row-handler: 10px; // 单个伸缩框中上下handler的高度" + }, + { + "key": "$width-col-handler", + "value": "10px", + "comment": "单个伸缩框中左右handler的宽度", + "category": "width", + "raw": "$width-col-handler: 10px; // 单个伸缩框中左右handler的宽度" + }, + { + "key": "$width-edge-handler", + "value": "20px", + "comment": "单个伸缩框中边角handler的宽度", + "category": "width", + "raw": "$width-edge-handler: 20px; // 单个伸缩框中边角handler的宽度" + }, + { + "key": "$height-edge-handler", + "value": "20px", + "comment": "单个伸缩框中边角handler的高度", + "category": "height", + "raw": "$height-edge-handler: 20px; // 单个伸缩框中边角handler的高度" + }, + { + "key": "$width-horizontal-handler", + "value": "10px", + "comment": "组合伸缩框中水平方向handler的宽度", + "category": "width", + "raw": "$width-horizontal-handler: 10px; // 组合伸缩框中水平方向handler的宽度" + }, + { + "key": "$height-vertical-handler", + "value": "10px", + "comment": "组合伸缩框中垂直方向handler的高度", + "category": "height", + "raw": "$height-vertical-handler: 10px; // 组合伸缩框中垂直方向handler的高度" } ], "scrolllist": [ diff --git a/docs/src/zh-CN/plus/audioPlayer/index.md b/docs/src/zh-CN/plus/audioPlayer/index.md new file mode 100644 index 00000000..70755b0f --- /dev/null +++ b/docs/src/zh-CN/plus/audioPlayer/index.md @@ -0,0 +1,175 @@ +--- +localeCode: zh-CN +order: 91 +category: Plus +title: AudioPlayer 音频播放器 +icon: doc-audioplayer +width: 60% +brief: 用于播放音频 +showNew: true +--- + +## 代码演示 + +### 如何引入 + +```jsx import +import { AudioPlayer } from '@kousum/semi-ui-vue'; +``` + + +### 基本用法 + +基本使用,通过`audioUrl`传入音频地址 +audioUrl 可以传入字符串,字符串数组,对象,对象数组, 具体参数参考 [AudioPlayer](#AudioPlayer) + +```jsx live=true noInline=true dir="column" +import { AudioPlayer } from '@kousum/semi-ui-vue'; + +function Demo() { + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3'; + const audioUrlArr = [ + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + ]; + const audioUrlObj = { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }; + const audioUrlArrObj = [ + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + title: '音频标题2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +export default Demo; + +``` + + +### 隐藏工具栏 + +showToolbar 设置为false,则隐藏工具栏 + + +```jsx live=true noInline=true dir="column" +import { AudioPlayer } from '@kousum/semi-ui-vue'; + +function Demo() { + const audioUrlObj = { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题' + }; + + return ( +
+ +
+ ); +} + +export default Demo; + +``` + +### 主题 + +通过 `theme` 设置音频播放器主题,支持 `light` 和 `dark`,默认 `dark` + + +```jsx live=true noInline=true dir="column" +import { AudioPlayer } from '@kousum/semi-ui-vue'; + +function Demo() { + const audioUrlArrObj = [ + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + title: '音频标题2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return ( +
+ +
+ ); +} + +export default Demo; + +``` + +## API 参考 + +### AudioPlayer + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|--------------| +| audioUrl | 音频地址 | string | string[] | AudioInfo | AudioInfo[] | - | +| autoPlay | 自动播放 | boolean | false | +| theme | 主题,可选值:`dark` 和 `light` | string | "dark" | +| showToolbar | 是否显示工具栏 | boolean | true | +| skipDuration | 跳转时间 | number | 10 | +| className | 类名 | string | - | +| style | 内联样式 | object | - | + +### AudioInfo + +| 属性 | 说明 | 类型 | 默认值 | +|-------------------|------------------------------------------------|---------------------------------|-----------| +| src | 音频地址 | string | - | +| title | 音频标题 | string | - | +| cover | 封面图片 | string | - | + + diff --git a/docs/src/zh-CN/plus/dragMove/index.md b/docs/src/zh-CN/plus/dragMove/index.md new file mode 100644 index 00000000..2389194a --- /dev/null +++ b/docs/src/zh-CN/plus/dragMove/index.md @@ -0,0 +1,276 @@ +--- +localeCode: zh-CN +order: 26 +category: Plus +title: DragMove 拖拽移动 +icon: doc-dragmove +dir: column +brief: 可通过拖拽改变位置 +showNew: true +--- + +## 使用场景 + +用于设置元素可被拖动改变位置,支持限制拖拽范围,支持自定义触发拖动的元素。 + +## 代码演示 + +### 如何引入 + +DragMove 从 v2.71.0 开始支持 + +```jsx +import { DragMove } from '@kousum/semi-ui-vue'; +``` + +### 基本用法 + +被 `DragMove` 包裹的元素将能够通过拖拽改变位置。 + +**_注意_** + +1. DragMove 会将可拖拽的元素设置为 absolute 定位 +2. DragMove 需要将 DOM 事件监听器应用到 children 中,如果子元素是自定义的组件,你需要确保它能将属性传递至底层的 DOM 元素。支持以下类型的 children: + 1. Class Component,不强制绑定 ref,但需要确保 props 可被透传至真实的 DOM 节点上 + 2. 使用 forwardRef 包裹后的函数式组件,将 props 与 ref 透传到 children 内真实的 DOM 节点上 + 3. 真实 DOM 节点, 如 span,div,p... + +```jsx live=true +import { DragMove } from '@kousum/semi-ui-vue'; + +function Demo() { + return ( + +
+ Drag me +
+
+ ); +} + +export default Demo; +``` + +### 限制拖动范围 + +传入 `constrainer`, 该函数返回限制可拖拽范围的元素。 + +**_注意:constrainer 设置的元素需要为 relative 定位_** + +```jsx live=true +import { DragMove } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +export default defineComponent(() => { + const containerRef = ref(); + return () => { + return ( +
+ Constrainer + containerRef.value}> +
+ Drag me +
+
+
+ ); + }; +}); +``` + +### 自定义触发拖动的元素 + +可通过 `handler` 自定义触发拖动的元素。如果不设置, 则点击任意位置均可拖动;如果设置,则仅点击 handler 部分可拖动。 + +```jsx live=true +import { IconTransparentStroked } from '@kousum/semi-icons-vue'; +import { DragMove } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +export default defineComponent(() => { + const handlerRef = ref(); + const containerRef = ref(); + return () => { + return ( +
+ Constrainer + handlerRef.value} constrainer={() => containerRef.value}> +
+
+ +
+
+
+
+ ); + }; +}); +``` + +### 自定义拖动后的位置处理 + +可通过 `customMove` 自定义拖动后的位置处理,该参数设置后,DragMove 组件内部将仅通过参数返回计算后的位置,不做设置,用户按需自行设置新位置。 + +```jsx live=true +import { DragMove } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +export default defineComponent(() => { + const containerRef = ref(); + const elementRef = ref(); + const startPoint = ref(); + + const customMove = (element, top, left) => { + if (left + 100 > containerRef.value.offsetWidth) { + element.style.right = `${containerRef.value.offsetWidth - left - element.offsetWidth}px`; + element.style.left = 'auto'; + } else { + element.style.left = left + 'px'; + } + element.style.top = top + 'px'; + }; + + const onMouseDown = (e) => { + startPoint.value = { + x: e.clientX, + y: e.clientY, + }; + }; + + const onMouseUp = (e) => { + if (startPoint.value) { + const { x, y } = startPoint.value; + if (Math.abs(e.clientX - x) < 5 && Math.abs(e.clientY - y) < 5) { + if (elementRef.value.style.width === '60px') { + elementRef.value.style.width = '100px'; + } else { + elementRef.value.style.width = '60px'; + } + } + } + startPoint.value = null; + }; + + return () => ( + <> + 蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制 +
+
+
+ Constrainer + containerRef.value} customMove={customMove}> +
+ Drag me +
+
+
+ + ); +}); +``` + +### API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| allowInputDrag | 点击原生 input/textarea 时是否允许拖动 | boolean | false | +| allowMove | 点击/触摸时是否允许拖动的判断函数 | (event: TouchEvent \|MouseEvent, element: ReactNode) => boolean | - | +| constrainer | 返回限制可拖拽的范围的元素 | () => ReactNode | - | +| customMove | 自定义拖动后的位置处理 | (element: ReactNode, top: number, left: number) => void | - | +| handler | 返回触发拖动的元素 | () => ReactNode | - | +| onMouseDown | 鼠标按下时的回调 | (e: MouseEvent) => void | - | +| onMouseMove | 鼠标移动时的回调 | (e: MouseEvent) => void | - | +| onMouseUp | 鼠标抬起时的回调 | (e: MouseEvent) => void | - | +| onTouchCancel | 触摸取消时的回调 | (e: TouchEvent) => void | - | +| onTouchEnd | 触摸结束时的回调 | (e: TouchEvent) => void | - | +| onTouchMove | 触摸移动时的回调 | (e: TouchEvent) => void | - | +| onTouchStart | 触摸开始时的回调 | (e: TouchEvent) => void | - | diff --git a/docs/src/zh-CN/show/cropper/index.md b/docs/src/zh-CN/show/cropper/index.md new file mode 100644 index 00000000..03527aa0 --- /dev/null +++ b/docs/src/zh-CN/show/cropper/index.md @@ -0,0 +1,308 @@ +--- +localeCode: zh-CN +order: 69 +category: Plus +title: Cropper 图片裁切 +icon: doc-cropper +dir: column +brief: 通过设定裁切框的宽高比例,自由裁切图片 +showNew: true +--- + +## 使用场景 + +Cropper 用于裁切图片,支持自定义裁切框样式,可通过拖动调整裁切框位置,被裁切图片位置;可缩放,旋转被裁切图片。 + + +## 代码演示 + +### 如何引入 + +Cropper 从 v2.73.0 开始支持 + +```jsx +import { Cropper } from '@kousum/semi-ui-vue'; +``` + +### 基本用法 + +通过 `sr` 设置被裁切的图片; 可通过 `shape` 设置裁切框形状,默认为方形。 + +```jsx live=true dir=column noInline=true height="1400" +import { Cropper, Button, RadioGroup, Radio } from '@kousum/semi-ui-vue'; +import { defineComponent, ref, } from 'vue'; + + + +const containerStyle = { + width: '550px', + height: '300px', + margin: '20px', +} + +const Demo = defineComponent(()=>{ + + const ref_ = ref(null); + const shape = ref('rect'); + function setShape(v){ + shape.value = v + } + + const onButtonClick = () => { + const value = ref_.value.getCropperCanvas(); + const previewContainer = document.getElementById('previewContainer'); + previewContainer.innerHTML = ''; + previewContainer.appendChild(value); + }; + + const onShapeChange = (e) => { + setShape(e.target.value); + } + + return ()=><> + + rect + round + roundRect + + + +
+ ; +}) + +export default Demo +``` + +### 自定义裁切框比例 + +可通过 `defaultAspectRatio` 初始的裁切框比例(默认为 1)。可通过 `aspectRatio` 设置固定的裁切框比例。 + +设置 `defaultAspectRatio`仅对初始的裁切框比例生效, 拖动时,裁切框比例会随着拖动而变化。 + +设置 `aspectRatio` 时,裁切框比例固定,拖动时将裁切框将以此比例变化。 + +```jsx live=true dir=column noInline=true height="1400" +import { Cropper, Button, RadioGroup, Radio } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +const containerStyle = { + width: '550px', + height: '300px', + margin: '20px', +} + +const Demo = defineComponent(()=>{ + + const ref_ = ref(null); + const shape = ref('rect'); + function setShape(v){ + shape.value = v + } + + const onButtonClick = () => { + const value = ref_.value.getCropperCanvas(); + const previewContainer = document.getElementById('previewContainer-aspect'); + previewContainer.innerHTML = ''; + previewContainer.appendChild(value); + }; + + return ()=> <> + + +
+ ; +}) + +export default Demo +``` + +### 受控旋转/缩放图片 + +通过 `rotate` 和 `zoom` 控制图片旋转和缩放, 可通过 `onZoomChange` 拿到最新的 `zoom` 值。 + +```jsx live=true dir=column noInline=true height="1400" +import { Cropper, Button, Slider } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +const containerStyle = { + width: '550px', + height: '300px', + margin: '20px', +} + +const actionStyle = { + marginTop: '20px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 'fit-content' +} + + +const Demo = defineComponent(()=>{ + + const rotate = ref(0); + function setRotate(v){ + rotate.value = v + } + const zoom = ref(1); + function setZoom(v){ + zoom.value = v + } + const ref_ = ref(); + + const onZoomChange = (value) => { + setZoom(value); + } + + const onSliderChange = (value) => { + setRotate(value); + } + + const onButtonClick = () => { + const value = ref_.value.getCropperCanvas(); + const previewContainer = document.getElementById('previewContainer-control'); + previewContainer.innerHTML = ''; + previewContainer.appendChild(value); + } + + return ()=>( +
+ +
+ Rotate + +
+
+ Zoom + +
+
+ +
+
+
+
+
+ ); +}) + +export default Demo +``` + +### 裁切框设置 + +可通过 `cropperBoxStyle`, `cropperBoxClassName` 自定义裁切框样式。可通过 `showResizeBox` 设置是否展示裁切框边角的调整块。 + +```jsx live=true dir=column noInline=true height="1400" +import { Cropper, Button, Switch } from '@kousum/semi-ui-vue'; +import { defineComponent, ref } from 'vue'; + +const containerStyle = { + width: '550px', + height: '300px', + margin: '20px', +} + +const centerStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 'fit-content' +} + + +const Demo = defineComponent(()=>{ + const ref_ = ref(null); + + const onButtonClick = () => { + const value = ref_.value.getCropperCanvas(); + const previewContainer = document.getElementById('previewContainer-cropperBox'); + previewContainer.innerHTML = ''; + previewContainer.appendChild(value); + } + + return ()=><> + showResizeBox = false,并修改边框颜色 + + +
+ ; +}) +export default Demo +``` + +### API + +| 属性 | 说明 | 类型 | 默认值 | +|-----|------|-----|------| +| aspectRatio | 裁切框比例 | number | - | +| className | 类名 | string | - | +| cropperBoxClassName | 裁切框类名 | string | - | +| cropperBoxStyle | 裁切框样式 | CSSProperties | - | +| defaultAspectRatio | 初始裁切框比例 | number | 1 | +| imgProps | 透传给 img 标签的属性 | object | - | +| fill | 裁切结果中非图片部分的填充色 | string | 'rgba(0, 0, 0, 0)' | +| maxZoom | 最大缩放倍数 | number | 3 | +| minZoom | 最小缩放倍数 | number | 0.1 | +| onZoomChange | 缩放回调 | (zoom: number) => void | - | +| rotate | 旋转角度 | number | - | +| shape | 裁切框形状 | 'rect' \| 'round' \| 'roundRect' | 'rect' | +| src | 图片地址 | string | - | +| showResizeBox | 是否展示调整块 | boolean | true | +| style | 样式 | CSSProperties | - | +| zoom | 缩放比例 | number | - | +| zoomStep | 缩放步长 | number | 0.1 | + +### Methods + +绑定在组件实例上的方法,可以通过 ref 调用实现某些特殊交互 + +| Name | Description | +|---------|--------------| +| getCropperCanvas | 获取裁剪图片的 canvas | + +## 设计变量 + + diff --git a/package.json b/package.json index 2c71c7a9..2df904f6 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "packages/vite-plugin-semi-theme" ], "dependencies": { - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", "@vue/repl": "4.4.2", "lodash": "^4.17.21", "vue": "^3.5.13" @@ -48,7 +48,7 @@ "@babel/preset-react": "^7.16.7", "@changesets/cli": "^2.27.1", "@chromatic-com/storybook": "1.3.4", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-theme-default": "2.73.0", "@kousum/semi-icons-lab-vue": "workspace: *", "@kousum/semi-icons-vue": "workspace: *", "@kousum/semi-illustrations-vue": "workspace: *", @@ -106,7 +106,8 @@ }, "pnpm": { "patchedDependencies": { - "@vue/repl@4.4.2": "patches/@vue__repl@4.4.2.patch" + "@vue/repl@4.4.2": "patches/@vue__repl@4.4.2.patch", + "@douyinfe/semi-foundation@2.73.0": "patches/@douyinfe__semi-foundation@2.73.0.patch" } } } diff --git a/packages/semi-animation-vue/package.json b/packages/semi-animation-vue/package.json index acd9bdbe..3d646684 100644 --- a/packages/semi-animation-vue/package.json +++ b/packages/semi-animation-vue/package.json @@ -38,10 +38,10 @@ "preview": "vite preview" }, "dependencies": { - "@douyinfe/semi-animation": "2.72.0", - "@douyinfe/semi-animation-styled": "2.72.0", - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-animation": "2.73.0", + "@douyinfe/semi-animation-styled": "2.73.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", "classnames": "^2.3.2", "sass": "^1.57.1", "vue": "^3.5.13" diff --git a/packages/semi-icons-lab-vue/package.json b/packages/semi-icons-lab-vue/package.json index fb23500f..f0516363 100644 --- a/packages/semi-icons-lab-vue/package.json +++ b/packages/semi-icons-lab-vue/package.json @@ -28,8 +28,8 @@ "preview": "vite preview" }, "dependencies": { - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", "classnames": "^2.3.2", "sass": "^1.57.1", "vue": "^3.5.13" diff --git a/packages/semi-icons-vue/package.json b/packages/semi-icons-vue/package.json index 1bb0a09c..20b2e3b3 100644 --- a/packages/semi-icons-vue/package.json +++ b/packages/semi-icons-vue/package.json @@ -22,8 +22,8 @@ "preview": "vite preview" }, "dependencies": { - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", "classnames": "^2.3.2", "sass": "^1.57.1", "vue": "^3.5.13" diff --git a/packages/semi-illustrations-vue/package.json b/packages/semi-illustrations-vue/package.json index 30ae0284..b92c6d51 100644 --- a/packages/semi-illustrations-vue/package.json +++ b/packages/semi-illustrations-vue/package.json @@ -26,8 +26,8 @@ "build:icon": "node scripts/build-illustration.cjs" }, "dependencies": { - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", "classnames": "^2.3.2", "vue": "^3.5.13" }, diff --git a/packages/semi-ui-vue/package.json b/packages/semi-ui-vue/package.json index 27937706..75c674f2 100644 --- a/packages/semi-ui-vue/package.json +++ b/packages/semi-ui-vue/package.json @@ -24,9 +24,9 @@ "url": "https://github.com/rashagu/semi-design-vue" }, "dependencies": { - "@douyinfe/semi-foundation": "2.72.0", - "@douyinfe/semi-theme-default": "2.72.0", - "@douyinfe/semi-animation": "2.72.0", + "@douyinfe/semi-foundation": "2.73.0", + "@douyinfe/semi-theme-default": "2.73.0", + "@douyinfe/semi-animation": "2.73.0", "@kousum/semi-animation-vue": "workspace:*", "@kousum/semi-icons-vue": "workspace:*", "@kousum/semi-illustrations-vue": "workspace:*", diff --git a/packages/semi-ui-vue/src/App.tsx b/packages/semi-ui-vue/src/App.tsx index a3f39699..2e40e580 100755 --- a/packages/semi-ui-vue/src/App.tsx +++ b/packages/semi-ui-vue/src/App.tsx @@ -168,6 +168,8 @@ import DragMoveDemo from './components/dragMove/__test__/DragMoveDemo'; import JsonViewerDemo from './components/jsonViewer/__test__/JsonViewerDemo'; import SelectTest from './components/select/__test__/SelectTest'; import { InputVModel } from './components'; +import CropperDemo from './components/cropper/__test__/CropperDemo'; +import AudioPlayerDemo from './components/audioPlayer/__test__/AudioPlayerDemo'; export interface ExampleProps { name?: string @@ -198,7 +200,9 @@ const App = defineComponent((props, {slots}) => {
{a.value} - + + {/**/} + {/**/} {/**/} {/**/} {/**/} diff --git a/packages/semi-ui-vue/src/components/audioPlayer/__stories__/Demo.stories.tsx b/packages/semi-ui-vue/src/components/audioPlayer/__stories__/Demo.stories.tsx new file mode 100644 index 00000000..f8bb9630 --- /dev/null +++ b/packages/semi-ui-vue/src/components/audioPlayer/__stories__/Demo.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import Demo from "../__test__/AudioPlayerDemo"; + +const meta = { + /* 👇 The title prop is optional. + * See https://storybook.js.org/docs/7.0/vue/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'BPlus 组件/AudioPlayer', + render: (args: any) => ({ + setup() { + return ()=>(
+ +
); + }, + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout + layout: 'fullscreen', + }, + // This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + }, +}; diff --git a/packages/semi-ui-vue/src/components/audioPlayer/__test__/AudioPlayerDemo.tsx b/packages/semi-ui-vue/src/components/audioPlayer/__test__/AudioPlayerDemo.tsx new file mode 100644 index 00000000..87f55733 --- /dev/null +++ b/packages/semi-ui-vue/src/components/audioPlayer/__test__/AudioPlayerDemo.tsx @@ -0,0 +1,76 @@ +import { defineComponent, ref, h, Fragment, useSlots } from 'vue'; +import { CombineProps } from '../../interface'; +import AudioPlayer from '../index'; + +interface AudioPlayerDemoProps { + name?: string; +} + +export const vuePropsType: CombineProps = { + name: String, +}; +const AudioPlayerDemo = defineComponent({ + props: { ...vuePropsType }, + name: 'AudioPlayerDemo', + setup(props, { attrs }) { + const slots = useSlots(); + const audioUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3'; + const audioUrlArr = [ + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + ]; + const audioUrlObj = { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }; + const audioUrlArrObj = [ + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio1.mp3', + title: '音频标题1', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + { + src: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/components/audio2.mp3', + title: '音频标题2', + cover: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg', + }, + ]; + + return () => ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ); + }, +}); + + +export default AudioPlayerDemo; + diff --git a/packages/semi-ui-vue/src/components/audioPlayer/audioSlider.tsx b/packages/semi-ui-vue/src/components/audioPlayer/audioSlider.tsx new file mode 100644 index 00000000..9e568ba5 --- /dev/null +++ b/packages/semi-ui-vue/src/components/audioPlayer/audioSlider.tsx @@ -0,0 +1,187 @@ +import cls from 'classnames'; +import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; +import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; +import Tooltip from '../tooltip'; +import { formatTime } from './utils'; +import { noop } from 'lodash'; +import { AudioPlayerTheme } from './index'; +import { defineComponent, h, PropType, reactive, shallowRef, useSlots } from 'vue'; +import { CombineProps } from '../interface'; +import { vuePropsMake } from '../PropTypes'; +import { styleNum } from '../_utils'; + +interface AudioSliderProps { + value: number; + onChange?: (value: number) => void; + className?: string; + max?: number; + vertical?: boolean; + width?: number | string; + height?: number | string; + showTooltip?: boolean; + disabled?: boolean; + theme?: AudioPlayerTheme; +} + +interface AudioSliderState { + isDragging: boolean; + movingInfo: { progress: number; offset: number } | null; + isHovering: boolean; +} +const prefixCls = cssClasses.PREFIX; +const propTypes: CombineProps = { + value: { + type: Number, + required: true, + }, + onChange: Function as PropType, + className: String, + max: Number, + vertical: Boolean, + width: [Number, String], + height: [Number, String], + showTooltip: Boolean, + disabled: Boolean, + theme: String as PropType, +}; +const defaultProps = { + value: 0, + onChange: noop, + max: 100, + vertical: false, + width: '100%', + height: 4, + showTooltip: true, + disabled: false, + theme: 'dark', +}; + +const vuePropsType = vuePropsMake(propTypes, defaultProps); +const AudioSlider = defineComponent({ + props: { ...vuePropsType }, + name: 'AudioSlider', + setup(props, { attrs }) { + const slots = useSlots(); + + const sliderRef = shallowRef(); + const handleRef = shallowRef(); + const state = reactive({ + isDragging: false, + isHovering: false, + movingInfo: null, + }); + + const handleMouseEnter = (e: MouseEvent) => { + state.isHovering = true; + handleMouseEvent(e, false); + }; + + const handleMouseDown = (e: MouseEvent) => { + state.isDragging = true; + handleMouseEvent(e, true); + }; + + const handleMouseUp = () => { + if (state.isDragging) { + state.isDragging = false; + } + }; + + const handleMouseEvent = (e: MouseEvent, shouldSetValue: boolean = true) => { + if (!sliderRef.value || props.disabled) return; + const rect = sliderRef.value.getBoundingClientRect(); + const offset = props.vertical ? rect.bottom - e.clientY : e.clientX - rect.left; + const total = props.vertical ? rect.height : rect.width; + const percentage = Math.min(Math.max(offset / total, 0), 1); + const value = percentage * props.max; + if (shouldSetValue && (state.isDragging || e.type === 'mousedown')) { + props.onChange(value); + } + + state.movingInfo = { + progress: percentage, + offset: props.vertical ? offset - rect.height / 2 : offset - rect.width / 2, + }; + }; + + const handleMouseMove = (e: MouseEvent) => { + handleMouseEvent(e, true); + }; + + const handleMouseLeave = () => { + state.isHovering = false; + state.isDragging = false; + }; + + return () => { + const { vertical, width, height, showTooltip, max, value: currentValue, theme } = props; + const { movingInfo, isHovering } = state; + const sliderContent = ( +
+
+
+
+
+
+ ); + + return showTooltip ? ( + + {sliderContent} + + ) : ( + sliderContent + ); + }; + }, +}); + +export default AudioSlider; diff --git a/packages/semi-ui-vue/src/components/audioPlayer/index.tsx b/packages/semi-ui-vue/src/components/audioPlayer/index.tsx new file mode 100644 index 00000000..1a7f3c4c --- /dev/null +++ b/packages/semi-ui-vue/src/components/audioPlayer/index.tsx @@ -0,0 +1,484 @@ +import cls from 'classnames'; +import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; +import Button from '../button'; +import Dropdown from '../dropdown'; +import Image from '../image'; +import Tooltip from '../tooltip'; +import Popover from '../popover'; +//TODO 放Dropdown后面 +import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; +import { + IconAlertCircle, + IconBackward, + IconFastForward, + IconPause, + IconPlay, + IconRefresh, + IconRestart, + IconVolume2, + IconVolumnSilent, +} from '@kousum/semi-icons-vue'; +import AudioSlider from './audioSlider'; +import AudioPlayerFoundation from '@douyinfe/semi-foundation/audioPlayer/foundation'; +import { AudioPlayerAdapter } from '@douyinfe/semi-foundation/audioPlayer/foundation'; +import { formatTime } from './utils'; +import { BaseProps, useBaseComponent } from '../_base/baseComponent'; +import { + type CSSProperties, + defineComponent, + h, + nextTick, + onMounted, + onUnmounted, PropType, + reactive, + shallowRef, + useSlots, +} from 'vue'; +import { vuePropsMake } from '../PropTypes'; +import { CombineProps } from '../interface'; + +type AudioSrc = string; +type AudioInfo = { + title?: string; + cover?: string; + src: string; +}; +type AudioUrlArray = (AudioInfo | string)[]; + +type AudioUrl = AudioSrc | AudioInfo | AudioUrlArray; + +export type AudioPlayerTheme = 'dark' | 'light'; + +export interface AudioPlayerProps extends BaseProps { + audioUrl: AudioUrl; + autoPlay?: boolean; + showToolbar?: boolean; + skipDuration?: number; + theme?: AudioPlayerTheme; + className?: string; + style?: CSSProperties; +} + +export interface AudioPlayerState { + isPlaying: boolean; + currentIndex: number; + totalTime: number; + currentTime: number; + currentRate: { label: string; value: number }; + volume: number; + error: boolean; +} + +const prefixCls = cssClasses.PREFIX; +const propTypes: CombineProps = { + audioUrl: { + type: [String, Object, Array] as PropType, + required: true + }, + autoPlay: Boolean as PropType, + showToolbar: Boolean, + skipDuration: Number, + theme: String as PropType, + className: String, + style: Object, +}; +const defaultProps: Partial = { + autoPlay: false, + showToolbar: true, + skipDuration: 10, + theme: 'dark', +}; +export const vuePropsType = vuePropsMake(propTypes, defaultProps); +const AudioPlayer = defineComponent({ + props: { ...vuePropsType }, + name: 'AudioPlayer', + setup(props, { attrs }) { + const slots = useSlots(); + const audioRef = shallowRef(); + const rateOptions = [ + { label: '0.5x', value: 0.5 }, + { label: '0.75x', value: 0.75 }, + { label: '1.0x', value: 1 }, + { label: '1.5x', value: 1.5 }, + { label: '2.0x', value: 2 }, + ]; + const state = reactive({ + isPlaying: false, + currentIndex: 0, + totalTime: 0, + currentTime: 0, + currentRate: { label: '1.0x', value: 1 }, + volume: 100, + error: false, + }); + + const { adapter: adapterInject, getDataAttr } = useBaseComponent(props, state); + function adapter_(): AudioPlayerAdapter { + return { + ...adapterInject(), + init: () => { + if (audioRef.value) { + audioRef.value.addEventListener('loadedmetadata', () => { + foundation.initAudioState(); + }); + audioRef.value.addEventListener('error', () => { + foundation.errorHandler(); + }); + audioRef.value.addEventListener('ended', () => { + foundation.endHandler(); + }); + } + }, + destroy: () => { + if (audioRef.value) { + audioRef.value.removeEventListener('loadedmetadata', () => { + foundation.initAudioState(); + }); + audioRef.value.removeEventListener('error', () => { + foundation.errorHandler(); + }); + audioRef.value.removeEventListener('ended', () => { + foundation.endHandler(); + }); + } + }, + handleStatusClick: () => { + if (!audioRef.value) return; + if (state.isPlaying) { + audioRef.value.pause(); + } else { + audioRef.value.play(); + } + state.isPlaying = !state.isPlaying; + }, + getAudioRef: () => audioRef.value, + resetAudioState: () => { + state.isPlaying = true; + state.currentTime = 0; + state.currentRate = { label: '1.0x', value: 1 }; + nextTick(() => { + if (audioRef.value) { + audioRef.value.currentTime = state.currentTime; + audioRef.value.playbackRate = state.currentRate.value; + audioRef.value.play(); + } + }); + }, + handleTimeUpdate: () => { + if (!audioRef.value) return; + state.currentTime = audioRef.value.currentTime; + }, + handleTrackChange: (direction: 'next' | 'prev') => { + if (!audioRef.value) return; + const { audioUrl } = props as AudioPlayerProps; + const isAudioUrlArray = Array.isArray(audioUrl); + if (isAudioUrlArray) { + if (direction === 'next') { + state.currentIndex = (state.currentIndex + 1) % audioUrl.length; + state.error = false; + } else { + state.currentIndex = (state.currentIndex - 1 + audioUrl.length) % audioUrl.length; + state.error = false; + } + } + foundation.resetAudioState(); + }, + handleTimeChange: (value: number) => { + if (!audioRef.value) return; + audioRef.value.currentTime = value; + state.currentTime = value; + }, + handleRefresh: () => { + if (!audioRef.value) return; + if (state.error) { + audioRef.value.load(); + } else { + audioRef.value.currentTime = 0; + state.currentTime = 0; + } + }, + handleSpeedChange: (value: { label: string; value: number }) => { + if (!audioRef.value) return; + audioRef.value.playbackRate = value.value; + state.currentRate = value; + }, + handleSeek: (direction: number) => { + if (!audioRef.value) return; + const { skipDuration = 10 } = props; + const newTime = Math.min( + Math.max(audioRef.value.currentTime + direction * skipDuration, 0), + audioRef.value.duration + ); + audioRef.value.currentTime = newTime; + }, + handleVolumeChange: (value: number) => { + if (!audioRef.value) return; + const volume = Math.floor(value); + audioRef.value.volume = volume / 100; + state.volume = volume; + }, + }; + } + const adapter = adapter_(); + const foundation = new AudioPlayerFoundation(adapter); + + onMounted(() => { + foundation.init(); + }); + onUnmounted(() => { + foundation.destroy(); + }); + + const handleStatusClick = () => { + foundation.handleStatusClick(); + }; + + const handleTrackChange = (direction: 'next' | 'prev') => { + foundation.handleTrackChange(direction); + }; + + const handleTimeChange = (value: number) => { + foundation.handleTimeChange(value); + }; + + const handleRefresh = () => { + foundation.handleRefresh(); + }; + + const handleSpeedChange = (value: { label: string; value: number }) => { + foundation.handleSpeedChange(value); + }; + + const handleSeek = (direction: number) => { + foundation.handleSeek(direction); + }; + + const handleTimeUpdate = () => { + foundation.handleTimeUpdate(); + }; + + const handleVolumeChange = (value: number) => { + foundation.handleVolumeChange(value); + }; + + const handleVolumeSilent = () => { + if (!audioRef.value) return; + audioRef.value.volume = state.volume === 0 ? 0.5 : 0; + state.volume = state.volume === 0 ? 50 : 0; + }; + + const getAudioInfo = (audioUrl: AudioUrl) => { + const isAudioUrlArray = Array.isArray(audioUrl); + if (isAudioUrlArray) { + const audioInfo = audioUrl[state.currentIndex]; + if (typeof audioInfo === 'string') { + return { src: audioInfo, audioTitle: null, audioCover: null }; + } else { + return { src: audioInfo.src, audioTitle: audioInfo.title, audioCover: audioInfo.cover }; + } + } else if (typeof audioUrl === 'string') { + return { src: audioUrl, audioTitle: null, audioCover: null }; + } else { + return { src: audioUrl.src, audioTitle: audioUrl.title, audioCover: audioUrl.cover }; + } + }; + + const renderControl = () => { + const { error } = state; + const isAudioUrlArray = Array.isArray(props.audioUrl); + const iconClass = cls(`${prefixCls}-control-button-icon`); + const circleStyle = { + borderRadius: '50%', + }; + const transparentStyle = { + background: 'transparent', + }; + const playStyle = { + marginLeft: '1px', + }; + return ( +
+ {isAudioUrlArray && ( + +
+ ); + }; + + const renderInfo = () => { + const { audioTitle, audioCover } = getAudioInfo(props.audioUrl); + const { theme } = props; + const { currentTime, totalTime, error } = state; + return ( +
+ {audioCover && } +
+ {audioTitle && ( +
+ {audioTitle} + {error && renderError()} +
+ )} + {!error && ( +
+ {formatTime(currentTime)} +
+ +
+ {formatTime(totalTime)} +
+ )} +
+
+ ); + }; + + const renderToolbar = () => { + const { volume, error } = state; + const { skipDuration = 10, theme = 'dark' } = props; + const iconClass = cls(`${prefixCls}-control-button-icon`); + const transparentStyle = { + background: 'transparent', + }; + const isVolumeSilent = volume === 0; + return !error ? ( +
+ +
{volume}%
+ +
+ } + > +
+ ) : ( +
+
+ ); + }; + + const renderError = () => ( +
+ + 音频加载失败 +
+ ); + + return () => { + const { audioUrl, autoPlay, className, style, showToolbar = true, theme = 'dark' } = props; + const src = getAudioInfo(audioUrl).src; + return ( +
+ + {renderControl()} + {renderInfo()} + {showToolbar && renderToolbar()} +
+ ); + }; + }, +}); + +export default AudioPlayer; diff --git a/packages/semi-ui-vue/src/components/audioPlayer/utils.ts b/packages/semi-ui-vue/src/components/audioPlayer/utils.ts new file mode 100644 index 00000000..87af2037 --- /dev/null +++ b/packages/semi-ui-vue/src/components/audioPlayer/utils.ts @@ -0,0 +1,5 @@ +export const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +}; \ No newline at end of file diff --git a/packages/semi-ui-vue/src/components/cropper/__stories__/Demo.stories.tsx b/packages/semi-ui-vue/src/components/cropper/__stories__/Demo.stories.tsx new file mode 100644 index 00000000..73977a0b --- /dev/null +++ b/packages/semi-ui-vue/src/components/cropper/__stories__/Demo.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import Demo from "../__test__/CropperDemo"; + +const meta = { + /* 👇 The title prop is optional. + * See https://storybook.js.org/docs/7.0/vue/configure/overview#configure-story-loading + * to learn how to generate automatic titles + */ + title: 'D展示类/Cropper', + render: (args: any) => ({ + setup() { + return ()=>(
+ +
); + }, + }), + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout + layout: 'fullscreen', + }, + // This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + }, +}; diff --git a/packages/semi-ui-vue/src/components/cropper/__test__/CropperDemo.tsx b/packages/semi-ui-vue/src/components/cropper/__test__/CropperDemo.tsx new file mode 100644 index 00000000..b423c5ca --- /dev/null +++ b/packages/semi-ui-vue/src/components/cropper/__test__/CropperDemo.tsx @@ -0,0 +1,79 @@ +import { defineComponent, ref, h, Fragment, useSlots, onMounted } from 'vue'; +import { CombineProps } from '../../interface'; +import { Group as RadioGroup, Radio } from '../../radio'; +import Cropper from '../index'; +import Button from '../../button'; + +interface CropperDemoProps { + name?: string; +} + +export const vuePropsType: CombineProps = { + name: String, +}; +const CropperDemo = defineComponent({ + props: { ...vuePropsType }, + name: 'CropperDemo', + setup(props, { attrs }) { + const slots = useSlots(); + const containerStyle = { + width: '550px', + height: '300px', + margin: '20px', + } + + const dom = ref(null); + const shape = ref('rect'); + + const onButtonClick = () => { + const value = dom.value.getCropperCanvas(); + const previewContainer = document.getElementById('previewContainer'); + previewContainer.innerHTML = ''; + previewContainer.appendChild(value); + }; + + const onShapeChange = (e) => { + shape.value = e.target.value; + }; + + function testRef(v){ + console.log(v); + } + + const a = ref(0) + onMounted(()=>{ + console.log(); + // setInterval(()=>{ + // a.value++ + // }, 1000) + + setTimeout(()=>{ + + // onButtonClick() + }, 500) + }) + + return () => ( +
+ {a.value} + + rect + round + roundRect + + + +
+
+ ); + }, +}); + + +export default CropperDemo; + diff --git a/packages/semi-ui-vue/src/components/cropper/__test__/test.spec.ts b/packages/semi-ui-vue/src/components/cropper/__test__/test.spec.ts new file mode 100644 index 00000000..93948c21 --- /dev/null +++ b/packages/semi-ui-vue/src/components/cropper/__test__/test.spec.ts @@ -0,0 +1,25 @@ +import { expect, test, describe, beforeAll } from 'vitest' +import Comp from "./CropperDemo"; +import { fireEvent, render, screen } from '@testing-library/vue'; + +beforeAll(() => { + global.ResizeObserver = class ResizeObserver { + observe() { + // do nothing + } + unobserve() { + // do nothing + } + disconnect() { + // do nothing + } + }; +}); + +test('Demo test', async () => { + render(Comp) + await new Promise((resolve) => setTimeout(resolve, 1000)); + const previewContainer = await screen.findByTestId("previewContainer"); + const width = previewContainer.clientWidth + expect(width).toBeGreaterThan(-1) +}) diff --git a/packages/semi-ui-vue/src/components/cropper/index.tsx b/packages/semi-ui-vue/src/components/cropper/index.tsx new file mode 100644 index 00000000..2fcef6f6 --- /dev/null +++ b/packages/semi-ui-vue/src/components/cropper/index.tsx @@ -0,0 +1,365 @@ +import { + defineComponent, + ref, + h, + Fragment, + useSlots, + type CSSProperties, + PropType, + shallowRef, + reactive, + watch, onMounted, onUnmounted +} from 'vue' +import {CombineProps} from '../interface'; +import cls from "classnames"; +import * as PropTypes from '../PropTypes'; +import "@douyinfe/semi-foundation/cropper/cropper.scss"; +import CropperFoundation, { + CropperAdapter, + ImageDataState, + CropperBox +} from '@douyinfe/semi-foundation/cropper/foundation'; +import {cssClasses, strings} from '@douyinfe/semi-foundation/cropper/constants'; +import ResizeObserver, {ObserverProperty} from '../resizeObserver'; +import {isUndefined} from 'lodash'; +import {vuePropsMake} from "../PropTypes"; +import {useBaseComponent} from "../_base/baseComponent"; + + +interface CropperProps { + className?: string; + style?: CSSProperties; + /* The address of the image that needs to be cropped */ + src?: string; + /* Parameters that need to be transparently transmitted to the img node */ + imgProps?: HTMLImageElement; + /* The shape to crop, defaults to rectangle */ + shape?: 'rect' | 'round' | 'roundRect'; + /* Controlled crop ratio */ + aspectRatio?: number; + /* The initial width-to-height ratio of the cropping box, default is 1 */ + defaultAspectRatio?: number; + /* controlled scaling */ + /* when img loaded,After the image is loaded, an initial layer of scaling + will be performed on the image to fit the zoom area. + The zoom parameter is to zoom based on the initial zoom. + */ + zoom?: number; + onZoomChange?: (zoom: number) => void; + /* Image rotation angle */ + rotate?: number; + /* Show crop box resizing box ?*/ + showResizeBox?: boolean; + cropperBoxStyle?: CSSProperties; + cropperBoxCls?: string; + /* The fill color of the non-picture parts in the cut result */ + fill?: string; + maxZoom?: number; + minZoom?: number; + zoomStep?: number +} + +interface CropperState { + imgData: ImageDataState; + cropperBox: CropperBox; + zoom: number; + rotate: number; + loaded: boolean +} + +const prefixCls = cssClasses.PREFIX; + + +const propTypes: CombineProps = { + className: PropTypes.string, + style: PropTypes.object, + src: PropTypes.string, + imgProps: PropTypes.object, + shape: PropTypes.string as PropType, + aspectRatio: PropTypes.number, + defaultAspectRatio: PropTypes.number, + zoom: PropTypes.number, + onZoomChange: PropTypes.func as PropType, + + rotate: PropTypes.number, + showResizeBox: PropTypes.bool, + cropperBoxStyle: PropTypes.object, + cropperBoxCls: PropTypes.string, + fill: PropTypes.string, + maxZoom: PropTypes.number, + minZoom: PropTypes.number, + zoomStep: PropTypes.number, +}; + +const defaultProps = { + shape: 'rect', + defaultAspectRatio: 1, + showResizeBox: true, + fill: 'rgba(0, 0, 0, 0)', + maxZoom: 3, + minZoom: 0.1, + zoomStep: 0.1, +} + +export const vuePropsType = vuePropsMake(propTypes, defaultProps) +const index = defineComponent({ + props: {...vuePropsType}, + name: 'Cropper', + setup(props, {expose, attrs}) { + const slots = useSlots() + + const containerRef = shallowRef(); + const imgRef = shallowRef(); + + const state = reactive({ + imgData: { + width: 0, + height: 0, + centerPoint: { + x: 0, + y: 0 + } + }, + cropperBox: { + width: 0, + height: 0, + centerPoint: { + x: 0, + y: 0, + } + }, + zoom: 1, + rotate: 0, + loaded: false, + }) + const {adapter: adapterInject, getDataAttr} = useBaseComponent(props, state) + + function adapter_(): CropperAdapter { + return { + ...adapterInject(), + getContainer: () => containerRef.value as HTMLElement, + notifyZoomChange: (zoom: number) => { + const {onZoomChange} = props; + onZoomChange?.(zoom); + }, + getImg: () => imgRef.value, + }; + } + + const adapter = adapter_() + const foundation = new CropperFoundation(adapter); + + + function getDerivedStateFromProps(nextProps: CropperProps, prevState: CropperState) { + const {rotate: newRotate, zoom: newZoom} = nextProps; + const {rotate, zoom, imgData, cropperBox, loaded} = prevState; + let nextWidth = imgData.width, nextHeight = imgData.height; + let nextImgCenter = {...imgData.centerPoint}; + const nextState = {} as any; + if (!loaded) { + return null; + } + if (!isUndefined(newRotate) && newRotate !== rotate) { + nextState.rotate = newRotate; + if (loaded) { + // 因为以裁切框的左上方顶点作为原点,所以centerPoint 的 y 坐标与实际的坐标系方向相反, + // 因此 y 方向需要先做变换,再使用旋转变换公式计算中心点坐标 + const rotateCenter = { + x: cropperBox.centerPoint.x, + y: -cropperBox.centerPoint.y + }; + const imgCenter = { + x: imgData.centerPoint.x, + y: -imgData.centerPoint.y + }; + const angle = (newRotate - rotate) * Math.PI / 180; + nextImgCenter = { + x: (imgCenter.x - rotateCenter.x) * Math.cos(angle) + (imgCenter.y - rotateCenter.y) * Math.sin(angle) + rotateCenter.x, + y: -(-(imgCenter.x - rotateCenter.x) * Math.sin(angle) + (imgCenter.y - rotateCenter.y) * Math.cos(angle) + rotateCenter.y), + }; + } + } + if (!isUndefined(newRotate) && newZoom !== zoom) { + nextState.zoom = newZoom; + if (loaded) { + // 同上 + const scaleCenter = { + x: cropperBox.centerPoint.x, + y: -cropperBox.centerPoint.y + }; + const currentImgCenter = { + x: nextImgCenter.x, + y: -nextImgCenter.y + }; + nextWidth = imgData.width / zoom * newZoom; + nextHeight = imgData.height / zoom * newZoom; + nextImgCenter = { + x: (currentImgCenter.x - scaleCenter.x) / zoom * newZoom + scaleCenter.x, + y: -[(currentImgCenter.y - scaleCenter.y) / zoom * newZoom + scaleCenter.y], + }; + } + } + + if ((newRotate !== rotate || newZoom !== zoom)) { + nextState.imgData = { + width: nextWidth, + height: nextHeight, + centerPoint: nextImgCenter, + }; + } + + if (Object.keys(nextState).length) { + return nextState; + } + return null; + } + + function updateState() { + const newState = getDerivedStateFromProps({...props}, {...state}) + Object.keys(newState || {}).forEach((key) => { + // @ts-ignore + state[key] = newState[key] + }) + } + + watch([ + () => props.rotate, + () => props.zoom, + // () => state.rotate, + // () => state.zoom, + // () => state.imgData, + // () => state.cropperBox, + // () => state.loaded, + ], () => { + updateState() + }, {immediate: false}) + + onMounted(() => { + foundation.init(); + }) + onUnmounted(() => { + foundation.destroy(); + unRegisterImageWrapRef(); + }) + const unRegisterImageWrapRef = (): void => { + if (containerRef.value) { + (containerRef.value as any).removeEventListener("wheel", foundation.handleWheel); + } + containerRef.value = null; + }; + + const registryImageWrapRef = (ref: any): void => { + //TODO 解决vue中重复执行的问题 + if(containerRef.value === ref){ + return + } + + unRegisterImageWrapRef(); + if (ref) { + // We need to use preventDefault to prevent the page from being enlarged when zooming in with two fingers. + ref.addEventListener("wheel", foundation.handleWheel, {passive: false}); + } + containerRef.value = ref; + }; + + // ref method: Get the cropped canvas + const getCropperCanvas = () => { + return foundation.getCropperCanvas(); + } + + expose({ + getCropperCanvas + }) + + return () => { + const {className, style, src, shape, showResizeBox, cropperBoxStyle, cropperBoxCls} = props; + const {imgData, cropperBox, rotate, loaded} = state; + const imgX = imgData.centerPoint.x - imgData.width / 2; + const imgY = imgData.centerPoint.y - imgData.height / 2; + const cropperBoxX = cropperBox.centerPoint.x - cropperBox.width / 2; + const cropperBoxY = cropperBox.centerPoint.y - cropperBox.height / 2; + const cropperImgX = imgX - cropperBoxX; + const cropperImgY = imgY - cropperBoxY; + + return ({ + foundation.handleResize() + }} + observerProperty={ObserverProperty.Width} + > +
+ {/* Img layer */} +
+ +
+ {/* Mask layer */} +
+ {/* Cropper box */} +
+
+ +
+ {/* 裁剪框的拖拽操作按钮 */} + {loaded && showResizeBox && (shape === 'round' ? strings.roundCorner : strings.corner).map(corner => ( +
+ ))} +
+
+ ); + } + } +}) + + +export default index + diff --git a/packages/semi-ui-vue/src/components/dragMove/index.ts b/packages/semi-ui-vue/src/components/dragMove/index.ts index 918f53b3..623cedef 100644 --- a/packages/semi-ui-vue/src/components/dragMove/index.ts +++ b/packages/semi-ui-vue/src/components/dragMove/index.ts @@ -3,7 +3,7 @@ import * as PropTypes from '../PropTypes'; import { cloneVNode, CSSProperties, defineComponent, - h, isRef, + h, isRef, onBeforeUnmount, onMounted, onUnmounted, PropType, @@ -132,7 +132,7 @@ const DragMove = defineComponent({ foundation.init(); }) - onUnmounted(()=>{ + onBeforeUnmount(()=>{ foundation.destroy(); }) @@ -147,20 +147,6 @@ const DragMove = defineComponent({ const { ref } = children as any; setRefJsx(ref, node) }, - onMousedown: (e: MouseEvent) => { - foundation.onMouseDown(e); - const { onMouseDown } = children.props; - if (typeof onMouseDown === 'function') { - onMouseDown(e); - } - }, - onTouchstart: (e: TouchEvent) => { - foundation.onTouchStart(e); - const { onMouseMove } = children.props; - if (typeof onMouseMove === 'function') { - onMouseMove(e); - } - }, }); return newChildren; }; diff --git a/packages/semi-ui-vue/src/components/dropdown/index.tsx b/packages/semi-ui-vue/src/components/dropdown/index.tsx index ca7c63f5..4f67650a 100755 --- a/packages/semi-ui-vue/src/components/dropdown/index.tsx +++ b/packages/semi-ui-vue/src/components/dropdown/index.tsx @@ -92,6 +92,7 @@ export interface DropdownProps extends TooltipProps { onEscKeyDown?: TooltipProps['onEscKeyDown']; name?: string; + tooltipStyle?: CSSProperties; } interface DropdownState { @@ -124,6 +125,7 @@ const propTypes: CombineProps = { spacing: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), menu: PropTypes.array, name: String, + tooltipStyle: PropTypes.object, }; export const DropdownVuePropsType = propTypes; const defaultProps = { @@ -255,6 +257,7 @@ const Dropdown = defineComponent({ className, motion, style, + tooltipStyle, prefixCls, render, @@ -297,6 +300,7 @@ const Dropdown = defineComponent({ showArrow={false} returnFocusOnClose={true} ref={tooltipRef} + style={tooltipStyle} {...attr} > {cloneVNode(children, { diff --git a/packages/semi-ui-vue/src/components/index.ts b/packages/semi-ui-vue/src/components/index.ts index 84a0e88c..c1fae0f6 100755 --- a/packages/semi-ui-vue/src/components/index.ts +++ b/packages/semi-ui-vue/src/components/index.ts @@ -192,6 +192,7 @@ export type { FormApi, FormFCChild, CommonFieldProps } from './form/index'; export { default as Image } from './image'; export { Preview as ImagePreview } from './image'; export type { RuleItem, BaseFormProps } from './form/interface'; +export { default as DragMove } from './dragMove'; //v-model export { @@ -223,3 +224,5 @@ export { default as Chat } from './chat'; export { default as ColorPicker } from './colorPicker'; export { default as HotKeys } from './hotKeys'; export { Resizable, ResizeGroup, ResizeHandler, ResizeItem } from './resizable'; +export { default as Cropper } from './cropper'; +export { default as AudioPlayer } from './audioPlayer'; diff --git a/packages/semi-ui-vue/src/components/jsonViewer/index.tsx b/packages/semi-ui-vue/src/components/jsonViewer/index.tsx index 9573553b..7cd32024 100644 --- a/packages/semi-ui-vue/src/components/jsonViewer/index.tsx +++ b/packages/semi-ui-vue/src/components/jsonViewer/index.tsx @@ -44,6 +44,7 @@ export interface JsonViewerProps extends BaseProps { value: string; width: number; height: number; + showSearch?: boolean; className?: string; style?: CSSProperties; onChange?: (value: string) => void; @@ -73,6 +74,7 @@ const propTypes: CombineProps = { type: Number, required: true, }, + showSearch: Boolean, width: { type: Number, required: true, @@ -86,6 +88,10 @@ const defaultProps: Partial = { width: 400, height: 400, value: '', + options: { + readOnly: false, + autoWrap: true + } }; export const vuePropsType = vuePropsMake(propTypes, defaultProps); const JsonViewerCom = defineComponent({ @@ -104,6 +110,7 @@ const JsonViewerCom = defineComponent({ const editorRef = shallowRef() const searchInputRef = shallowRef() const replaceInputRef = shallowRef() + let isComposing: boolean = false; const { adapter: adapterInject, getDataAttr } = useBaseComponent(props, state) @@ -230,6 +237,16 @@ const JsonViewerCom = defineComponent({ className={`${prefixCls}-search-bar-input`} onChange={(_value, e) => { e.preventDefault(); + if (!isComposing) { + searchHandler(); + } + searchInputRef.value?.focus(); + }} + onCompositionstart={() => { + isComposing = true; + }} + onCompositionend={() => { + isComposing = false; searchHandler(); searchInputRef.value?.focus(); }} @@ -264,6 +281,8 @@ const JsonViewerCom = defineComponent({ } function renderReplaceBar() { + const { readOnly } = props.options; + return (
- + }
); diff --git a/patches/@douyinfe__semi-foundation@2.73.0.patch b/patches/@douyinfe__semi-foundation@2.73.0.patch new file mode 100644 index 00000000..5f8e263a --- /dev/null +++ b/patches/@douyinfe__semi-foundation@2.73.0.patch @@ -0,0 +1,77 @@ +diff --git a/.idea/git_toolbox_blame.xml b/.idea/git_toolbox_blame.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..04ede99c250fc313e4e715cdedbe81b684fe8f89 +--- /dev/null ++++ b/.idea/git_toolbox_blame.xml +@@ -0,0 +1,6 @@ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/modules.xml b/.idea/modules.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..7402153a5c4af1773015a0132813b7518c3e6d79 +--- /dev/null ++++ b/.idea/modules.xml +@@ -0,0 +1,8 @@ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/semi-foundation@2.73.0.iml b/.idea/semi-foundation@2.73.0.iml +new file mode 100644 +index 0000000000000000000000000000000000000000..0b872d82d9c39f70bff219b3b4b60145430f8984 +--- /dev/null ++++ b/.idea/semi-foundation@2.73.0.iml +@@ -0,0 +1,12 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/audioPlayer/foundation.ts b/audioPlayer/foundation.ts +index 01d6bfbf39c166cff3429e02c72a87649837f7a3..0033ceefffceb73a524de0bbe32c8ec6bf546e2c 100644 +--- a/audioPlayer/foundation.ts ++++ b/audioPlayer/foundation.ts +@@ -8,7 +8,7 @@ export interface AudioPlayerAdapter

, S = Record void; + handleTimeUpdate: () => void; + handleTrackChange: (direction: 'next' | 'prev') => void; +- getAudioRef: () => React.RefObject; ++ getAudioRef: () => HTMLAudioElement; + handleTimeChange: (value: number) => void; + handleSpeedChange: (value: { label: string; value: number }) => void; + handleSeek: (direction: number) => void; +@@ -25,12 +25,12 @@ class AudioPlayerFoundation extends BaseFoundation { + initAudioState() { + const audioRef = this.getAudioRef(); + const props = this.getProps(); +- ++ + this.setState({ +- totalTime: audioRef.current?.duration || 0, ++ totalTime: audioRef?.duration || 0, + isPlaying: props.autoPlay, +- volume: audioRef.current?.volume * 100 || 100, +- currentRate: { label: '1.0x', value: audioRef.current?.playbackRate || 1 }, ++ volume: audioRef?.volume * 100 || 100, ++ currentRate: { label: '1.0x', value: audioRef?.playbackRate || 1 }, + }); + } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1343cbe..27de596e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,11 @@ settings: excludeLinksFromLockfile: false patchedDependencies: + '@douyinfe/semi-foundation@2.73.0': + hash: 68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9 + path: patches/@douyinfe__semi-foundation@2.73.0.patch '@vue/repl@4.4.2': - hash: 4i22gbsbdjboratjcc7c56w2hq + hash: a8bb7e4c6f697eb0965370d9a0537e82f338ad17107466497fdb72e20b60680b path: patches/@vue__repl@4.4.2.patch importers: @@ -14,14 +17,14 @@ importers: .: dependencies: '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 '@vue/repl': specifier: 4.4.2 - version: 4.4.2(patch_hash=4i22gbsbdjboratjcc7c56w2hq) + version: 4.4.2(patch_hash=a8bb7e4c6f697eb0965370d9a0537e82f338ad17107466497fdb72e20b60680b) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -216,17 +219,17 @@ importers: packages/semi-animation-vue: dependencies: '@douyinfe/semi-animation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 '@douyinfe/semi-animation-styled': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 classnames: specifier: ^2.3.2 version: 2.5.1 @@ -310,11 +313,11 @@ importers: packages/semi-icons-lab-vue: dependencies: '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 classnames: specifier: ^2.3.2 version: 2.5.1 @@ -395,11 +398,11 @@ importers: packages/semi-icons-vue: dependencies: '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 classnames: specifier: ^2.3.2 version: 2.5.1 @@ -480,11 +483,11 @@ importers: packages/semi-illustrations-vue: dependencies: '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 classnames: specifier: ^2.3.2 version: 2.5.1 @@ -577,14 +580,14 @@ importers: specifier: 0.0.6-beta-20241014170855 version: 0.0.6-beta-20241014170855 '@douyinfe/semi-animation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 '@douyinfe/semi-foundation': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9) '@douyinfe/semi-theme-default': - specifier: 2.72.0 - version: 2.72.0 + specifier: 2.73.0 + version: 2.73.0 '@kousum/dnd-kit-vue': specifier: 0.0.6-beta-20241014170855 version: 0.0.6-beta-20241014170855(vue@3.5.13(typescript@5.5.4)) @@ -1720,20 +1723,20 @@ packages: search-insights: optional: true - '@douyinfe/semi-animation-styled@2.72.0': - resolution: {integrity: sha512-zJba1jtUdlTXo1/GAAv4ImGfllT04CRH0wOT876a408lVXHauJjrGJby0ESbkMKaFTAdNYh76fJ+sp+qLOweXQ==} + '@douyinfe/semi-animation-styled@2.73.0': + resolution: {integrity: sha512-DC5meqG7Ooxpual6UEVrK0t6oo+PJQUv6r+TG7XMPaXmJRehwmd11L55aDq7h0xs6IO8TJEI3kfHCvnvcCCKBA==} - '@douyinfe/semi-animation@2.72.0': - resolution: {integrity: sha512-6K0YUysNA20akUWwzUdeED4XQXU2iD0gJdmnW44ZwbKGv5thKo38bQ0YYPcmjsk60EtlmQy8xSfnTNoRCq/FBg==} + '@douyinfe/semi-animation@2.73.0': + resolution: {integrity: sha512-oNJepL4gXcdB/poFbYoLke/lfmcrHq85oR/ax/xPeh8Z5A4816MYcqNXjW/nG7hXBnKeoE8TPpAYWDxqyINq5Q==} - '@douyinfe/semi-foundation@2.72.0': - resolution: {integrity: sha512-2Z+cdPYGRwCY+eCsptq5UCPQZ9ltIWcY7lpe5rudDIBI8Z2GprXvpmMG2HbdpqCz047AMvI+g/x9xzhHi5FR+A==} + '@douyinfe/semi-foundation@2.73.0': + resolution: {integrity: sha512-9QfpGJs/iY7TUhuyXj85s2p7ILLJnptTO2LAfHoq9o0B3+nhptLb1sDIqG7BS1PJrTcABxavl0IbSrxcITln2w==} - '@douyinfe/semi-json-viewer-core@2.72.0': - resolution: {integrity: sha512-A7mJXKu1pz/iFbXLcWTPmnvl5J+Yj9UZYR/NNx9UczqvCmYaUz9px+Aa9ynKsdsWhSD1B4xWbp8x6A4FlBJCAA==} + '@douyinfe/semi-json-viewer-core@2.73.0': + resolution: {integrity: sha512-E7jzWuQl5OPMUkoyVeT9HHC9G3PiEG5yvas6c0Cw3jwFBRGgJ6X0FjYDJsUdcfuOkhMbly2AQvwN2cIe9cmBGA==} - '@douyinfe/semi-theme-default@2.72.0': - resolution: {integrity: sha512-l8SOMqiaDFkZKgRmRKe7bTADprVQc5VsJIAbu14qSoo8pdQvJMPnRgeCsTRkTzLTw8YU1IpGeD6514dZP6qlvA==} + '@douyinfe/semi-theme-default@2.73.0': + resolution: {integrity: sha512-b8g4/l0p8m4iJHiHzw4kX2O8ZDT9QfDS+vB30olXlPnKUZ4QI6Slw8NeYoZUKKM8Xyf2fEcazYQuX4VBjUbMpA==} '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} @@ -7565,8 +7568,8 @@ packages: vue-component-type-helpers@2.0.19: resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} - vue-component-type-helpers@2.1.10: - resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==} + vue-component-type-helpers@2.2.0: + resolution: {integrity: sha512-cYrAnv2me7bPDcg9kIcGwjJiSB6Qyi08+jLDo9yuvoFQjzHiPTzML7RnkJB1+3P6KMsX/KbCD4QE3Tv/knEllw==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -9744,16 +9747,16 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@douyinfe/semi-animation-styled@2.72.0': {} + '@douyinfe/semi-animation-styled@2.73.0': {} - '@douyinfe/semi-animation@2.72.0': + '@douyinfe/semi-animation@2.73.0': dependencies: bezier-easing: 2.1.0 - '@douyinfe/semi-foundation@2.72.0': + '@douyinfe/semi-foundation@2.73.0(patch_hash=68d845fc596cc3ad70725cb975aaae3949f2c948a8baaa5c78f39a79ebc011b9)': dependencies: - '@douyinfe/semi-animation': 2.72.0 - '@douyinfe/semi-json-viewer-core': 2.72.0 + '@douyinfe/semi-animation': 2.73.0 + '@douyinfe/semi-json-viewer-core': 2.73.0 '@mdx-js/mdx': 3.0.1 async-validator: 3.5.2 classnames: 2.5.1 @@ -9769,11 +9772,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@douyinfe/semi-json-viewer-core@2.72.0': + '@douyinfe/semi-json-viewer-core@2.73.0': dependencies: jsonc-parser: 3.3.1 - '@douyinfe/semi-theme-default@2.72.0': {} + '@douyinfe/semi-theme-default@2.73.0': {} '@esbuild/aix-ppc64@0.20.2': optional: true @@ -10531,7 +10534,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.5.13(typescript@5.5.4) - vue-component-type-helpers: 2.1.10 + vue-component-type-helpers: 2.2.0 '@svgr/babel-plugin-add-jsx-attribute@5.4.0': {} @@ -11057,7 +11060,7 @@ snapshots: dependencies: '@vue/shared': 3.5.13 - '@vue/repl@4.4.2(patch_hash=4i22gbsbdjboratjcc7c56w2hq)': {} + '@vue/repl@4.4.2(patch_hash=a8bb7e4c6f697eb0965370d9a0537e82f338ad17107466497fdb72e20b60680b)': {} '@vue/runtime-core@3.4.27': dependencies: @@ -16447,7 +16450,7 @@ snapshots: vue-component-type-helpers@2.0.19: {} - vue-component-type-helpers@2.1.10: {} + vue-component-type-helpers@2.2.0: {} vue-demi@0.14.10(vue@3.5.13(typescript@5.5.4)): dependencies: