From 9ccf0df0690575a6044128248a35e0194b3a83e2 Mon Sep 17 00:00:00 2001 From: qqq Date: Sat, 7 Aug 2021 20:52:40 +0800 Subject: [PATCH 1/9] feat: add carousel component --- devui/carousel/carousel.scss | 110 ++++++++++ devui/carousel/carousel.tsx | 286 +++++++++++++++++++++++++- devui/carousel/demo/carousel-demo.tsx | 8 +- devui/carousel/demo/demo1/index.scss | 7 + devui/carousel/demo/demo1/index.tsx | 28 +++ devui/carousel/item.tsx | 17 ++ devui/carousel/types.ts | 40 ++++ 7 files changed, 485 insertions(+), 11 deletions(-) create mode 100644 devui/carousel/carousel.scss create mode 100644 devui/carousel/demo/demo1/index.scss create mode 100644 devui/carousel/demo/demo1/index.tsx create mode 100644 devui/carousel/item.tsx create mode 100644 devui/carousel/types.ts diff --git a/devui/carousel/carousel.scss b/devui/carousel/carousel.scss new file mode 100644 index 00000000..9888352b --- /dev/null +++ b/devui/carousel/carousel.scss @@ -0,0 +1,110 @@ +@import '../style/theme/color'; +@import '../style/theme/shadow'; +@import '../style/core/animation'; + +@mixin fixed-arrow-button() { + position: absolute; + top: -18px; + z-index: 2; + cursor: pointer; + width: 36px; + height: 36px; + border-radius: (36px/2); + background: $devui-highlight-overlay; + box-shadow: $devui-shadow-length-hover $devui-light-shadow; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover { + background: $devui-area; + } + + svg polygon { + fill: $devui-text; + } +} + +.devui-carousel-container { + display: block; + position: relative; + + .devui-carousel-arrow { + position: absolute; + width: 100%; + top: 50%; + + .arrow-left { + @include fixed-arrow-button(); + + left: 10px; + } + + .arrow-right { + @include fixed-arrow-button(); + + right: 10px; + } + } + + .devui-carousel-item-wrapper { + position: relative; + overflow: hidden; + height: 100%; + + .devui-carousel-item-container { + display: flex; + height: 100%; + position: relative; + + .d-carousel-item { + flex: 1; + position: relative; + height: 100%; + } + } + } + + .devui-carousel-dots { + position: absolute; + display: flex; + justify-content: center; + width: 100%; + + &.bottom { + bottom: 8px; + } + + &.top { + top: 8px; + } + + .dot-item { + width: 6px; + height: 6px; + border-radius: (6px/2); + margin-right: 8px; + background: $devui-icon-fill; + + &:hover { + cursor: pointer; + background: $devui-list-item-hover-bg; + } + + &.active { + width: 24px; + background: $devui-list-item-active-bg; + transition: all $devui-animation-duration-slow $devui-animation-ease-in-smooth; + } + } + } +} + +.devui-carousel-container { + .devui-carousel-arrow { + .arrow-left, + .arrow-right { + transition: background-color $devui-animation-duration-slow $devui-animation-ease-in-smooth; + } + } +} diff --git a/devui/carousel/carousel.tsx b/devui/carousel/carousel.tsx index 9c1640c2..17fd505f 100644 --- a/devui/carousel/carousel.tsx +++ b/devui/carousel/carousel.tsx @@ -1,12 +1,282 @@ -import { defineComponent } from 'vue' +import { defineComponent, ref, watch, onMounted, Fragment, Comment } from "vue"; +import { carouselProps, DotTrigger } from "./types"; + +import "./carousel.scss"; export default defineComponent({ - name: 'd-carousel', - props: { + name: "d-carousel", + emits: ["update:activeIndex"], + props: carouselProps, + setup(props, { emit }) { + const { arrowTrigger, autoplay, autoplaySpeed, dotTrigger, activeIndex } = + props; + const transitionSpeed = 500; + + const itemCount = ref(0); + const showArrow = ref(false); + const currentIndex = ref(0); + const wrapperRef = ref(null); + const containerRef = ref(null); + const scheduledId = ref(null); + + watch( + () => arrowTrigger, + () => { + showArrow.value = arrowTrigger === "always"; + }, + { immediate: true } + ); + watch( + () => activeIndex, + () => { + currentIndex.value = activeIndex; + } + ); + + // 翻页位移 + const translatePosition = (size: number) => { + if (containerRef.value) containerRef.value.style.left = `${-size * 100}%`; + }; + // 调整首尾翻页后的动画 + const adjustTransition = (targetEl: HTMLElement) => { + setTimeout(() => { + if (containerRef.value) containerRef.value.style.transition = ""; + + targetEl.style.transform = ""; + translatePosition(currentIndex.value); + }, transitionSpeed); + }; + + // 调整首尾翻动时的位置 + const adjustPosition = (targetEl: HTMLElement, firstToLast: boolean) => { + if (wrapperRef.value) { + const wrapperRect = wrapperRef.value.getBoundingClientRect(); + + targetEl.style.transform = `translateX(${ + (firstToLast ? -itemCount.value : itemCount.value) * wrapperRect.width + }px)`; + } + }; + + // 指定跳转位置 + const goto = (index: number) => { + if ( + index === currentIndex.value || + !wrapperRef.value || + !containerRef.value + ) + return; + + containerRef.value.style.transition = `left ${transitionSpeed}ms ease`; + + let latestIndex = currentIndex.value; + if (index < 0 && currentIndex.value === 0) { + // 第一个卡片向前切换 + latestIndex = itemCount.value - 1; + const targetEl = containerRef.value.children[ + latestIndex + ] as HTMLElement; + adjustPosition(targetEl, true); + translatePosition(-1); + adjustTransition(targetEl); + } else if ( + index >= itemCount.value && + currentIndex.value === itemCount.value - 1 + ) { + // 最后一个卡片向后切换 + latestIndex = 0; + + const targetEl = containerRef.value.children[ + latestIndex + ] as HTMLElement; + adjustPosition(targetEl, false); + translatePosition(itemCount.value); + adjustTransition(targetEl); + } else { + latestIndex = + index < 0 + ? 0 + : index > itemCount.value - 1 + ? itemCount.value - 1 + : index; + + translatePosition(latestIndex); + } + + currentIndex.value = latestIndex; + emit("update:activeIndex", latestIndex); + autoScheduleTransition(); + }; + // 向前切换 + const prev = () => { + goto(currentIndex.value - 1); + }; + // 向后切换 + const next = () => { + goto(currentIndex.value + 1); + }; + + // 切换箭头监听事件,用于处理hover方式 + const arrowMouseEvent = (type: "enter" | "leave") => { + if (arrowTrigger !== "hover") return; + + showArrow.value = type === "enter"; + }; + // 指示器触发切换函数 + const switchStep = (index: number, type: DotTrigger) => { + if (type === dotTrigger) goto(index); + }; + + // 清除自动轮播任务 + const clearScheduledTransition = () => { + if (scheduledId.value) { + clearTimeout(scheduledId.value); + scheduledId.value = null; + } + }; + // 自动轮播调度任务 + const autoScheduleTransition = () => { + clearScheduledTransition(); + if (autoplay && autoplaySpeed) { + scheduledId.value = setTimeout(() => { + next(); + }, autoplaySpeed); + } + }; + const changeItemCount = (val: number) => { + itemCount.value = val; + autoScheduleTransition(); + }; + + onMounted(() => { + if (containerRef.value) { + containerRef.value.style.transition = `left ${transitionSpeed}ms ease`; + containerRef.value.style.left = "0%"; + } + + autoScheduleTransition(); + }); + + return { + wrapperRef, + containerRef, + + showArrow, + currentIndex, + itemCount, + changeItemCount, + + goto, + prev, + next, + arrowMouseEvent, + switchStep, + }; }, - setup(props, ctx) { - return () => { - return
devui-carousel
+ + render() { + const { + showArrow, + currentIndex, + itemCount, + + arrowTrigger, + height, + showDots, + dotPosition, + + prev, + next, + arrowMouseEvent, + switchStep, + changeItemCount, + + $slots, + } = this; + const slot: any[] = $slots.default?.() ?? []; + + // 在jsx中,使用map生成slot项会在外层包裹一个Fragment + let children = slot; + if (children.length === 1 && children[0].type === Fragment) { + children = (children[0].children || []).filter( + (item) => item?.type !== Comment + ); + } + if (children.length !== itemCount) { + changeItemCount(children.length); } - } -}) \ No newline at end of file + + return ( + + ); + }, +}); diff --git a/devui/carousel/demo/carousel-demo.tsx b/devui/carousel/demo/carousel-demo.tsx index 4bb23c7e..1be9767a 100644 --- a/devui/carousel/demo/carousel-demo.tsx +++ b/devui/carousel/demo/carousel-demo.tsx @@ -1,12 +1,14 @@ import { defineComponent } from 'vue' +import Demo1 from './demo1' + export default defineComponent({ name: 'd-carousel-demo', - props: { - }, setup(props, ctx) { return () => { - return
devui-carousel-demo
+ return } } }) \ No newline at end of file diff --git a/devui/carousel/demo/demo1/index.scss b/devui/carousel/demo/demo1/index.scss new file mode 100644 index 00000000..fe7a45bc --- /dev/null +++ b/devui/carousel/demo/demo1/index.scss @@ -0,0 +1,7 @@ +@import '../../../style/devui.scss'; + +.d-carousel-item { + text-align: center; + line-height: 200px; + background: $devui-global-bg; +} \ No newline at end of file diff --git a/devui/carousel/demo/demo1/index.tsx b/devui/carousel/demo/demo1/index.tsx new file mode 100644 index 00000000..a391e20b --- /dev/null +++ b/devui/carousel/demo/demo1/index.tsx @@ -0,0 +1,28 @@ +import { defineComponent, ref, watch } from 'vue'; +import Carousel from '../../carousel'; +import Item from '../../item'; + +import './index.scss' + +const items = ["item 1", 'item 2', 'item 3', 'item 4'] +export default defineComponent({ + name: 'd-button-primary', + setup() { + const activeIndex = ref(0) + + return { + activeIndex + } + }, + render() { + return ( +
+ + { + items.map(item => { item }) + } + +
+ ) + } +}); \ No newline at end of file diff --git a/devui/carousel/item.tsx b/devui/carousel/item.tsx new file mode 100644 index 00000000..ce93118a --- /dev/null +++ b/devui/carousel/item.tsx @@ -0,0 +1,17 @@ +import { defineComponent } from 'vue' + +export default defineComponent({ + name: 'd-carousel-demo', + render() { + const { + $slots + } = this + const children = $slots.default?.() + + return ( + + ) + } +}) \ No newline at end of file diff --git a/devui/carousel/types.ts b/devui/carousel/types.ts new file mode 100644 index 00000000..a2b288e5 --- /dev/null +++ b/devui/carousel/types.ts @@ -0,0 +1,40 @@ +import { PropType } from 'vue'; + +export type ArrowTrigger = 'hover' | 'never' | 'always'; +export type DotTrigger = 'click' | 'hover'; +export type DotPosition = 'bottom' | 'top'; + +export const carouselProps = { + arrowTrigger: { + type: String as PropType, + default: 'always' + }, + autoplay: { + type: Boolean, + default: false, + }, + autoplaySpeed: { + type: Number, + default: 3000 + }, + height: { + type: String, + default: '100%', + }, + showDots: { + type: Boolean, + default: true + }, + dotTrigger: { + type: String as PropType, + default: 'click', + }, + dotPosition: { + type: String as PropType, + default: 'bottom', + }, + activeIndex: { + type: Number, + default: 0 + }, +} as const; \ No newline at end of file -- Gitee From 4084f66144b566b56124b2a1d0a8bcbdab4ffd2e Mon Sep 17 00:00:00 2001 From: qqq Date: Sat, 7 Aug 2021 21:37:45 +0800 Subject: [PATCH 2/9] feat: carousel demo crreate --- devui/carousel/carousel.tsx | 3 +- .../demo/{demo1 => autoplay}/index.scss | 0 devui/carousel/demo/autoplay/index.tsx | 28 ++++++++++ devui/carousel/demo/basic/index.scss | 7 +++ .../carousel/demo/{demo1 => basic}/index.tsx | 4 +- devui/carousel/demo/carousel-demo.tsx | 48 +++++++++++++++-- devui/carousel/demo/indicator/index.scss | 7 +++ devui/carousel/demo/indicator/index.tsx | 28 ++++++++++ devui/carousel/demo/operate/index.scss | 17 ++++++ devui/carousel/demo/operate/index.tsx | 52 +++++++++++++++++++ devui/carousel/types.ts | 2 +- 11 files changed, 188 insertions(+), 8 deletions(-) rename devui/carousel/demo/{demo1 => autoplay}/index.scss (100%) create mode 100644 devui/carousel/demo/autoplay/index.tsx create mode 100644 devui/carousel/demo/basic/index.scss rename devui/carousel/demo/{demo1 => basic}/index.tsx (85%) create mode 100644 devui/carousel/demo/indicator/index.scss create mode 100644 devui/carousel/demo/indicator/index.tsx create mode 100644 devui/carousel/demo/operate/index.scss create mode 100644 devui/carousel/demo/operate/index.tsx diff --git a/devui/carousel/carousel.tsx b/devui/carousel/carousel.tsx index 17fd505f..891acd00 100644 --- a/devui/carousel/carousel.tsx +++ b/devui/carousel/carousel.tsx @@ -5,7 +5,7 @@ import "./carousel.scss"; export default defineComponent({ name: "d-carousel", - emits: ["update:activeIndex"], + emits: ["update:activeIndex", "activeIndexChange"], props: carouselProps, setup(props, { emit }) { const { arrowTrigger, autoplay, autoplaySpeed, dotTrigger, activeIndex } = @@ -105,6 +105,7 @@ export default defineComponent({ currentIndex.value = latestIndex; emit("update:activeIndex", latestIndex); + emit("activeIndexChange", latestIndex); autoScheduleTransition(); }; // 向前切换 diff --git a/devui/carousel/demo/demo1/index.scss b/devui/carousel/demo/autoplay/index.scss similarity index 100% rename from devui/carousel/demo/demo1/index.scss rename to devui/carousel/demo/autoplay/index.scss diff --git a/devui/carousel/demo/autoplay/index.tsx b/devui/carousel/demo/autoplay/index.tsx new file mode 100644 index 00000000..2f769101 --- /dev/null +++ b/devui/carousel/demo/autoplay/index.tsx @@ -0,0 +1,28 @@ +import { defineComponent, ref, watch } from 'vue'; +import Carousel from '../../carousel'; +import Item from '../../item'; + +import './index.scss' + +const items = ["page 1", 'page 2', 'page 3', 'page 4'] +export default defineComponent({ + name: 'd-button-primary', + setup() { + const activeIndex = ref(0) + + return { + activeIndex + } + }, + render() { + return ( +
+ + { + items.map(item => { item }) + } + +
+ ) + } +}); \ No newline at end of file diff --git a/devui/carousel/demo/basic/index.scss b/devui/carousel/demo/basic/index.scss new file mode 100644 index 00000000..fe7a45bc --- /dev/null +++ b/devui/carousel/demo/basic/index.scss @@ -0,0 +1,7 @@ +@import '../../../style/devui.scss'; + +.d-carousel-item { + text-align: center; + line-height: 200px; + background: $devui-global-bg; +} \ No newline at end of file diff --git a/devui/carousel/demo/demo1/index.tsx b/devui/carousel/demo/basic/index.tsx similarity index 85% rename from devui/carousel/demo/demo1/index.tsx rename to devui/carousel/demo/basic/index.tsx index a391e20b..59cae8f0 100644 --- a/devui/carousel/demo/demo1/index.tsx +++ b/devui/carousel/demo/basic/index.tsx @@ -4,7 +4,7 @@ import Item from '../../item'; import './index.scss' -const items = ["item 1", 'item 2', 'item 3', 'item 4'] +const items = ["page 1", 'page 2', 'page 3', 'page 4'] export default defineComponent({ name: 'd-button-primary', setup() { @@ -17,7 +17,7 @@ export default defineComponent({ render() { return (
- + { items.map(item => { item }) } diff --git a/devui/carousel/demo/carousel-demo.tsx b/devui/carousel/demo/carousel-demo.tsx index 1be9767a..13c2978f 100644 --- a/devui/carousel/demo/carousel-demo.tsx +++ b/devui/carousel/demo/carousel-demo.tsx @@ -1,14 +1,54 @@ import { defineComponent } from 'vue' +import CodeBox from '../../shared/devui-codebox/devui-codebox' -import Demo1 from './demo1' +import Basic from './basic' +import BasicCode from './basic/index.tsx?raw' +import Indicator from './indicator' +import IndicatorCode from './indicator/index.tsx?raw' +import Autoplay from './autoplay' +import AutoplayCode from './autoplay/index.tsx?raw' +import Custom from './operate' +import CustomCode from './operate/index.tsx?raw' export default defineComponent({ name: 'd-carousel-demo', - setup(props, ctx) { + setup() { + const basicTsCode: any[] = [{title: 'TSX', language: 'TSX', code: BasicCode}]; + const indicatorCode: any[] = [{title: 'TSX', language: 'TSX', code: IndicatorCode}]; + const autoplayCode: any[] = [{title: 'TSX', language: 'TSX', code: AutoplayCode}]; + const customCode: any[] = [{title: 'TSX', language: 'TSX', code: CustomCode}]; + return () => { - return