release: template版本发布
This commit is contained in:
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div class="avatars-wrap">
|
||||
<template v-if="tooltip">
|
||||
<el-tooltip
|
||||
v-for="(item, $index) in avatarsData"
|
||||
:key="$index"
|
||||
:content="item.text"
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
showAvatar ? 'avatars-item-img' : ['avatars-item', `avatars-${item.type || 'default'}`]
|
||||
"
|
||||
>
|
||||
<el-avatar v-if="showAvatar" :size="40" :src="item.url">
|
||||
<img :src="defaultImg" />
|
||||
</el-avatar>
|
||||
<span v-else>{{ item.text.substr(0, 1) }}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div v-if="max && data.length - max > 0" :class="['avatars-item', 'avatars-item-img']">
|
||||
<span>+{{ data.length - max }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(item, $index) in avatarsData"
|
||||
:key="$index"
|
||||
:class="
|
||||
showAvatar ? 'avatars-item-img' : ['avatars-item', `avatars-${item.type || 'default'}`]
|
||||
"
|
||||
>
|
||||
<el-avatar v-if="showAvatar" :size="40" :src="item.url">
|
||||
<img :src="defaultImg" />
|
||||
</el-avatar>
|
||||
<span v-else>{{ item.text.substr(0, 1) }}</span>
|
||||
</div>
|
||||
<div v-if="max && data.length - max > 0" :class="['avatars-item', 'avatars-item-img']">
|
||||
<span>+{{ data.length - max }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Avatars">
|
||||
import { PropType, computed } from 'vue'
|
||||
import { deepClone } from '@/utils'
|
||||
import { AvatarConfig } from './types'
|
||||
import defaultImg from '@/assets/img/default-avatar.png'
|
||||
|
||||
const props = defineProps({
|
||||
// 展示的数据
|
||||
data: {
|
||||
type: Array as PropType<AvatarConfig[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 最大展示数量
|
||||
max: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0
|
||||
},
|
||||
// 是否使用头像
|
||||
showAvatar: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 是否显示完整名称
|
||||
tooltip: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const avatarsData = computed(() => {
|
||||
if (props.max) {
|
||||
if (props.data.length <= props.max) {
|
||||
return props.data
|
||||
} else {
|
||||
const data = deepClone(props.data).splice(0, props.max)
|
||||
return data
|
||||
}
|
||||
} else {
|
||||
return props.data
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatars-wrap {
|
||||
display: flex;
|
||||
|
||||
.avatars-item {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: #2d8cf0;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatars-item-img {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
|
||||
.el-avatar--circle {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.avatars-item-img + .avatars-item-img {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.avatars-item + .avatars-item {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.avatars-default {
|
||||
color: #bae7ff;
|
||||
background: #096dd9;
|
||||
}
|
||||
|
||||
.avatars-success {
|
||||
color: #f6ffed;
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
.avatars-danger {
|
||||
color: #fff1f0;
|
||||
background: #f5222d;
|
||||
}
|
||||
|
||||
.avatars-warning {
|
||||
color: #fffbe6;
|
||||
background: #faad14;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface AvatarConfig {
|
||||
text: string
|
||||
type?: string
|
||||
url?: string
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<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'
|
||||
|
||||
const props = defineProps(countToProps)
|
||||
|
||||
const emit = defineEmits(['mounted', 'callback'])
|
||||
|
||||
defineExpose({
|
||||
pauseResume,
|
||||
reset,
|
||||
start,
|
||||
pause
|
||||
})
|
||||
|
||||
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>
|
||||
@@ -1,62 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
<template>
|
||||
<div ref="editorRef"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Editor">
|
||||
import { PropType, watch, computed, onMounted, onBeforeUnmount, ref, unref } from 'vue'
|
||||
import E from 'wangeditor'
|
||||
import hljs from 'highlight.js' // 这个蠢货插件,9以上的版本都不支持IE。辣鸡!!
|
||||
import 'highlight.js/styles/monokai-sublime.css'
|
||||
import { oneOf } from '@/utils'
|
||||
import { EditorConfig } from './types'
|
||||
import { Message } from '_c/Message'
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<EditorConfig>,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
valueType: {
|
||||
type: String as PropType<'html' | 'text'>,
|
||||
default: 'html',
|
||||
validator: (val: string) => {
|
||||
return oneOf(val, ['html', 'text'])
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'focus', 'blur'])
|
||||
|
||||
defineExpose({
|
||||
getHtml,
|
||||
getJSON,
|
||||
getText
|
||||
})
|
||||
|
||||
let editor: Nullable<E> = null
|
||||
const value = computed(() => props.value)
|
||||
const editorRef = ref<Nullable<HTMLElement>>(null)
|
||||
|
||||
watch(
|
||||
value,
|
||||
(val: string) => {
|
||||
if (editor) {
|
||||
editor.txt.html(val)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function createdEditor() {
|
||||
editor = new E(unref(editorRef.value) as HTMLElement)
|
||||
initConfig()
|
||||
editor.create()
|
||||
editor.txt.html(value.value)
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
const config = props.config as EditorConfig
|
||||
const editorRef = editor as E
|
||||
|
||||
// // 设置编辑区域高度为 500px
|
||||
editorRef.config.height = config.height || 500
|
||||
|
||||
// // 设置zIndex
|
||||
editorRef.config.zIndex = config.zIndex || 0
|
||||
|
||||
// // 设置 placeholder 提示文字
|
||||
editorRef.config.placeholder = config.placeholder || '请输入文本'
|
||||
|
||||
// // 设置是否自动聚焦
|
||||
editorRef.config.focus = config.focus || false
|
||||
|
||||
// 配置菜单
|
||||
editorRef.config.menus = config.menus || [
|
||||
'head',
|
||||
'bold',
|
||||
'fontSize',
|
||||
'fontName',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikeThrough',
|
||||
'indent',
|
||||
'lineHeight',
|
||||
'foreColor',
|
||||
'backColor',
|
||||
'link',
|
||||
'list',
|
||||
'justify',
|
||||
'quote',
|
||||
'emoticon',
|
||||
'image',
|
||||
'video',
|
||||
'table',
|
||||
'code',
|
||||
'splitLine',
|
||||
'undo',
|
||||
'redo'
|
||||
]
|
||||
|
||||
// 配置颜色(文字颜色、背景色)
|
||||
editorRef.config.colors = config.colors || ['#000000', '#eeece0', '#1c487f', '#4d80bf']
|
||||
|
||||
// 配置字体
|
||||
editorRef.config.fontNames = config.fontNames || [
|
||||
'黑体',
|
||||
'仿宋',
|
||||
'楷体',
|
||||
'标楷体',
|
||||
'华文仿宋',
|
||||
'华文楷体',
|
||||
'宋体',
|
||||
'微软雅黑',
|
||||
'Arial',
|
||||
'Tahoma',
|
||||
'Verdana',
|
||||
'Times New Roman',
|
||||
'Courier New'
|
||||
]
|
||||
|
||||
// 配置行高
|
||||
editorRef.config.lineHeights = config.lineHeights || ['1', '1.15', '1.6', '2', '2.5', '3']
|
||||
|
||||
// // 代码高亮
|
||||
editorRef.highlight = hljs
|
||||
|
||||
// // 配置全屏
|
||||
editorRef.config.showFullScreen = config.showFullScreen || true
|
||||
|
||||
// 编辑器 customAlert 是对全局的alert做了统一处理,默认为 window.alert。
|
||||
// 如觉得浏览器自带的alert体验不佳,可自定义 alert,以便于达到与自身项目统一的alert效果。
|
||||
editorRef.config.customAlert =
|
||||
config.customAlert ||
|
||||
function (s: string, t: string) {
|
||||
switch (t) {
|
||||
case 'success':
|
||||
Message.success(s)
|
||||
break
|
||||
case 'info':
|
||||
Message.info(s)
|
||||
break
|
||||
case 'warning':
|
||||
Message.warning(s)
|
||||
break
|
||||
case 'error':
|
||||
Message.error(s)
|
||||
break
|
||||
default:
|
||||
Message.info(s)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传默认使用base64
|
||||
editorRef.config.uploadImgShowBase64 = true
|
||||
|
||||
// 配置 onchange 回调函数
|
||||
editorRef.config.onchange = (html: string) => {
|
||||
const text = editorRef.txt.text()
|
||||
emitFun(editor, props.valueType === 'html' ? html : text, 'change')
|
||||
}
|
||||
// 配置触发 onchange 的时间频率,默认为 200ms
|
||||
editorRef.config.onchangeTimeout = config.onchangeTimeout || 1000
|
||||
|
||||
// 编辑区域 focus(聚焦)和 blur(失焦)时触发的回调函数。
|
||||
editorRef.config.onblur = (html: string) => {
|
||||
emitFun(editor, html, 'blur')
|
||||
}
|
||||
editorRef.config.onfocus = (html: string) => {
|
||||
emitFun(editor, html, 'focus')
|
||||
}
|
||||
}
|
||||
|
||||
function emitFun(editor: any, _: string, type: 'change' | 'focus' | 'blur'): void {
|
||||
if (editor) {
|
||||
emit(type, props.valueType === 'html' ? (editor as E).txt.html() : (editor as E).txt.text())
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml() {
|
||||
if (editor) {
|
||||
return (editor as E).txt.html()
|
||||
}
|
||||
}
|
||||
|
||||
function getText() {
|
||||
if (editor) {
|
||||
return (editor as E).txt.text()
|
||||
}
|
||||
}
|
||||
|
||||
function getJSON() {
|
||||
if (editor) {
|
||||
return (editor as E).txt.getJSON()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createdEditor()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editor) {
|
||||
;(editor as E).destroy()
|
||||
editor = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,13 +0,0 @@
|
||||
export interface EditorConfig {
|
||||
height?: number // 富文本高度
|
||||
zIndex?: number // 层级
|
||||
placeholder?: string // 提示文字
|
||||
focus?: boolean // 是否聚焦
|
||||
onchangeTimeout?: number // 几秒监听一次变化
|
||||
customAlert?: (s: string, t: string) => {} // 自定义提示
|
||||
menus?: string[] // 按钮菜单
|
||||
colors?: string[] // 颜色
|
||||
fontNames?: string[] // 字体
|
||||
lineHeights?: string[] // 行间距
|
||||
showFullScreen?: boolean // 是否全屏
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { defineComponent, PropType, computed, h } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Highlight',
|
||||
props: {
|
||||
tag: {
|
||||
type: String as PropType<string>,
|
||||
default: 'span'
|
||||
},
|
||||
keys: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => []
|
||||
},
|
||||
color: {
|
||||
type: String as PropType<string>,
|
||||
default: '#2d8cf0'
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const keyNodes = computed(() => {
|
||||
return props.keys.map((key) => {
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
onClick: () => {
|
||||
emit('click', key)
|
||||
},
|
||||
style: {
|
||||
color: props.color,
|
||||
cursor: 'pointer'
|
||||
}
|
||||
},
|
||||
key
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function parseText(text: string) {
|
||||
props.keys.forEach((key, index) => {
|
||||
const regexp = new RegExp(key, 'g')
|
||||
text = text.replace(regexp, `{{${index}}}`)
|
||||
})
|
||||
return text.split(/{{|}}/)
|
||||
}
|
||||
|
||||
return {
|
||||
keyNodes,
|
||||
parseText
|
||||
}
|
||||
},
|
||||
render(props: any) {
|
||||
if (!props.$slots.default) return null
|
||||
const node = props.$slots.default()[0].children
|
||||
if (!node) {
|
||||
console.warn('Highlight组件的插槽必须要是文本')
|
||||
return props.$slots.default()[0]
|
||||
}
|
||||
const textArray = props.parseText(node)
|
||||
const regexp = /^[0-9]*$/
|
||||
const nodes = textArray.map((t: any) => {
|
||||
if (regexp.test(t)) {
|
||||
return props.keyNodes[Math.floor(t)] || t
|
||||
}
|
||||
return t
|
||||
})
|
||||
return h(props.tag, nodes)
|
||||
}
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
<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'
|
||||
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>
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="qrcode__wrap" :style="wrapStyle">
|
||||
<component :is="tag" ref="wrapRef" @click="clickCode" />
|
||||
<div v-if="disabled" class="disabled__wrap" @click="disabledClick">
|
||||
<div>
|
||||
<i class="el-icon-refresh-right"></i>
|
||||
<div>{{ disabledText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Qrcode">
|
||||
import { PropType, nextTick, ref, watch, computed, unref } from 'vue'
|
||||
import type { LogoTypes } from './types'
|
||||
import QRCode from 'qrcode'
|
||||
import { QRCodeRenderersOptions } from 'qrcode'
|
||||
import { deepClone } from '@/utils'
|
||||
import { isString } from '@/utils/validate'
|
||||
const { toCanvas, toDataURL } = QRCode
|
||||
|
||||
const props = defineProps({
|
||||
// img 或者 canvas,img不支持logo嵌套
|
||||
tag: {
|
||||
type: String as PropType<'canvas' | 'img'>,
|
||||
default: 'canvas',
|
||||
validator: (v: string) => ['canvas', 'img'].includes(v)
|
||||
},
|
||||
// 二维码内容
|
||||
text: {
|
||||
type: [String, Array] as PropType<string | any[]>,
|
||||
default: null
|
||||
},
|
||||
// qrcode.js配置项
|
||||
options: {
|
||||
type: Object as PropType<QRCodeRenderersOptions>,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 宽度
|
||||
width: {
|
||||
type: Number as PropType<number>,
|
||||
default: 200
|
||||
},
|
||||
// logo
|
||||
logo: {
|
||||
type: [String, Object] as PropType<Partial<LogoTypes> | string>,
|
||||
default: ''
|
||||
},
|
||||
// 是否过期
|
||||
disabled: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 过期提示内容
|
||||
disabledText: {
|
||||
type: String as PropType<string>,
|
||||
default: '二维码已失效'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['done', 'click', 'disabled-click'])
|
||||
const loading = ref<boolean>(true)
|
||||
const wrapRef = ref<HTMLCanvasElement | HTMLImageElement | null>(null)
|
||||
const renderText = computed(() => String(props.text))
|
||||
const wrapStyle = computed(() => {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
height: props.width + 'px'
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => renderText.value,
|
||||
(val) => {
|
||||
if (!val) return
|
||||
initQrcode()
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化
|
||||
function initQrcode() {
|
||||
nextTick(async () => {
|
||||
const options = deepClone(props.options || {})
|
||||
if (props.tag === 'canvas') {
|
||||
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
|
||||
options.errorCorrectionLevel =
|
||||
options.errorCorrectionLevel || getErrorCorrectionLevel(renderText.value)
|
||||
getOriginWidth(renderText.value, options).then(async (_width) => {
|
||||
options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
|
||||
const canvasRef: any = await toCanvas(unref(wrapRef as any), renderText.value, options)
|
||||
if (props.logo) {
|
||||
const url = await createLogoCode(canvasRef)
|
||||
emit('done', url)
|
||||
loading.value = false
|
||||
} else {
|
||||
emit('done', canvasRef.toDataURL())
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const url = await toDataURL(renderText.value, {
|
||||
errorCorrectionLevel: 'H',
|
||||
width: props.width,
|
||||
...options
|
||||
})
|
||||
unref(wrapRef as any).src = url
|
||||
emit('done', url)
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生成logo
|
||||
function createLogoCode(canvasRef: HTMLCanvasElement) {
|
||||
const canvasWidth = canvasRef.width
|
||||
const logoOptions: LogoTypes = Object.assign(
|
||||
{
|
||||
logoSize: 0.15,
|
||||
bgColor: '#ffffff',
|
||||
borderSize: 0.05,
|
||||
crossOrigin: 'anonymous',
|
||||
borderRadius: 8,
|
||||
logoRadius: 0
|
||||
},
|
||||
isString(props.logo) ? {} : props.logo
|
||||
)
|
||||
const {
|
||||
logoSize = 0.15,
|
||||
bgColor = '#ffffff',
|
||||
borderSize = 0.05,
|
||||
crossOrigin = 'anonymous',
|
||||
borderRadius = 8,
|
||||
logoRadius = 0
|
||||
} = logoOptions
|
||||
const logoSrc = isString(props.logo) ? props.logo : props.logo.src
|
||||
const logoWidth = canvasWidth * logoSize
|
||||
const logoXY = (canvasWidth * (1 - logoSize)) / 2
|
||||
const logoBgWidth = canvasWidth * (logoSize + borderSize)
|
||||
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
|
||||
|
||||
const ctx = canvasRef.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// logo 底色
|
||||
canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
|
||||
ctx.fillStyle = bgColor
|
||||
ctx.fill()
|
||||
|
||||
// logo
|
||||
const image = new Image()
|
||||
if (crossOrigin || logoRadius) {
|
||||
image.setAttribute('crossOrigin', crossOrigin)
|
||||
}
|
||||
;(image as any).src = logoSrc
|
||||
|
||||
// 使用image绘制可以避免某些跨域情况
|
||||
const drawLogoWithImage = (image: HTMLImageElement) => {
|
||||
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
|
||||
}
|
||||
|
||||
// 使用canvas绘制以获得更多的功能
|
||||
const drawLogoWithCanvas = (image: HTMLImageElement) => {
|
||||
const canvasImage = document.createElement('canvas')
|
||||
canvasImage.width = logoXY + logoWidth
|
||||
canvasImage.height = logoXY + logoWidth
|
||||
const imageCanvas = canvasImage.getContext('2d')
|
||||
if (!imageCanvas || !ctx) return
|
||||
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
|
||||
|
||||
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
|
||||
if (!ctx) return
|
||||
const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
|
||||
if (fillStyle) {
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
// 将 logo绘制到 canvas上
|
||||
return new Promise((resolve: any) => {
|
||||
image.onload = () => {
|
||||
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
|
||||
resolve(canvasRef.toDataURL())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
|
||||
function getOriginWidth(content: string, options: QRCodeRenderersOptions) {
|
||||
const _canvas = document.createElement('canvas')
|
||||
return toCanvas(_canvas, content, options).then(() => _canvas.width)
|
||||
}
|
||||
|
||||
// 对于内容少的QrCode,增大容错率
|
||||
function getErrorCorrectionLevel(content: string) {
|
||||
if (content.length > 36) {
|
||||
return 'M'
|
||||
} else if (content.length > 16) {
|
||||
return 'Q'
|
||||
} else {
|
||||
return 'H'
|
||||
}
|
||||
}
|
||||
|
||||
// 点击二维码
|
||||
function clickCode() {
|
||||
emit('click')
|
||||
}
|
||||
|
||||
// 失效点击事件
|
||||
function disabledClick() {
|
||||
emit('disabled-click')
|
||||
}
|
||||
|
||||
// copy来的方法,用于绘制圆角
|
||||
function canvasRoundRect(ctx: CanvasRenderingContext2D) {
|
||||
return (x: number, y: number, w: number, h: number, r: number) => {
|
||||
const minSize = Math.min(w, h)
|
||||
if (r > minSize / 2) {
|
||||
r = minSize / 2
|
||||
}
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r)
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r)
|
||||
ctx.arcTo(x, y + h, x, y, r)
|
||||
ctx.arcTo(x, y, x + w, y, r)
|
||||
ctx.closePath()
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qrcode__wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.disabled__wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-weight: bold;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
i {
|
||||
margin-bottom: 10px;
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface LogoTypes {
|
||||
src?: string
|
||||
logoSize?: number
|
||||
bgColor?: string
|
||||
borderSize?: number
|
||||
crossOrigin?: string
|
||||
borderRadius?: number
|
||||
logoRadius?: number
|
||||
}
|
||||
Reference in New Issue
Block a user