Merge pull request #10 from catchonme/master

add audio component
This commit is contained in:
liaoxuezhi 2019-05-05 14:59:59 +08:00 committed by GitHub
commit 179858deb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 547 additions and 18 deletions

View File

@ -3465,6 +3465,28 @@ CRUD 支持三种模式:`table`、`cards`、`list`,默认为 `table`。
}
```
## Audio
音频播放器
|属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | `string` | `"audio"` | 指定为 audio 渲染器 |
| className | `string` | | 外层 Dom 的类名 |
| inline | `boolean` | true | 是否是内联模式 |
| src | `string` | | 音频地址 |
| loop | `boolean` | false | 是否循环播放 |
| autoPlay | `boolean` | false | 是否自动播放 |
| rates | `array` | `[1.0, 2.0, 4.0]` | 加速播放 |
```schema:height="500" scope="body"
{
"type": "audio",
"autoPlay": false,
"src": ""
}
```
## Video
视频播放器。

View File

@ -1178,15 +1178,63 @@ $Tree-folderIconContent: "\f07b" !default;
$Tree-itemText--onChecked-color: $Form-selectValue-color !default;
// IconPicker
$IconPicker-tabs-bg: #F0F3F4;
$IconPicker-tab-padding: 0 px2rem(5px);
$IconPicker-tab-height: px2rem(30px);
$IconPicker-tab-lineHeight: px2rem(30px);
$IconPicker-tab-onActive-bg: $white;
$IconPicker-content-maxHeight: px2rem(350px);
$IconPicker-singleVendor-padding: px2rem(5px) 0 px2rem(5px) px2rem(13px);
$IconPicker-multiVendor-padding: px2rem(35px) 0 px2rem(5px) px2rem(13px);
$IconPicker-sugItem-width: px2rem(28px);
$IconPicker-sugItem-height: px2rem(28px);
$IconPicker-sugItem-lineHeight: px2rem(28px);
$IconPicker-selectedIcon-marginRight: px2rem(5px);
$IconPicker-tabs-bg: #F0F3F4 !default;
$IconPicker-tab-padding: 0 px2rem(5px) !default;
$IconPicker-tab-height: px2rem(30px) !default;
$IconPicker-tab-lineHeight: px2rem(30px) !default;
$IconPicker-tab-onActive-bg: $white !default;
$IconPicker-content-maxHeight: px2rem(350px) !default;
$IconPicker-singleVendor-padding: px2rem(5px) 0 px2rem(5px) px2rem(13px) !default;
$IconPicker-multiVendor-padding: px2rem(35px) 0 px2rem(5px) px2rem(13px) !default;
$IconPicker-sugItem-width: px2rem(28px) !default;
$IconPicker-sugItem-height: px2rem(28px) !default;
$IconPicker-sugItem-lineHeight: px2rem(28px) !default;
$IconPicker-selectedIcon-marginRight: px2rem(5px) !default;
// Audio
$Audio-width: px2rem(300px) !default;
$Audio-height: px2rem(50px) !default;
$Audio-lineHeight: px2rem(50px) !default;
$Audio-border: px2rem(1px) solid #dee2e6 !default;
$Audio-rate-padding: 0 px2rem(5px) !default;
$Audio-rate-width: px2rem(40px) !default;
$Audio-rate-height: px2rem(50px) !default;
$Audio-rate-lineHeight: px2rem(50px) !default;
$Audio-rate-bg: #dee2e6 !default;
$Audio-rateControlItem-padding: px2rem(16px) px2rem(7px) !default;
$Audio-rateControlItem-bg: #dee2e6 !default;
$Audio-rateControlItem-borderRight: px2rem(1px) solid #d3dae0 !default;
$Audio-play-width: px2rem(25px) !default;
$Audio-play-top: px2rem(5px) !default;
$Audio-play-marginLeft: px2rem(10px) !default;
$Audio-times-width: px2rem(80px) !default;
$Audio-times-margin: 0 px2rem(10px) !default;
$Audio-process-width: px2rem(80px) !default;
$Audio-volume-width: px2rem(22px) !default;
$Audio-volume-height: px2rem(22px) !default;
$Audio-volume-lineHeight: px2rem(30px) !default;
$Audio-volume-borderRadius: px2rem(15px) !default;
$Audio-volume-top: px2rem(5px) !default;
$Audio-volume-left: px2rem(10px) !default;
$Audio-volumeControl-width: px2rem(130px) !default;
$Audio-volumeControl-height: px2rem(30px) !default;
$Audio-volumeControl-lineHeight: px2rem(30px) !default;
$Audio-volumeControl-right: px2rem(-6px) !default;
$Audio-volumeControl-top: px2rem(-4px) !default;
$Audio-volumeControl-paddingLeft: px2rem(10px) !default;
$Audio-volumeControl-bg: #E6E7E9 !default;
$Audio-volumeControl-border: px2rem(2px) solid transparent !default;
$Audio-volumeControl-borderRadius: px2rem(10px) !default;
$Audio-input-width: px2rem(80px) !default;
$Audio-volumeControl-input-margin: px2rem(-3px) px2rem(10px) 0 0 !default;
$Audio-track-height: px2rem(6px) !default;
$Audio-track-bg: #d7dbdd !default;
$Audio-track-borderRadius: px2rem(3px) !default;
$Audio-track-border: px2rem(1px) solid transparent !default;
$Audio-thumb-width: px2rem(14px) !default;
$Audio-thumb-height: px2rem(14px) !default;
$Audio-thumb-bg: #606670 !default;
$Audio-thumb-marginTop: px2rem(-5px) !default;
$Audio-svg-width: px2rem(20px) !default;
$Audio-svg-height: px2rem(20px) !default;
$Audio-svg-top: px2rem(4px) !default;

148
scss/components/_audio.scss Normal file
View File

@ -0,0 +1,148 @@
@mixin input-range {
width: $Audio-input-width;
display: inline-block;
-webkit-appearance: none;
vertical-align: middle;
outline: none;
border: none;
padding: 0;
background: none;
&::-webkit-slider-runnable-track {
background-color: $Audio-track-bg;
height: $Audio-track-height;
border-radius: $Audio-track-borderRadius;
border: $Audio-track-border;
}
&::-webkit-slider-thumb {
-webkit-appearance: none !important;
border-radius: 100%;
cursor: pointer;
background: $Audio-thumb-bg;
height: $Audio-thumb-width;
width: $Audio-thumb-height;
margin-top: $Audio-thumb-marginTop;
}
}
@mixin svg {
width: $Audio-svg-width;
height: $Audio-svg-height;
}
.#{$ns}Audio-original {
display: none;
}
.#{$ns}Audio--inline {
display: inline;
}
.#{$ns}Audio {
width: $Audio-width;
height: $Audio-height;
line-height: $Audio-lineHeight;
border: $Audio-border;
display: inline-block;
&-rates {
display: inline-block;
position: relative;
.#{$ns}Audio-rate {
display: block;
padding: $Audio-rate-padding;
width: $Audio-rate-width;
height: $Audio-rate-height;
line-height: $Audio-rate-lineHeight;
text-align: center;
background: $Audio-rate-bg;
cursor: pointer;
}
.#{$ns}Audio-rateControl {
position: absolute;
top: 0;
left: 0;
z-index: 1;
.#{$ns}Audio-rateControlItem {
padding: $Audio-rateControlItem-padding;
background: $Audio-rateControlItem-bg;
cursor: pointer;
user-select: none;
border-right: $Audio-rateControlItem-borderRight;
}
}
}
&-play {
display: inline-block;
width: $Audio-play-width;
position: relative;
top: $Audio-play-top;
cursor: pointer;
margin-left: $Audio-play-marginLeft;
svg {
@include svg();
}
}
&-times {
display: inline-block;
width: $Audio-times-width;
margin: $Audio-times-margin;
cursor: default;
}
&-process {
display: inline-block;
width: $Audio-process-width;
cursor: pointer;
input[type=range] {
@include input-range();
}
}
&-volume {
display: inline-block;
width: $Audio-volume-width;
height: $Audio-volume-height;
line-height: $Audio-volume-lineHeight;
border-radius: $Audio-volume-borderRadius;
position: relative;
top: $Audio-volume-top;
left: $Audio-volume-left;
cursor: pointer;
svg {
@include svg();
}
.#{$ns}Audio-volumeControl {
position: absolute;
right: $Audio-volumeControl-right;
top: $Audio-volumeControl-top;
padding-left: $Audio-volumeControl-paddingLeft;
height: $Audio-volumeControl-height;
line-height: $Audio-volumeControl-lineHeight;
border-radius: $Audio-volumeControl-borderRadius;
background: $Audio-volumeControl-bg;
border: $Audio-volumeControl-border;
width: $Audio-volumeControl-width;
input[type=range] {
margin: $Audio-volumeControl-input-margin;
@include input-range();
}
.#{$ns}Audio-volumeControlIcon {
display: inline-block;
cursor: pointer;
}
svg {
position: relative;
top: $Audio-svg-top;
@include svg();
}
}
}
}

View File

@ -413,6 +413,7 @@ $TagControl-sugTip-color: $primary;
@import "../components/remark";
@import "../components/chart";
@import "../components/video";
@import "../components/audio";
@import "../components/panel";
@import "../components/service";
@import "../components/spinner";

View File

@ -39,6 +39,7 @@ $Form-input-borderColor: #cfdadd;
@import "../components/remark";
@import "../components/chart";
@import "../components/video";
@import "../components/audio";
@import "../components/panel";
@import "../components/service";
@import "../components/spinner";

View File

@ -3,3 +3,7 @@ export const closeIcon = (<svg className="icon" viewBox="0 0 1024 1024" version=
export const unDoIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M661.333333 341.333333H167.04l183.253333-183.253333L320 128 85.333333 362.666667l234.666667 234.666666 30.08-30.08L167.04 384H661.333333a234.666667 234.666667 0 0 1 0 469.333333H448v42.666667h213.333333a277.333333 277.333333 0 0 0 0-554.666667z"></path></svg>);
export const reDoIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M704 128l-30.08 30.08L856.96 341.333333H362.666667a277.333333 277.333333 0 0 0 0 554.666667h213.333333v-42.666667H362.666667a234.666667 234.666667 0 0 1 0-469.333333h494.293333l-183.253333 183.253333L704 597.333333l234.666667-234.666666z"></path></svg>)
export const enterIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M864 192c-19.2 0-32 12.8-32 32v224c0 89.6-70.4 160-160 160H236.8l105.6-105.6c12.8-12.8 12.8-32 0-44.8s-32-12.8-44.8 0l-160 160c-3.2 3.2-6.4 6.4-6.4 9.6-3.2 6.4-3.2 16 0 25.6 3.2 3.2 3.2 6.4 6.4 9.6l160 160c6.4 6.4 12.8 9.6 22.4 9.6s16-3.2 22.4-9.6c12.8-12.8 12.8-32 0-44.8L236.8 672H672c124.8 0 224-99.2 224-224V224c0-19.2-12.8-32-32-32z"></path></svg>)
export const volumeIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M536.319574 5.11991a63.99888 63.99888 0 0 0-69.758779 13.439765L229.764939 255.99552H64.00784a63.99888 63.99888 0 0 0-63.99888 63.99888v383.99328a63.99888 63.99888 0 0 0 63.99888 63.99888h165.757099l236.795856 237.435845A63.99888 63.99888 0 0 0 512 1023.98208a53.759059 53.759059 0 0 0 24.319574-5.11991A63.99888 63.99888 0 0 0 575.99888 959.9832V63.99888a63.99888 63.99888 0 0 0-39.679306-58.87897zM192.0056 639.9888H128.00672V383.99328h63.99888z m255.99552 165.757099l-127.99776-127.99776V346.233941l127.99776-127.99776zM879.353571 148.477402a63.99888 63.99888 0 0 0-94.718342 87.038476 402.552955 402.552955 0 0 1 0 552.950324A63.99888 63.99888 0 0 0 831.9944 895.98432a63.99888 63.99888 0 0 0 46.719183-20.479641 531.830693 531.830693 0 0 0 0-727.027277z" fill="#606670" p-id="3605"></path><path d="M751.9958 277.11515a63.99888 63.99888 0 0 0-95.99832 85.7585A218.236181 218.236181 0 0 1 703.99664 511.99104a221.436125 221.436125 0 0 1-47.359171 149.117391 63.99888 63.99888 0 0 0 4.479921 90.23842A63.99888 63.99888 0 0 0 703.99664 767.98656a63.99888 63.99888 0 0 0 47.359171-21.11963A349.433885 349.433885 0 0 0 831.9944 511.99104a353.273818 353.273818 0 0 0-79.9986-234.87589z" fill="#606670" p-id="3606"></path></svg>)
export const muteIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M536.310615 5.11991a63.99888 63.99888 0 0 0-69.75878 13.439765L229.755979 255.99552H63.99888a63.99888 63.99888 0 0 0-63.99888 63.99888v383.99328a63.99888 63.99888 0 0 0 63.99888 63.99888h165.757099l236.795856 237.435845A63.99888 63.99888 0 0 0 511.99104 1023.98208a53.759059 53.759059 0 0 0 24.319575-5.11991A63.99888 63.99888 0 0 0 575.98992 959.9832V63.99888a63.99888 63.99888 0 0 0-39.679305-58.87897zM191.99664 639.9888H127.99776V383.99328h63.99888z m255.99552 165.757099l-127.99776-127.99776V346.233941l127.99776-127.99776zM914.543995 511.99104l90.87841-90.238421a63.99888 63.99888 0 1 0-90.87841-90.878409l-90.23842 90.878409-90.238421-90.878409a63.99888 63.99888 0 0 0-90.87841 90.878409L734.067154 511.99104l-90.87841 90.238421a63.99888 63.99888 0 0 0 90.87841 90.87841l90.238421-90.87841 90.23842 90.87841a63.99888 63.99888 0 1 0 90.87841-90.87841z" fill="#606670" p-id="2312"></path></svg>)
export const playIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M852.727563 392.447107C956.997809 458.473635 956.941389 565.559517 852.727563 631.55032L281.888889 993.019655C177.618644 1059.046186 93.090909 1016.054114 93.090909 897.137364L93.090909 126.860063C93.090909 7.879206 177.675064-35.013033 281.888889 30.977769L852.727563 392.447107 852.727563 392.447107Z" p-id="4494" fill="#606670"></path></svg>)
export const pauseIcon = (<svg className="icon" viewBox="0 0 1024 1024" version="1.1"><path d="M757.52 73.107h-62.493c-34.526 0-62.498 27.984-62.498 62.511v749.948c0 34.526 27.974 62.493 62.498 62.493h62.493c34.516 0 62.502-27.968 62.502-62.493v-749.953c-0.001-34.524-27.984-62.509-62.502-62.509z" p-id="7567" fill="#606670"></path><path d="M320.054 73.107h-62.502c-34.526 0-62.498 27.984-62.498 62.511v749.948c0 34.526 27.974 62.493 62.498 62.493h62.502c34.505 0 62.493-27.968 62.493-62.493v-749.953c-0.001-34.524-27.984-62.509-62.493-62.509z" p-id="7568" fill="#606670"></path></svg>)

View File

@ -141,6 +141,7 @@ import './renderers/Chart';
import './renderers/Container';
import './renderers/Service';
import './renderers/Video';
import './renderers/Audio';
import './renderers/Nav';
import './renderers/Tasks';
import './renderers/Drawer';

304
src/renderers/Audio.tsx Normal file
View File

@ -0,0 +1,304 @@
import * as React from 'react';
import {
Renderer,
RendererProps
} from '../factory';
import { autobind } from '../utils/helper';
import { volumeIcon, muteIcon, playIcon, pauseIcon} from '../components/icons';
export interface AudioProps extends RendererProps {
className?: string;
inline?: boolean,
src?: string,
autoPlay?: boolean,
loop?: boolean,
rates?: number[]
}
export interface AudioState {
isReady?: boolean,
muted?: boolean,
playing?: boolean,
played: number,
seeking?: boolean,
volume: number,
prevVolume: number,
loaded?: number,
playbackRate: number,
showHandlePlaybackRate: boolean,
showHandleVolume: boolean
}
export class Audio extends React.Component<AudioProps, AudioState> {
audio: any;
progressTimeout: any;
static defaultProps:Pick<AudioProps, 'inline' | 'autoPlay' | 'playbackRate' | 'loop' | 'rates' | 'progressInterval'> = {
inline: true,
autoPlay: false,
playbackRate: 1,
loop: false,
rates: [1.0, 2.0, 4.0],
progressInterval: 1000
};
state:AudioState = {
isReady: false,
muted: false,
playing: false,
played: 0,
seeking: false,
volume: 0.8,
prevVolume: 0.8,
loaded: 0,
playbackRate: 1.0,
showHandlePlaybackRate: false,
showHandleVolume: false
}
componentWillUnmount () {
clearTimeout(this.progressTimeout);
}
componentDidMount() {
const autoPlay = this.props.autoPlay;
const playing = autoPlay ? true : false;
this.setState({
playing: playing
}, this.progress);
}
@autobind
progress() {
clearTimeout(this.progressTimeout);
if (this.props.src && this.audio) {
const currentTime = this.audio.currentTime || 0;
const duration = this.audio.duration;
const played = currentTime / duration;
let playing = this.state.playing;
playing = (played != 1 && playing) ? true : false;
this.setState({
played,
playing
});
this.progressTimeout = setTimeout(this.progress, (this.props.progressInterval / this.state.playbackRate))
}
}
@autobind
audioRef(audio:any) {
this.audio = audio;
}
@autobind
load() {
this.setState({
isReady: true
});
}
@autobind
handlePlaybackRate(rate:number) {
this.audio.playbackRate = rate;
this.setState({
playbackRate: rate,
showHandlePlaybackRate: false
});
}
@autobind
handleMute() {
const {muted, prevVolume} = this.state;
const curVolume = !muted ? 0 : prevVolume;
this.audio.muted = !muted;
this.setState({
muted: !muted,
volume: curVolume
});
}
@autobind
handlePlaying() {
let playing = this.state.playing;
playing ? this.audio.pause() : this.audio.play();
this.setState({
playing: !playing
});
}
@autobind
getCurrentTime() {
if (!this.audio || !this.state.isReady) return 0;
const duration = this.audio.duration;
const played = this.state.played;
return this.formatTime(duration * (played || 0));
}
@autobind
getDuration() {
if (!this.audio || !this.state.isReady) return 0;
const { duration, seekable } = this.audio;
// on iOS, live streams return Infinity for the duration
// so instead we use the end of the seekable timerange
if (duration === Infinity && seekable.length > 0) {
return seekable.end(seekable.length - 1)
}
return this.formatTime(duration);
}
@autobind
onSeekChange(e:any) {
const played = e.target.value;
this.setState({ played: played });
}
@autobind
onSeekMouseDown() {
this.setState({ seeking: true });
}
@autobind
onSeekMouseUp(e:any) {
if (!this.state.seeking) return;
const played = e.target.value;
const duration = this.audio.duration;
this.audio.currentTime = duration * played;
const loop = this.props.loop;
let playing = this.state.playing;
playing = (played < 1 || loop) ? playing : false;
this.setState({
playing: playing,
seeking: false
});
}
@autobind
setVolume(e:any) {
const volume = e.target.value;
this.audio.volume = volume;
this.setState({
volume: volume,
prevVolume: volume
});
}
@autobind
formatTime(seconds:number) {
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
const mm = date.getUTCMinutes();
const ss = this.pad(date.getUTCSeconds());
if (hh) {
return `${hh}:${this.pad(mm)}:${ss}`;
}
return `${mm}:${ss}`;
}
@autobind
pad(string:number) {
return ('0' + string).slice(-2)
}
@autobind
toggleHandlePlaybackRate() {
this.setState({
showHandlePlaybackRate: !this.state.showHandlePlaybackRate
});
}
@autobind
toggleHandleVolume(type:boolean) {
this.setState({
showHandleVolume: type
});
}
render() {
const {
className,
inline,
src,
autoPlay,
loop,
rates,
classnames: cx
} = this.props;
const {
playing,
played,
volume,
muted,
isReady,
playbackRate,
showHandlePlaybackRate,
showHandleVolume
} = this.state;
return (
<div className={cx(inline ? 'Audio--inline' : '')}>
<audio
className={cx('Audio-original')}
ref={this.audioRef}
onCanPlay={this.load}
autoPlay={autoPlay}
controls
muted={muted}
loop={loop}>
<source src={src}/>
</audio>
{isReady ? (<div className={cx('Audio', className)}>
{rates ? (<div className={cx('Audio-rates')}>
<div className={cx('Audio-rate')}
onClick={this.toggleHandlePlaybackRate}>
x{playbackRate.toFixed(1)}
</div>
{showHandlePlaybackRate ? (<div className={cx('Audio-rateControl')}>
{rates.map((rate, index) =>
<span className={cx('Audio-rateControlItem')}
key={index}
onClick={() => this.handlePlaybackRate(rate)}>
x{rate.toFixed(1)}
</span>
)} </div>) : null}
</div>)
: null }
<div className={cx('Audio-play')} onClick={this.handlePlaying}>
{playing ? pauseIcon : playIcon}
</div>
<div className={cx('Audio-times')}>{this.getCurrentTime()} / {this.getDuration()}</div>
<div className={cx('Audio-process')}>
<input
type="range"
min={0} max={1} step="any"
value={played || 0}
onMouseDown={this.onSeekMouseDown}
onChange={this.onSeekChange}
onMouseUp={this.onSeekMouseUp}/>
</div>
<div className={cx('Audio-volume')}
onMouseEnter={() => this.toggleHandleVolume(true)}
onMouseLeave={() => this.toggleHandleVolume(false)}>
{showHandleVolume ?
(<div className={cx('Audio-volumeControl')}>
<input
type='range' min={0} max={1} step='any'
value={volume}
onChange={this.setVolume} />
<div className={cx('Audio-volumeControlIcon')}
onClick={this.handleMute}>
{volume > 0 ? volumeIcon : muteIcon}
</div></div>)
: volume > 0 ? volumeIcon : muteIcon}
</div>
</div>) : null}
</div>
);
}
}
@Renderer({
test: /(^|\/)audio/,
name: 'audio'
})
export class AudioRenderer extends Audio {};