wip: vite版重构中

This commit is contained in:
kailong321200875
2021-10-17 11:46:40 +08:00
parent a8163874dc
commit 0f5c55c36d
37 changed files with 2048 additions and 247 deletions

View File

@@ -0,0 +1,155 @@
<template>
<span>
{{ displayValue }}
</span>
</template>
<script setup lang="ts" name="CountTo">
import { reactive, computed, watch, onMounted, unref, toRef } from 'vue'
import { countToProps } from './props'
import { isNumber } from '@/utils/validate'
import { requestAnimationFrame, cancelAnimationFrame } from '@/utils/animation'
const props = defineProps(countToProps)
const emit = defineEmits(['mounted', 'callback'])
defineExpose({
pauseResume,
reset
})
const state = reactive<{
localStartVal: number
printVal: number | null
displayValue: string
paused: boolean
localDuration: number | null
startTime: number | null
timestamp: number | null
rAF: any
remaining: number | null
}>({
localStartVal: props.startVal,
displayValue: formatNumber(props.startVal),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null
})
const displayValue = toRef(state, 'displayValue')
onMounted(() => {
if (props.autoplay) {
start()
}
emit('mounted')
})
const getCountDown = computed(() => {
return props.startVal > props.endVal
})
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start()
}
})
function start() {
const { startVal, duration } = props
state.localStartVal = startVal
state.startTime = null
state.localDuration = duration
state.paused = false
state.rAF = requestAnimationFrame(count)
}
function pauseResume() {
if (state.paused) {
resume()
state.paused = false
} else {
pause()
state.paused = true
}
}
function pause() {
cancelAnimationFrame(state.rAF)
}
function resume() {
state.startTime = null
state.localDuration = +(state.remaining as number)
state.localStartVal = +(state.printVal as number)
requestAnimationFrame(count)
}
function reset() {
state.startTime = null
cancelAnimationFrame(state.rAF)
state.displayValue = formatNumber(props.startVal)
}
function count(timestamp: number) {
const { useEasing, easingFn, endVal } = props
if (!state.startTime) state.startTime = timestamp
state.timestamp = timestamp
const progress = timestamp - state.startTime
state.remaining = (state.localDuration as number) - progress
if (useEasing) {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
} else {
state.printVal = easingFn(
progress,
state.localStartVal,
endVal - state.localStartVal,
state.localDuration as number
)
}
} else {
if (unref(getCountDown)) {
state.printVal =
state.localStartVal -
(state.localStartVal - endVal) * (progress / (state.localDuration as number))
} else {
state.printVal =
state.localStartVal +
(endVal - state.localStartVal) * (progress / (state.localDuration as number))
}
}
if (unref(getCountDown)) {
state.printVal = state.printVal < endVal ? endVal : state.printVal
} else {
state.printVal = state.printVal > endVal ? endVal : state.printVal
}
state.displayValue = formatNumber(state.printVal)
if (progress < (state.localDuration as number)) {
state.rAF = requestAnimationFrame(count)
} else {
emit('callback')
}
}
function formatNumber(num: number | string) {
const { decimals, decimal, separator, suffix, prefix } = props
num = Number(num).toFixed(decimals)
num += ''
const x = num.split('.')
let x1 = x[0]
const x2 = x.length > 1 ? decimal + x[1] : ''
const rgx = /(\d+)(\d{3})/
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2')
}
}
return prefix + x1 + x2 + suffix
}
</script>

View File

@@ -0,0 +1,62 @@
import { PropType } from 'vue'
export const countToProps = {
startVal: {
type: Number as PropType<number>,
required: false,
default: 0
},
endVal: {
type: Number as PropType<number>,
required: false,
default: 2017
},
duration: {
type: Number as PropType<number>,
required: false,
default: 3000
},
autoplay: {
type: Boolean as PropType<boolean>,
required: false,
default: true
},
decimals: {
type: Number as PropType<number>,
required: false,
default: 0,
validator(value: number) {
return value >= 0
}
},
decimal: {
type: String as PropType<string>,
required: false,
default: '.'
},
separator: {
type: String as PropType<string>,
required: false,
default: ','
},
prefix: {
type: String as PropType<string>,
required: false,
default: ''
},
suffix: {
type: String as PropType<string>,
required: false,
default: ''
},
useEasing: {
type: Boolean as PropType<boolean>,
required: false,
default: true
},
easingFn: {
type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
default(t: number, b: number, c: number, d: number) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
}
}
}

