release: template版本发布

This commit is contained in:
kailong321200875
2021-10-24 13:03:37 +08:00
parent b79a56753d
commit 2a7f3d2c46
92 changed files with 75 additions and 9281 deletions

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
export interface AvatarConfig {
text: string
type?: string
url?: string
}

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -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>

View File

@@ -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 // 是否全屏
}

View File

@@ -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)
}
})

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -1,9 +0,0 @@
export interface LogoTypes {
src?: string
logoSize?: number
bgColor?: string
borderSize?: number
crossOrigin?: string
borderRadius?: number
logoRadius?: number
}