-
Notifications
You must be signed in to change notification settings - Fork 189
feat(新增ELXButton): 为整合组件内的按钮组件,新增ELXButton,兼容目前的零散按钮,传 elxButtonType … #354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <script setup lang="ts"> | ||
| import { Brush } from '@element-plus/icons-vue'; | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- ClearButton 清理按钮 --> | ||
| <el-icon><Brush /></el-icon> | ||
| </template> | ||
|
|
||
| <style scoped lang="less"></style> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <script setup lang="ts"></script> | ||
|
|
||
| <template> | ||
| <!-- LoadingButton loading 按钮 --> | ||
| <svg | ||
| class="loading-svg" | ||
| viewBox="0 0 1000 1000" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| xmlns:xlink="http://www.w3.org/1999/xlink" | ||
| > | ||
| <title>Loading</title> | ||
| <rect | ||
| fill="currentColor" | ||
| height="250" | ||
| rx="24" | ||
| ry="24" | ||
| width="250" | ||
| x="375" | ||
| y="375" | ||
| /> | ||
| <circle | ||
| cx="500" | ||
| cy="500" | ||
| fill="none" | ||
| r="450" | ||
| stroke="currentColor" | ||
| stroke-width="100" | ||
| opacity="0.45" | ||
| /> | ||
| <circle | ||
| cx="500" | ||
| cy="500" | ||
| fill="none" | ||
| r="450" | ||
| stroke="currentColor" | ||
| stroke-width="100" | ||
| stroke-dasharray="600 9999999" | ||
| > | ||
| <animateTransform | ||
| attributeName="transform" | ||
| dur="1s" | ||
| from="0 500 500" | ||
| repeatCount="indefinite" | ||
| to="360 500 500" | ||
| type="rotate" | ||
| /> | ||
| </circle> | ||
| </svg> | ||
| </template> | ||
|
|
||
| <style scoped lang="less"></style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <script setup lang="ts"> | ||
| import { Top } from '@element-plus/icons-vue'; | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- SendButton 发送按钮 --> | ||
| <el-icon><Top /></el-icon> | ||
| </template> | ||
|
|
||
| <style scoped lang="less"></style> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <script setup lang="ts"> | ||
| import { Microphone } from '@element-plus/icons-vue'; | ||
| </script> | ||
|
|
||
| <template> | ||
| <!-- SpeechButton 语音按钮 --> | ||
| <el-icon><Microphone /></el-icon> | ||
| </template> | ||
|
|
||
| <style scoped lang="less"></style> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,68 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| withDefaults(defineProps<{ className?: string }>(), { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| className: 'loading-svg' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // 定义常量 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const SIZE = 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const COUNT = 4; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const RECT_WIDTH = 140; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const RECT_RADIUS = RECT_WIDTH / 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const RECT_HEIGHT_MIN = 250; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const RECT_HEIGHT_MAX = 500; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const DURATION = 0.8; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // 计算矩形的位置和高度范围 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const rects = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dest = (SIZE - RECT_WIDTH * COUNT) / (COUNT - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return Array.from({ length: COUNT }).map((_, index) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const x = index * (dest + RECT_WIDTH); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const yMin = SIZE / 2 - RECT_HEIGHT_MIN / 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const yMax = SIZE / 2 - RECT_HEIGHT_MAX / 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { x, yMin, yMax }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import
-<script setup lang="ts">
+<script setup lang="ts">
+import { computed } from 'vue';📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :class="className" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| color="currentColor" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :viewBox="`0 0 ${SIZE} ${SIZE}`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| xmlns="http://www.w3.org/2000/svg" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| xmlns:xlink="http://www.w3.org/1999/xlink" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <title>Speech Recording</title> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| <template v-for="(item, index) in rects" :key="index"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <rect | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="currentColor" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :rx="RECT_RADIUS" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :ry="RECT_RADIUS" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :height="RECT_HEIGHT_MIN" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :width="RECT_WIDTH" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :x="item.x" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :y="item.yMin" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <animate | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributeName="height" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :values="`${RECT_HEIGHT_MIN}; ${RECT_HEIGHT_MAX}; ${RECT_HEIGHT_MIN}`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| keyTimes="0; 0.5; 1" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :dur="`${DURATION}s`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :begin="`${(DURATION / COUNT) * index}s`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| repeatCount="indefinite" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <animate | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributeName="y" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :values="`${item.yMin}; ${item.yMax}; ${item.yMin}`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| keyTimes="0; 0.5; 1" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :dur="`${DURATION}s`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| :begin="`${(DURATION / COUNT) * index}s`" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| repeatCount="indefinite" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </rect> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| <style scoped lang="less"></style> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import type { SetupContext } from 'vue'; | ||
| import { buttonEmits, buttonProps } from 'element-plus'; | ||
|
|
||
| export const elxButtonTypes = [ | ||
| 'clear', | ||
| 'loading', | ||
| 'send', | ||
| 'speech', | ||
| 'speechLoading', | ||
| '' | ||
| ] as const; | ||
|
|
||
| export const elxButtonProps = { | ||
| /** | ||
| * @description xButton type | ||
| */ | ||
| elxButtonType: { | ||
| type: String, | ||
| values: elxButtonTypes, | ||
| default: '' | ||
| }, | ||
| ...buttonProps | ||
| }; | ||
|
|
||
| export const elxButtonEmits = { | ||
| clear: (evt: MouseEvent) => evt instanceof MouseEvent, | ||
| cancel: (evt: MouseEvent) => evt instanceof MouseEvent, | ||
| submit: (evt: MouseEvent) => evt instanceof MouseEvent, | ||
| ...buttonEmits | ||
| }; | ||
|
|
||
| export type ELXButtonProps = Partial<ExtractPropTypes<typeof elxButtonProps>>; | ||
| export type ELXButtonEmits = typeof elxButtonEmits; | ||
| export type ELXButtonType = ELXButtonProps['elxButtonType']; | ||
|
|
||
| export function useButton( | ||
| props: ELXButtonProps, | ||
| emit: SetupContext<ELXButtonEmits>['emit'] | ||
| ) { | ||
| const handleClick = (evt: MouseEvent) => { | ||
| if (props.disabled || props.loading) { | ||
| evt.stopPropagation(); | ||
| return; | ||
| } | ||
| if (props.elxButtonType?.trim()?.length) { | ||
| if (props.elxButtonType === 'clear') { | ||
| return emit('clear', evt); | ||
| } else if (['loading', 'speechLoading'].includes(props.elxButtonType)) { | ||
| return emit('cancel', evt); | ||
| } else if (props.elxButtonType === 'send') { | ||
| return emit('submit', evt); | ||
| } | ||
| } | ||
| emit('click', evt); | ||
| }; | ||
|
|
||
| return { | ||
| handleClick | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||
| <!-- ClearButton 清理按钮 --> | ||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||
| import type { ELXButtonType } from '@components/ELXButton/index.ts'; | ||||||||||||||||
| import { | ||||||||||||||||
| elxButtonEmits, | ||||||||||||||||
| elxButtonProps, | ||||||||||||||||
| useButton | ||||||||||||||||
| } from '@components/ELXButton/index.ts'; | ||||||||||||||||
| import { ElButton } from 'element-plus'; | ||||||||||||||||
| import { h } from 'vue'; | ||||||||||||||||
| import ELXClearButton from './components/ELXClearButton.vue'; | ||||||||||||||||
| import ELXLoadingButton from './components/ELXLoadingButton.vue'; | ||||||||||||||||
| import ELXSendButton from './components/ELXSendButton.vue'; | ||||||||||||||||
| import ELXSpeechButton from './components/ELXSpeechButton.vue'; | ||||||||||||||||
| import ELXSpeechButtonLoading from './components/ELXSpeechLoadingButton.vue'; | ||||||||||||||||
|
|
||||||||||||||||
| const props = defineProps(elxButtonProps); | ||||||||||||||||
| const emit = defineEmits(elxButtonEmits); | ||||||||||||||||
| const slots = useSlots(); | ||||||||||||||||
|
|
||||||||||||||||
| const { handleClick } = useButton(props, emit); | ||||||||||||||||
|
|
||||||||||||||||
| const elxButtonTypeEmpty = computed(() => { | ||||||||||||||||
| return props.elxButtonType?.trim()?.length > 0; | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| const classNameVars = computed(() => { | ||||||||||||||||
| const classNames = ['elx-button']; | ||||||||||||||||
| if (elxButtonTypeEmpty.value) { | ||||||||||||||||
| classNames.push(`elx-button-type-${props.elxButtonType}`); | ||||||||||||||||
| } | ||||||||||||||||
| return classNames.join(' '); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| const ELXButtonMap: Record<ELXButtonType & string, Component> = { | ||||||||||||||||
| clear: ELXClearButton, | ||||||||||||||||
| loading: ELXLoadingButton, | ||||||||||||||||
| send: ELXSendButton, | ||||||||||||||||
| speech: ELXSpeechButton, | ||||||||||||||||
| speechLoading: ELXSpeechButtonLoading | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| const handleDefaultSlot = computed(() => { | ||||||||||||||||
| return h(ELXButtonMap[props.elxButtonType] ?? slots.default); | ||||||||||||||||
| }); | ||||||||||||||||
|
Comment on lines
+43
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential issue with slot rendering The computed property returns a VNode from Apply this diff to fix the slot handling: const handleDefaultSlot = computed(() => {
- return h(ELXButtonMap[props.elxButtonType] ?? slots.default);
+ const Component = ELXButtonMap[props.elxButtonType];
+ return Component ? h(Component) : slots.default?.();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| const coverProps = computed(() => { | ||||||||||||||||
| let circle = props.circle; | ||||||||||||||||
| let loading = props.loading; | ||||||||||||||||
| if (elxButtonTypeEmpty.value) { | ||||||||||||||||
| circle = true; | ||||||||||||||||
| loading = false; | ||||||||||||||||
| } | ||||||||||||||||
| return { | ||||||||||||||||
| circle, | ||||||||||||||||
| loading | ||||||||||||||||
| }; | ||||||||||||||||
| }); | ||||||||||||||||
| </script> | ||||||||||||||||
|
|
||||||||||||||||
| <template> | ||||||||||||||||
| <component | ||||||||||||||||
| :is=" | ||||||||||||||||
| h( | ||||||||||||||||
| ElButton, | ||||||||||||||||
| { | ||||||||||||||||
| ...$attrs, | ||||||||||||||||
| ...props, | ||||||||||||||||
| ...coverProps, | ||||||||||||||||
| class: classNameVars, | ||||||||||||||||
| onClick: handleClick | ||||||||||||||||
| }, | ||||||||||||||||
| { ...$slots, default: () => handleDefaultSlot } | ||||||||||||||||
| ) | ||||||||||||||||
| " | ||||||||||||||||
| /> | ||||||||||||||||
| </template> | ||||||||||||||||
|
|
||||||||||||||||
| <style scoped lang="scss" src="./style.scss"></style> | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| .elx-button { | ||
| &.elx-button-type-clear { | ||
| .el-icon { | ||
| // 旋转180 | ||
| transform: rotate(180deg); | ||
| } | ||
| } | ||
|
|
||
| &.elx-button-type-loading { | ||
| padding: 0; | ||
|
|
||
| .loading-svg { | ||
| color: var(--el-color-primary); | ||
| width: 100%; | ||
| } | ||
| } | ||
|
|
||
| &.elx-button-type-send { | ||
| } | ||
|
|
||
| &.elx-button-type-speech { | ||
| } | ||
|
|
||
| &.elx-button-type-speechLoading { | ||
| padding: 0; | ||
|
|
||
| .loading-svg { | ||
| color: var(--el-color-primary); | ||
| width: 16px; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace SMIL with CSS animation for cross‑browser support (Safari).
SMIL
<animateTransform>has spotty support (notably Safari). Use CSS keyframes to rotate the arc instead.Apply:
And add styles:
Also applies to: 51-51