View File

@@ -0,0 +1,91 @@
<template>
<div ref="echartRef" :class="className" :style="{ height: height, width: width }"></div>
</template>
<script setup lang="ts" name="Echart">
import { PropType, onMounted, watch, computed, onBeforeUnmount, onActivated, ref, unref } from 'vue'
import type { EChartsOption } from 'echarts'
import echarts from '@/plugins/echarts'
import { debounce } from 'lodash-es'
import 'echarts-wordcloud'
type ThemeType = 'light' | 'dark' | 'default'
const props = defineProps({
options: {
type: Object as PropType<EChartsOption>,
required: true
},
className: {
type: String as PropType<string>,
default: ''
},
height: {
type: String as PropType<string>,
default: '500px'
},
width: {
type: String as PropType<string>,
default: ''
},
theme: {
type: String as PropType<ThemeType>,
default: 'default'
}
})
let chartRef: Nullable<echarts.ECharts> = null
let sidebarElm: Nullable<Element | any> = null
let __resizeHandler: Nullable<any> = null
const echartOptions = computed(() => props.options)
const echartRef = ref<Nullable<HTMLElement>>(null)
watch(
echartOptions,
(options: EChartsOption) => {
;(chartRef as echarts.ECharts).setOption(options)
},
{
deep: true
}
)
function initChart() {
chartRef = echarts.init(unref(echartRef) as HTMLElement, props.theme)
chartRef.setOption(props.options)
}
function sidebarResizeHandler(e: any): void {
if (e.propertyName === 'width') {
if (__resizeHandler) {
__resizeHandler()
}
}
}
onMounted(() => {
initChart()
__resizeHandler = debounce(() => {
if (chartRef) {
chartRef.resize()
}
}, 100)
window.addEventListener('resize', __resizeHandler)
sidebarElm = document.getElementsByClassName('sidebar__wrap')[0]
sidebarElm && sidebarElm.addEventListener('transitionend', sidebarResizeHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', __resizeHandler)
sidebarElm && sidebarElm.removeEventListener('transitionend', sidebarResizeHandler)
})
onActivated(() => {
// 防止keep-alive之后图表变形
if (chartRef) {
chartRef.resize()
}
})
</script>

View File

@@ -0,0 +1,33 @@
import ImgPreview from './index.vue'
import { isClient } from '@/utils/validate'
import type { Options, Props } from './types'
import { createVNode, render } from 'vue'
let instance: any = null
export function createImgPreview(options: Options) {
if (!isClient) return
const {
imageList,
show = true,
index = 0,
onSelect = null,
onClose = null,
zIndex = 500
} = options
const propsData: Partial<Props> = {}
const container = document.createElement('div')
propsData.imageList = imageList
propsData.show = show
propsData.index = index
propsData.zIndex = zIndex
propsData.onSelect = onSelect
propsData.onClose = onClose
document.body.appendChild(container)
instance = createVNode(ImgPreview, propsData)
render(instance, container)
}

View File

@@ -0,0 +1,429 @@
<template>
<transition name="viewer-fade">
<div
v-show="show"
ref="wrapElRef"
tabindex="-1"
:style="{ 'z-index': zIndex }"
class="image-viewer__wrapper"
>
<div class="image-viewer__mask"></div>
<!-- CLOSE -->
<span class="image-viewer__btn image-viewer__close" @click="hide">
<i class="el-icon-circle-close iconfont"></i>
</span>
<!-- ARROW -->
<template v-if="!isSingle">
<span
class="image-viewer__btn image-viewer__prev"
:class="{ 'is-disabled': !infinite && isFirst }"
@click="prev"
>
<i class="el-icon-arrow-left iconfont"></i>
</span>
<span
class="image-viewer__btn image-viewer__next"
:class="{ 'is-disabled': !infinite && isLast }"
@click="next"
>
<i class="el-icon-arrow-right iconfont"></i>
</span>
</template>
<!-- ACTIONS -->
<div class="image-viewer__btn image-viewer__actions">
<div class="image-viewer__actions__inner">
<svg-icon class="iconfont" icon-class="unscale" @click="handleActions('zoomOut')" />
<svg-icon class="iconfont" icon-class="scale" @click="handleActions('zoomIn')" />
<svg-icon class="iconfont" icon-class="resume" @click="toggleMode" />
<svg-icon
class="iconfont"
icon-class="unrotate"
@click="handleActions('anticlocelise')"
/>
<svg-icon class="iconfont" icon-class="rotate" @click="handleActions('clocelise')" />
</div>
</div>
<!-- CANVAS -->
<div class="image-viewer__canvas">
<img
ref="imgRef"
:src="currentImg"
:style="imgStyle"
class="image-viewer__img"
@load="handleImgLoad"
@error="handleImgError"
@mousedown="handleMouseDown"
@click="select"
/>
</div>
</div>
</transition>
</template>
<script setup lang="ts" name="Preview">
import { ref, reactive, computed, watch, nextTick, unref } from 'vue'
import { previewProps } from './props'
import { isFirefox } from '@/utils/validate'
import { on, off } from '@/utils/dom-utils'
import throttle from 'lodash-es/throttle'
import SvgIcon from '_c/SvgIcon/index.vue'
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
const props = defineProps(previewProps)
const infinite = ref<boolean>(true)
const loading = ref<boolean>(false)
const show = ref<boolean>(props.show)
const index = ref<number>(props.index)
const transform = reactive({
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false
})
const isSingle = computed((): boolean => props.imageList.length <= 1)
const isFirst = computed((): boolean => index.value === 0)
const isLast = computed((): boolean => index.value === props.imageList.length - 1)
const currentImg = computed((): string => props.imageList[index.value])
const imgStyle = computed(() => {
const { scale, deg, offsetX, offsetY, enableTransition } = transform
const style = {
transform: `scale(${scale}) rotate(${deg}deg)`,
transition: enableTransition ? 'transform .3s' : '',
'margin-left': `${offsetX}px`,
'margin-top': `${offsetY}px`
}
return style
})
const wrapElRef = ref<HTMLElement | null>(null)
const imgRef = ref<HTMLElement | null>(null)
let _keyDownHandler: Function | null = null
let _mouseWheelHandler: Function | null = null
let _dragHandler: Function | null = null
watch(
() => index.value,
() => {
reset()
}
)
watch(
() => currentImg.value,
() => {
nextTick(() => {
const $img = unref(imgRef) as any
if (!$img.complete) {
loading.value = true
}
})
}
)
watch(
() => show.value,
(show: boolean) => {
if (show) {
nextTick(() => {
;(unref(wrapElRef) as any).focus()
document.body.style.overflow = 'hidden'
deviceSupportInstall()
})
} else {
nextTick(() => {
document.body.style.overflow = 'auto'
deviceSupportUninstall()
})
}
},
{
immediate: true
}
)
function hide(): void {
show.value = false
if (typeof props.onClose === 'function') {
props.onClose(index.value)
}
}
function select(): void {
if (typeof props.onSelect === 'function') {
props.onSelect(index.value)
}
}
function deviceSupportInstall(): void {
_keyDownHandler = throttle((e: any) => {
const keyCode = e.keyCode
switch (keyCode) {
// ESC
case 27:
hide()
break
// SPACE
case 32:
toggleMode()
break
// LEFT_ARROW
case 37:
prev()
break
// UP_ARROW
case 38:
handleActions('zoomIn')
break
// RIGHT_ARROW
case 39:
next()
break
// DOWN_ARROW
case 40:
handleActions('zoomOut')
break
}
})
_mouseWheelHandler = throttle((e: any) => {
const delta = e.wheelDelta ? e.wheelDelta : -e.detail
if (delta > 0) {
handleActions('zoomIn', {
zoomRate: 0.015,
enableTransition: false
})
} else {
handleActions('zoomOut', {
zoomRate: 0.015,
enableTransition: false
})
}
})
on(document, 'keydown', _keyDownHandler as any)
on(document, mousewheelEventName, _mouseWheelHandler as any)
}
function deviceSupportUninstall(): void {
off(document, 'keydown', _keyDownHandler)
off(document, mousewheelEventName, _mouseWheelHandler)
_keyDownHandler = null
_mouseWheelHandler = null
}
function handleImgLoad(): void {
loading.value = false
}
function handleImgError(e: any): void {
loading.value = false
e.target.alt = '加载失败'
}
function handleMouseDown(e: any): void {
if (loading.value || e.button !== 0) return
const { offsetX, offsetY } = transform
const startX = e.pageX
const startY = e.pageY
_dragHandler = throttle((ev: any) => {
transform.offsetX = offsetX + ev.pageX - startX
transform.offsetY = offsetY + ev.pageY - startY
})
on(document, 'mousemove', _dragHandler as any)
on(document, 'mouseup', () => {
off(document, 'mousemove', _dragHandler as any)
})
e.preventDefault()
}
function reset(): void {
transform.scale = 1
transform.deg = 0
transform.offsetX = 0
transform.offsetY = 0
transform.enableTransition = false
}
function toggleMode(): void {
if (loading.value) return
reset()
}
function prev(): void {
if (isFirst.value && !infinite.value) return
const len = props.imageList.length
index.value = (index.value - 1 + len) % len
}
function next(): void {
if (isLast.value && !infinite.value) return
const len = props.imageList.length
index.value = (index.value + 1) % len
}
function handleActions(action: string, options: any = {}): void {
if (loading.value) return
const style = {
zoomRate: 0.2,
rotateDeg: 90,
enableTransition: true,
...options
}
const { zoomRate, rotateDeg, enableTransition } = style
switch (action) {
case 'zoomOut':
if (transform.scale > 0.2) {
transform.scale = parseFloat((transform.scale - zoomRate).toFixed(3))
}
break
case 'zoomIn':
transform.scale = parseFloat((transform.scale + zoomRate).toFixed(3))
break
case 'clocelise':
transform.deg += rotateDeg
break
case 'anticlocelise':
transform.deg -= rotateDeg
break
}
transform.enableTransition = enableTransition
}
</script>
<style lang="less" scoped>
.iconfont {
cursor: pointer;
}
.image-viewer__wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.image-viewer__btn {
position: absolute;
z-index: 1;
display: flex;
cursor: pointer;
border-radius: 50%;
opacity: 0.8;
box-sizing: border-box;
user-select: none;
align-items: center;
justify-content: center;
}
.image-viewer__close {
top: 40px;
right: 40px;
width: 40px;
height: 40px;
font-size: 40px;
color: #fff;
}
.image-viewer__canvas {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
.image-viewer__actions {
bottom: 30px;
left: 50%;
width: 282px;
height: 44px;
padding: 0 23px;
background-color: #606266;
border-color: #fff;
border-radius: 22px;
transform: translateX(-50%);
.image-viewer__actions__inner {
display: flex;
width: 100%;
height: 100%;
font-size: 23px;
color: #fff;
text-align: justify;
cursor: default;
align-items: center;
justify-content: space-around;
}
}
.image-viewer__prev {
top: 50%;
left: 40px;
width: 44px;
height: 44px;
font-size: 24px;
color: #fff;
background-color: #606266;
border-color: #fff;
transform: translateY(-50%);
}
.image-viewer__next {
top: 50%;
right: 40px;
width: 44px;
height: 44px;
font-size: 24px;
color: #fff;
text-indent: 2px;
background-color: #606266;
border-color: #fff;
transform: translateY(-50%);
}
.image-viewer__mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
opacity: 0.5;
}
.viewer-fade-enter-active {
animation: viewer-fade-in 0.3s;
}
.viewer-fade-leave-active {
animation: viewer-fade-out 0.3s;
}
@keyframes viewer-fade-in {
0% {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes viewer-fade-out {
0% {
opacity: 1;
transform: translate3d(0, 0, 0);
}
100% {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
}
</style>

View File

@@ -0,0 +1,28 @@
import { PropType } from 'vue'
export const previewProps = {
index: {
type: Number as PropType<number>,
default: 0
},
zIndex: {
type: Number as PropType<number>,
default: 100
},
show: {
type: Boolean as PropType<boolean>,
default: false
},
imageList: {
type: [Array] as PropType<string[]>,
default: []
},
onClose: {
type: Function as PropType<Function>,
default: null
},
onSelect: {
type: Function as PropType<Function>,
default: null
}
}

View File

@@ -0,0 +1,18 @@
export interface Options {
show?: boolean
imageList: string[]
index?: number
zIndex?: number
onSelect?: Function | null
onClose?: Function | null
}
export interface Props {
show: boolean
instance: Props
imageList: string[]
index: number
zIndex: number
onSelect: Function | null
onClose: Function | null
}