From 42ebbd2d50a316d56e10c73d24c4558251737c9a Mon Sep 17 00:00:00 2001 From: catchonme Date: Thu, 16 May 2019 11:04:55 +0800 Subject: [PATCH] =?UTF-8?q?carousel=20=E5=88=9D=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/components/Audio.jsx | 25 +++ examples/components/CRUD/Fields.jsx | 5 + examples/components/Page/Simple.jsx | 71 ++++++- mock/crud/list.js | 6 +- scss/components/_carousel.scss | 141 ++++++++++++++ scss/themes/cxd.scss | 29 +-- scss/themes/default.scss | 29 +-- src/components/icons.tsx | 25 +++ src/index.tsx | 1 + src/renderers/Audio.tsx | 4 +- src/renderers/Carousel.tsx | 292 ++++++++++++++++++++++++++++ 11 files changed, 596 insertions(+), 32 deletions(-) create mode 100644 scss/components/_carousel.scss create mode 100644 src/renderers/Carousel.tsx diff --git a/examples/components/Audio.jsx b/examples/components/Audio.jsx index 01aaf83ac..0fc5ad39b 100644 --- a/examples/components/Audio.jsx +++ b/examples/components/Audio.jsx @@ -6,6 +6,31 @@ export default { "type": "audio", "autoPlay": false, "src": "http://www.ytmp3.cn/down/32791.mp3", + }, + { + "type": 'form', + "title": '', + "actions": [], + "className": 'b v-middle inline w-lg h-xs', + "controls": [ + { + "type": "card", + "className": 'v-middle w inline no-border', + "header": { + "title": "歌曲名称", + "subTitle": "专辑名称", + "description": "description", + "avatarClassName": "pull-left thumb-md avatar m-r no-border", + "avatar": "http://hiphotos.baidu.com/fex/%70%69%63/item/c9fcc3cec3fdfc03ccabb38edd3f8794a4c22630.jpg" + } + }, + { + "type": "audio", + "className": 'v-middle no-border', + "src": "http://www.ytmp3.cn/down/32791.mp3", + "controls": ['play'] + } + ] } ] } \ No newline at end of file diff --git a/examples/components/CRUD/Fields.jsx b/examples/components/CRUD/Fields.jsx index 7bdcc66d4..b8956a989 100644 --- a/examples/components/CRUD/Fields.jsx +++ b/examples/components/CRUD/Fields.jsx @@ -10,6 +10,11 @@ export default { label: "ID", type: "text" }, + { + name: "carousel", + label: "轮播图", + type: "carousel" + }, { name: "text", label: "文本", diff --git a/examples/components/Page/Simple.jsx b/examples/components/Page/Simple.jsx index 792f3e13e..d1e117d02 100644 --- a/examples/components/Page/Simple.jsx +++ b/examples/components/Page/Simple.jsx @@ -2,7 +2,76 @@ export default { type: 'page', title: '标题', remark: '提示 Tip', - body: "内容部分. 可以使用 \\${var} 获取变量。如: `\\$date`: ${date}", + data: { + id: 1, + image: "https://www.baidu.com/img/bd_logo1.png", + carousel: [ + { + html: '

This is data

' + }, + /*{ + label: 'This is data and is label' + },*/ + /*{ + image: 'https://www.baidu.com/img/bd_logo1.png' + },*/ + { + // 狗 + image: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg' + }, + { + image: 'https://gss0.bdstatic.com/5bVWsj_p_tVS5dKfpU_Y_D3/res/r/image/2018-09-29/208e4ce8af2846d584cbe55b245a4134.jpeg' + }/*, + { + image: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg' + // image: 'https://gss0.bdstatic.com/5bVWsj_p_tVS5dKfpU_Y_D3/res/r/image/2017-09-27/297f5edb1e984613083a2d3cc0c5bb36.png' + }*/ + ] + }, + body: [{ + type: 'form', + title: '表单', + controls: [ + { + type: 'carousel', + controlTheme: 'light', + name: 'carousel', + label: 'carousel', + animation: 'slideLeft' + } + ] + }, + /*{ + "type": "card", + className: 'v-middle w', + "header": { + "title": "Title", + "subTitle": "Sub Title", + "description": "description", + "avatarClassName": "pull-left thumb-md avatar b-3x m-r", + "avatar": "http://hiphotos.baidu.com/fex/%70%69%63/item/c9fcc3cec3fdfc03ccabb38edd3f8794a4c22630.jpg" + } + }*/ + /*{ + type: 'carousel', + width: '500', + height: '200', + controlTheme: 'dark', + controls: ['arrows', 'dots'], + animation: 'slideRight', + options: [ + { + html: '

This is data

' + }, + { + label: 'This is data and is label' + }, + { + image: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg' + } + ] + }*/ + ], aside: '边栏部分', toolbar: '工具栏', initApi: '/api/mock2/page/initData' diff --git a/mock/crud/list.js b/mock/crud/list.js index f04dd2f4b..fa895ed26 100644 --- a/mock/crud/list.js +++ b/mock/crud/list.js @@ -23,7 +23,11 @@ module.exports = function(req, res) { list: repeat(() => ({ title: '{{name.title}}', description: '{{lorem.words}}' - }), Math.round(Math.random() * 10)), + }), Math.round(Math.random() * 3)), + carousel: repeat(() => ({ + image: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg', + // html: '

{{lorem.words}}

' + }), Math.round(Math.random() * 5)), date: Math.round(Date.now() / 1000), // image: '{{image.imageUrl}}', image: 'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg', diff --git a/scss/components/_carousel.scss b/scss/components/_carousel.scss new file mode 100644 index 000000000..34fa8fb3a --- /dev/null +++ b/scss/components/_carousel.scss @@ -0,0 +1,141 @@ +@mixin arrow-control { + width: 20px; + height: 20px; + cursor: pointer; + position: absolute; + bottom: 50%; + font: 16px/30px sans-serif; + color: rgba(255, 255, 255, 0.8); + transition-duration: 300ms; +} + +@mixin svg-theme { + &.light { + svg { + fill: white; + } + } + + &.dark { + svg { + fill: black; + } + } +} + +.#{$ns}Carousel { + position: relative; + display: block; + + &-container { + min-width: 100px; + height: 200px; + position: relative; + overflow: hidden; + + .#{$ns}Carousel-item { + top:0; + left: 0; + width: 100%; + height: 100%; + position: absolute; + background: lightblue; + transition: ease-out all 0.5s; + + &.fade { + opacity: 0; + } + + &.fade.in { + opacity: 1; + } + + &.slideLeft { + transform: translateX(100%); + } + + &.slideLeft.in { + transform: translateX(0); + z-index: 1; + } + + &.slideLeft.out { + transform: translateX(-100%); + z-index: -1; + } + + &.slideRight { + transform: translateX(-100%); + } + + &.slideRight.in { + transform: translateX(0); + z-index: 1; + } + + &.slideRight.out { + transform: translateX(100%); + z-index: -1; + } + + > span > img { + width: 100%; + height: 100%; + } + } + } + + &-control-dots { + position: absolute; + width: 100%; + z-index: 100; + bottom: 0px; + text-align: center; + + .#{$ns}Carousel-dot { + display: inline-block; + height: 8px; + width: 8px; + border-radius: 4px; + + margin: 7px 5px; + transition-duration: 300ms; + + &.light { + background-color: white; + } + + &.dark { + background-color: black; + } + + &.is-default { + opacity: 0.3; + } + + &.is-active { + opacity: 1; + } + } + } + + &-control-arrows { + position: absolute; + width: 100%; + height: inherit; + z-index: 100; + text-align: center; + + .#{$ns}Carousel-leftArrow { + @include arrow-control; + @include svg-theme; + left: 10px; + } + + .#{$ns}Carousel-rightArrow { + @include arrow-control; + @include svg-theme; + right: 10px; + } + } +} \ No newline at end of file diff --git a/scss/themes/cxd.scss b/scss/themes/cxd.scss index 6a3eafe67..0a40ea3d6 100644 --- a/scss/themes/cxd.scss +++ b/scss/themes/cxd.scss @@ -418,20 +418,21 @@ $TagControl-sugTip-color: $primary; @import "../components/button"; @import "../components/button-group"; @import "../components/dropdown"; -@import "../components/collapse"; -@import "../components/wizard"; -@import "../components/crud"; -@import "../components/table"; -@import "../components/list"; -@import "../components/cards"; -@import "../components/card"; -@import "../components/quick-edit"; -@import "../components/popoverable"; -@import "../components/copyable"; -@import "../components/divider"; -@import "../components/pagination"; -@import "../components/wrapper"; -@import "../components/status"; +@import"../components/collapse"; +@import"../components/wizard"; +@import"../components/crud"; +@import"../components/table"; +@import"../components/list"; +@import"../components/cards"; +@import"../components/card"; +@import"../components/quick-edit"; +@import"../components/popoverable"; +@import"../components/copyable"; +@import"../components/divider"; +@import"../components/pagination"; +@import"../components/wrapper"; +@import"../components/status"; +@import"../components/carousel"; @import "../components/form/fieldset"; @import "../components/form/group"; diff --git a/scss/themes/default.scss b/scss/themes/default.scss index a000a0aa6..99ab64e63 100644 --- a/scss/themes/default.scss +++ b/scss/themes/default.scss @@ -45,20 +45,21 @@ $Form-input-borderColor: #cfdadd; @import "../components/button"; @import "../components/button-group"; @import "../components/dropdown"; -@import "../components/collapse"; -@import "../components/wizard"; -@import "../components/crud"; -@import "../components/table"; -@import "../components/list"; -@import "../components/cards"; -@import "../components/card"; -@import "../components/quick-edit"; -@import "../components/popoverable"; -@import "../components/copyable"; -@import "../components/divider"; -@import "../components/pagination"; -@import "../components/wrapper"; -@import "../components/status"; +@import"../components/collapse"; +@import"../components/wizard"; +@import"../components/crud"; +@import"../components/table"; +@import"../components/list"; +@import"../components/cards"; +@import"../components/card"; +@import"../components/quick-edit"; +@import"../components/popoverable"; +@import"../components/copyable"; +@import"../components/divider"; +@import"../components/pagination"; +@import"../components/wrapper"; +@import"../components/status"; +@import"../components/carousel"; @import "../components/form/fieldset"; @import "../components/form/group"; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 70b5bafa5..6613f21fb 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -75,3 +75,28 @@ export const pauseIcon = ( /> ); +export const dotIcon = ( + + + +); +export const leftArrowIcon = ( + + + +); +export const rightArrowIcon = ( + + + +); \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 6aee2df09..986d26c03 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -149,6 +149,7 @@ import './renderers/Wrapper'; import './renderers/IFrame'; import './renderers/QRCode'; import './renderers/Icon'; +import './renderers/Carousel'; import Scoped, {ScopedContext} from './Scoped'; import { diff --git a/src/renderers/Audio.tsx b/src/renderers/Audio.tsx index ad4963bb6..311af06d7 100644 --- a/src/renderers/Audio.tsx +++ b/src/renderers/Audio.tsx @@ -379,7 +379,7 @@ export class Audio extends React.Component { const {muted} = this.state; return ( -
+
-
+
{controls && controls.map((control:string, index:number) => { control = 'render' + upperFirst(control); const method:'renderRates'|'renderPlay'|'renderTime'|'renderProcess'|'renderVolume'|'render' = control as any; diff --git a/src/renderers/Carousel.tsx b/src/renderers/Carousel.tsx new file mode 100644 index 000000000..e5cb04530 --- /dev/null +++ b/src/renderers/Carousel.tsx @@ -0,0 +1,292 @@ +import * as React from 'react'; +import Transition, {ENTERED, ENTERING, EXITED, EXITING} from 'react-transition-group/Transition'; +import {Renderer, RendererProps} from '../factory'; +import {autobind} from '../utils/helper'; +import {dotIcon, leftArrowIcon, rightArrowIcon} from '../components/icons'; + +const animationStyles: { + [propName: string]: string; +} = { + [ENTERING]: 'in', + [ENTERED]: 'in', + [EXITING]: 'out', + // [EXITED]: 'out' +}; +export interface CarouselProps extends RendererProps { + className?: string; + auto?: boolean; + value?: any; + placeholder?: any; + width?: number; + height?: number; + controls: string[]; + interval: number; + duration: number; + controlTheme: 'light' | 'dark'; + animation: 'fade' | 'slideLeft' | 'slideRight'; +} + +export interface CarouselState { + current: number; + options: any[]; + showArrows: boolean; + nextAnimation?: 'slideLeft' | 'slideRight' | ''; +} + +export class Carousel extends React.Component { + wrapperRef: HTMLDivElement; + intervalTimeout: any; + durationTimeout: any; + + static defaultProps: Pick< + CarouselProps, + 'auto' | 'interval' | 'duration' | 'controlTheme' | 'animation' | 'controls' | 'placeholder' + > = { + auto: true, + interval: 2000, + duration: 500, + controlTheme: 'light', + animation: 'fade', + controls: ['dots', 'arrows'], + placeholder: '' + }; + + state:CarouselState = { + current: 0, + options: this.props.value ? this.props.value : this.props.options ? this.props.options : [], + showArrows: false, + nextAnimation: '' + }; + + componentDidMount() { + this.prepareAutoSlide(); + } + + componentWillUnmount() { + this.clearAutoTimeout() + } + + @autobind + prepareAutoSlide () { + if (this.state.options.length < 2) { + return; + } + + this.clearAutoTimeout(); + if (this.props.auto) { + this.intervalTimeout = setTimeout(this.autoSlide, this.props.interval); + } + } + + @autobind + autoSlide (rel?:string) { + this.clearAutoTimeout(); + const {animation} = this.props; + let {nextAnimation} = this.state; + + switch (rel) { + case 'prev': + animation.includes('slide') ? nextAnimation = 'slideLeft' : nextAnimation = ''; + this.transitFramesTowards('right', nextAnimation); + break; + case 'next': + animation.includes('slide') ? nextAnimation = 'slideRight' : nextAnimation = ''; + this.transitFramesTowards('left', nextAnimation); + break; + default: + nextAnimation = ''; + this.transitFramesTowards('left', nextAnimation); + break; + } + + this.durationTimeout = setTimeout(this.prepareAutoSlide, this.props.duration); + } + + @autobind + transitFramesTowards (direction:string, nextAnimation: 'slideLeft' | 'slideRight' | '') { + let {current} = this.state; + + switch (direction) { + case 'left': + current = this.getFrameId('next'); + break; + case 'right': + current = this.getFrameId('prev'); + break; + } + + this.setState({ + current, + nextAnimation + }); + } + + @autobind + getFrameId (pos?:string) { + const {options, current} = this.state; + const total = options.length; + switch (pos) { + case 'prev': + return (current - 1 + total) % total; + case 'next': + return (current + 1) % total; + default: + return current; + } + } + + @autobind + next () { + this.autoSlide('next'); + } + + @autobind + prev () { + this.autoSlide('prev'); + } + + @autobind + clearAutoTimeout () { + clearTimeout(this.intervalTimeout); + clearTimeout(this.durationTimeout); + } + + setWrapperRef = (wrapper:HTMLDivElement) => { + this.wrapperRef = wrapper; + } + + renderDots() { + const {classnames: cx, controlTheme} = this.props; + const {current, options} = this.state; + return ( +
+ {Array.from({length: options.length}).map((_, i) => + + )} +
+ ) + } + + renderButtons() { + const {classnames: cx, controlTheme} = this.props; + return ( +
+
{leftArrowIcon}
+
{rightArrowIcon}
+
+ ) + } + + @autobind + defaultSchema() { + return { + type: 'tpl', + tpl: "<% if (data.image) { %> \" /> <% } else if (data.label) { %>
<%= data.label %>
<% } else if (data.html) { %> <%= data.html %> <% } %>" + } + } + + @autobind + handleMouseEnter() { + this.setState({ + showArrows: true + }); + this.clearAutoTimeout(); + } + + @autobind + handleMouseLeave() { + this.setState({ + showArrows: false + }); + this.prepareAutoSlide(); + } + + render() { + const { + render, + className, + classnames: cx, + itemSchema, + animation, + width, + height, + controls, + placeholder + } = this.props; + const { + options, + showArrows, + current, + nextAnimation + } = this.state; + + let body:JSX.Element | null = null; + const [dots, arrows] = [controls.indexOf('dots') > -1, controls.indexOf('arrows') > -1]; + const animationName = nextAnimation || animation; + const style = { + width: width + 'px', + height: height + 'px' + }; + + if (options && options.length) { + body = ( +
+ {options.map((option:any, key:number) => ( + + {(status:string) => { + if (status === ENTERING) { + this.wrapperRef.childNodes.forEach((item:HTMLElement) => item.offsetHeight); + } + + return ( +
+ {render(`${current}/body`, itemSchema ? itemSchema : this.defaultSchema(), { + data: option + })} +
+ ); + }} +
+ ))} + {dots ? this.renderDots() : null} + {arrows && showArrows ? this.renderButtons() : null} +
+ ); + } + + return ( +
+ {body ? body : placeholder} +
+ ); + } +} + +@Renderer({ + test: /(^|\/)carousel/, + name: 'carousel', +}) +export class CarouselRenderer extends Carousel {}