feat: 🎸 初始化项目

This commit is contained in:
chenkl
2020-12-14 17:32:37 +08:00
commit 26d4c7c568
221 changed files with 23505 additions and 0 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,13 @@
<template>
<div />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'SilderItem'
})
</script>
<style>
</style>

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

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

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

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

View File

@@ -0,0 +1,3 @@
import Table from './Table'
export default Table

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