wip: 重构中
This commit is contained in:
@@ -1,133 +0,0 @@
|
||||
<template>
|
||||
<div class="avatars-wrap">
|
||||
<template v-if="tooltip">
|
||||
<el-tooltip
|
||||
v-for="(item, $index) in avatarsData"
|
||||
:key="$index"
|
||||
:content="item.text"
|
||||
effect="dark"
|
||||
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="require('@/assets/img/avatar.png')">
|
||||
</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="require('@/assets/img/avatar.png')">
|
||||
</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 lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue'
|
||||
import { deepClone } from '@/utils'
|
||||
import { DataConfig } from './types'
|
||||
export default defineComponent({
|
||||
name: 'Avatars',
|
||||
props: {
|
||||
// 展示的数据
|
||||
data: {
|
||||
type: Array as PropType<DataConfig[]>,
|
||||
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
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
avatarsData
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatars-wrap {
|
||||
display: flex;
|
||||
.avatars-item {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #fff;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background: #2d8cf0;
|
||||
}
|
||||
.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 DataConfig {
|
||||
text: string
|
||||
type?: string
|
||||
url?: string
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, computed, watch, onMounted, unref, toRef } from 'vue'
|
||||
import { countToProps } from './props'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { requestAnimationFrame, cancelAnimationFrame } from '@/utils/animation'
|
||||
export default defineComponent({
|
||||
name: 'CountTo',
|
||||
props: countToProps,
|
||||
emits: ['mounted', 'callback'],
|
||||
setup(props, { emit }) {
|
||||
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
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
reset,
|
||||
resume,
|
||||
start,
|
||||
pauseResume,
|
||||
displayValue: toRef(state, 'displayValue')
|
||||
}
|
||||
}
|
||||
})
|
||||
</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 class="detail__wrap">
|
||||
<div class="detail__wrap--header" @click="toggleClick">
|
||||
<div class="detail__wrap--title">
|
||||
<div v-if="title">
|
||||
{{ title }}
|
||||
<el-tooltip
|
||||
v-if="message"
|
||||
effect="dark"
|
||||
:content="message"
|
||||
placement="right"
|
||||
>
|
||||
<i class="el-icon-warning-outline" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<i v-if="collapsed" :class="['el-icon-arrow-down', { 'el-icon-arrow-down-transform': !show }]" />
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div
|
||||
v-show="show"
|
||||
class="detail__content"
|
||||
:style="contentStyleObj"
|
||||
>
|
||||
<el-row type="flex">
|
||||
<el-col
|
||||
v-for="(item, $index) in schema"
|
||||
:key="$index"
|
||||
:span="item.span || 12"
|
||||
>
|
||||
<div
|
||||
class="detail__content--item"
|
||||
:class="{'detail__content--flex': !vertical}"
|
||||
>
|
||||
<div class="content__item--label" :style="labelStyleObj">
|
||||
<slot :name="item.field" :row="item">
|
||||
{{ item.label }}
|
||||
</slot>
|
||||
</div>
|
||||
<div class="content__item--message" :style="messageStyleObj">
|
||||
<slot :name="`${item.field}Content`" :row="data">
|
||||
{{ data[item.field] }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, PropType, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Detail',
|
||||
props: {
|
||||
// 详情标题
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
// 是否可折叠
|
||||
collapsed: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
},
|
||||
// 辅助提示
|
||||
message: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
// 是否需要边框
|
||||
border: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
},
|
||||
// 需要展示的数据
|
||||
data: {
|
||||
type: Object as PropType<object>,
|
||||
required: true
|
||||
},
|
||||
// 布局展示的数据
|
||||
schema: {
|
||||
type: Array as PropType<any[]>,
|
||||
required: true
|
||||
},
|
||||
// 是否标题和内容各占一行 垂直布局
|
||||
vertical: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 标题宽度
|
||||
labelWidth: {
|
||||
type: String as PropType<string>,
|
||||
default: '150px'
|
||||
},
|
||||
// 标题位置
|
||||
labelAlign: {
|
||||
type: String as PropType<string>,
|
||||
default: 'left'
|
||||
},
|
||||
// 边框颜色
|
||||
borderColor: {
|
||||
type: String as PropType<string>,
|
||||
default: '#f0f0f0'
|
||||
},
|
||||
// 标题背景颜色
|
||||
labelBg: {
|
||||
type: String as PropType<string>,
|
||||
default: '#fafafa'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const show = ref<boolean>(true)
|
||||
|
||||
const contentStyleObj = computed(() => {
|
||||
return {
|
||||
borderTop: props.border ? `1px solid ${props.borderColor}` : '',
|
||||
borderLeft: props.border ? `1px solid ${props.borderColor}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
const labelStyleObj = computed(() => {
|
||||
return {
|
||||
width: props.vertical ? `calc(100% - 33px)` : props.labelWidth,
|
||||
textAlign: props.labelAlign,
|
||||
backgroundColor: props.border ? props.labelBg : '',
|
||||
borderRight: props.border ? `1px solid ${props.borderColor}` : '',
|
||||
borderBottom: props.border ? `1px solid ${props.borderColor}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
const messageStyleObj = computed(() => {
|
||||
return {
|
||||
width: props.vertical ? `calc(100% - 33px)` : '100%',
|
||||
borderRight: props.border ? `1px solid ${props.borderColor}` : '',
|
||||
borderBottom: props.border ? `1px solid ${props.borderColor}` : ''
|
||||
}
|
||||
})
|
||||
|
||||
function toggleClick() {
|
||||
if (props.collapsed) {
|
||||
show.value = !show.value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
contentStyleObj, labelStyleObj, messageStyleObj,
|
||||
toggleClick
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail__wrap {
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
.detail__wrap--header {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
margin-bottom: 10px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.detail__wrap--title {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
&:after {
|
||||
content: "";
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: #2d8cf0;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: -10px;
|
||||
}
|
||||
}
|
||||
.el-icon-arrow-down {
|
||||
transition: all .2s;
|
||||
}
|
||||
.el-icon-arrow-down-transform {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
.detail__content {
|
||||
@{deep}(.el-row) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.detail__content--flex {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.content__item--label {
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.content__item--message {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
line-height: 20px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,205 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
ref="dialogRef"
|
||||
v-bind="getBindValue"
|
||||
:fullscreen="fullscreen"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
:close-on-click-modal="false"
|
||||
top="10vh"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
<svg-icon
|
||||
v-if="showFullscreen"
|
||||
:icon-class="fullscreen ? 'exit-fullscreen' : 'fullscreen'"
|
||||
class-name="dialog__icon"
|
||||
@click="toggleFull"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<el-scrollbar
|
||||
:class="fullscreen && slots.footer
|
||||
? 'com-dialog__content--footer'
|
||||
: (fullscreen && !slots.footer
|
||||
? 'com-dialog__content--fullscreen'
|
||||
: 'com-dialog__content')"
|
||||
>
|
||||
<div class="content__wrap">
|
||||
<slot />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template v-if="slots.footer" #footer>
|
||||
<slot name="footer" />
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, nextTick, unref } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
export default defineComponent({
|
||||
name: 'Dialog',
|
||||
components: {
|
||||
SvgIcon
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示全屏按钮
|
||||
showFullscreen: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 是否可以拖拽
|
||||
draggable: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
const dialogRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const fullscreen = ref<boolean>(false)
|
||||
|
||||
const getBindValue = computed((): any => {
|
||||
const delArr: string[] = ['showFullscreen', 'draggable']
|
||||
const obj = { ...attrs, ...props }
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
function toggleFull(): void {
|
||||
fullscreen.value = !fullscreen.value
|
||||
// 全屏的时候需要重新定义left top
|
||||
if (fullscreen.value && props.draggable) {
|
||||
const dragDom = unref(dialogRef as any).$refs.dialogRef
|
||||
dragDom.style.cssText += `;left:0px;top:0px;`
|
||||
}
|
||||
}
|
||||
|
||||
function initDraggable() {
|
||||
nextTick(() => {
|
||||
const dragDom = unref(dialogRef as any).$refs.dialogRef
|
||||
const dialogHeaderEl = dragDom.querySelector('.el-dialog__header') as HTMLElement
|
||||
dragDom.style.cssText += ';top:0px;'
|
||||
dialogHeaderEl.style.cssText += ';cursor:move;user-select:none;'
|
||||
dialogHeaderEl.onmousedown = (e) => {
|
||||
const disX = e.clientX - dialogHeaderEl.offsetLeft
|
||||
const disY = e.clientY - dialogHeaderEl.offsetTop
|
||||
|
||||
const dragDomWidth = dragDom.offsetWidth
|
||||
const dragDomHeight = dragDom.offsetHeight
|
||||
|
||||
const screenWidth = document.body.clientWidth
|
||||
const screenHeight = document.body.clientHeight
|
||||
|
||||
const minDragDomLeft = dragDom.offsetLeft
|
||||
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
|
||||
|
||||
const minDragDomTop = dragDom.offsetTop
|
||||
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
|
||||
|
||||
const styleLeftStr = getComputedStyle(dragDom).left
|
||||
const styleTopStr = getComputedStyle(dragDom).top
|
||||
if (!styleLeftStr || !styleTopStr) return
|
||||
let styleLeft: number
|
||||
let styleTop: number
|
||||
|
||||
// Format may be "##%" or "##px"
|
||||
if (styleLeftStr.includes('%')) {
|
||||
styleLeft = +document.body.clientWidth * (+styleLeftStr.replace(/%/g, '') / 100)
|
||||
styleTop = +document.body.clientHeight * (+styleTopStr.replace(/%/g, '') / 100)
|
||||
} else {
|
||||
styleLeft = +styleLeftStr.replace(/px/g, '')
|
||||
styleTop = +styleTopStr.replace(/px/g, '')
|
||||
}
|
||||
|
||||
document.onmousemove = (e) => {
|
||||
let left = e.clientX - disX
|
||||
let top = e.clientY - disY
|
||||
|
||||
// Handle edge cases
|
||||
if (-(left) > minDragDomLeft) {
|
||||
left = -minDragDomLeft
|
||||
} else if (left > maxDragDomLeft) {
|
||||
left = maxDragDomLeft
|
||||
}
|
||||
if (-(top) > minDragDomTop) {
|
||||
top = -minDragDomTop
|
||||
} else if (top > maxDragDomTop) {
|
||||
top = maxDragDomTop
|
||||
}
|
||||
|
||||
// Move current element
|
||||
dragDom.style.cssText += `;left:${left + styleLeft}px;top:${top + styleTop}px;`
|
||||
}
|
||||
|
||||
document.onmouseup = () => {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (props.draggable) {
|
||||
initDraggable()
|
||||
}
|
||||
|
||||
return {
|
||||
dialogRef,
|
||||
fullscreen,
|
||||
getBindValue,
|
||||
slots,
|
||||
toggleFull,
|
||||
initDraggable
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dialog__icon {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: 45px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
.com-dialog__content {
|
||||
.content__wrap {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@{deep}(.el-scrollbar__wrap ) {
|
||||
max-height: 600px; // 最大高度
|
||||
overflow-x: hidden; // 隐藏横向滚动栏
|
||||
}
|
||||
}
|
||||
.com-dialog__content--fullscreen {
|
||||
@{deep}(.el-scrollbar__wrap) {
|
||||
height: calc(~"100vh - 46px - 60px"); // 最大高度
|
||||
}
|
||||
}
|
||||
.com-dialog__content--footer {
|
||||
@{deep}(.el-scrollbar__wrap) {
|
||||
max-height: calc(~"100vh - 46px - 60px - 66px"); // 最大高度
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="chartRef"
|
||||
:class="className"
|
||||
:style="{height: height, width: width}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onActivated, PropType, onMounted, onBeforeMount, unref, ref, watch, nextTick } from 'vue'
|
||||
import { debounce } from 'lodash-es'
|
||||
import type { EChartOption, ECharts } from 'echarts'
|
||||
import echarts from 'echarts'
|
||||
const tdTheme = require('./theme.json') // 引入默认主题
|
||||
echarts.registerTheme('tdTheme', tdTheme) // 覆盖默认主题
|
||||
export default defineComponent({
|
||||
name: 'Echarts',
|
||||
props: {
|
||||
className: {
|
||||
type: String as PropType<string>,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: String as PropType<string>,
|
||||
default: '200px'
|
||||
},
|
||||
options: {
|
||||
type: Object as PropType<EChartOption | undefined>,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const chartRef = ref<HTMLCanvasElement | null>(null)
|
||||
let chart: ECharts | null = null
|
||||
let sidebarElm: HTMLElement | any = null
|
||||
let __resizeHandler: Function | null = null
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
(options: EChartOption) => {
|
||||
nextTick(() => {
|
||||
if (chart) {
|
||||
chart.setOption(options, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 设置异步,不然图例一开始的宽度不正确。
|
||||
initChart()
|
||||
__resizeHandler = debounce(() => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}, 100);
|
||||
(window as any).addEventListener('resize', __resizeHandler)
|
||||
sidebarElm = document.getElementsByClassName('sidebar__wrap')[0]
|
||||
sidebarElm && sidebarElm.addEventListener('transitionend', sidebarResizeHandler)
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
// 防止keep-alive之后图表变形
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
(window as any).removeEventListener('resize', __resizeHandler)
|
||||
|
||||
sidebarElm && sidebarElm.removeEventListener('transitionend', sidebarResizeHandler)
|
||||
})
|
||||
|
||||
function initChart(): void {
|
||||
// 初始化echart
|
||||
const chartRefWrap = unref(chartRef)
|
||||
if (chartRefWrap) {
|
||||
chart = echarts.init(chartRefWrap, 'tdTheme')
|
||||
chart.setOption(props.options as EChartOption, true)
|
||||
}
|
||||
}
|
||||
|
||||
function sidebarResizeHandler(e: any): void {
|
||||
if (e.propertyName === 'width') {
|
||||
if (__resizeHandler) {
|
||||
__resizeHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chartRef
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,490 +0,0 @@
|
||||
{
|
||||
"color": [
|
||||
"#2d8cf0",
|
||||
"#19be6b",
|
||||
"#ff9900",
|
||||
"#E46CBB",
|
||||
"#9A66E4",
|
||||
"#ed3f14"
|
||||
],
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#516b91"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#93b7e3"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "2"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "2"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"color": "#edafda",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#d680bc",
|
||||
"borderColor0": "#8fd3e8",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": 1,
|
||||
"color": "#aaa"
|
||||
}
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true,
|
||||
"color": [
|
||||
"#2d8cf0",
|
||||
"#19be6b",
|
||||
"#f5ae4a",
|
||||
"#9189d5",
|
||||
"#56cae2",
|
||||
"#cbb0e3"
|
||||
],
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "rgba(165,231,240,1)",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "rgb(81,107,145)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "rgba(165,231,240,1)",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "rgb(81,107,145)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"normal": {
|
||||
"borderColor": "#999"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderColor": "#666"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#ccc",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#ccc",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"controlStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
}
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "rgba(138,124,168,0.37)"
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": [
|
||||
"#516b91",
|
||||
"#59c4e6",
|
||||
"#a5e7f0"
|
||||
]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"dataBackgroundColor": "rgba(255,255,255,0.3)",
|
||||
"fillerColor": "rgba(167,183,204,0.4)",
|
||||
"handleColor": "#a7b7cc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#eee"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div ref="editorRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onBeforeUnmount, unref, watch } from 'vue'
|
||||
import { editorProps } from './props'
|
||||
import E from 'wangeditor'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/monokai-sublime.css'
|
||||
export default defineComponent({
|
||||
name: 'Editor',
|
||||
props: editorProps,
|
||||
emits: ['change', 'focus', 'blur', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const editorRef = ref<HTMLElement | null>(null)
|
||||
const editor = ref<E | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
createdEditor()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editor.value) {
|
||||
editor.value.destroy()
|
||||
editor.value = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value: string) => {
|
||||
if (editor.value) {
|
||||
editor.value.txt.html(value)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function createdEditor(): void {
|
||||
editor.value = new E(unref(editorRef) as any)
|
||||
initConfig(editor.value)
|
||||
editor.value.create()
|
||||
editor.value.txt.html(props.value)
|
||||
}
|
||||
|
||||
function initConfig(editor: any): void {
|
||||
const {
|
||||
height,
|
||||
zIndex,
|
||||
placeholder,
|
||||
focus,
|
||||
customAlert,
|
||||
menus,
|
||||
colors,
|
||||
fontNames,
|
||||
lineHeights,
|
||||
showFullScreen,
|
||||
onchangeTimeout
|
||||
} = props.config
|
||||
|
||||
// 设置编辑区域高度为 500px
|
||||
editor.config.height = height
|
||||
|
||||
// 设置zIndex
|
||||
editor.config.zIndex = zIndex
|
||||
|
||||
// 设置 placeholder 提示文字
|
||||
editor.config.placeholder = placeholder
|
||||
|
||||
// 设置是否自动聚焦
|
||||
editor.config.focus = focus
|
||||
|
||||
// 配置菜单
|
||||
editor.config.menus = menus
|
||||
|
||||
// 配置颜色(文字颜色、背景色)
|
||||
editor.config.colors = colors
|
||||
|
||||
// 配置字体
|
||||
editor.config.fontNames = fontNames
|
||||
|
||||
// 配置行高
|
||||
editor.config.lineHeights = lineHeights
|
||||
|
||||
// 代码高亮
|
||||
editor.highlight = hljs
|
||||
|
||||
// 配置全屏
|
||||
editor.config.showFullScreen = showFullScreen
|
||||
|
||||
// 编辑器 customAlert 是对全局的alert做了统一处理,默认为 window.alert。
|
||||
// 如觉得浏览器自带的alert体验不佳,可自定义 alert,以便于达到与自身项目统一的alert效果。
|
||||
editor.config.customAlert = customAlert
|
||||
|
||||
// 图片上传默认使用base64
|
||||
editor.config.uploadImgShowBase64 = true
|
||||
|
||||
// 配置 onchange 回调函数
|
||||
editor.config.onchange = (html: string) => {
|
||||
const text = editor.txt.text()
|
||||
emitFun(editor, props.valueType === 'html' ? html : text, 'change')
|
||||
// emit('update:modelValue', props.valueType === 'html' ? html : text)
|
||||
}
|
||||
// 配置触发 onchange 的时间频率,默认为 200ms
|
||||
editor.config.onchangeTimeout = onchangeTimeout
|
||||
|
||||
// 编辑区域 focus(聚焦)和 blur(失焦)时触发的回调函数。
|
||||
editor.config.onblur = (html: string) => {
|
||||
emitFun(editor, html, 'blur')
|
||||
}
|
||||
editor.config.onfocus = (html: string) => {
|
||||
emitFun(editor, html, 'focus')
|
||||
}
|
||||
}
|
||||
|
||||
function emitFun(editor: any, html: string, type: 'change' | 'focus' | 'blur'): void {
|
||||
const text = editor.txt.text()
|
||||
emit(type, props.valueType === 'html' ? html : text)
|
||||
}
|
||||
|
||||
function getHtml() {
|
||||
if (editor.value) {
|
||||
return unref(editor.value as any).txt.html()
|
||||
}
|
||||
}
|
||||
|
||||
function getText() {
|
||||
if (editor.value) {
|
||||
return unref(editor.value as any).txt.text()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
editorRef,
|
||||
getHtml, getText
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,101 +0,0 @@
|
||||
import { PropType } from 'vue'
|
||||
import { Message } from '_c/Message'
|
||||
import { oneOf } from '@/utils'
|
||||
|
||||
import { Config } from './types'
|
||||
|
||||
export const editorProps = {
|
||||
// 基础配置
|
||||
config: {
|
||||
type: Object as PropType<Config>,
|
||||
default: () => {
|
||||
return {
|
||||
height: 500,
|
||||
zIndex: 0,
|
||||
placeholder: '请输入文本',
|
||||
focus: false,
|
||||
onchangeTimeout: 1000,
|
||||
customAlert: (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
|
||||
}
|
||||
},
|
||||
menus: [
|
||||
'head',
|
||||
'bold',
|
||||
'fontSize',
|
||||
'fontName',
|
||||
'italic',
|
||||
'underline',
|
||||
'strikeThrough',
|
||||
'indent',
|
||||
'lineHeight',
|
||||
'foreColor',
|
||||
'backColor',
|
||||
'link',
|
||||
'list',
|
||||
'justify',
|
||||
'quote',
|
||||
'emoticon',
|
||||
'image',
|
||||
'video',
|
||||
'table',
|
||||
'code',
|
||||
'splitLine',
|
||||
'undo',
|
||||
'redo'
|
||||
],
|
||||
colors: [
|
||||
'#000000',
|
||||
'#eeece0',
|
||||
'#1c487f',
|
||||
'#4d80bf'
|
||||
],
|
||||
fontNames: [
|
||||
'黑体',
|
||||
'仿宋',
|
||||
'楷体',
|
||||
'标楷体',
|
||||
'华文仿宋',
|
||||
'华文楷体',
|
||||
'宋体',
|
||||
'微软雅黑',
|
||||
'Arial',
|
||||
'Tahoma',
|
||||
'Verdana',
|
||||
'Times New Roman',
|
||||
'Courier New'
|
||||
],
|
||||
lineHeights: ['1', '1.15', '1.6', '2', '2.5', '3'],
|
||||
showFullScreen: true
|
||||
}
|
||||
}
|
||||
},
|
||||
// 绑定的值的类型, enum: ['html', 'text']
|
||||
valueType: {
|
||||
type: String as PropType<string>,
|
||||
default: 'html',
|
||||
validator: (val: string) => {
|
||||
return oneOf(val, ['html', 'text'])
|
||||
}
|
||||
},
|
||||
// 文本内容
|
||||
value: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export interface Config {
|
||||
height: number
|
||||
zIndex: number
|
||||
placeholder: string
|
||||
focus: boolean
|
||||
customAlert: () => any
|
||||
menus: string[]
|
||||
colors: string[]
|
||||
fontNames: string[]
|
||||
lineHeights: string[]
|
||||
showFullScreen: boolean
|
||||
onchangeTimeout: number
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" src="@/assets/img/404.png" alt="404">
|
||||
<img class="pic-404__child left" src="@/assets/img/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child mid" src="@/assets/img/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child right" src="@/assets/img/404_cloud.png" alt="404">
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">
|
||||
OOPS!
|
||||
</div>
|
||||
<div class="bullshit__headline">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="bullshit__info">
|
||||
请检查您输入的网址是否正确,请点击以下按钮返回主页
|
||||
</div>
|
||||
<router-link to="/">
|
||||
<a href="" class="bullshit__return-home">返回首页</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Page404',
|
||||
setup() {
|
||||
const message = ref<string>('网管说这个页面你不能进......')
|
||||
return {
|
||||
message
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.wscn-http404-container{
|
||||
transform: translate(-50%,-50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/components/HelloWorld.vue
Normal file
52
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<p>
|
||||
Recommended IDE setup:
|
||||
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
|
||||
</p>
|
||||
|
||||
<p>See <code>README.md</code> for more information.</p>
|
||||
|
||||
<p>
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank">
|
||||
Vite Docs
|
||||
</a>
|
||||
|
|
||||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
|
||||
</p>
|
||||
|
||||
<button type="button" @click="count++">count is: {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #eee;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
color: #304455;
|
||||
}
|
||||
</style>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script lang="ts">
|
||||
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)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div ref="wrapRef" class="markdown" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
onMounted,
|
||||
unref,
|
||||
PropType,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
watchEffect
|
||||
} from 'vue'
|
||||
import Vditor from 'vditor'
|
||||
import 'vditor/dist/index.css'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
height: {
|
||||
type: Number as PropType<number>,
|
||||
default: 500
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:value'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const wrapRef = ref<HTMLDivElement | null>(null)
|
||||
const vditorRef = ref<Vditor | null>(null)
|
||||
const initedRef = ref(false)
|
||||
|
||||
function init() {
|
||||
const wrapEl = unref(wrapRef)
|
||||
if (!wrapEl) return
|
||||
const data = { ...attrs, ...props }
|
||||
vditorRef.value = new Vditor(wrapEl, {
|
||||
mode: 'sv',
|
||||
preview: {
|
||||
actions: []
|
||||
},
|
||||
input: (v) => {
|
||||
emit('update:value', v)
|
||||
},
|
||||
...data,
|
||||
cache: {
|
||||
enable: false
|
||||
}
|
||||
})
|
||||
initedRef.value = true
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
nextTick(() => {
|
||||
const vditor = unref(vditorRef)
|
||||
if (unref(initedRef) && props.value && vditor) {
|
||||
vditor.setValue(props.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
init()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const vditorInstance = unref(vditorRef)
|
||||
if (!vditorInstance) return
|
||||
vditorInstance.destroy()
|
||||
})
|
||||
|
||||
return {
|
||||
wrapRef,
|
||||
getVditor: (): Vditor => vditorRef.value!
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,4 +0,0 @@
|
||||
import Vditor from 'vditor'
|
||||
export interface MarkDownActionType {
|
||||
getVditor: () => Vditor
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
let messageInstance: any | null = null
|
||||
|
||||
const resetMessage = (options: any) => {
|
||||
if (messageInstance) {
|
||||
messageInstance.close()
|
||||
}
|
||||
messageInstance = ElMessage(options)
|
||||
}
|
||||
['error', 'success', 'info', 'warning'].forEach((type: string) => {
|
||||
resetMessage[type] = (options: any) => {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
message: options
|
||||
}
|
||||
}
|
||||
options.type = type
|
||||
return resetMessage(options)
|
||||
}
|
||||
})
|
||||
|
||||
export const Message = resetMessage as any
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div class="more__item clearfix" :style="styleWrapObj">
|
||||
<p class="more__item--text" :style="styleTextObj">{{ content }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'More',
|
||||
props: {
|
||||
// 内容
|
||||
content: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
// 默认展示几行
|
||||
lineClamp: {
|
||||
type: Number as PropType<number>,
|
||||
default: 1
|
||||
},
|
||||
// style
|
||||
style: {
|
||||
type: Object as PropType<object>,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const styleWrapObj = computed(() => {
|
||||
return props.style
|
||||
})
|
||||
|
||||
const styleTextObj = computed(() => {
|
||||
if (props.lineClamp === 1) {
|
||||
// 默认展示一行
|
||||
return {
|
||||
'white-space': 'nowrap'
|
||||
}
|
||||
} else {
|
||||
// 展示多少行
|
||||
return {
|
||||
display: '-webkit-box',
|
||||
'-webkit-line-clamp': props.lineClamp,
|
||||
'-webkit-box-orient': 'vertical'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
styleWrapObj,
|
||||
styleTextObj
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.more__item {
|
||||
float: left;
|
||||
.more__item--text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
color: #545c63;
|
||||
transition: all .1s;
|
||||
text-align: left;
|
||||
&:hover {
|
||||
background: #fff;
|
||||
height: auto;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 16px 0 rgba(7,17,27,.2);
|
||||
-webkit-line-clamp: inherit !important;
|
||||
padding: 10px;
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.clearfix:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<keep-alive :include="getCaches">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</template>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import { useCache } from './useCache'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { getCaches } = useCache(false)
|
||||
|
||||
return {
|
||||
getCaches
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,54 +0,0 @@
|
||||
import { computed, ref, unref, ComponentInternalInstance, getCurrentInstance } from 'vue'
|
||||
|
||||
import { tagsViewStore, PAGE_LAYOUT_KEY } from '_@/store/modules/tagsView'
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
function tryTsxEmit<T extends any = ComponentInternalInstance>(
|
||||
fn: (_instance: T) => Promise<void> | void
|
||||
) {
|
||||
const instance = getCurrentInstance() as any
|
||||
instance && fn.call(null, instance)
|
||||
}
|
||||
|
||||
const ParentLayoutName = 'ParentLayout'
|
||||
export function useCache(isPage: boolean) {
|
||||
const name = ref('')
|
||||
const { currentRoute } = useRouter()
|
||||
|
||||
tryTsxEmit((instance) => {
|
||||
const routeName = instance.type.name
|
||||
if (routeName && ![ParentLayoutName].includes(routeName)) {
|
||||
name.value = routeName
|
||||
} else {
|
||||
const matched = currentRoute.value.matched
|
||||
const len = matched.length
|
||||
if (len < 2) return
|
||||
name.value = matched[len - 2].name as string
|
||||
}
|
||||
})
|
||||
|
||||
const getCaches = computed((): string[] => {
|
||||
const cached = tagsViewStore.cachedViews
|
||||
|
||||
if (isPage) {
|
||||
// page Layout
|
||||
// not parent layout
|
||||
return (cached as any).get(PAGE_LAYOUT_KEY) || []
|
||||
}
|
||||
const cacheSet = new Set<string>()
|
||||
cacheSet.add(unref(name))
|
||||
|
||||
const list = (cached as any).get(unref(name))
|
||||
|
||||
if (!list) {
|
||||
return Array.from(cacheSet)
|
||||
}
|
||||
(list as string[]).forEach((item: string) => {
|
||||
cacheSet.add(item)
|
||||
})
|
||||
|
||||
return Array.from(cacheSet)
|
||||
})
|
||||
return { getCaches }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import ImgPreview from './index.vue'
|
||||
import { isClient } from '@/utils/is'
|
||||
|
||||
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
|
||||
|
||||
instance = createVNode(ImgPreview, propsData)
|
||||
render(instance, container)
|
||||
document.body.appendChild(container)
|
||||
}
|
||||
@@ -1,438 +0,0 @@
|
||||
<template>
|
||||
<transition name="viewer-fade">
|
||||
<div
|
||||
v-if="show"
|
||||
ref="wrapElRef"
|
||||
tabindex="-1"
|
||||
:style="{ 'z-index': zIndex }"
|
||||
class="image-viewer__wrapper"
|
||||
>
|
||||
<div class="image-viewer__mask" />
|
||||
<!-- CLOSE -->
|
||||
<span class="image-viewer__btn image-viewer__close" @click="hide">
|
||||
<i class="el-icon-circle-close iconfont" />
|
||||
</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" />
|
||||
</span>
|
||||
<span
|
||||
class="image-viewer__btn image-viewer__next"
|
||||
:class="{ 'is-disabled': !infinite && isLast }"
|
||||
@click="next"
|
||||
>
|
||||
<i class="el-icon-arrow-right iconfont" />
|
||||
</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 lang="ts">
|
||||
import { defineComponent, ref, reactive, computed, watch, nextTick, unref } from 'vue'
|
||||
import { previewProps } from './props'
|
||||
import { isFirefox } from '@/utils/is'
|
||||
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'
|
||||
export default defineComponent({
|
||||
name: 'Preview',
|
||||
components: {
|
||||
SvgIcon
|
||||
},
|
||||
props: previewProps,
|
||||
setup(props) {
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
infinite, loading, show, index, transform,
|
||||
isSingle, isFirst, isLast, currentImg, imgStyle,
|
||||
imgRef, wrapElRef,
|
||||
hide, select,
|
||||
handleImgLoad, handleImgError, handleMouseDown,
|
||||
prev, next, toggleMode, handleActions
|
||||
}
|
||||
}
|
||||
})
|
||||
</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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.image-viewer__close {
|
||||
top: 40px;
|
||||
right: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.image-viewer__canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-viewer__actions {
|
||||
left: 50%;
|
||||
bottom: 30px;
|
||||
transform: translateX(-50%);
|
||||
width: 282px;
|
||||
height: 44px;
|
||||
padding: 0 23px;
|
||||
background-color: #606266;
|
||||
border-color: #fff;
|
||||
border-radius: 22px;
|
||||
|
||||
.image-viewer__actions__inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: justify;
|
||||
cursor: default;
|
||||
font-size: 23px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.image-viewer__prev {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
background-color: #606266;
|
||||
border-color: #fff;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.image-viewer__next {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
background-color: #606266;
|
||||
border-color: #fff;
|
||||
right: 40px;
|
||||
text-indent: 2px;
|
||||
}
|
||||
|
||||
.image-viewer__mask {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: .5;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.viewer-fade-enter-active {
|
||||
animation: viewer-fade-in .3s;
|
||||
}
|
||||
|
||||
.viewer-fade-leave-active {
|
||||
animation: viewer-fade-out .3s;
|
||||
}
|
||||
|
||||
@keyframes viewer-fade-in {
|
||||
0% {
|
||||
transform: translate3d(0, -20px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes viewer-fade-out {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(0, -20px, 0);
|
||||
opacity: 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,274 +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" />
|
||||
<div>{{ disabledText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, nextTick, ref, watch, computed, unref } from 'vue'
|
||||
import type { LogoTypes } from './types'
|
||||
import QRCode from 'qrcode'
|
||||
import type { QRCodeRenderersOptions } from 'qrcode'
|
||||
import { deepClone } from '@/utils'
|
||||
import { isString } from '@/utils/is'
|
||||
const { toCanvas, toDataURL } = QRCode
|
||||
export default defineComponent({
|
||||
name: 'Qrcode',
|
||||
props: {
|
||||
// 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: null
|
||||
},
|
||||
// 宽度
|
||||
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: '二维码已失效'
|
||||
}
|
||||
},
|
||||
emits: ['done', 'click', 'disabled-click'],
|
||||
setup(props, { emit }) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
wrapRef,
|
||||
renderText,
|
||||
wrapStyle,
|
||||
clickCode,
|
||||
disabledClick
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qrcode__wrap {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
.disabled__wrap {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255,255,255,0.95);
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
&>div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
i {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface LogoTypes {
|
||||
src?: string
|
||||
logoSize?: number
|
||||
bgColor?: string
|
||||
borderSize?: number
|
||||
crossOrigin?: string
|
||||
borderRadius?: number
|
||||
logoRadius?: number
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, unref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { currentRoute, replace } = useRouter()
|
||||
const { params, query } = unref(currentRoute)
|
||||
const { path } = params
|
||||
const _path = Array.isArray(path) ? path.join('/') : path
|
||||
replace({
|
||||
path: '/' + _path,
|
||||
query
|
||||
})
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,334 +0,0 @@
|
||||
<template>
|
||||
<div :class="{ search__col: layout === 'right' }">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="layout === 'right' ? 22 : 24">
|
||||
<el-form
|
||||
ref="ruleForm"
|
||||
inline
|
||||
:model="formInline"
|
||||
:rules="rules"
|
||||
:label-width="labelWidth"
|
||||
:label-position="labelPosition"
|
||||
:hide-required-asterisk="hideRequiredAsterisk"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item
|
||||
v-for="(item, $index) in data"
|
||||
:key="$index"
|
||||
:label="item.label"
|
||||
:prop="item.field"
|
||||
:rules="item.rules"
|
||||
>
|
||||
<template v-if="item.itemType === 'switch'">
|
||||
<el-switch
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.itemType === 'input'">
|
||||
<el-input
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.itemType === 'select'">
|
||||
<el-select
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
>
|
||||
<el-option
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
:value="item.optionValue ? v[item.optionValue] : v.value"
|
||||
:label="item.optionLabel ? v[item.optionLabel] : v.title"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template v-if="item.itemType === 'radio'">
|
||||
<el-radio-group
|
||||
v-model="formInline[item.field]"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
>
|
||||
<template v-if="item.radioType === 'radio'">
|
||||
<el-radio
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
:label="item.optionValue ? v[item.optionValue] : v.value"
|
||||
>
|
||||
{{ item.optionLabel ? v[item.optionLabel] : v.label }}
|
||||
</el-radio>
|
||||
</template>
|
||||
<template v-else-if="item.radioType === 'button'">
|
||||
<el-radio-button
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
:label="item.optionValue ? v[item.optionValue] : v.value"
|
||||
>
|
||||
{{ item.optionLabel ? v[item.optionLabel] : v.label }}
|
||||
</el-radio-button>
|
||||
</template>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
|
||||
<!-- element近期会新增treeSelect组件,所以不打算在自己维护一套。等待ing -->
|
||||
<!-- <template v-if="item.itemType === 'treeSelect'">
|
||||
<el-tree-select
|
||||
v-model:value="formInline[item.field]"
|
||||
:size="item.size"
|
||||
:dropdown-style="item.dropdownStyle"
|
||||
:tree-data="item.options"
|
||||
:placeholder="item.placeholder"
|
||||
:tree-checkable="item.treeCheckable"
|
||||
:max-tag-count="item.maxTagCount"
|
||||
:tree-default-expand-all="item.treeDefaultExpandAll"
|
||||
:allow-clear="item.allowClear"
|
||||
style="min-width: 201px;"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
>
|
||||
<template #title="{ title }">
|
||||
<span>{{ title }}</span>
|
||||
</template>
|
||||
</el-tree-select>
|
||||
</template> -->
|
||||
|
||||
<template v-if="item.itemType === 'timePicker'">
|
||||
<el-time-picker
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.itemType === 'timeSelect'">
|
||||
<el-time-select
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.itemType === 'datePicker' || item.itemType === 'dateTimePicker'">
|
||||
<el-date-picker
|
||||
v-model="formInline[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="data.length > 0 && layout === 'classic'">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-search"
|
||||
@click="submitForm"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showReset"
|
||||
icon="el-icon-refresh-right"
|
||||
@click="resetForm"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="layout === 'right' ? 2 : 24">
|
||||
<div
|
||||
v-if="data.length > 0 && (layout === 'bottom' || layout === 'right')"
|
||||
class="search__bottom"
|
||||
:class="{ 'search__bottom--col': layout === 'right' }"
|
||||
>
|
||||
<div class="search__bottom--button">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-search"
|
||||
@click="submitForm"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search__bottom--button">
|
||||
<el-button
|
||||
v-if="showReset"
|
||||
:style="{
|
||||
'margin-left': layout !== 'right' ? '15px' : '0',
|
||||
'margin-top': layout === 'right' ? '27px' : '0'
|
||||
}"
|
||||
icon="el-icon-refresh-right"
|
||||
@click="resetForm"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, watch, ref, unref } from 'vue'
|
||||
import { deepClone } from '@/utils'
|
||||
export default defineComponent({
|
||||
name: 'Search',
|
||||
props: {
|
||||
// 表单域标签的宽度,例如 '50px'。作为 Form 直接子元素的 form-item 会继承该值。支持 auto。
|
||||
labelWidth: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
labelPosition: {
|
||||
type: String as PropType<'right' | 'left' | 'top'>,
|
||||
default: 'right'
|
||||
},
|
||||
// 隐藏所有表单项的必选标记
|
||||
hideRequiredAsterisk: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
},
|
||||
// 表单数据对象
|
||||
data: {
|
||||
type: Object as PropType<{ [key: string]: any }>,
|
||||
default: () => {}
|
||||
},
|
||||
// 表单验证规则
|
||||
rules: {
|
||||
type: Object as PropType<{ [key: number]: any }>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示重置按钮
|
||||
showReset: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
},
|
||||
// 是否显示导出按钮
|
||||
showExport: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 风格
|
||||
layout: {
|
||||
type: String as PropType<'classic' | 'bottom' | 'right'>,
|
||||
default: 'classic'
|
||||
}
|
||||
},
|
||||
emits: ['search-submit', 'reset-submit', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const ruleForm = ref<HTMLElement | null>(null)
|
||||
const formInline = ref<{ [key: string]: any }>({})
|
||||
watch(
|
||||
() => props.data,
|
||||
(data) => {
|
||||
initForm(data)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function getItemBindValue(item: any) {
|
||||
const delArr: string[] = ['label', 'itemType', 'value', 'field']
|
||||
const obj = deepClone(item)
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
function initForm(data: any): void {
|
||||
for (const v of data) {
|
||||
formInline.value[v.field] = formInline.value[v.field] || v.value
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
const form = unref(ruleForm) as any
|
||||
if (!form) return
|
||||
try {
|
||||
form.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
emit('search-submit', unref(formInline))
|
||||
} else {
|
||||
console.log('error submit!!')
|
||||
return false
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function resetForm(): Promise<void> {
|
||||
const form = unref(ruleForm) as any
|
||||
if (!form) return
|
||||
await form.resetFields()
|
||||
emit('reset-submit', unref(formInline))
|
||||
}
|
||||
|
||||
function changeVal(val: any, item: any): void {
|
||||
if (item.onChange) {
|
||||
emit('change', {
|
||||
field: item.field,
|
||||
value: unref(formInline.value[item.field])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getItemBindValue,
|
||||
ruleForm,
|
||||
formInline,
|
||||
submitForm,
|
||||
resetForm,
|
||||
changeVal
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ant-form-inline {
|
||||
.ant-form-item {
|
||||
min-height: 60px;
|
||||
}
|
||||
.ant-form-item-with-help {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.search__bottom {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
.search__bottom--button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.search__bottom--col {
|
||||
padding-bottom: 0;
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
.search__bottom--button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.search__bottom--col::before {
|
||||
content: "";
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true" v-on="$attrs">
|
||||
<use :xlink:href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
iconClass: string,
|
||||
className: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
setup(props: Props) {
|
||||
const iconName = (computed((): string => `#icon-${props.iconClass}`))
|
||||
const svgClass = (computed((): string => {
|
||||
if (props.className) {
|
||||
return 'svg-icon ' + props.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
}))
|
||||
|
||||
return {
|
||||
iconName,
|
||||
svgClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, h, PropType } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Slot',
|
||||
props: {
|
||||
row: {
|
||||
type: Object as PropType<object>,
|
||||
default: () => null
|
||||
},
|
||||
index: {
|
||||
type: Number as PropType<number>,
|
||||
default: null
|
||||
},
|
||||
column: {
|
||||
type: Object as PropType<object>,
|
||||
default: () => null
|
||||
},
|
||||
slotName: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(props: any) {
|
||||
const _this: any = inject('tableRoot')
|
||||
return h('span', _this.slots[props.slotName]({
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
$index: props.index
|
||||
}))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<el-table-column v-bind="{...getItemBindValue(child)}" :prop="child.key">
|
||||
<template v-for="item in child.children">
|
||||
<!-- 树型数据 -->
|
||||
<template v-if="item.children && item.children.length">
|
||||
<table-column
|
||||
:key="item[item.field]"
|
||||
:child="item"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-table-column
|
||||
:key="item[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
:prop="item.field"
|
||||
>
|
||||
<!-- 表头插槽 -->
|
||||
<template v-if="item.slots && item.slots.header" #header="scope">
|
||||
<table-slot
|
||||
v-if="item.slots && item.slots.header"
|
||||
:slot-name="item.slots.header"
|
||||
:column="item"
|
||||
:index="scope.$index"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 表格内容插槽自定义 -->
|
||||
<template v-if="item.slots && item.slots.default" #default="scope">
|
||||
<table-slot
|
||||
:slot-name="item.slots.default"
|
||||
:row="scope.row"
|
||||
:column="item"
|
||||
:index="scope.$index"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import TableSlot from './Slot.vue'
|
||||
import { deepClone } from '@/utils'
|
||||
export default defineComponent({
|
||||
name: 'TableColumn',
|
||||
components: {
|
||||
TableSlot
|
||||
},
|
||||
props: {
|
||||
child: {
|
||||
type: Object as PropType<object>,
|
||||
default: () => null,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
function getItemBindValue(item: any) {
|
||||
const delArr: string[] = ['children']
|
||||
const obj = deepClone(item)
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
return {
|
||||
getItemBindValue
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table ref="elTable" :border="true" v-bind="getBindValue" @header-dragend="headerDragend">
|
||||
<!-- 多选 -->
|
||||
<el-table-column
|
||||
v-if="selection"
|
||||
type="selection"
|
||||
:reserve-selection="reserveSelection"
|
||||
width="40"
|
||||
/>
|
||||
<template v-for="item in columns">
|
||||
<!-- 自定义索引 -->
|
||||
<template v-if="item.type === 'index'">
|
||||
<el-table-column
|
||||
:key="item[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
type="index"
|
||||
:index="item.index"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 树型数据 -->
|
||||
<template v-else-if="item.children && item.children.length">
|
||||
<table-column
|
||||
:key="item[item.field]"
|
||||
:child="item"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-table-column
|
||||
:key="item[item.field]"
|
||||
v-bind="{...getItemBindValue(item)}"
|
||||
:prop="item.field"
|
||||
>
|
||||
<!-- 表头插槽 -->
|
||||
<template v-if="item.slots && item.slots.header" #header="scope">
|
||||
<table-slot
|
||||
v-if="item.slots && item.slots.header"
|
||||
:slot-name="item.slots.header"
|
||||
:column="item"
|
||||
:index="scope.$index"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 表格内容插槽自定义 -->
|
||||
<template v-if="item.slots && item.slots.default" #default="scope">
|
||||
<table-slot
|
||||
v-if="item.slots && item.slots.default"
|
||||
:slot-name="item.slots.default"
|
||||
:row="scope.row"
|
||||
:column="item"
|
||||
:index="scope.$index"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<div v-if="pagination" class="pagination__wrap">
|
||||
<el-pagination
|
||||
:style="paginationStyle"
|
||||
:page-sizes="[10, 20, 30, 40, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
v-bind="getPaginationBindValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, provide, getCurrentInstance, ref, unref } from 'vue'
|
||||
import { deepClone } from '@/utils'
|
||||
import { isObject } from '@/utils/is'
|
||||
import TableColumn from './components/TableColumn.vue'
|
||||
import TableSlot from './components/Slot.vue'
|
||||
export default defineComponent({
|
||||
name: 'ComTable',
|
||||
components: {
|
||||
TableSlot,
|
||||
TableColumn
|
||||
},
|
||||
props: {
|
||||
// 表头
|
||||
columns: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
},
|
||||
// 是否多选
|
||||
selection: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
// 是否展示分页
|
||||
pagination: {
|
||||
type: [Boolean, Object] as PropType<boolean | object>,
|
||||
default: false
|
||||
},
|
||||
// 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
|
||||
reserveSelection: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { attrs, slots }) {
|
||||
const elTable = ref<HTMLElement | null>(null)
|
||||
function getTableRef() {
|
||||
return unref(elTable as any)
|
||||
}
|
||||
|
||||
const _this = getCurrentInstance() as any
|
||||
provide('tableRoot', _this)
|
||||
|
||||
const getBindValue = computed((): any => {
|
||||
const bindValue = { ...attrs, ...props }
|
||||
delete bindValue.columns
|
||||
return bindValue
|
||||
})
|
||||
|
||||
function getItemBindValue(item: any) {
|
||||
const delArr: string[] = []
|
||||
const obj = deepClone(item)
|
||||
for (const key in obj) {
|
||||
if (delArr.indexOf(key) !== -1) {
|
||||
delete obj[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
const getPaginationBindValue = computed((): any => {
|
||||
const PaginationBindValue = props.pagination && isObject(props.pagination)
|
||||
? { ...(props.pagination as any) }
|
||||
: {}
|
||||
return PaginationBindValue
|
||||
})
|
||||
|
||||
const paginationStyle = computed(() => {
|
||||
return {
|
||||
textAlign: props.pagination && (props.pagination as any).position || 'right'
|
||||
}
|
||||
})
|
||||
|
||||
function headerDragend(newWidth: number, oldWidth: number, column: any) {
|
||||
// 不懂为啥无法自动计算宽度,只能手动去计算了。。失望ing,到时候看看能不能优化吧。
|
||||
const htmlArr = document.getElementsByClassName(column.id)
|
||||
for (const v of htmlArr) {
|
||||
if (v.firstElementChild) {
|
||||
(v.firstElementChild as any).style.width = newWidth + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
elTable,
|
||||
getBindValue, getItemBindValue,
|
||||
slots,
|
||||
getTableRef,
|
||||
getPaginationBindValue, paginationStyle,
|
||||
headerDragend
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.pagination__wrap {
|
||||
margin-top: 15px;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { App } from 'vue'
|
||||
import SvgIcon from './SvgIcon/index.vue'// svg组件
|
||||
import Dialog from './Dialog/index.vue'// Dialog组件
|
||||
import ComTable from './Table/index.vue'// Table组件
|
||||
import ComSearch from './Search/index.vue'// Search组件
|
||||
import ComDetail from './Detail/index.vue'// Detail组件
|
||||
|
||||
import '@/assets/icons' // 引入svg图标
|
||||
|
||||
export function setupGlobCom(app: App<Element>): void {
|
||||
app.component('ComDialog', Dialog)
|
||||
app.component('ComTable', ComTable)
|
||||
app.component('ComSearch', ComSearch)
|
||||
app.component('ComDetail', ComDetail)
|
||||
app.component('SvgIcon', SvgIcon)
|
||||
}
|
||||
Reference in New Issue
Block a user