wip: 重构中

This commit is contained in:
kailong321200875
2021-10-10 09:59:52 +08:00
parent 4c4903e806
commit 41ca05dce2
272 changed files with 1173 additions and 43458 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,215 +0,0 @@
<template>
<div 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
import Vditor from 'vditor'
export interface MarkDownActionType {
getVditor: () => Vditor
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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