feat: 🎸 初始化项目
This commit is contained in:
96
src/components/Breadcrumb/Breadcrumb.vue
Normal file
96
src/components/Breadcrumb/Breadcrumb.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div ref="breadcrumbRef" class="breadcrumb">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { defineComponent, provide, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Breadcrumb',
|
||||
props: {
|
||||
separator: {
|
||||
type: String as PropType<string>,
|
||||
default: '/'
|
||||
},
|
||||
separatorClass: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const breadcrumbRef = ref<HTMLElement | null>(null)
|
||||
|
||||
provide('breadcrumb', props)
|
||||
|
||||
return {
|
||||
breadcrumbRef
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="less">
|
||||
.breadcrumb {
|
||||
padding-right: 20px;
|
||||
font-size: 12px;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
display: table;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
margin: 0 9px;
|
||||
font-weight: 700;
|
||||
color: #6e90a7;
|
||||
|
||||
&[class*='icon'] {
|
||||
margin: 0 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: inline-block;
|
||||
color: #6e90a7;
|
||||
|
||||
a {
|
||||
font-weight: 700;
|
||||
color: #2c3a61;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
&.is-link:hover {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__item:last-child .breadcrumb__inner,
|
||||
&__item:last-child &__inner a,
|
||||
&__item:last-child &__inner a:hover,
|
||||
&__item:last-child &__inner:hover {
|
||||
font-weight: 400;
|
||||
color: #6e90a7;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&__item:last-child &__separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/components/Breadcrumb/BreadcrumbItem.vue
Normal file
51
src/components/Breadcrumb/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<span class="breadcrumb__item">
|
||||
<span ref="linkRef" :class="['breadcrumb__inner']">
|
||||
<slot />
|
||||
</span>
|
||||
<i v-if="separatorClass" class="breadcrumb__separator" :class="separatorClass" />
|
||||
<span v-else class="breadcrumb__separator">{{ separator }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, ref, onMounted, unref } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BreadcrumbItem',
|
||||
props: {
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | object>,
|
||||
default: ''
|
||||
},
|
||||
replace: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const linkRef = ref<HTMLElement | null>(null)
|
||||
const parent = inject('breadcrumb') as {
|
||||
separator: string
|
||||
separatorClass: string
|
||||
}
|
||||
const { push, replace } = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
const link = unref(linkRef)
|
||||
if (!link) return
|
||||
const { to } = props
|
||||
if (!props.to) return
|
||||
props.replace ? replace(to) : push(to)
|
||||
})
|
||||
|
||||
return {
|
||||
linkRef,
|
||||
separator: parent.separator && parent.separator,
|
||||
separatorClass: parent.separatorClass && parent.separatorClass
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
107
src/components/Breadcrumb/index.vue
Normal file
107
src/components/Breadcrumb/index.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<Breadcrumb class="app-breadcrumb">
|
||||
<transition-group name="breadcrumb">
|
||||
<BreadcrumbItem
|
||||
v-for="(item,index) in levelList"
|
||||
:key="item.path"
|
||||
>
|
||||
<svg-icon v-if="item.meta.icon" :icon-class="item.meta.icon" class="icon-breadcrumb" />
|
||||
<span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">
|
||||
{{ item.meta.title }}
|
||||
</span>
|
||||
<a v-else @click.prevent="handleLink(item)">
|
||||
{{ item.meta.title }}
|
||||
</a>
|
||||
</BreadcrumbItem>
|
||||
</transition-group>
|
||||
</Breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent, watch } from 'vue'
|
||||
import type { RouteRecordRaw, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { compile } from 'path-to-regexp'
|
||||
import Breadcrumb from './Breadcrumb.vue'
|
||||
import BreadcrumbItem from './BreadcrumbItem.vue'
|
||||
export default defineComponent({
|
||||
name: 'BreadcrumbWrap',
|
||||
components: {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem
|
||||
},
|
||||
setup() {
|
||||
const { currentRoute, push } = useRouter()
|
||||
|
||||
const levelList = ref<RouteRecordRaw[]>([])
|
||||
|
||||
function getBreadcrumb() {
|
||||
let matched: any[] = currentRoute.value.matched.filter((item: RouteLocationMatched) => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
|
||||
if (!isDashboard(first)) {
|
||||
matched = [{ path: '/dashboard', meta: { title: '首页', icon: 'dashboard' }}].concat(matched)
|
||||
}
|
||||
|
||||
levelList.value = matched.filter((item: RouteLocationMatched) => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
}
|
||||
|
||||
function isDashboard(route: RouteLocationMatched) {
|
||||
const name = route && route.name
|
||||
if (!name) {
|
||||
return false
|
||||
}
|
||||
return (name as any).trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||
}
|
||||
|
||||
function pathCompile(path: string): string {
|
||||
const { params } = currentRoute.value
|
||||
const toPath = compile(path)
|
||||
return toPath(params)
|
||||
}
|
||||
|
||||
function handleLink(item: RouteRecordRaw): void {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
push(redirect as string)
|
||||
return
|
||||
}
|
||||
push(pathCompile(path))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.path.startsWith('/redirect/')) {
|
||||
return
|
||||
}
|
||||
getBreadcrumb()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
levelList,
|
||||
handleLink
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
.icon-breadcrumb {
|
||||
color: #97a8be;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
src/components/Button/index.vue
Normal file
73
src/components/Button/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<Button v-bind="getBindValue" :class="[getColor, $attrs.class]">
|
||||
<template #default="data">
|
||||
<slot name="icon" />
|
||||
<slot v-bind="data" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { defineComponent, computed, VNodeChild } from 'vue'
|
||||
import { Button } from 'ant-design-vue'
|
||||
export default defineComponent({
|
||||
name: 'AButton',
|
||||
components: { Button },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
// 按钮类型
|
||||
type: {
|
||||
type: String as PropType<'primary' | 'default' | 'danger' | 'dashed' | 'link' | 'warning' | 'success' | 'info'>,
|
||||
default: 'default'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
htmlType: {
|
||||
type: String as PropType<'button' | 'submit' | 'reset' | 'menu'>,
|
||||
default: 'button'
|
||||
},
|
||||
icon: {
|
||||
type: Object as PropType<VNodeChild | JSX.Element>,
|
||||
default: () => null
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'small' | 'large' | 'default'>,
|
||||
default: 'default'
|
||||
},
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
ghost: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
block: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
const getColor = computed(() => {
|
||||
const res: string[] = []
|
||||
const { type, disabled } = props
|
||||
type && res.push(`ant-btn-${type}`)
|
||||
disabled && res.push('is-disabled')
|
||||
return res
|
||||
})
|
||||
|
||||
const getBindValue = computed((): any => {
|
||||
const otherTypes = ['warning', 'success', 'info']
|
||||
const bindValue = { ...attrs, ...props }
|
||||
if (otherTypes.indexOf(props.type) !== -1) {
|
||||
bindValue.type = 'default'
|
||||
}
|
||||
return bindValue
|
||||
})
|
||||
|
||||
return { getBindValue, getColor }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
160
src/components/CountTo/index.vue
Normal file
160
src/components/CountTo/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<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>
|
||||
62
src/components/CountTo/props.ts
Normal file
62
src/components/CountTo/props.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/components/Echart/index.vue
Normal file
109
src/components/Echart/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<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(() => {
|
||||
// 设置异步,不然图例一开始的宽度不正确。
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
}, 10)
|
||||
__resizeHandler = debounce(() => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}, 100);
|
||||
(window as any).addEventListener('resize', __resizeHandler)
|
||||
sidebarElm = document.getElementsByClassName('sidebar-container-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>
|
||||
490
src/components/Echart/theme.json
Normal file
490
src/components/Echart/theme.json
Normal file
@@ -0,0 +1,490 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/components/Editor/index.vue
Normal file
132
src/components/Editor/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<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:value'],
|
||||
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, html, 'change')
|
||||
emit('update:value', 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)
|
||||
}
|
||||
|
||||
return {
|
||||
editorRef
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
101
src/components/Editor/props.ts
Normal file
101
src/components/Editor/props.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { PropType } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { oneOf } from '@/utils'
|
||||
|
||||
import { Config } from './types'
|
||||
|
||||
export const editorProps = {
|
||||
// 基础配置
|
||||
config: {
|
||||
type: Object as PropType<Config>,
|
||||
default: () => {
|
||||
return {
|
||||
height: 500,
|
||||
zIndex: 500,
|
||||
placeholder: '请输入文本',
|
||||
focus: false,
|
||||
onchangeTimeout: 500,
|
||||
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: ''
|
||||
}
|
||||
}
|
||||
13
src/components/Editor/types.ts
Normal file
13
src/components/Editor/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
234
src/components/Error/404.vue
Normal file
234
src/components/Error/404.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<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>
|
||||
55
src/components/Hamburger/index.vue
Normal file
55
src/components/Hamburger/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="toggleCollapsed(!collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="toggleCollapsed(!collapsed)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
// import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
|
||||
export default defineComponent({
|
||||
name: 'Hamburger',
|
||||
// components: {
|
||||
// MenuUnfoldOutlined,
|
||||
// MenuFoldOutlined
|
||||
// },
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['toggleClick'],
|
||||
setup(props, { emit }) {
|
||||
function toggleCollapsed(collapsed: boolean) {
|
||||
emit('toggleClick', collapsed)
|
||||
}
|
||||
|
||||
return {
|
||||
toggleCollapsed
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.trigger {
|
||||
display: inline-block;
|
||||
transition: color 0.3s;
|
||||
height: @navbarHeight;
|
||||
line-height: @navbarHeight;
|
||||
}
|
||||
.trigger:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
243
src/components/Image/index.vue
Normal file
243
src/components/Image/index.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div ref="imageRef" class="image">
|
||||
<slot v-if="loading" name="placeholder">
|
||||
<div class="image__placeholder" />
|
||||
</slot>
|
||||
<slot v-else-if="error" name="error">
|
||||
<div class="image__error">加载失败</div>
|
||||
</slot>
|
||||
<img
|
||||
v-else
|
||||
v-bind="$attrs"
|
||||
:src="src"
|
||||
:style="imageStyle"
|
||||
:class="{ 'image__inner--center': alignCenter }"
|
||||
class="image__inner"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed, watch, onMounted, onBeforeUnmount, getCurrentInstance, unref } from 'vue'
|
||||
import { on, off, getScrollContainer, isInContainer } from '@/utils/dom-utils'
|
||||
import { isString, isElement } from '@/utils/is'
|
||||
import throttle from 'lodash-es/throttle'
|
||||
|
||||
const isSupportObjectFit = () => document.documentElement.style.objectFit !== undefined
|
||||
|
||||
const ObjectFit = {
|
||||
NONE: 'none',
|
||||
CONTAIN: 'contain',
|
||||
COVER: 'cover',
|
||||
FILL: 'fill',
|
||||
SCALE_DOWN: 'scale-down'
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Image',
|
||||
// inheritAttrs: false,
|
||||
props: {
|
||||
src: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
fit: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
scrollContainer: {
|
||||
type: Object as PropType<any>,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['error'],
|
||||
setup(props, { emit }) {
|
||||
const { ctx } = getCurrentInstance() as any
|
||||
|
||||
const imageRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const loading = ref<boolean>(true)
|
||||
const error = ref<boolean>(false)
|
||||
const show = ref<boolean>(!props.lazy)
|
||||
const imageWidth = ref<number>(0)
|
||||
const imageHeight = ref<number>(0)
|
||||
const imageStyle = computed((): any => {
|
||||
const { fit } = props
|
||||
// if (!isServer && fit) {
|
||||
if (fit) {
|
||||
return isSupportObjectFit()
|
||||
? { 'object-fit': fit }
|
||||
: getImageStyle(fit)
|
||||
}
|
||||
return {}
|
||||
})
|
||||
const alignCenter = computed((): boolean => {
|
||||
const { fit } = props
|
||||
// return !isServer && !isSupportObjectFit() && fit !== ObjectFit.FILL
|
||||
return !isSupportObjectFit() && fit !== ObjectFit.FILL
|
||||
})
|
||||
|
||||
let _scrollContainer: any = null
|
||||
let _lazyLoadHandler: any = null
|
||||
|
||||
watch(
|
||||
() => show.value,
|
||||
(show: boolean) => {
|
||||
show && loadImage()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
show.value && loadImage()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.lazy) {
|
||||
addLazyLoadListener()
|
||||
} else {
|
||||
loadImage()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
props.lazy && removeLazyLoadListener()
|
||||
})
|
||||
|
||||
function loadImage(): void {
|
||||
// reset status
|
||||
loading.value = true
|
||||
error.value = false
|
||||
|
||||
const img = new Image()
|
||||
img.onload = (e: any) => handleLoad(e, img)
|
||||
img.onerror = (e: any) => handleError(e)
|
||||
|
||||
// bind html attrs
|
||||
// so it can behave consistently
|
||||
Object.keys(ctx.$attrs)
|
||||
.forEach((key) => {
|
||||
const value = ctx.$attrs[key]
|
||||
img.setAttribute(key, value)
|
||||
})
|
||||
img.src = props.src
|
||||
}
|
||||
|
||||
function handleLoad(e: any, img: any): void {
|
||||
imageWidth.value = img.width
|
||||
imageHeight.value = img.height
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleError(e: any): void {
|
||||
loading.value = false
|
||||
error.value = true
|
||||
emit('error', e)
|
||||
}
|
||||
|
||||
function handleLazyLoad(): void {
|
||||
const imageRefWrap = unref(imageRef) as any
|
||||
if (isInContainer(imageRefWrap, _scrollContainer)) {
|
||||
show.value = true
|
||||
removeLazyLoadListener()
|
||||
}
|
||||
}
|
||||
|
||||
function addLazyLoadListener(): void {
|
||||
// if (isServer) return
|
||||
|
||||
const { scrollContainer } = props
|
||||
let __scrollContainer = null
|
||||
|
||||
if (isElement(scrollContainer)) {
|
||||
__scrollContainer = scrollContainer
|
||||
} else if (isString(scrollContainer)) {
|
||||
__scrollContainer = document.querySelector(scrollContainer as any)
|
||||
} else {
|
||||
const imageRefWrap = unref(imageRef) as any
|
||||
__scrollContainer = getScrollContainer(imageRefWrap)
|
||||
}
|
||||
if (__scrollContainer) {
|
||||
_scrollContainer = __scrollContainer
|
||||
_lazyLoadHandler = throttle(handleLazyLoad, 200)
|
||||
on(__scrollContainer, 'scroll', _lazyLoadHandler)
|
||||
handleLazyLoad()
|
||||
}
|
||||
}
|
||||
|
||||
function removeLazyLoadListener(): void {
|
||||
// if (isServer || !_scrollContainer || !_lazyLoadHandler) return
|
||||
if (!_scrollContainer || !_lazyLoadHandler) return
|
||||
|
||||
off(_scrollContainer, 'scroll', _lazyLoadHandler)
|
||||
_scrollContainer = null
|
||||
_lazyLoadHandler = null
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate object-fit behavior to compatible with IE11 and other browsers which not support object-fit
|
||||
*/
|
||||
function getImageStyle(fit: string): object {
|
||||
const imageRefWrap = unref(imageRef) as any
|
||||
const {
|
||||
clientWidth: containerWidth,
|
||||
clientHeight: containerHeight
|
||||
} = imageRefWrap
|
||||
|
||||
if (!imageWidth.value || !imageHeight.value || !containerWidth || !containerHeight) return {}
|
||||
|
||||
const vertical: boolean = imageWidth.value / imageHeight.value < 1
|
||||
|
||||
if (fit === ObjectFit.SCALE_DOWN) {
|
||||
const isSmaller: boolean = imageWidth.value < containerWidth && imageHeight.value < containerHeight
|
||||
fit = isSmaller ? ObjectFit.NONE : ObjectFit.CONTAIN
|
||||
}
|
||||
|
||||
switch (fit) {
|
||||
case ObjectFit.NONE:
|
||||
return { width: 'auto', height: 'auto' }
|
||||
case ObjectFit.CONTAIN:
|
||||
return vertical ? { width: 'auto' } : { height: 'auto' }
|
||||
case ObjectFit.COVER:
|
||||
return vertical ? { height: 'auto' } : { width: 'auto' }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
imageRef,
|
||||
loading, error, show,
|
||||
imageStyle, alignCenter
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.image {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
.image__placeholder {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.image__error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
src/components/Logo/index.vue
Normal file
77
src/components/Logo/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<router-link :class="['app-logo', 'app-logo-' + theme]" to="/">
|
||||
<img :src="require('@/assets/img/logo.png')">
|
||||
<div v-if="show" class="sidebar-title">{{ title }}</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, PropType } from 'vue'
|
||||
import config from '_p/index/config'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Logo',
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
required: true
|
||||
},
|
||||
theme: {
|
||||
type: String as PropType<'light' | 'dark'>,
|
||||
default: 'dark'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const show = ref<boolean>(true)
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(collapsed: boolean) => {
|
||||
if (!collapsed) {
|
||||
setTimeout(() => {
|
||||
show.value = !collapsed
|
||||
}, 400)
|
||||
} else {
|
||||
show.value = !collapsed
|
||||
}
|
||||
}
|
||||
)
|
||||
return {
|
||||
show,
|
||||
title: config.title
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 18px;
|
||||
cursor: pointer;
|
||||
height: @topSilderHeight;
|
||||
max-width: 200px;
|
||||
img {
|
||||
width: 37px;
|
||||
height: 37px;
|
||||
}
|
||||
.sidebar-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
transition: .5s;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
.app-logo-dark {
|
||||
background-color: @menuBg;
|
||||
.sidebar-title {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.app-logo-light {
|
||||
background-color: #fff;
|
||||
.sidebar-title {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/Markdown/index.vue
Normal file
81
src/components/Markdown/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
4
src/components/Markdown/types.ts
Normal file
4
src/components/Markdown/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Vditor from 'vditor'
|
||||
export interface MarkDownActionType {
|
||||
getVditor: () => Vditor
|
||||
}
|
||||
26
src/components/ParentView/index.vue
Normal file
26
src/components/ParentView/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<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>
|
||||
54
src/components/ParentView/useCache.ts
Normal file
54
src/components/ParentView/useCache.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { computed, ref, unref, ComponentInternalInstance, getCurrentInstance } from 'vue'
|
||||
|
||||
import { tagsViewStore, PAGE_LAYOUT_KEY } from '_p/index/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 }
|
||||
}
|
||||
26
src/components/Preview/functional.ts
Normal file
26
src/components/Preview/functional.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
442
src/components/Preview/index.vue
Normal file
442
src/components/Preview/index.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<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">
|
||||
<CloseCircleOutlined class="iconfont" />
|
||||
</span>
|
||||
<!-- ARROW -->
|
||||
<template v-if="!isSingle">
|
||||
<span
|
||||
class="image-viewer__btn image-viewer__prev"
|
||||
:class="{ 'is-disabled': !infinite && isFirst }"
|
||||
@click="prev"
|
||||
>
|
||||
<LeftOutlined class="iconfont" />
|
||||
</span>
|
||||
<span
|
||||
class="image-viewer__btn image-viewer__next"
|
||||
:class="{ 'is-disabled': !infinite && isLast }"
|
||||
@click="next"
|
||||
>
|
||||
<RightOutlined class="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'
|
||||
import { CloseCircleOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'
|
||||
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
|
||||
export default defineComponent({
|
||||
name: 'Preview',
|
||||
components: {
|
||||
SvgIcon,
|
||||
CloseCircleOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined
|
||||
},
|
||||
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>
|
||||
28
src/components/Preview/props.ts
Normal file
28
src/components/Preview/props.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
18
src/components/Preview/types.ts
Normal file
18
src/components/Preview/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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
|
||||
}
|
||||
21
src/components/Redirect/index.vue
Normal file
21
src/components/Redirect/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
60
src/components/Screenfull/index.vue
Normal file
60
src/components/Screenfull/index.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<a-tooltip placement="bottom">
|
||||
<template #title>
|
||||
<span>{{ isFullscreen ? '退出全屏' : '全屏' }}</span>
|
||||
</template>
|
||||
<div style="height: 100%;">
|
||||
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import screenfull from 'screenfull'
|
||||
// import { message } from 'ant-design-vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Screenfull',
|
||||
setup() {
|
||||
const isFullscreen = ref<boolean>(false)
|
||||
const sf = screenfull
|
||||
|
||||
function init(): void {
|
||||
if (sf.isEnabled) {
|
||||
sf.on('change', () => {
|
||||
isFullscreen.value = sf.isFullscreen
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function click(): void | Boolean {
|
||||
if (!sf.isEnabled) {
|
||||
// message.warning('you browser can not work')
|
||||
return false
|
||||
}
|
||||
sf.toggle()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
return {
|
||||
isFullscreen,
|
||||
click
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.screenfull-svg {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
fill: #5a5e66;;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: 10px;
|
||||
}
|
||||
</style>
|
||||
122
src/components/ScrollPane/index.vue
Normal file
122
src/components/ScrollPane/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<scrollbar
|
||||
ref="scrollContainer"
|
||||
:show-x="false"
|
||||
class="scroll-container"
|
||||
@wheel="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, unref, nextTick } from 'vue'
|
||||
import Scrollbar from '_c/Scrollbar/index.vue'
|
||||
import { useScrollTo } from '@/hooks/useScrollTo'
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScrollPane',
|
||||
components: {
|
||||
Scrollbar
|
||||
},
|
||||
setup() {
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
function handleScroll(e: any): void {
|
||||
const eventDelta: number = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper: any = (unref(scrollContainer) as any).$.wrap
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
}
|
||||
|
||||
function moveToTarget(currentTag: any) {
|
||||
const $container: any = (unref(scrollContainer) as any).$el
|
||||
const $containerWidth: number = $container.offsetWidth
|
||||
const $scrollWrapper: any = (unref(scrollContainer) as any).$.wrap
|
||||
const tagList = (unref(scrollContainer) as any).$parent.$parent.tagRefs
|
||||
|
||||
let firstTag: any = null
|
||||
let lastTag: any = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex: number = tagList.findIndex((item: any) => item === currentTag)
|
||||
const prevTag = tagList[currentIndex - 1]
|
||||
const nextTag = tagList[currentIndex + 1]
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
nextTick(() => {
|
||||
const { start } = useScrollTo({
|
||||
el: $scrollWrapper,
|
||||
position: 'scrollLeft',
|
||||
to: afterNextTagOffsetLeft - $containerWidth,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
})
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
nextTick(() => {
|
||||
const { start } = useScrollTo({
|
||||
el: $scrollWrapper,
|
||||
position: 'scrollLeft',
|
||||
to: beforePrevTagOffsetLeft,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveTo(to: number) {
|
||||
const $scrollWrapper: any = (unref(scrollContainer) as any).$.wrap
|
||||
nextTick(() => {
|
||||
const { start } = useScrollTo({
|
||||
el: $scrollWrapper,
|
||||
position: 'scrollLeft',
|
||||
to: $scrollWrapper.scrollLeft + to,
|
||||
duration: 500
|
||||
})
|
||||
start()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
handleScroll,
|
||||
scrollContainer,
|
||||
moveToTarget,
|
||||
moveTo
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
// .el-scrollbar__wrap {
|
||||
// height: 49px;
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
128
src/components/Scrollbar/Bar.vue
Normal file
128
src/components/Scrollbar/Bar.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div
|
||||
ref="elRef"
|
||||
:class="['scrollbar__bar', 'is-' + bar.key]"
|
||||
@mousedown="clickTrackHandler"
|
||||
>
|
||||
<div
|
||||
ref="thumbRef"
|
||||
class="scrollbar__thumb"
|
||||
:style="renderThumbStyle({ size, move, bar })"
|
||||
@mousedown="clickThumbHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { defineComponent, computed, unref, inject, Ref, reactive, ref, onBeforeUnmount } from 'vue'
|
||||
import { renderThumbStyle, BAR_MAP } from './util'
|
||||
import { on, off } from '@/utils/dom-utils'
|
||||
export default defineComponent({
|
||||
name: 'Bar',
|
||||
props: {
|
||||
vertical: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
move: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const thumbRef = ref<HTMLElement | null>(null)
|
||||
const elRef = ref<HTMLElement | null>(null)
|
||||
const commonState = reactive<any>({})
|
||||
const getBarRef = computed(() => {
|
||||
return BAR_MAP[props.vertical ? 'vertical' : 'horizontal']
|
||||
})
|
||||
const bar = unref(getBarRef)
|
||||
const parentElRef = inject('scroll-bar-wrap') as Ref<HTMLElement>
|
||||
|
||||
function clickThumbHandler(e: any) {
|
||||
const { ctrlKey, button, currentTarget } = e
|
||||
// prevent click event of right button
|
||||
if (ctrlKey || button === 2 || !currentTarget) {
|
||||
return
|
||||
}
|
||||
startDrag(e)
|
||||
const bar = unref(getBarRef)
|
||||
commonState[bar.axis] =
|
||||
currentTarget[bar.offset] -
|
||||
(e[bar.client as keyof typeof e] - currentTarget.getBoundingClientRect()[bar.direction])
|
||||
}
|
||||
|
||||
function clickTrackHandler(e: any) {
|
||||
const bar = unref(getBarRef)
|
||||
const offset = Math.abs(e.target.getBoundingClientRect()[bar.direction] - e[bar.client])
|
||||
const thumbEl = unref(thumbRef) as any
|
||||
const parentEl = unref(parentElRef) as any
|
||||
const el = unref(elRef) as any
|
||||
if (!thumbEl || !el || !parentEl) return
|
||||
const thumbHalf = thumbEl[bar.offset] / 2
|
||||
const thumbPositionPercentage = ((offset - thumbHalf) * 100) / el[bar.offset]
|
||||
parentEl[bar.scroll] = (thumbPositionPercentage * parentEl[bar.scrollSize]) / 100
|
||||
}
|
||||
|
||||
function startDrag(e: Event) {
|
||||
e.stopImmediatePropagation()
|
||||
commonState.cursorDown = true
|
||||
|
||||
on(document, 'mousemove', mouseMoveDocumentHandler)
|
||||
on(document, 'mouseup', mouseUpDocumentHandler)
|
||||
document.onselectstart = () => false
|
||||
}
|
||||
|
||||
function mouseMoveDocumentHandler(e: any) {
|
||||
if (commonState.cursorDown === false) return
|
||||
const bar = unref(getBarRef)
|
||||
const prevPage = commonState[bar.axis]
|
||||
const el = unref(elRef) as any
|
||||
const parentEl = unref(parentElRef) as any
|
||||
const thumbEl = unref(thumbRef) as any
|
||||
if (!prevPage || !el || !thumbEl || !parentEl) return
|
||||
const rect = el.getBoundingClientRect() as any
|
||||
const offset = (rect[bar.direction] - e[bar.client]) * -1
|
||||
const thumbClickPosition = thumbEl[bar.offset] - prevPage
|
||||
const thumbPositionPercentage = ((offset - thumbClickPosition) * 100) / el[bar.offset]
|
||||
|
||||
parentEl[bar.scroll] = (thumbPositionPercentage * parentEl[bar.scrollSize]) / 100
|
||||
}
|
||||
|
||||
function mouseUpDocumentHandler() {
|
||||
const bar = unref(getBarRef)
|
||||
commonState.cursorDown = false
|
||||
commonState[bar.axis] = 0
|
||||
off(document, 'mousemove', mouseMoveDocumentHandler)
|
||||
document.onselectstart = null
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
off(document, 'mouseup', mouseUpDocumentHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
thumbRef,
|
||||
elRef,
|
||||
bar,
|
||||
clickThumbHandler,
|
||||
clickTrackHandler,
|
||||
renderThumbStyle
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.scrollbar__bar.is-vertical>div {
|
||||
width: 100%;
|
||||
}
|
||||
.scrollbar__bar.is-horizontal>div {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
254
src/components/Scrollbar/index.vue
Normal file
254
src/components/Scrollbar/index.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="scrollbar">
|
||||
<template v-if="!native">
|
||||
<div
|
||||
ref="wrapElRef"
|
||||
:style="style"
|
||||
:class="[wrapClass, 'scrollbar__wrap', gutter ? '' : 'scrollbar__wrap--hidden-default']"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
ref="resizeRef"
|
||||
:class="['scrollbar__view', viewClass]"
|
||||
:style="viewStyle"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<bar v-if="showX" :move="state.moveX" :size="state.sizeWidth" />
|
||||
<bar v-if="showY" vertical :move="state.moveY" :size="state.sizeHeight" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
ref="wrap"
|
||||
:class="[wrapClass, 'scrollbar__wrap']"
|
||||
:style="style"
|
||||
>
|
||||
<div
|
||||
ref="resizeRef"
|
||||
:class="['scrollbar__view', viewClass]"
|
||||
:style="viewStyle"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
unref,
|
||||
reactive,
|
||||
ref,
|
||||
toRef,
|
||||
provide,
|
||||
onMounted,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
getCurrentInstance
|
||||
} from 'vue'
|
||||
import { addResizeListener, removeResizeListener } from '@/utils/event/resize-event'
|
||||
import scrollbarWidth from '@/utils/scrollbar-width'
|
||||
import { isString } from '@/utils/is'
|
||||
import { toObject } from './util'
|
||||
import Bar from './Bar.vue'
|
||||
export default defineComponent({
|
||||
name: 'Scrollbar',
|
||||
components: {
|
||||
Bar
|
||||
},
|
||||
props: {
|
||||
native: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
wrapStyle: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => null
|
||||
},
|
||||
wrapClass: {
|
||||
type: String as PropType<string>, required: false,
|
||||
default: ''
|
||||
},
|
||||
viewClass: {
|
||||
type: String as PropType<string>,
|
||||
default: ''
|
||||
},
|
||||
viewStyle: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => {}
|
||||
},
|
||||
noresize: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false
|
||||
},
|
||||
showX: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
},
|
||||
showY: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true
|
||||
}
|
||||
// tag: {
|
||||
// type: String as PropType<string>,
|
||||
// default: 'div'
|
||||
// }
|
||||
},
|
||||
setup(props) {
|
||||
const resizeRef = ref<HTMLElement | null>(null)
|
||||
const wrapElRef = ref<HTMLElement | null>(null)
|
||||
provide('scroll-bar-wrap', wrapElRef)
|
||||
const state = reactive({
|
||||
sizeWidth: '0',
|
||||
sizeHeight: '0',
|
||||
moveX: 0,
|
||||
moveY: 0
|
||||
})
|
||||
let style: any = toRef(props, 'wrapStyle')
|
||||
const gutter = scrollbarWidth()
|
||||
if (gutter) {
|
||||
const gutterWith = `-${gutter}px`
|
||||
const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith};`
|
||||
|
||||
if (Array.isArray(props.wrapStyle)) {
|
||||
style = toObject(props.wrapStyle)
|
||||
style.value.marginRight = style.value.marginBottom = gutterWith
|
||||
} else if (isString(props.wrapStyle)) {
|
||||
style.value += gutterStyle
|
||||
} else {
|
||||
style = gutterStyle
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const warpEl = unref(wrapElRef)
|
||||
if (!warpEl) return
|
||||
const { scrollTop, scrollLeft, clientHeight, clientWidth } = warpEl
|
||||
|
||||
state.moveY = (scrollTop * 100) / clientHeight
|
||||
state.moveX = (scrollLeft * 100) / clientWidth
|
||||
}
|
||||
function update() {
|
||||
const warpEl = unref(wrapElRef)
|
||||
if (!warpEl) return
|
||||
const { scrollHeight, scrollWidth, clientHeight, clientWidth } = warpEl
|
||||
const heightPercentage = (clientHeight * 100) / scrollHeight
|
||||
const widthPercentage = (clientWidth * 100) / scrollWidth
|
||||
|
||||
state.sizeHeight = heightPercentage < 100 ? heightPercentage + '%' : ''
|
||||
state.sizeWidth = widthPercentage < 100 ? widthPercentage + '%' : ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const instance = getCurrentInstance() as any
|
||||
if (instance) {
|
||||
instance.wrap = unref(wrapElRef)
|
||||
}
|
||||
|
||||
const { native, noresize } = props
|
||||
const resizeEl = unref(resizeRef)
|
||||
const warpEl = unref(wrapElRef)
|
||||
if (native || !resizeEl || !warpEl) return
|
||||
nextTick(update)
|
||||
if (!noresize) {
|
||||
addResizeListener(resizeEl, update)
|
||||
addResizeListener(warpEl, update)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
const { native, noresize } = props
|
||||
const resizeEl = unref(resizeRef)
|
||||
const warpEl = unref(wrapElRef)
|
||||
if (native || !resizeEl || !warpEl) return
|
||||
if (!noresize) {
|
||||
removeResizeListener(resizeEl, update)
|
||||
removeResizeListener(warpEl, update)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
resizeRef, wrapElRef,
|
||||
state, gutter, style,
|
||||
handleScroll
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.scrollbar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&__wrap {
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
&--hidden-default {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@{deep}(&__thumb) {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
cursor: pointer;
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
border-radius: inherit;
|
||||
transition: 0.3s background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(144, 147, 153, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 120ms ease-out;
|
||||
transition: opacity 120ms ease-out;
|
||||
|
||||
&.is-vertical {
|
||||
top: 2px;
|
||||
width: 6px;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-horizontal {
|
||||
left: 2px;
|
||||
height: 6px;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar:active > .scrollbar__bar,
|
||||
.scrollbar:focus > .scrollbar__bar,
|
||||
.scrollbar:hover > .scrollbar__bar {
|
||||
opacity: 1;
|
||||
transition: opacity 280ms ease-out;
|
||||
}
|
||||
</style>
|
||||
14
src/components/Scrollbar/types.ts
Normal file
14
src/components/Scrollbar/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface BarMapItem {
|
||||
offset: string
|
||||
scroll: string
|
||||
scrollSize: string
|
||||
size: string
|
||||
key: string
|
||||
axis: string
|
||||
client: string
|
||||
direction: string
|
||||
}
|
||||
export interface BarMap {
|
||||
vertical: BarMapItem
|
||||
horizontal: BarMapItem
|
||||
}
|
||||
49
src/components/Scrollbar/util.ts
Normal file
49
src/components/Scrollbar/util.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { BarMap } from './types'
|
||||
export const BAR_MAP: BarMap = {
|
||||
vertical: {
|
||||
offset: 'offsetHeight',
|
||||
scroll: 'scrollTop',
|
||||
scrollSize: 'scrollHeight',
|
||||
size: 'height',
|
||||
key: 'vertical',
|
||||
axis: 'Y',
|
||||
client: 'clientY',
|
||||
direction: 'top'
|
||||
},
|
||||
horizontal: {
|
||||
offset: 'offsetWidth',
|
||||
scroll: 'scrollLeft',
|
||||
scrollSize: 'scrollWidth',
|
||||
size: 'width',
|
||||
key: 'horizontal',
|
||||
axis: 'X',
|
||||
client: 'clientX',
|
||||
direction: 'left'
|
||||
}
|
||||
}
|
||||
|
||||
export function renderThumbStyle({ move, size, bar }: any) {
|
||||
const style = {} as any
|
||||
const translate = `translate${bar.axis}(${move}%)`
|
||||
|
||||
style[bar.size] = size
|
||||
style.transform = translate
|
||||
style.msTransform = translate
|
||||
style.webkitTransform = translate
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
function extend<T, K>(to: T, _from: K): T & K {
|
||||
return Object.assign(to, _from)
|
||||
}
|
||||
|
||||
export function toObject<T>(arr: Array<T>): Record<string, T> {
|
||||
const res = {}
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i]) {
|
||||
extend(res, arr[i])
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
387
src/components/Search/index.vue
Normal file
387
src/components/Search/index.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div :class="{ search__col: layout === 'right' }">
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="layout === 'right' ? 22 : 24">
|
||||
<a-form
|
||||
ref="ruleForm"
|
||||
layout="inline"
|
||||
:model="formInline"
|
||||
:rules="rules"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
:label-align="labelAlign"
|
||||
:hide-required-mark="hideRequiredMark"
|
||||
@submit.prevent
|
||||
>
|
||||
<a-form-item
|
||||
v-for="(item, $index) in data"
|
||||
:key="$index"
|
||||
:label="item.label"
|
||||
:name="item.field"
|
||||
:rules="item.rules"
|
||||
>
|
||||
<template v-if="item.type === 'switch'">
|
||||
<a-switch
|
||||
v-model:checked="formInline[item.field]"
|
||||
:size="item.size"
|
||||
:checked-children="item.checkedChildren"
|
||||
:un-checked-children="item.unCheckedChildren"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'input'">
|
||||
<a-input
|
||||
v-model:value="formInline[item.field]"
|
||||
:size="item.size"
|
||||
:maxlength="item.maxlength"
|
||||
:placeholder="item.placeholder"
|
||||
:addon-before="item.addonBefore"
|
||||
:addon-after="item.addonAfter"
|
||||
:allow-clear="item.allowClear"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'select'">
|
||||
<a-select
|
||||
v-model:value="formInline[item.field]"
|
||||
:size="item.size"
|
||||
:placeholder="item.placeholder"
|
||||
:allow-clear="item.allowClear"
|
||||
style="min-width: 201px;"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
:value="item.optionValue ? v[item.optionValue] : v.value"
|
||||
>
|
||||
{{ item.optionLabel ? v[item.optionLabel] : v.title }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'radio'">
|
||||
<a-radio-group
|
||||
v-model:value="formInline[item.field]"
|
||||
:size="item.size"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
>
|
||||
<template v-if="item.radioType === 'radio'">
|
||||
<a-radio
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
:value="item.optionValue ? v[item.optionValue] : v.value"
|
||||
>
|
||||
{{ item.optionLabel ? v[item.optionLabel] : v.label }}
|
||||
</a-radio>
|
||||
</template>
|
||||
<template v-else-if="item.radioType === 'button'">
|
||||
<a-radio-button
|
||||
v-for="v in item.options"
|
||||
:key="item.optionValue ? v[item.optionValue] : v.value"
|
||||
:value="item.optionValue ? v[item.optionValue] : v.value"
|
||||
>
|
||||
{{ item.optionLabel ? v[item.optionLabel] : v.label }}
|
||||
</a-radio-button>
|
||||
</template>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'treeSelect'">
|
||||
<a-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>
|
||||
</a-tree-select>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'datePicker'">
|
||||
<a-date-picker
|
||||
v-model:value="formInline[item.field]"
|
||||
:format="item.format"
|
||||
:mode="item.mode"
|
||||
:value-format="item.valueFormat"
|
||||
:placeholder="item.placeholder"
|
||||
:size="item.size"
|
||||
:allow-clear="item.allowClear"
|
||||
:disabled-date="item.disabledDate"
|
||||
:disabled-time="item.disabledDateTime"
|
||||
:show-time="item.showTime"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'monthPicker'">
|
||||
<a-month-picker
|
||||
v-model:value="formInline[item.field]"
|
||||
:format="item.format"
|
||||
:mode="item.mode"
|
||||
:value-format="item.valueFormat"
|
||||
:placeholder="item.placeholder"
|
||||
:size="item.size"
|
||||
:allow-clear="item.allowClear"
|
||||
:disabled-date="item.disabledDate"
|
||||
:disabled-time="item.disabledDateTime"
|
||||
:show-time="item.showTime"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'rangePicker'">
|
||||
<a-range-picker
|
||||
v-model:value="formInline[item.field]"
|
||||
:format="item.format"
|
||||
:mode="item.mode"
|
||||
:value-format="item.valueFormat"
|
||||
:placeholder="item.placeholder"
|
||||
:size="item.size"
|
||||
:allow-clear="item.allowClear"
|
||||
:disabled-date="item.disabledDate"
|
||||
:disabled-time="item.disabledDateTime"
|
||||
:show-time="item.showTime"
|
||||
:ranges="item.ranges"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'weekPicker'">
|
||||
<a-week-picker
|
||||
v-model:value="formInline[item.field]"
|
||||
:format="item.format"
|
||||
:mode="item.mode"
|
||||
:value-format="item.valueFormat"
|
||||
:placeholder="item.placeholder"
|
||||
:size="item.size"
|
||||
:allow-clear="item.allowClear"
|
||||
:disabled-date="item.disabledDate"
|
||||
:disabled-time="item.disabledDateTime"
|
||||
:show-time="item.showTime"
|
||||
@change="((val) => {changeVal(val, item)})"
|
||||
/>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="data.length > 0 && layout === 'classic'">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="showReset"
|
||||
@click="resetForm"
|
||||
>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
<a-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">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="submitForm"
|
||||
>
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
查询
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="search__bottom--button">
|
||||
<a-button
|
||||
v-if="showReset"
|
||||
:style="{
|
||||
'margin-left': layout !== 'right' ? '15px' : '0',
|
||||
'margin-top': layout === 'right' ? '27px' : '0'
|
||||
}"
|
||||
@click="resetForm"
|
||||
>
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, watch, ref, unref } from 'vue'
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
export default defineComponent({
|
||||
name: 'Search',
|
||||
components: {
|
||||
SearchOutlined,
|
||||
ReloadOutlined
|
||||
},
|
||||
props: {
|
||||
// label 标签布局,同 <Col> 组件,设置 span offset 值,如 {span: 3, offset: 12} 或 sm: {span: 3, offset: 12}
|
||||
labelCol: {
|
||||
type: Object as PropType<{ span: number }>,
|
||||
default: () => {}
|
||||
},
|
||||
// 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol
|
||||
wrapperCol: {
|
||||
type: Object as PropType<{ span: number }>,
|
||||
default: () => {}
|
||||
},
|
||||
// label 标签的文本对齐方式
|
||||
labelAlign: {
|
||||
type: String as PropType<'left' | 'right'>,
|
||||
default: 'right'
|
||||
},
|
||||
// 隐藏所有表单项的必选标记
|
||||
hideRequiredMark: {
|
||||
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 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 {
|
||||
const data = await form.validate()
|
||||
if (data) {
|
||||
emit('search-submit', data)
|
||||
}
|
||||
} 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', formInline.value)
|
||||
}
|
||||
|
||||
function changeVal(val: any, item: any): void {
|
||||
if (item.onChange) {
|
||||
emit('change', {
|
||||
field: item.field,
|
||||
value: formInline.value[item.field]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
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>
|
||||
13
src/components/Sider/SiderItem.vue
Normal file
13
src/components/Sider/SiderItem.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'SilderItem'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
70
src/components/Sider/index.vue
Normal file
70
src/components/Sider/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div :class="{'has-logo':show_logo}">
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="collapsed"
|
||||
:unique-opened="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sider-item
|
||||
v-for="route in routers"
|
||||
:key="route.path"
|
||||
:item="route"
|
||||
:base-path="route.path"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { permissionStore } from '_p/index/store/modules/permission'
|
||||
import { appStore } from '_p/index/store/modules/app'
|
||||
import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import SiderItem from './SiderItem.vue'
|
||||
// import variables from '@/styles/variables.less'
|
||||
import config from '_p/index/config'
|
||||
const { show_logo } = config
|
||||
|
||||
export default defineComponent({
|
||||
components: { SiderItem },
|
||||
setup() {
|
||||
const { currentRoute, push } = useRouter()
|
||||
const routers = computed((): RouteRecordRaw[] => {
|
||||
return permissionStore.routers
|
||||
})
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = currentRoute.value
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
})
|
||||
const collapsed = computed(() => appStore.collapsed)
|
||||
|
||||
return {
|
||||
routers,
|
||||
activeMenu,
|
||||
collapsed,
|
||||
show_logo
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sidebar-container {
|
||||
height: 100%;
|
||||
}
|
||||
.has-logo {
|
||||
height: calc(~"100% - @{topSilderHeight}");
|
||||
}
|
||||
.menu-wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
53
src/components/SvgIcon/index.vue
Normal file
53
src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
94
src/components/Table/Table.tsx
Normal file
94
src/components/Table/Table.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { defineComponent, PropType, computed } from 'vue'
|
||||
import { Table } from 'ant-design-vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComTable',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup(props, { attrs, slots }) {
|
||||
const getBindValue = computed((): any => {
|
||||
const bindValue = { ...attrs, ...props }
|
||||
delete bindValue.columns
|
||||
return bindValue
|
||||
})
|
||||
|
||||
function renderTabelItem(columns: any[]) {
|
||||
return columns.map((v: any) => {
|
||||
const vSlots: any = v.slots || {}
|
||||
if (v.children) {
|
||||
const slotData = {
|
||||
title: () => vSlots.title && slots[vSlots.title] && slots[vSlots.title]!(),
|
||||
default: () => {renderTabelItem(v.children)}
|
||||
}
|
||||
if (!vSlots.title) {
|
||||
delete slotData.title
|
||||
}
|
||||
return (
|
||||
<Table.ColumnGroup
|
||||
v-slots={{...slotData}}
|
||||
>
|
||||
</Table.ColumnGroup>
|
||||
)
|
||||
} else {
|
||||
const slotData = {
|
||||
title: () => vSlots.title && slots[vSlots.title] && slots[vSlots.title]!(),
|
||||
default: ({ text, record, index, column }: any) => {
|
||||
if (vSlots.customRender) {
|
||||
return slots[vSlots.customRender] && slots[vSlots.customRender]!({ text, record, index, column })
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
},
|
||||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, column }: any) => vSlots.filterDropdown && slots[vSlots.filterDropdown] && slots[vSlots.filterDropdown]!({ setSelectedKeys, selectedKeys, confirm, clearFilters, column }),
|
||||
filterIcon: (filtered: any) => vSlots.filterIcon && slots[vSlots.filterIcon] && slots[vSlots.filterIcon]!(filtered)
|
||||
}
|
||||
if (!vSlots.title) {
|
||||
delete slotData.title
|
||||
}
|
||||
if (!vSlots.filterDropdown) {
|
||||
delete slotData.filterDropdown
|
||||
}
|
||||
if (!vSlots.filterIcon) {
|
||||
delete slotData.filterIcon
|
||||
}
|
||||
return (
|
||||
<Table.Column
|
||||
{...v}
|
||||
v-slots={{...slotData}}
|
||||
>
|
||||
</Table.Column>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
const tableSlot = {
|
||||
title: (currentPageData: any) => slots.title && slots.title(currentPageData),
|
||||
footer: (currentPageData: any) => slots.footer && slots.footer(currentPageData),
|
||||
expandedRowRender: ({ record, index, indent, expanded }: any) => slots.expandedRowRender && slots.expandedRowRender({ record, index, indent, expanded }),
|
||||
}
|
||||
if (!slots.title) {
|
||||
delete tableSlot.title
|
||||
}
|
||||
if (!slots.footer) {
|
||||
delete tableSlot.footer
|
||||
}
|
||||
if (!slots.expandedRowRender) {
|
||||
delete tableSlot.expandedRowRender
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
{...(getBindValue as any)}
|
||||
v-slots={tableSlot}
|
||||
>
|
||||
{renderTabelItem(props.columns)}
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
53
src/components/Table/TableItem.vue
Normal file
53
src/components/Table/TableItem.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<template v-if="item.children" />
|
||||
<template v-else>
|
||||
<a-table-column
|
||||
v-bind="getItemBindValue(item)"
|
||||
>
|
||||
<!-- title slot -->
|
||||
<template v-if="item.slots && item.slots.title" #title>
|
||||
<slot :name="item.slots.title" />
|
||||
</template>
|
||||
<!-- default slot -->
|
||||
<template v-if="item.slots && item.slots.customRender" #default="{ text, record, index, column }">
|
||||
<slot :name="item.slots.customRender" :text="text" :record="record" :index="index" :column="column" />
|
||||
</template>
|
||||
<!-- filterDropdown slot -->
|
||||
<template v-if="item.slots && item.slots.filterDropdown" #filterDropdown="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }">
|
||||
<slot :name="item.slots.filterDropdown" :setSelectedKeys="setSelectedKeys" :selectedKeys="selectedKeys" :confirm="confirm" :clearFilters="clearFilters" :column="column" />
|
||||
</template>
|
||||
<!-- filterIcon slot -->
|
||||
<template v-if="item.slots && item.slots.filterIcon" #filterIcon="filtered">
|
||||
<slot :name="item.slots.filterIcon" :filtered="filtered" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'TableItem',
|
||||
functional: true,
|
||||
props: {
|
||||
item: {
|
||||
type: Object as PropType<object>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props, { attrs }) {
|
||||
alert(',,,,')
|
||||
const getItemBindValue = computed(() => {
|
||||
return function(item: any) {
|
||||
return { ...item, ...attrs, ...props }
|
||||
}
|
||||
})
|
||||
return {
|
||||
getItemBindValue
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
3
src/components/Table/index.ts
Normal file
3
src/components/Table/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Table from './Table'
|
||||
|
||||
export default Table
|
||||
51
src/components/Table/index.vue
Normal file
51
src/components/Table/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a-table v-bind="getBindValue">
|
||||
<table-item
|
||||
v-for="item in columns"
|
||||
:key="item.key || item.dataIndex"
|
||||
:item="item"
|
||||
/>
|
||||
<template v-if="slots.title" #title="currentPageData">
|
||||
<slot name="title" :currentPageData="currentPageData" />
|
||||
</template>
|
||||
<!-- <template v-for="item in columns" :key="item.key || item.dataIndex"> -->
|
||||
|
||||
<!-- </template> -->
|
||||
<template v-if="slots.footer" #footer="currentPageData">
|
||||
<slot name="footer" :currentPageData="currentPageData" />
|
||||
</template>
|
||||
</a-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue'
|
||||
import TableItem from './TableItem.vue'
|
||||
export default defineComponent({
|
||||
name: 'ComTable',
|
||||
components: {
|
||||
TableItem
|
||||
},
|
||||
props: {
|
||||
columns: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup(props, { attrs, slots }) {
|
||||
console.log(TableItem)
|
||||
const getBindValue = computed((): any => {
|
||||
const bindValue = { ...attrs, ...props }
|
||||
delete bindValue.columns
|
||||
return bindValue
|
||||
})
|
||||
|
||||
return {
|
||||
getBindValue,
|
||||
slots
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
6
src/components/index.ts
Normal file
6
src/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { App } from 'vue'
|
||||
// import Button from '@/components/Button/index.vue'// Button组件
|
||||
|
||||
export function setupGlobCom(app: App<Element>): void {
|
||||
// app.component('AButton', Button)
|
||||
}
|
||||
Reference in New Issue
Block a user