Commit f053c5f1 authored by 余成's avatar 余成

init

parents
{
"id": "disk-turntable",
"name": "圆形转盘",
"type": 0,
"desc": "实现了圆形转盘",
"exports": []
}
This diff is collapsed.
{
"name": "@spark/disk-turntable",
"version": "1.0.27",
"main": "dist/bundle.js",
"types": "types/index.d.ts",
"license": "MIT",
"scripts": {
"dev": "npm-run-all -p -r dev:**",
"dev:me": "spark package pack -s src -o test/src/bundle.js -w",
"dev:test": "cd test && npm run dev",
"build": "gr && tsc --outDir 'src-js' -t es2017 -d --declarationDir 'types' --jsx preserve && node scripts/copy-others.js 'src-js'",
"pack": "spark package pack -s src -o dist/bundle.js -p",
"declare": "tsc -d --declarationDir 'types' --emitDeclarationOnly",
"pub": "npm run build && spark package publish -s src"
},
"dependencies": {
"db-react-helper": "^1.0.2"
},
"devDependencies": {
"@types/react": "^16.9.56",
"fs-extra": "^9.0.1",
"less": "^4.1.0",
"npm-run-all": "^4.1.5",
"tslib": "^2.0.1",
"typescript": "^4.1.3"
},
"meta": "{\"id\":\"disk-turntable\",\"name\":\"圆形转盘\",\"type\":0,\"desc\":\"实现了圆形转盘\",\"exports\":[]}"
}
# ${id}
${name}
${desc}
![转盘](http://yun.duiba.com.cn/aurora/assets/c5be7bd36b6c7a525c80ac20ee0fef9313f02277.png)
## Install
`yarn add @spark/${id}`
## Usage
```js
import {DiskTurntable, ${exportIds}} from '@spark/${id}'
```
${exports}
### DiskTurntable
```jsx
//方式一:动态属性渲染样式
<DiskTurntable className='turntable'
options={options}
angleOffset={30}
renderBackground={<span className="bg"/>}
renderStartButton={<span className="start-button" onClick={onClickStartButton}/>}
renderOption={(optionData, index) => {
return <div className="option">
<img className="option-icon" src={optionData.icon}/>
<span className="option-name">{optionData.name}</span>
</div>
}}
/>
//方式二:静态子节点渲染样式
<DiskTurntable className='turntable'
options={options}
angleOffset={30}
>
<span __slot="background" className="bg"/>
<span __slot="startButton" className="start-button" onClick={onClickStartButton}/>
<div __slot="option" className="option">
<img __bind_src="icon" className="option-icon"/>
<span __bind="name" className="option-name"/>
</div>
</DiskTurntable>
```
#### 方式二说明
方式二是将各渲染元素当做子节点传入的,且有`__slot`特殊属性:
* `background` 转盘背景
* `startButton` 开始按钮
* `option` 单个奖项
#### 属性
| 属性 | 类型 | 必填 | 默认 | 描述 |
| :---- |:---- | :---- |:----|:--------------------|
| children | React.ReactChildren | 否 | | 子节点 |
| angleOffset | number | 否 | `0` | 角度偏移量 |
| radian | number | 否 | `120` | 奖项半径 |
| options | any[] | 是 | true | 奖项数组 |
| renderBackground | React.ReactElement | (() => React.ReactElement) | 否 | | 渲染转盘背景 |
| renderStartButton | React.ReactElement | (() => React.ReactElement) | 否 | | 渲染开始按钮 |
| renderOption | React.ReactElement | ((optionData: any, index: number) => React.ReactElement) | 否 | | 渲染单个奖项 |
| didLaunched | () => void | 否 | | 启动时回调 |
| didStop | () => void | 否 | | 停止时回调 |
| className | string | 否 | | 设置样式类 |
| style | CSSProperties | 否 | | 设置内联样式 |
| launchDuration | number | 否 | `3000` | 转盘启动时间 |
| brakingDuration | number | 否 | `3000` | 转盘制动时间,如果不设置则使用启动时间 |
| maxSpeed | number | 否 | `20` | 转动最大角速度 |
#### 方法
`stopAt: (optionIndex: number)=>void`
转盘立刻停驻在指定奖项的索引
`launch: ()=>void`
转盘启动
`braking: (optionIndex: number, options?:{immediately?: boolean, randomOffset?: boolean})=>void`
转盘制动:
> optionIndex: 最终落在奖项的索引
> immediately: 是否立刻停止
> randomOffset: 是否模拟随机角度偏移(效果更加真实)
## Contribute
1. `yarn dev` to develop package
2. `cd test && yarn && yarn dev` to develop test
# disk-turntable
圆形转盘
实现了圆形转盘
![转盘](http://yun.duiba.com.cn/aurora/assets/c5be7bd36b6c7a525c80ac20ee0fef9313f02277.png)
## Install
`yarn add @spark/disk-turntable`
## Usage
```js
import {DiskTurntable, } from '@spark/disk-turntable'
```
### DiskTurntable
```jsx
//方式一:动态属性渲染样式
<DiskTurntable className='turntable'
options={options}
angleOffset={30}
renderBackground={<span className="bg"/>}
renderStartButton={<span className="start-button" onClick={onClickStartButton}/>}
renderOption={(optionData, index) => {
return <div className="option">
<img className="option-icon" src={optionData.icon}/>
<span className="option-name">{optionData.name}</span>
</div>
}}
/>
//方式二:静态子节点渲染样式
<DiskTurntable className='turntable'
options={options}
angleOffset={30}
>
<span __slot="background" className="bg"/>
<span __slot="startButton" className="start-button" onClick={onClickStartButton}/>
<div __slot="option" className="option">
<img __bind_src="icon" className="option-icon"/>
<span __bind="name" className="option-name"/>
</div>
</DiskTurntable>
```
#### 方式二说明
方式二是将各渲染元素当做子节点传入的,且有`__slot`特殊属性:
* `background` 转盘背景
* `startButton` 开始按钮
* `option` 单个奖项
#### 属性
| 属性 | 类型 | 必填 | 默认 | 描述 |
| :---- |:---- | :---- |:----|:--------------------|
| children | React.ReactChildren | 否 | | 子节点 |
| angleOffset | number | 否 | `0` | 角度偏移量 |
| radian | number | 否 | `120` | 奖项半径 |
| options | any[] | 是 | true | 奖项数组 |
| renderBackground | React.ReactElement | (() => React.ReactElement) | 否 | | 渲染转盘背景 |
| renderStartButton | React.ReactElement | (() => React.ReactElement) | 否 | | 渲染开始按钮 |
| renderOption | React.ReactElement | ((optionData: any, index: number) => React.ReactElement) | 否 | | 渲染单个奖项 |
| didLaunched | () => void | 否 | | 启动时回调 |
| didStop | () => void | 否 | | 停止时回调 |
| className | string | 否 | | 设置样式类 |
| style | CSSProperties | 否 | | 设置内联样式 |
| launchDuration | number | 否 | `3000` | 转盘启动时间 |
| brakingDuration | number | 否 | `3000` | 转盘制动时间,如果不设置则使用启动时间 |
| maxSpeed | number | 否 | `20` | 转动最大角速度 |
#### 方法
`stopAt: (optionIndex: number)=>void`
转盘立刻停驻在指定奖项的索引
`launch: ()=>void`
转盘启动
`braking: (optionIndex: number, options?:{immediately?: boolean, randomOffset?: boolean})=>void`
转盘制动:
> optionIndex: 最终落在奖项的索引
> immediately: 是否立刻停止
> randomOffset: 是否模拟随机角度偏移(效果更加真实)
## Contribute
1. `yarn dev` to develop package
2. `cd test && yarn && yarn dev` to develop test
/**
* Created by rockyl on 2021/1/18.
*/
const fs = require('fs-extra')
const path = require('path')
function filter(file) {
let extname = path.extname(file);
return !(extname === '.tsx' || extname === '.ts');
}
fs.copySync('src', process.argv[2], {filter})
/**
* Created by rockyl on 2021/9/10.
*/
const tickDelta = 1000 / 60;
export function useRotate(target, options = {}) {
let _rotating = false;
let _braking = false;
let _brakingPromise;
const [start, stop] = useTick(onTick, {
onStart: () => {
_rotating = true;
},
onStop: () => {
_rotating = false;
},
});
const { launchDuration = 3000, maxSpeed = 20, } = options;
const brakingDuration = options.brakingDuration || launchDuration;
let _stage = 0, _launchTime, _brakingTime, _speed = 0, _rotation, _nextBrakingR, _shouldBraking;
let _didStop;
function launch(force = false) {
if (force || !_rotating) {
_rotating = true;
_rotation = getRotation(target);
_shouldBraking = false;
_launchTime = 0;
_stage = 1;
//console.log('开始启动')
start();
}
}
function braking(toRotation, immediately = false, didStop) {
return new Promise(resolve => {
_didStop = didStop;
_brakingPromise = { resolve };
if (!_rotating || _braking) {
return;
}
if (immediately) {
_braking = false;
stop();
_didStop && _didStop();
}
else {
_braking = true;
_brakingTime = 0;
const t = brakingDuration / tickDelta;
const deltaR = maxSpeed * t + (-maxSpeed / t) / 2 * (t * t); //计算制动的总角度
_nextBrakingR = toRotation - deltaR % 360;
if (_nextBrakingR < 0) {
_nextBrakingR += 360;
}
//console.log(deltaR, _nextBrakingR, deltaR + _nextBrakingR)
_shouldBraking = true;
}
});
}
function stopAt(rotation) {
_rotation = rotation;
_speed = 0;
_stage = 0;
_applyRotation();
}
function onTick(delta, timestamp) {
delta = tickDelta;
switch (_stage) {
case 0: //静止状态
break;
case 1: //启动阶段
_launchTime += delta;
const percentLaunch = Math.min(_launchTime / launchDuration, 1);
if (percentLaunch == 1) {
_stage = 2;
//console.log('达到匀速')
}
_speed = maxSpeed * percentLaunch;
break;
case 2: //匀速阶段
if (_shouldBraking) { //开始制动
const r = _rotation % 360;
//console.log(r)
if (r == _nextBrakingR) {
_stage = 3;
//console.log('开始减速', _rotation)
}
else if (r < _nextBrakingR && r + _speed >= _nextBrakingR) {
_rotation = _nextBrakingR - maxSpeed / 2;
_stage = 3;
//console.log('开始减速', _rotation)
}
}
break;
case 3: //制动阶段
_brakingTime += delta;
const percentBraking = Math.min(_brakingTime / brakingDuration, 1);
if (percentBraking == 1) {
_stage = 0;
_braking = false;
stop();
_brakingPromise && _brakingPromise.resolve();
_brakingPromise = null;
//console.log('停止', _rotation)
_didStop && _didStop();
}
_speed = maxSpeed * (1 - percentBraking);
break;
}
_rotation += _speed;
_applyRotation();
}
function _applyRotation() {
target.style.transform = `rotate(${_rotation}deg)`;
target.style.webkitTransform = `rotate(${_rotation}deg)`;
}
return [launch, braking, stopAt];
}
export function useTick(callback, options = {}) {
let startTs, lastTs, shouldStop;
function start() {
shouldStop = false;
startTs = undefined;
lastTs = undefined;
const { onStart } = options;
onStart && onStart();
requestAnimationFrame(tick);
}
function stop() {
const { onStop } = options;
onStop && onStop();
shouldStop = true;
}
function tick(timestamp) {
if (startTs === undefined) {
startTs = timestamp;
}
const current = timestamp - startTs;
if (lastTs === undefined) {
lastTs = current;
}
const delta = current - lastTs;
lastTs = current;
callback(delta, current);
if (shouldStop) {
shouldStop = false;
}
else {
requestAnimationFrame(tick);
}
}
return [start, stop];
}
export function getRotation(target) {
const st = getComputedStyle(target, null);
const tr = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"none";
if (tr === 'none') {
return 0;
}
const values = tr.substring(7, tr.length - 2).split(',');
const a = parseFloat(values[0]);
const b = parseFloat(values[1]);
return Math.round(Math.atan2(b, a) * (180 / Math.PI));
}
/**
* Created by rockyl on 2020/9/19.
*/
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle, } from "react";
import './index.less';
import { useRotate } from "./helper";
import { compileTemplate, getChildBySlot } from "db-react-helper";
function classes(...classes) {
return classes.join(' ');
}
/**
* 圆形转盘
* @desc 圆形转盘
* @ctype DOM_COMPONENT
*/
function DiskTurntableImpl(props, ref) {
let { renderBackground, renderOption, renderStartButton, } = props;
const { options, angleOffset = 0, radian = 120, className, style, children, launchDuration, brakingDuration, maxSpeed, beforeLaunch, didStop, } = props;
const fanRef = useRef();
const [ctrl, setCtrl] = useState(undefined);
useEffect(() => {
const [launch, braking, stopAt] = useRotate(fanRef.current, { launchDuration, brakingDuration, maxSpeed });
setCtrl({ launch, braking, stopAt });
return function () {
braking(0, true);
};
}, []);
if (!renderBackground) {
const child = getChildBySlot(children, 'background');
if (child) {
renderBackground = child;
}
else {
console.warn(`属性[renderBackground]为空`);
}
}
if (!renderStartButton) {
const child = getChildBySlot(children, 'startButton');
if (child) {
renderStartButton = child;
}
else {
console.warn(`属性[renderStartButton]为空`);
}
}
if (!renderOption) {
const child = getChildBySlot(children, 'option');
if (child) {
renderOption = child;
}
else {
console.warn(`属性[renderOption]为空`);
}
}
const optionCount = options.length;
const perDeg = 360 / optionCount;
useImperativeHandle(ref, () => ({
stopAt(optionIndex) {
let rotation = 360 - (optionIndex * perDeg);
ctrl.stopAt(rotation);
},
launch() {
beforeLaunch && beforeLaunch();
ctrl.launch();
},
async braking(optionIndex, options = {}) {
const { immediately, randomOffset } = options;
let rotation = 360 - (optionIndex * perDeg); // + perDeg / 2
if (randomOffset) {
rotation += (perDeg / 2 - 3) * Math.random() * (Math.random() > 0.5 ? 1 : -1);
}
await ctrl.braking(rotation, immediately, didStop);
},
}), [ctrl]);
function _renderBackground() {
return typeof renderBackground === 'function' ? renderBackground() : renderBackground;
}
function _renderStartButton() {
return typeof renderStartButton === 'function' ? renderStartButton() : renderStartButton;
}
function _renderOption(optionData, index) {
let optionNode;
if (typeof renderOption === 'function') {
optionNode = renderOption(optionData, index);
}
else {
optionNode = compileTemplate(renderOption, optionData, index);
}
return <div key={index} className="tt_option" style={{
transform: `rotate(${perDeg * index + angleOffset}deg)`,
transformOrigin: `center ${radian / 100}rem`
}}>
<div className="tt_option-wrapper">
{optionNode}
</div>
</div>;
}
return <div className={classes('disk-turntable', className)} style={style}>
<div className="tt_wrapper">
<div className={classes('tt_fan')} ref={fanRef}>
<div className={classes('tt_background')}>
{_renderBackground()}
</div>
<div className={classes('tt_options')} style={{ transform: `translateY(-${radian / 100}rem)` }}>
{options.map(_renderOption)}
</div>
</div>
<div className={classes('tt_start-button')}>
{_renderStartButton()}
</div>
</div>
</div>;
}
// @ts-ignore
export const DiskTurntable = forwardRef(DiskTurntableImpl);
.disk-turntable {
position: relative;
float: left;
.tt_wrapper {
position: relative;
.tt_fan {
position: relative;
.tt_background {
position: relative;
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
.tt_options {
position: absolute;
left: 50%;
top: 50%;
.tt_option {
position: absolute;
bottom: 0;
left: 0;
.tt_option-wrapper {
transform: translateX(-50%);
position: absolute;
bottom: 0;
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
}
}
}
.tt_start-button {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
}
}
\ No newline at end of file
{
"id": "disk-turntable",
"name": "圆形转盘",
"desc": "实现了圆形转盘",
"externals": {
"db-react-helper": "db-react-helper"
}
}
declare function getMetaConfig(id: string);
declare const PROCESS = 1;
declare const DOM_COMPONENT = 2;
declare const CANVAS_WIDGET = 3;
/**
* Created by rockyl on 2021/9/10.
*/
export interface RotateOptions {
launchDuration?: number //启动时间
brakingDuration?: number //制动时间
maxSpeed?: number //最大角速度
}
const tickDelta = 1000 / 60
export function useRotate(target: HTMLElement, options: RotateOptions = {}): [
() => void,
(toRotation: number, immediately: boolean) => void,
(rotation: number) => void,
] {
let _rotating = false
let _braking = false
let _brakingPromise
const [start, stop] = useTick(onTick, {
onStart: () => {
_rotating = true
},
onStop: () => {
_rotating = false
},
})
const {launchDuration = 3000, maxSpeed = 20,} = options
const brakingDuration = options.brakingDuration || launchDuration
let _stage = 0, _launchTime, _brakingTime, _speed = 0, _rotation, _nextBrakingR, _shouldBraking
let _didStop
function launch(force: boolean = false) {
if (force || !_rotating) {
_rotating = true
_rotation = getRotation(target)
_shouldBraking = false
_launchTime = 0
_stage = 1
//console.log('开始启动')
start()
}
}
function braking(toRotation: number, immediately: boolean = false, didStop?) {
return new Promise(resolve => {
_didStop = didStop
_brakingPromise = {resolve}
if (!_rotating || _braking) {
return
}
if (immediately) {
_braking = false
stop()
_didStop && _didStop()
} else {
_braking = true
_brakingTime = 0
const t = brakingDuration / tickDelta
const deltaR = maxSpeed * t + (-maxSpeed / t) / 2 * (t * t) //计算制动的总角度
_nextBrakingR = toRotation - deltaR % 360
if (_nextBrakingR < 0) {
_nextBrakingR += 360
}
//console.log(deltaR, _nextBrakingR, deltaR + _nextBrakingR)
_shouldBraking = true
}
})
}
function stopAt(rotation) {
_rotation = rotation
_speed = 0
_stage = 0
_applyRotation()
}
function onTick(delta: number, timestamp: number) {
delta = tickDelta
switch (_stage) {
case 0: //静止状态
break
case 1: //启动阶段
_launchTime += delta
const percentLaunch = Math.min(_launchTime / launchDuration, 1)
if (percentLaunch == 1) {
_stage = 2
//console.log('达到匀速')
}
_speed = maxSpeed * percentLaunch
break
case 2: //匀速阶段
if (_shouldBraking) { //开始制动
const r = _rotation % 360
//console.log(r)
if (r == _nextBrakingR) {
_stage = 3
//console.log('开始减速', _rotation)
} else if (r < _nextBrakingR && r + _speed >= _nextBrakingR) {
_rotation = _nextBrakingR - maxSpeed / 2
_stage = 3
//console.log('开始减速', _rotation)
}
}
break
case 3: //制动阶段
_brakingTime += delta
const percentBraking = Math.min(_brakingTime / brakingDuration, 1)
if (percentBraking == 1) {
_stage = 0
_braking = false
stop()
_brakingPromise && _brakingPromise.resolve()
_brakingPromise = null
//console.log('停止', _rotation)
_didStop && _didStop()
}
_speed = maxSpeed * (1 - percentBraking)
break
}
_rotation += _speed
_applyRotation()
}
function _applyRotation() {
target.style.transform = `rotate(${_rotation}deg)`
target.style.webkitTransform = `rotate(${_rotation}deg)`
}
return [launch, braking, stopAt]
}
interface TickOptions {
onStart?: () => void
onStop?: () => void
}
export function useTick(callback: (delta: number, timestamp: number) => void, options: TickOptions = {}) {
let startTs, lastTs, shouldStop
function start() {
shouldStop = false
startTs = undefined
lastTs = undefined
const {onStart} = options
onStart && onStart()
requestAnimationFrame(tick)
}
function stop() {
const {onStop} = options
onStop && onStop()
shouldStop = true
}
function tick(timestamp) {
if (startTs === undefined) {
startTs = timestamp
}
const current = timestamp - startTs
if (lastTs === undefined) {
lastTs = current
}
const delta = current - lastTs
lastTs = current
callback(delta, current)
if (shouldStop) {
shouldStop = false
} else {
requestAnimationFrame(tick)
}
}
return [start, stop]
}
export function getRotation(target: HTMLElement) {
const st = getComputedStyle(target, null)
const tr = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"none"
if (tr === 'none') {
return 0
}
const values = tr.substring(7, tr.length - 2).split(',')
const a = parseFloat(values[0])
const b = parseFloat(values[1])
return Math.round(Math.atan2(b, a) * (180 / Math.PI))
}
.disk-turntable {
position: relative;
float: left;
.tt_wrapper {
position: relative;
.tt_fan {
position: relative;
.tt_background {
position: relative;
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
.tt_options {
position: absolute;
left: 50%;
top: 50%;
.tt_option {
position: absolute;
bottom: 0;
left: 0;
.tt_option-wrapper {
transform: translateX(-50%);
position: absolute;
bottom: 0;
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
}
}
}
.tt_start-button {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
> :nth-child(1n) {
position: relative !important;
top: unset !important;
left: unset !important;
display: block !important;
}
}
}
}
\ No newline at end of file
/**
* Created by rockyl on 2020/9/19.
*/
import React, {
FC,
useState,
useEffect,
useRef,
forwardRef,
useImperativeHandle,
ReactElement,
CSSProperties,
} from "react"
import './index.less'
import {RotateOptions, useRotate} from "./helper";
import {compileTemplate, getChildBySlot} from "db-react-helper";
interface DiskTurntableProps extends RotateOptions {
children?: React.ReactElement //子节点
options: any[] //奖项数组
angleOffset?: number //角度偏移量
radian?: number //奖项半径
renderBackground?: React.ReactElement | (() => React.ReactElement) //渲染转盘背景
renderStartButton?: React.ReactElement | (() => React.ReactElement) //渲染开始按钮
renderOption?: React.ReactElement | ((optionData: any, index: number) => React.ReactElement) //渲染单个奖项
beforeLaunch?: () => void //启动时回调
didStop?: () => void //停止时回调
className?: string //设置样式类
style?: CSSProperties //设置内联样式
}
function classes(...classes) {
return classes.join(' ')
}
interface BrakingOptions {
immediately?: boolean
randomOffset?: boolean
}
/**
* 圆形转盘
* @desc 圆形转盘
* @ctype DOM_COMPONENT
*/
function DiskTurntableImpl(props: DiskTurntableProps, ref) {
let {renderBackground, renderOption, renderStartButton,} = props
const {
options, angleOffset = 0, radian = 120,
className,
style,
children,
launchDuration,
brakingDuration,
maxSpeed,
beforeLaunch, didStop,
} = props
const fanRef = useRef()
const [ctrl, setCtrl] = useState(undefined)
useEffect(() => {
const [launch, braking, stopAt] = useRotate(fanRef.current, {launchDuration, brakingDuration, maxSpeed})
setCtrl({launch, braking, stopAt})
return function () {
braking(0, true)
}
}, [])
// TODO 这里是对参入的参数进行兼容性处理
if (!renderBackground) {
const child = getChildBySlot(children, 'background')
if (child) {
renderBackground = child
} else {
console.warn(`属性[renderBackground]为空`)
}
}
if (!renderStartButton) {
const child = getChildBySlot(children, 'startButton')
if (child) {
renderStartButton = child
} else {
console.warn(`属性[renderStartButton]为空`)
}
}
if (!renderOption) {
const child = getChildBySlot(children, 'option')
if (child) {
renderOption = child
} else {
console.warn(`属性[renderOption]为空`)
}
}
const optionCount = options.length
const perDeg = 360 / optionCount // TODO 这里如果除不清 是无线小数改怎么处理
useImperativeHandle(ref, () => ({
stopAt(optionIndex: number) {
let rotation = 360 - (optionIndex * perDeg)
ctrl.stopAt(rotation)
},
launch() {
beforeLaunch && beforeLaunch()
ctrl.launch()
},
async braking(optionIndex: number, options: BrakingOptions = {}) {
const {immediately, randomOffset} = options
let rotation = 360 - (optionIndex * perDeg)// + perDeg / 2
if (randomOffset) {
rotation += (perDeg / 2 - 3) * Math.random() * (Math.random() > 0.5 ? 1 : -1)
}
await ctrl.braking(rotation, immediately, didStop)
},
}), [ctrl])
// TODO 进行函数方式支持
function _renderBackground() {
return typeof renderBackground === 'function' ? renderBackground() : renderBackground
}
function _renderStartButton() {
return typeof renderStartButton === 'function' ? renderStartButton() : renderStartButton
}
function _renderOption(optionData, index) {
let optionNode: ReactElement
if (typeof renderOption === 'function') {
optionNode = renderOption(optionData, index)
} else {
optionNode = compileTemplate(renderOption, optionData, index)
}
return <div key={index} className="tt_option" style={{
transform: `rotate(${perDeg * index + angleOffset}deg)`,
transformOrigin: `center ${radian / 100}rem`
}}>
<div className="tt_option-wrapper">
{optionNode}
</div>
</div>
}
return <div className={classes('disk-turntable', className)} style={style}>
<div className="tt_wrapper">
<div className={classes('tt_fan')} ref={fanRef}>
<div className={classes('tt_background')}>
{_renderBackground()}
</div>
<div className={classes('tt_options')} style={{transform: `translateY(-${radian / 100}rem)`}}>
{options.map(_renderOption)}
</div>
</div>
<div className={classes('tt_start-button')}>
{_renderStartButton()}
</div>
</div>
</div>
}
// @ts-ignore
export const DiskTurntable: FC<DiskTurntableProps> = forwardRef(DiskTurntableImpl)
{
"id": "disk-turntable",
"name": "圆形转盘",
"desc": "实现了圆形转盘",
"externals": {
"db-react-helper": "db-react-helper"
}
}
/src/bundle.js
/src/meta.json
/dist
import MD from 'spark-utils/out/md/index.js';
let appId = CFG.appID;
const dcm = '202.' + CFG.projectId + '.0.0';
const domain = '//embedlog.duiba.com.cn';
let MDList = [
{
ele: `.test-md1`,
data: {
dpm: `${appId}.110.5.1`,
dcm,
domain,
appId
},
once: false
}
];
export default () =>
MD({
show: MDList, // 曝光
click: MDList // 点击
});
const { assets } = require("spark-assets");
const args = process.argv.splice(2);
let argsObj = {
imgmin: false,
imgup: false
}
if (args.length == 1) {
argsObj.imgmin = 'imgmin' == args[0];
argsObj.imgup = 'imgup' == args[0];
} else if (args.length == 2) {
argsObj.imgmin = 'imgmin' == args[0];
argsObj.imgup = 'imgup' == args[1];
}
assets(argsObj)
\ No newline at end of file
exports.SPARK_CONFIG_DIR_KEY = ['OUTPUT_DIR', 'SOURCE_DIR', 'TEMP_DIR', 'ENTRY', 'TEMPLATE']
exports.SPARK_CONFIG = 'sparkrc.js'
//对应项目在线素材存储的cdn配置,用于迭代开发从线上拉取素材到本地
exports.SPARK_CDN_RES_CFG='sparkrescfg.json'
\ No newline at end of file
const loaderUtils = require('loader-utils');
module.exports = function (source) {
const options = loaderUtils.getOptions(this);
let result = source;
if (options.arr) {
options.arr.map(op => {
result = result.replace(op.replaceFrom, op.replaceTo);
})
} else {
result = source.replace(options.replaceFrom, options.replaceTo);
}
return result
};
// 端口是否被占用
exports.getProcessIdOnPort=function(port) {
try {
const execOptions = {
encoding: 'utf8',
stdio: [
'pipe',
'pipe',
'ignore',
],
};
return execSync('lsof -i:' + port + ' -P -t -sTCP:LISTEN', execOptions)
.split('\n')[0]
.trim();
} catch (e) {
return null;
}
}
const childProcessSync=async function(cmd, params, cwd, printLog = true) {
return new Promise((resolve, reject) => {
let proc = childProcess(cmd, params, cwd, printLog);
proc.on('close', (code) => {
if (code === 0) {
resolve(proc['logContent']);
} else {
reject(code);
}
});
});
}
const getGitBranch=async function(cwd) {
try {
const result = await childProcessSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd, false);
if (!result.startsWith('fatal:')) {
return result.trim();
}
} catch (e) {
return undefined;
}
}
const getProjectNameByPackage=function() {
return require(`${process.cwd()}/package.json`).name
}
/**
* 理论上每个项目独一无二的文件夹名字-默认取分支名
* 如果当前未创建分支,取包名+日期
* (实际很多情况是直接clone老项目,包名相同,以防资源被替换,所以用日期加一下)
*/
exports.getCdnFolderName=async function() {
const branch = await getGitBranch(process.cwd());
const date = Date.now();
if (branch) {
return branch + "/" + date;
}
let foldername = getProjectNameByPackage() + "/" + date;
return foldername;
}
\ No newline at end of file
const path = require('path');
const fs = require("fs");
const { SPARK_CONFIG_DIR_KEY, SPARK_CONFIG } = require('./scripts/constant');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
module.exports = function (isProd) {
const appPath = process.cwd();
const sparkConfig = require(path.join(appPath, SPARK_CONFIG));
const isEslint = fs.existsSync(`${appPath}/.eslintrc.js`);
const cssReg = /\.(css|less)$/;
// 处理相对路径
SPARK_CONFIG_DIR_KEY.map((key) => {
sparkConfig[key] = path.resolve(appPath, sparkConfig[key]);
});
const stylePlugins = [
require("autoprefixer")({
overrideBrowserslist: ["> 1%", "last 2 versions", "not ie <= 8"],
})
];
if (sparkConfig.PX2REM) {
stylePlugins.push(
require("postcss-px2rem-exclude")({
remUnit: 100, // 注意算法,这是750设计稿,html的font-size按照750比例
exclude: /node_modules/i,
})
);
}
const styleLoader = (cssOptions = {}) => {
return [
{
loader: "style-loader",
},
isProd && {
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false,
},
},
{
loader: "css-loader",
options: {
...cssOptions,
importLoaders: 2, // 如果遇到css里面的 @import 执行后面两个loader。 不然如果import了less,css-loader是解析不了
},
},
{
loader: "postcss-loader",
options: {
sourceMap: isProd,
plugins: stylePlugins,
},
},
{
loader: require.resolve("less-loader"),
options: {
sourceMap: isProd,
},
},
].filter(Boolean);
};
return {
entry: sparkConfig.ENTRY,
mode: isProd ? 'production' : 'development',
devtool: isProd ? "none" : "cheap-module-source-map",
output: {
path: path.resolve(__dirname, sparkConfig.OUTPUT_DIR),
filename: "js/[name].js",
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
"@src": path.resolve(__dirname, sparkConfig.SOURCE_DIR),
},
},
module: {
rules: [
// 提前进行eslint, 默认从下往上,通过enforce pre提前
isEslint && {
test: /\.js|jsx$/,
enforce: "pre",
loader: "eslint-loader",
options: {
cache: true,
formatter: require("eslint-friendly-formatter"),
fix: true,
failOnError: true,
configFile: `${appPath}/.eslintrc.js`,
},
include: sparkConfig.SOURCE_DIR,
},
{
test: cssReg,
use: styleLoader(),
//include: sparkConfig.SOURCE_DIR,
},
{
test: /\.(js|jsx)$/,
loader: require.resolve("babel-loader"),
exclude: [path.resolve("node_modules")],
options: {
presets: [
require("@babel/preset-env").default,
require("@babel/preset-react").default
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }],
require("@babel/plugin-transform-runtime").default,
],
sourceType: 'unambiguous'
},
},
{
test: [/\.(jpg|jpeg|png|svg|bmp)$/, /\.(eot|woff2?|ttf|svg)$/],
loader: require.resolve("url-loader"),
options: {
name: "[path][name].[ext]", // name默认是加上hash值。这里做了更改,不让加
outputPath: "images",
limit: 10240, // url-loader处理图片默认是转成base64, 这里配置如果小于10kb转base64,否则使用file-loader打包到images文件夹下
},
},
].filter(Boolean),
},
plugins: [
isProd &&
new MiniCssExtractPlugin({
filename: "styles/[name].[hash].css",
}),
new HtmlWebpackPlugin({
template: sparkConfig.TEMPLATE,
minify: !sparkConfig.UNMINIFY_INDEX && isProd,
}),
new CleanWebpackPlugin({
// cleanOnceBeforeBuildPatterns:['**/*', 'dist'] // 这里不用写 是默认的。 路径会根据output 输出的路径去清除
}),
new ProgressBarPlugin(),
].filter(Boolean),
optimization: {
minimize: false,
minimizer: [
// 替换的js压缩 因为uglifyjs不支持es6语法,
new TerserPlugin({
cache: true,
extractComments: false, // 提取注释
parallel: true, // 多线程
terserOptions: {
compress: {
pure_funcs: ["console.log"],
},
},
}),
// 压缩css
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require("cssnano"),
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
canPrint: true,
}),
],
// 修改文件的ids的形成方式,避免单文件修改,会导致其他文件的hash值变化,影响缓存
moduleIds: "hashed",
splitChunks: {
chunks: "all",
minSize: 30000, //小于这个限制的会打包进Main.js
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 优先级权重,层级 相当于z-index。 谁值越大权会按照谁的规则打包
name: "vendors",
},
},
},
// chunks 映射关系的 list单独从 app.js里提取出来
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
};
}
const { SPARK_CONFIG } = require("./scripts/constant");
const Webpack = require("webpack");
const webpackBaseConfig = require("./webpack.common.config");
const WebpackMerge = require("webpack-merge");
const WebpackDevServer = require("webpack-dev-server");
const opn = require("opn");
const apiMocker = require('mocker-api');
const path = require('path');
const { getProcessIdOnPort } = require("./scripts/utils");
const sparkConfig = require(path.resolve(SPARK_CONFIG));
const webpackDevConfig = function () {
return {
devServer: {
useLocalIp: true,
open: false,
hot: true,
host: "0.0.0.0",
// hotOnly: true
before(app) {
if (sparkConfig.API_MOCK) {
apiMocker(app, path.resolve('./mock/index.js'), {
changeHost: true,
})
}
}
},
plugins: [
// new Webpack.WatchIgnorePlugin([/[\\/]mock[\\/]/]),
new Webpack.HotModuleReplacementPlugin()
]
};
};
const buildDev = async function (config) {
let { port } = config;
return new Promise((resolve, reject) => {
const config = WebpackMerge(webpackBaseConfig(false), webpackDevConfig());
const compiler = Webpack(config);
const devServerOptions = Object.assign({}, config.devServer);
console.log('devServerOptions', devServerOptions);
const server = new WebpackDevServer(compiler, devServerOptions);
if (getProcessIdOnPort(port)) {
reject(`端口 ${port} 已被使用`);
return;
} else {
server.listen(
port || 8088,
"0.0.0.0",
() => {
console.log(`Starting server on http://localhost:${port}`);
opn(`http://localhost:${port || 8088}`);
resolve();
},
(err) => {
if (err) console.error("server linsten err--", err);
reject();
}
);
}
});
};
const args = process.argv.splice(2);
const port = args[0] || 8088
buildDev({
port: Number(port)
})
const path = require("path");
const chalk = require("chalk");
const Webpack = require("webpack");
const WebpackMerge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.common.config");
const { uploadFiles } = require("spark-assets");
const isProd = true;
const { getCdnFolderName } = require("./scripts/utils");
const { SPARK_CONFIG } = require("./scripts/constant");
const webpackProdConfig = function (cdnFolderName, resPathProd) {
return {
output: {
publicPath: `//yun.duiba.com.cn/spark/v2/${cdnFolderName}/`,
filename: isProd ? "js/[name].[contenthash:8].js" : "js/[name].[contenthash:4].js",
},
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, './scripts/loaders')]
},
module: {
rules: [
{
test: /sparkrc\.js$/,
exclude: [path.resolve("node_modules")],
use: [
{
loader: 'replaceLoader',
options: {
arr: [
{
replaceFrom: /(MOCK_STATUS: true)|(MOCK_STATUS:true)|("MOCK_STATUS": true)|("MOCK_STATUS":true)/,
replaceTo: '"MOCK_STATUS": false'
},
{
replaceFrom: /(RES_PATH:'\/src\/assets\/')|(RES_PATH: '\/src\/assets\/')|("RES_PATH":"\/src\/assets\/")|("RES_PATH": "\/src\/assets\/")/,
replaceTo: `"RES_PATH":"${resPathProd}/"`
}
]
}
}
]
}
]
},
plugins: [
new Webpack.IgnorePlugin(/[\\/]mock[\\/]/)
]
};
};
const buildProd = async function () {
const cdnFolderName = await getCdnFolderName();
const appPath = process.cwd();
const sparkConfig = require(path.join(appPath, SPARK_CONFIG));
const _webpackProdConfig = await webpackProdConfig(cdnFolderName, sparkConfig.RES_PATH_PROD || '');
return new Promise((resolve, reject) => {
const config = WebpackMerge(webpackBaseConfig(isProd), _webpackProdConfig);
const compiler = Webpack(config);
compiler.run(async (error, stats) => {
if (error) {
return reject(error);
}
console.log(
stats.toString({
chunks: false, // 使构建过程更静默无输出
colors: true, // 在控制台展示颜色
})
);
console.log(`${chalk.yellow("打包成功, 等待上传")}\n`);
await uploadFiles(config.output.path, '', cdnFolderName);
resolve();
});
});
};
buildProd();
\ No newline at end of file
/**
* Created by rockyl on 2021/4/19.
*/
module.exports = {
'/drawPrize_1/query.do': {
"code": null,
"data": {
"addLeftCount": 0,
"addTotalSize": 0,
"configFreeSize": 3,
"configJoinSpName": "sp_1",
"configJoinType": 2,
"configJoinValue": 3,
"configLimitSize": 9,
"endTimestamp": null,
"extra": null,
"freeLeftCount": 0,
"joinCount": 0,
"myCreditsOrSpValue": 2,
"options": [
{
"extra": null,
"optionId": "o693fc73e",
"optionImg": null,
"optionName": "游戏x1",
"position": 1,
"prizeId": "sp_1",
"prizeType": 1,
"ruleId": "ru_1",
"sendCount": null,
"url": null,
"userRecordId": null
}
],
"otherLeftCount": 6,
"ruleId": null,
"startTimestamp": null,
"timestamp": 1618828276590
},
"message": null,
"success": true
},
'POST /drawPrize_1/drawPrize.do': {
"code": null,
"data": {
"extra": null,
"options": [
{
"optionId": "o693fc73e",
"optionImg": null,
"optionName": "游戏x1",
"position": null,
"prizeId": "sp_1",
"prizeType": 1,
"ruleId": "ru_1",
"url": "null18519"
}
]
},
"message": null,
"success": true
}
}
const proxy = {
...require('./drawPrize'),
};
module.exports = proxy;
module.exports = {
region: 'oss-cn-hangzhou',
id: 'LTAI4Fw25WcfcGv7FvcHoiHK',
secret: 'NZk1NtT9J5HFaAolNbtQdzTzLLvLYm',
bucket: 'duiba',
output: '/spark/assets/test2',
entry: 'dist',
internal: true,
};
{
"name": "sparkproject-1615778539507",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "node ./config/webpack.dev.config.js 8090",
"imgmin": "node ./config/scripts/assets/index.js imgmin",
"imgup": "node ./config/scripts/assets/index.js imgup",
"imgminup": "node ./config/scripts/assets/index.js imgmin imgup",
"prod": "node ./config/webpack.prod.config.js",
"build": "node ./config/scripts/assets/index.js imgmin imgup && node ./config/webpack.prod.config.js",
"upload": "ali-oss-publish -c oss.config.js",
"pub-skin": "spark px publish",
"release": "yarn prod && yarn upload && yarn pub-skin"
},
"dependencies": {
"@spark/api-base": "^2.0.7",
"@spark/projectx": "^2.0.5",
"@spark/ui": "^2.0.28",
"@spark/utils": "^2.0.17",
"axios": "^0.19.2",
"css-loader": "^3.6.0",
"duiba-utils": "^1.0.2",
"history": "^4.10.1",
"mobx-react": "^7.1.0",
"moment": "^2.29.1",
"postcss-loader": "^3.0.0",
"prettier": "^2.0.5",
"qs": "^6.9.4",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-redux": "^5.0.7",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"spark-utils": "^0.0.12",
"style-loader": "^1.2.1"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-proposal-decorators": "^7.13.5",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"autoprefixer": "^9.8.6",
"babel-loader": "^8.2.2",
"chalk": "^4.1.0",
"clean-webpack-plugin": "^3.0.0",
"eslint-loader": "^4.0.2",
"fs-extra": "^9.0.1",
"html-webpack-plugin": "^4.5.1",
"less": "^4.1.0",
"less-loader": "^7.2.1",
"mini-css-extract-plugin": "^1.3.4",
"mocker-api": "^2.7.5",
"mockjs": "^1.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-px2rem-exclude": "0.0.6",
"progress-bar-webpack-plugin": "^2.1.0",
"spark-assets": "^1.1.1",
"url-loader": "^4.1.1",
"webpack": "^4.43.0",
"webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2"
}
}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>抽奖玩法demo</title>
<script src="//yun.duiba.com.cn/js-libs/rem/1.1.3/rem.min.js"></script>
<script src="//yun.duiba.com.cn/h5/lib/zepto.min.js"></script>
<script>
function getApp() {
return {
cloud: {},
cloudName: "clientTemplate2C",
requestType: "mock"
}
}
var CFG = CFG || {};
CFG.projectId = location.pathname.split('/')[2] || '1';
function getUrlParam(name) {
var search = window.location.search;
var matched = search
.slice(1)
.match(new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'));
return search.length ? matched && matched[2] : null;
}
CFG.appID = '${APPID}';
if (!getUrlParam("appID")) {
//alert("【警告】检测到活动url中没有appID参数\n缺少该参数会导致埋点、分享、app信息获取错误。")
}
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
{
"skinFile": "dist/index.html",
"skinName": "抽奖玩法预设测试",
"envs": {
"dev": {
"projectId": "pe6bda11c",
"skinId": "preset-drawPrize",
"skinType": "2"
}
}
}
module.exports ={
"OUTPUT_DIR": "dist",
"SOURCE_DIR": "src",
"TEMP_DIR": "./.temp",
"ENTRY": "src/app.jsx",
"TEMPLATE": "./public/index.html",
"API_MOCK": true,
"PX2REM": true,
"IMAGE_Q1": 0.6,
"IMAGE_Q2": 0.8,
"RES_PATH": "/src/assets/",
"RES_PATH_PROD": "//yun.duiba.com.cn/spark/v2/sparkproject-1615778539507/1655691357512"
}
\ No newline at end of file
{"assetsPathArr":[]}
\ No newline at end of file
import React, {Component} from "react";
import ReactDOM from "react-dom";
import "./App.less";
import Test from "./Test";
ReactDOM.render(<Test/>, document.getElementById("root"))
* {
margin: 0;
padding: 0;
}
html,
body {
font-size: 24px;
width: 100%;
height: 100%;
-webkit-text-size-adjust: 100% !important;
text-size-adjust: 100% !important;
-moz-text-size-adjust: 100% !important;
}
import React, {useState, useRef, useEffect} from 'react'
import './Test.less';
import {DiskTurntable} from "./bundle"
function Test() {
const options = [
{name: '奖项0', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
{name: '奖项1', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
{name: '奖项2', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
{name: '奖项3', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
{name: '奖项4', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
{name: '奖项5', icon: '//yun.duiba.com.cn/aurora/assets/2e4adf3d8646ffbd027038cb2c6627a6bca44e44.jpg'},
]
const ttRef = useRef()
useEffect(()=>{
setTimeout(()=>{
//ttRef.current.stopAt(3)
}, 1000)
}, [])
async function onClickStartButton() {
ttRef.current.launch()
const index = await fetchApi()
ttRef.current.braking(index, {randomOffset: false})
}
function fetchApi() {
return new Promise(resolve => {
setTimeout(() => {
resolve(5)
}, 1000)
})
}
return (
<div className="App">
<DiskTurntable className='turntable'
ref={ttRef}
options={options}
angleOffset={0}
radian={120}
launchDuration={1000}
brakingDuration={1000}
maxSpeed={10}
renderBackground={<span className="bg"/>}
renderStartButton={<span className="start-button" onClick={onClickStartButton}/>}
renderOption={(optionData, index) => {
return <div className="option">
<img className="option-icon" src={optionData.icon}/>
<span className="option-name">{optionData.name}</span>
</div>
}}
>
{/*<span __slot="background" className="bg"/>
<span __slot="startButton" className="start-button" onClick={onClickStartButton}/>
<div __slot="option" className="option">
<img __bind_src="icon" className="option-icon"/>
<span __bind="name" className="option-name"/>
</div>*/}
</DiskTurntable>
</div>
)
}
export default Test;
.App {
.turntable {
.bg {
width: 628px;
height: 628px;
left: 0px;
top: 0px;
position: absolute;
transform: rotate(30deg);
background: url("//yun.duiba.com.cn/aurora/assets/55c62b3e0cd72a22853ceaf8e8cc0f1f194917d0.png") no-repeat top left / 100% 100%;
}
.start-button {
width: 166px;
height: 203px;
left: 232px;
top: 185px;
position: absolute;
background: url("//yun.duiba.com.cn/aurora/assets/ecf139095f2d389264440fcbbc7a167655e0819c.png") no-repeat top left / 100% 100%;
}
.option {
width: 120px;
height: 140px;
position: absolute;
top: 62px;
left: 254px;
.option-icon {
position: absolute;
left: 10px;
width: 80px;
height: 80px;
}
.option-name {
position: absolute;
left: 0;
top: 80px;
display: block;
width: 100px;
height: 20px;
text-align: center;
}
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
This diff is collapsed.
{
"compilerOptions": {
"module": "ES6",
"target": "ES5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"lib": [
"ES2015",
"DOM"
]
},
"include": ["src"]
}
/**
* Created by rockyl on 2021/9/10.
*/
export interface RotateOptions {
launchDuration?: number;
brakingDuration?: number;
maxSpeed?: number;
}
export declare function useRotate(target: HTMLElement, options?: RotateOptions): [
() => void,
(toRotation: number, immediately: boolean) => void,
(rotation: number) => void
];
interface TickOptions {
onStart?: () => void;
onStop?: () => void;
}
export declare function useTick(callback: (delta: number, timestamp: number) => void, options?: TickOptions): (() => void)[];
export declare function getRotation(target: HTMLElement): number;
export {};
/**
* Created by rockyl on 2020/9/19.
*/
import React, { FC, CSSProperties } from "react";
import './index.less';
import { RotateOptions } from "./helper";
interface DiskTurntableProps extends RotateOptions {
children?: React.ReactElement;
options: any[];
angleOffset?: number;
radian?: number;
renderBackground?: React.ReactElement | (() => React.ReactElement);
renderStartButton?: React.ReactElement | (() => React.ReactElement);
renderOption?: React.ReactElement | ((optionData: any, index: number) => React.ReactElement);
beforeLaunch?: () => void;
didStop?: () => void;
className?: string;
style?: CSSProperties;
}
export declare const DiskTurntable: FC<DiskTurntableProps>;
export {};
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment