init: v3 init

This commit is contained in:
陈凯龙
2021-12-07 14:36:07 +08:00
parent 2a7f3d2c46
commit 1ae75500de
206 changed files with 90 additions and 25740 deletions

View File

@@ -1,210 +0,0 @@
<template>
<div class="detail__wrap">
<div v-if="title" class="detail__wrap--header" @click="toggleClick">
<div class="detail__wrap--title">
<div>
{{ title }}
<el-tooltip v-if="message" :content="message" placement="right">
<i class="el-icon-warning-outline"></i>
</el-tooltip>
</div>
</div>
<i
v-if="collapsed"
:class="['el-icon-arrow-down', { 'el-icon-arrow-down-transform': !show }]"
></i>
</div>
<el-collapse-transition>
<div v-show="show" class="detail__content" :style="contentStyleObj">
<el-row>
<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 setup lang="ts" name="ComDetail">
import { PropType, ref, computed } from 'vue'
import { SchemaConfig } from './types'
const props = defineProps({
// 详情标题
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<IObj>,
default: () => {
return {}
},
required: true
},
// 布局展示的数据
schema: {
type: Array as PropType<SchemaConfig[]>,
default: () => [],
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'
},
classic: {
type: Boolean as PropType<boolean>,
default: false
}
})
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((): any => {
return {
width: props.vertical ? `100%` : props.labelWidth,
textAlign: props.labelAlign,
backgroundColor: props.border && !props.classic ? props.labelBg : '',
borderRight: props.border && !props.classic ? `1px solid ${props.borderColor}` : '',
borderBottom: props.border && !props.classic ? `1px solid ${props.borderColor}` : ''
}
})
const messageStyleObj = computed(() => {
return {
width: props.vertical ? `100%` : `calc(100% - ${props.labelWidth})`,
borderRight: props.border && !props.classic ? `1px solid ${props.borderColor}` : '',
borderBottom: props.border && !props.classic ? `1px solid ${props.borderColor}` : ''
}
})
function toggleClick() {
if (props.collapsed) {
show.value = !show.value
}
}
</script>
<style lang="less" scoped>
.detail__wrap {
padding: 10px;
background: #fff;
border-radius: 2px;
.detail__wrap--header {
display: flex;
height: 32px;
margin-bottom: 10px;
justify-content: space-between;
align-items: center;
// cursor: pointer;
.detail__wrap--title {
position: relative;
display: inline-block;
margin-left: 10px;
font-family: MicrosoftYaHei-Bold;
font-size: 18px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
&::after {
position: absolute;
top: 1px;
left: -10px;
width: 4px;
height: 90%;
background: var(--main-color);
content: '';
}
}
.el-icon-arrow-down {
transition: all 0.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 {
padding: 8px 16px;
font-size: 14px;
line-height: 20px;
color: #918e8d;
}
.content__item--message {
padding: 8px 16px;
flex: 1;
font-size: 14px;
line-height: 20px;
color: #606266;
overflow-wrap: break-word;
}
}
}
</style>

View File

@@ -1,5 +0,0 @@
export interface SchemaConfig {
field: string // 字段名
label?: string // label名
span?: number // 列的数量
}

View File

@@ -1,198 +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></slot>
</div>
</el-scrollbar>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="ComDialog">
import { ref, computed, PropType, nextTick, unref, useAttrs, useSlots } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
const slots = useSlots()
const props = defineProps({
title: {
type: String as PropType<string>,
default: ''
},
// 是否显示全屏按钮
showFullscreen: {
type: Boolean as PropType<boolean>,
default: true
},
// 是否可以拖拽
draggable: {
type: Boolean as PropType<boolean>,
default: true
}
})
const dialogRef = ref<HTMLElement | null>(null)
const fullscreen = ref<boolean>(false)
const getBindValue = computed((): any => {
const delArr: string[] = ['showFullscreen', 'draggable']
const attrs = useAttrs()
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()
}
</script>
<style lang="less" scoped>
.dialog__icon {
position: absolute;
top: 22px;
right: 45px;
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 - 70px'); // 最大高度
}
}
</style>

View File

@@ -1,343 +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 setup lang="ts" name="ComSearch">
import { PropType, watch, ref, unref } from 'vue'
import { deepClone } from '@/utils'
const props = defineProps({
// 表单域标签的宽度,例如 '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: () => {
return {}
}
},
// 是否显示重置按钮
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'
}
})
const emit = defineEmits(['search-submit', 'reset-submit', 'change'])
const ruleForm = ref<HTMLElement | null>(null)
const formInline = ref<IObj>({})
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: val
})
}
}
</script>
<style lang="less" scoped>
.ant-form-inline {
.ant-form-item {
min-height: 60px;
}
.ant-form-item-with-help {
margin-bottom: 0;
}
}
.search__bottom {
padding-bottom: 20px;
text-align: center;
.search__bottom--button {
display: inline-block;
}
}
.search__bottom--col {
position: relative;
padding-bottom: 0;
margin-top: 5px;
.search__bottom--button {
display: inline-block;
}
}
.search__bottom--col::before {
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
border-left: 1px solid #d9d9d9;
content: '';
}
</style>

View File

@@ -1,37 +0,0 @@
<script lang="ts">
import { defineComponent, inject, h, PropType } from 'vue'
export default defineComponent({
name: 'Slot',
props: {
row: {
type: Object as PropType<IObj>,
default: () => null
},
index: {
type: Number as PropType<number>,
default: null
},
column: {
type: Object as PropType<IObj>,
default: () => null
},
slotName: {
type: String as PropType<string>,
default: ''
}
},
render(props) {
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,64 +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 setup lang="ts" name="TableColumn">
import { PropType } from 'vue'
import TableSlot from './Slot.vue'
import { deepClone } from '@/utils'
defineProps({
child: {
type: Object as PropType<IObj>,
required: true
}
})
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
}
</script>
<style></style>

View File

@@ -1,159 +0,0 @@
<template>
<div>
<el-table ref="tableRef" :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 setup lang="ts" name="ComTable">
import { PropType, computed, useAttrs, ref, getCurrentInstance, provide } from 'vue'
import { deepClone } from '@/utils'
import { isObject } from '@/utils/validate'
import TableColumn from './components/TableColumn.vue'
import TableSlot from './components/Slot.vue'
const props = defineProps({
// 表头
columns: {
type: Array as PropType<IObj[]>,
default: () => []
},
// 是否多选
selection: {
type: Boolean as PropType<boolean>,
default: false
},
// 是否展示分页
pagination: {
type: [Boolean, Object] as PropType<boolean | IObj>,
default: false
},
// 仅对 type=selection 的列有效,类型为 Boolean为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key
reserveSelection: {
type: Boolean as PropType<boolean>,
default: false
}
})
const attrs = useAttrs()
const tableRef = ref<HTMLElement | null>(null)
function getTableRef() {
return tableRef.value as any
}
const _this = getCurrentInstance()
provide('tableRoot', _this)
const getBindValue = computed((): IObj => {
const bindValue = { ...attrs, ...props } as IObj
delete bindValue.columns
return bindValue
})
function getItemBindValue(item: IObj) {
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((): IObj => {
const PaginationBindValue =
props.pagination && isObject(props.pagination) ? { ...props.pagination } : {}
return PaginationBindValue
})
const paginationStyle = computed(() => {
return {
textAlign: (props.pagination && (props.pagination as IObj).position) || 'right'
}
})
function headerDragend(newWidth: number, _: number, column: IObj) {
// 不懂为啥无法自动计算宽度只能手动去计算了。。失望ing到时候看看能不能优化吧。
const htmlArr = document.getElementsByClassName(column.id)
for (const v of htmlArr as any) {
if (v.firstElementChild) {
;(v.firstElementChild as any).style.width = newWidth + 'px'
}
}
}
defineExpose({
getTableRef
})
</script>
<style lang="less" scoped>
.pagination__wrap {
padding: 10px;
margin-top: 15px;
background: #fff;
}
</style>

View File

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

View File

@@ -1,246 +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 setup lang="ts" name="404">
import { ref } from 'vue'
const message = ref<string>('网管说这个页面你不能进......')
</script>
<style lang="less" scoped>
.wscn-http404-container {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -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 {
@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;
}
}
position: absolute;
&.left {
top: 17px;
left: 220px;
width: 80px;
opacity: 0;
animation-delay: 1s;
animation-duration: 2s;
animation-name: cloudLeft;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
&.mid {
top: 10px;
left: 420px;
width: 46px;
opacity: 0;
animation-delay: 1.2s;
animation-duration: 2s;
animation-name: cloudMid;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
&.right {
top: 100px;
left: 500px;
width: 62px;
opacity: 0;
animation-delay: 1s;
animation-duration: 2s;
animation-name: cloudRight;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
}
}
.bullshit {
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
margin-bottom: 20px;
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
animation-duration: 0.5s;
animation-name: slideUp;
animation-fill-mode: forwards;
}
&__headline {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
line-height: 24px;
color: #222;
opacity: 0;
animation-delay: 0.1s;
animation-duration: 0.5s;
animation-name: slideUp;
animation-fill-mode: forwards;
}
&__info {
margin-bottom: 30px;
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
animation-delay: 0.2s;
animation-duration: 0.5s;
animation-name: slideUp;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
font-size: 14px;
line-height: 36px;
color: #fff;
text-align: center;
cursor: pointer;
background: #1482f0;
border-radius: 100px;
opacity: 0;
animation-delay: 0.3s;
animation-duration: 0.5s;
animation-name: slideUp;
animation-fill-mode: forwards;
}
}
}
</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,23 +0,0 @@
import { ElMessage } from 'element-plus'
let messageInstance: Nullable<any> = 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,16 +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 setup lang="ts" name="ParentView">
import { useCache } from './useCache'
const { getCaches } = useCache(false)
</script>

View File

@@ -1,55 +0,0 @@
import { computed, ref, unref, ComponentInternalInstance, getCurrentInstance } from 'vue'
import { useTagsViewStoreWithOut, PAGE_LAYOUT_KEY } from '@/store/modules/tags-view'
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 tagsViewStore = useTagsViewStoreWithOut()
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.getCachedViews
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,16 +0,0 @@
<template>
<div></div>
</template>
<script setup lang="ts">
import { unref } from 'vue'
import { useRouter } from 'vue-router'
const { currentRoute, replace } = useRouter()
const { params, query } = unref(currentRoute)
const { path } = params
const _path = Array.isArray(path) ? path.join('/') : path
replace({
path: '/' + _path,
query
})
</script>

View File

@@ -1,41 +0,0 @@
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$attrs">
<use :xlink:href="iconName" />
</svg>
</template>
<script setup lang="ts" name="SvgIcon">
import { PropType, computed } from 'vue'
const props = defineProps({
// svg文件名
iconClass: {
type: String as PropType<string>,
required: true
},
// 自定义类名
className: {
type: String as PropType<string>,
default: ''
}
})
const iconName = computed(() => `#icon-${props.iconClass}`)
const svgClass = computed(() => {
if (props.className) {
return `svg-icon ${props.className}`
} else {
return 'svg-icon'
}
})
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
overflow: hidden;
vertical-align: -0.15em;
fill: currentColor;
}
</style>

View File

@@ -1,14 +0,0 @@
import type { App } from 'vue'
import SvgIcon from './SvgIcon/index.vue' // svg组件
import ComSearch from './ComSearch/index.vue' // search组件
import ComDialog from './ComDialog/index.vue' // dialog组件
import ComDetail from './ComDetail/index.vue' // detail组件
import ComTable from './ComTable/index.vue' // table组件
export function setupGlobCom(app: App<Element>): void {
app.component('SvgIcon', SvgIcon)
app.component('ComSearch', ComSearch)
app.component('ComDialog', ComDialog)
app.component('ComDetail', ComDetail)
app.component('ComTable', ComTable)
}