feat: 初始化项目结构并添加基础配置
添加前后端基础项目结构,包括.gitignore、package.json等配置文件 实现前端基础功能模块,包括路由、状态管理、API请求封装等 添加前端UI组件库和样式体系 配置开发环境Mock系统和构建工具链
This commit is contained in:
135
front-end/src/plugins/VabAnchor/index.vue
Normal file
135
front-end/src/plugins/VabAnchor/index.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="vab-anchor">
|
||||
<div v-for="(item, key) in floorList" :key="key" :class="'floor' + key">
|
||||
<slot v-if="key === key" :name="'floor' + key" />
|
||||
</div>
|
||||
<vab-card
|
||||
:body-style="{
|
||||
padding: '20px 10px 20px 10px',
|
||||
}"
|
||||
shadow="never"
|
||||
style="position: fixed; top: 170px; right: 68px"
|
||||
>
|
||||
<el-tabs
|
||||
v-model="step"
|
||||
tab-position="right"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(item, key) in floorList"
|
||||
:key="key"
|
||||
:label="item.title"
|
||||
/>
|
||||
</el-tabs>
|
||||
</vab-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VabAnchor',
|
||||
props: {
|
||||
floorList: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [
|
||||
{ title: '锚点1' },
|
||||
{ title: '锚点2' },
|
||||
{ title: '锚点3' },
|
||||
{ title: '锚点4' },
|
||||
{ title: '锚点5' },
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
step: '0',
|
||||
scrolltop: 0,
|
||||
floorObject: {},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scrolltop(val) {
|
||||
val += 200
|
||||
const floorObject = this.floorObject
|
||||
for (let i = 0; i <= this.floorList.length + 1; i++) {
|
||||
if (
|
||||
val > floorObject[`floor${i}`] &&
|
||||
(val <= floorObject[`floor${Number.parseInt(i + 1)}`] ||
|
||||
val <= Number.POSITIVE_INFINITY)
|
||||
) {
|
||||
this.step = `${i}`
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getFloorDistance()
|
||||
document.querySelector('#app').addEventListener('scroll', () => {
|
||||
this.scrolltop = document.querySelector('#app').scrollTop
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleClick({ index }) {
|
||||
this.anchors(index)
|
||||
},
|
||||
anchors(item) {
|
||||
this.pulleyRoll(
|
||||
this.floorObject[`floor${item}`],
|
||||
this.scrolltop
|
||||
)
|
||||
},
|
||||
pulleyRoll(top, distance) {
|
||||
if (distance < top) {
|
||||
const smallInterval = (top - distance) / 50
|
||||
let i = 0
|
||||
const timer = setInterval(() => {
|
||||
i++
|
||||
distance += smallInterval
|
||||
document.querySelector('#app').scrollTop = distance
|
||||
if (i == 50) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 10)
|
||||
} else if (distance > top) {
|
||||
const smallInterval = (distance - top) / 50
|
||||
let i = 0
|
||||
const timer = setInterval(() => {
|
||||
i++
|
||||
distance -= smallInterval
|
||||
document.querySelector('#app').scrollTop = distance
|
||||
if (i == 50) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
},
|
||||
getFloorDistance() {
|
||||
for (let i = 0; i < this.floorList.length; i++) {
|
||||
this.floorObject[`floor${i}`] =
|
||||
document.getElementsByClassName(
|
||||
`floor${i}`
|
||||
)[0].offsetTop
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-anchor {
|
||||
[class*='floor'] {
|
||||
height: 780px;
|
||||
padding: $base-padding;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: #b5ff8a;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #6db9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
front-end/src/plugins/VabAvatarList/index.vue
Normal file
33
front-end/src/plugins/VabAvatarList/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
const props: any = defineProps({
|
||||
avatarList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vab-avatar-list">
|
||||
<el-tooltip
|
||||
v-for="(item, index) in props.avatarList"
|
||||
:key="index"
|
||||
:content="item.username"
|
||||
effect="dark"
|
||||
placement="top-start"
|
||||
>
|
||||
<el-avatar :size="40" :src="item.avatar" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-avatar-list {
|
||||
:deep(.el-avatar) {
|
||||
display: inline-block;
|
||||
margin-left: -15px;
|
||||
cursor: pointer;
|
||||
border: 3px solid var(--el-color-white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1122
front-end/src/plugins/VabCalendar.ts
Normal file
1122
front-end/src/plugins/VabCalendar.ts
Normal file
File diff suppressed because it is too large
Load Diff
287
front-end/src/plugins/VabChart/index.vue
Normal file
287
front-end/src/plugins/VabChart/index.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="echarts" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import debounce from 'lodash/debounce'
|
||||
import theme from './theme/vab-echarts-theme.json'
|
||||
import { addListener, removeListener } from 'resize-detector'
|
||||
|
||||
const INIT_TRIGGERS = ['theme', 'initOptions', 'autoResize']
|
||||
const REWATCH_TRIGGERS = ['manualUpdate', 'watchShallow']
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
option: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
theme: {
|
||||
type: [String, Object],
|
||||
default: () => {},
|
||||
},
|
||||
initOptions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autoResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
watchShallow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
manualUpdate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastArea: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
group(group) {
|
||||
this.chart.group = group
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initOptionsWatcher()
|
||||
INIT_TRIGGERS.forEach((prop) => {
|
||||
this.$watch(
|
||||
prop,
|
||||
() => {
|
||||
this.refresh()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
})
|
||||
REWATCH_TRIGGERS.forEach((prop) => {
|
||||
this.$watch(prop, () => {
|
||||
this.initOptionsWatcher()
|
||||
this.refresh()
|
||||
})
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
if (this.option) {
|
||||
echarts.registerTheme('vab-echarts-theme', theme)
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
activated() {
|
||||
if (this.autoResize) {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
if (this.chart) {
|
||||
this.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mergeOptions(option, notMerge, lazyUpdate) {
|
||||
if (this.manualUpdate) {
|
||||
this.manualOptions = option
|
||||
}
|
||||
if (!this.chart) {
|
||||
this.init(option)
|
||||
} else {
|
||||
this.delegateMethod(
|
||||
'setOption',
|
||||
option,
|
||||
notMerge,
|
||||
lazyUpdate
|
||||
)
|
||||
}
|
||||
},
|
||||
appendData(params) {
|
||||
this.delegateMethod('appendData', params)
|
||||
},
|
||||
resize(option) {
|
||||
this.delegateMethod('resize', option)
|
||||
},
|
||||
dispatchAction(payload) {
|
||||
this.delegateMethod('dispatchAction', payload)
|
||||
},
|
||||
convertToPixel(finder, value) {
|
||||
return this.delegateMethod('convertToPixel', finder, value)
|
||||
},
|
||||
convertFromPixel(finder, value) {
|
||||
return this.delegateMethod('convertFromPixel', finder, value)
|
||||
},
|
||||
containPixel(finder, value) {
|
||||
return this.delegateMethod('containPixel', finder, value)
|
||||
},
|
||||
showLoading(type, option) {
|
||||
this.delegateMethod('showLoading', type, option)
|
||||
},
|
||||
hideLoading() {
|
||||
this.delegateMethod('hideLoading')
|
||||
},
|
||||
getDataURL(option) {
|
||||
return this.delegateMethod('getDataURL', option)
|
||||
},
|
||||
getConnectedDataURL(option) {
|
||||
return this.delegateMethod('getConnectedDataURL', option)
|
||||
},
|
||||
clear() {
|
||||
this.delegateMethod('clear')
|
||||
},
|
||||
dispose() {
|
||||
this.delegateMethod('dispose')
|
||||
},
|
||||
delegateMethod(name, ...args) {
|
||||
if (!this.chart) {
|
||||
this.init()
|
||||
}
|
||||
return this.chart[name](...args)
|
||||
},
|
||||
delegateGet(methodName) {
|
||||
if (!this.chart) {
|
||||
this.init()
|
||||
}
|
||||
return this.chart[methodName]()
|
||||
},
|
||||
getArea() {
|
||||
return this.$el.offsetWidth * this.$el.offsetHeight
|
||||
},
|
||||
init(option) {
|
||||
if (this.chart) {
|
||||
return
|
||||
}
|
||||
const chart = echarts.init(
|
||||
this.$el,
|
||||
this.theme,
|
||||
this.initOptions
|
||||
)
|
||||
if (this.group) {
|
||||
chart.group = this.group
|
||||
}
|
||||
chart.clear()
|
||||
chart.setOption(
|
||||
option || this.manualOptions || this.option || {},
|
||||
true
|
||||
)
|
||||
Object.keys(this.$attrs).forEach((event) => {
|
||||
const handler = this.$attrs[event]
|
||||
if (event.indexOf('zr:') === 0) {
|
||||
chart.getZr().on(event.slice(3), handler)
|
||||
} else {
|
||||
chart.on(event, handler)
|
||||
}
|
||||
})
|
||||
if (this.autoResize) {
|
||||
this.lastArea = this.getArea()
|
||||
this.__resizeHandler = debounce(
|
||||
() => {
|
||||
if (this.lastArea === 0) {
|
||||
this.mergeOptions({}, true)
|
||||
this.resize()
|
||||
this.mergeOptions(
|
||||
this.option || this.manualOptions || {},
|
||||
true
|
||||
)
|
||||
} else {
|
||||
this.resize()
|
||||
}
|
||||
this.lastArea = this.getArea()
|
||||
},
|
||||
100,
|
||||
{ leading: true }
|
||||
)
|
||||
addListener(this.$el, this.__resizeHandler)
|
||||
}
|
||||
this.chart = chart
|
||||
Object.defineProperties(this, {
|
||||
width: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getWidth')
|
||||
},
|
||||
},
|
||||
height: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getHeight')
|
||||
},
|
||||
},
|
||||
isDisposed: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return !!this.delegateGet('isDisposed')
|
||||
},
|
||||
},
|
||||
computedOptions: {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
return this.delegateGet('getOption')
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
initOptionsWatcher() {
|
||||
if (this.__unwatchOptions) {
|
||||
this.__unwatchOptions()
|
||||
this.__unwatchOptions = null
|
||||
}
|
||||
if (!this.manualUpdate) {
|
||||
this.__unwatchOptions = this.$watch(
|
||||
'option',
|
||||
(val, oldVal) => {
|
||||
if (!this.chart && val) {
|
||||
this.init()
|
||||
} else {
|
||||
this.chart.setOption(val, val !== oldVal)
|
||||
}
|
||||
},
|
||||
{ deep: !this.watchShallow }
|
||||
)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
if (this.autoResize) {
|
||||
removeListener(this.$el, this.__resizeHandler)
|
||||
}
|
||||
this.dispose()
|
||||
this.chart = null
|
||||
},
|
||||
refresh() {
|
||||
if (this.chart) {
|
||||
this.destroy()
|
||||
this.init()
|
||||
}
|
||||
},
|
||||
},
|
||||
connect(group) {
|
||||
if (typeof group !== 'string') {
|
||||
group = group.map((chart) => chart.chart)
|
||||
}
|
||||
echarts.connect(group)
|
||||
},
|
||||
disconnect(group) {
|
||||
echarts.disConnect(group)
|
||||
},
|
||||
getMap(mapName) {
|
||||
return echarts.getMap(mapName)
|
||||
},
|
||||
registerMap(mapName, geoJSON, specialAreas) {
|
||||
echarts.registerMap(mapName, geoJSON, specialAreas)
|
||||
},
|
||||
graphic: echarts.graphic,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.echarts {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
317
front-end/src/plugins/VabChart/theme/vab-echarts-theme.json
Normal file
317
front-end/src/plugins/VabChart/theme/vab-echarts-theme.json
Normal file
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
|
||||
"backgroundColor": "rgba(252,252,252,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#666666"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "3"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "3"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"color": "#e6a0d2",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#e6a0d2",
|
||||
"borderColor0": "#1890FF",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"lineStyle": {
|
||||
"normal": {
|
||||
"width": "1",
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false,
|
||||
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"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": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"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,
|
||||
"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,
|
||||
"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": {
|
||||
"borderColor": "#999999"
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#ffffff",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#ffffff",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#4ECB73",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"color": "#4ECB73",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#4ECB73",
|
||||
"borderColor": "#4ECB73",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#1890FF",
|
||||
"borderColor": "rgba(63,177,227,0.15)"
|
||||
},
|
||||
"label": {
|
||||
"color": "#4ECB73"
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": ["#1890FF", "#afe8ff"]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(255,255,255,0)",
|
||||
"dataBackgroundColor": "rgba(222,222,222,1)",
|
||||
"fillerColor": "rgba(114,230,212,0.25)",
|
||||
"handleColor": "#cccccc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
222
front-end/src/plugins/VabCount/index.vue
Normal file
222
front-end/src/plugins/VabCount/index.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<!-- eslint-disable -->
|
||||
<template>
|
||||
<span>{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
cancelAnimationFrame,
|
||||
requestAnimationFrame,
|
||||
} from './requestAnimationFrame'
|
||||
|
||||
export default {
|
||||
name: 'VabCount',
|
||||
props: {
|
||||
startVal: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
endVal: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 3000,
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
decimals: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
validator(value) {
|
||||
return value >= 0
|
||||
},
|
||||
},
|
||||
decimal: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '.',
|
||||
},
|
||||
separator: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ',',
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
useEasing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
easingFn: {
|
||||
type: Function,
|
||||
default(t, b, c, d) {
|
||||
return (c * (-(2 ** ((-10 * t) / d)) + 1) * 1024) / 1023 + b
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localStartVal: this.startVal,
|
||||
displayValue: this.formatNumber(this.startVal),
|
||||
printVal: null,
|
||||
paused: false,
|
||||
localDuration: this.duration,
|
||||
startTime: null,
|
||||
timestamp: null,
|
||||
remaining: null,
|
||||
rAF: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countDown() {
|
||||
return this.startVal > this.endVal
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
startVal() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
endVal() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.autoplay) {
|
||||
this.start()
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
this.$emit('mountedCallback')
|
||||
},
|
||||
unmounted() {
|
||||
cancelAnimationFrame(this.rAF)
|
||||
},
|
||||
methods: {
|
||||
start() {
|
||||
this.localStartVal = this.startVal
|
||||
this.startTime = null
|
||||
this.localDuration = this.duration
|
||||
this.paused = false
|
||||
this.rAF = requestAnimationFrame(this.count)
|
||||
},
|
||||
pauseResume() {
|
||||
if (this.paused) {
|
||||
this.resume()
|
||||
this.paused = false
|
||||
} else {
|
||||
this.pause()
|
||||
this.paused = true
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
cancelAnimationFrame(this.rAF)
|
||||
},
|
||||
resume() {
|
||||
this.startTime = null
|
||||
this.localDuration = +this.remaining
|
||||
this.localStartVal = +this.printVal
|
||||
requestAnimationFrame(this.count)
|
||||
},
|
||||
reset() {
|
||||
this.startTime = null
|
||||
cancelAnimationFrame(this.rAF)
|
||||
this.displayValue = this.formatNumber(this.startVal)
|
||||
},
|
||||
count(timestamp) {
|
||||
if (!this.startTime) this.startTime = timestamp
|
||||
this.timestamp = timestamp
|
||||
const progress = timestamp - this.startTime
|
||||
this.remaining = this.localDuration - progress
|
||||
|
||||
if (this.useEasing) {
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.localStartVal -
|
||||
this.easingFn(
|
||||
progress,
|
||||
0,
|
||||
this.localStartVal - this.endVal,
|
||||
this.localDuration
|
||||
)
|
||||
} else {
|
||||
this.printVal = this.easingFn(
|
||||
progress,
|
||||
this.localStartVal,
|
||||
this.endVal - this.localStartVal,
|
||||
this.localDuration
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.localStartVal -
|
||||
(this.localStartVal - this.endVal) *
|
||||
(progress / this.localDuration)
|
||||
} else {
|
||||
this.printVal =
|
||||
this.localStartVal +
|
||||
(this.endVal - this.localStartVal) *
|
||||
(progress / this.localDuration)
|
||||
}
|
||||
}
|
||||
if (this.countDown) {
|
||||
this.printVal =
|
||||
this.printVal < this.endVal
|
||||
? this.endVal
|
||||
: this.printVal
|
||||
} else {
|
||||
this.printVal =
|
||||
this.printVal > this.endVal
|
||||
? this.endVal
|
||||
: this.printVal
|
||||
}
|
||||
|
||||
this.displayValue = this.formatNumber(this.printVal)
|
||||
if (progress < this.localDuration) {
|
||||
this.rAF = requestAnimationFrame(this.count)
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
this.$emit('callback')
|
||||
}
|
||||
},
|
||||
isNumber(val) {
|
||||
return !isNaN(Number.parseFloat(val))
|
||||
},
|
||||
formatNumber(num) {
|
||||
num = num.toFixed(this.decimals)
|
||||
num += ''
|
||||
const x = num.split('.')
|
||||
let x1 = x[0]
|
||||
const x2 = x.length > 1 ? this.decimal + x[1] : ''
|
||||
const rgx = /(\d+)(\d{3})/
|
||||
if (this.separator && !this.isNumber(this.separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, `$1${this.separator}$2`)
|
||||
}
|
||||
}
|
||||
return this.prefix + x1 + x2 + this.suffix
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
45
front-end/src/plugins/VabCount/requestAnimationFrame.js
Normal file
45
front-end/src/plugins/VabCount/requestAnimationFrame.js
Normal file
@@ -0,0 +1,45 @@
|
||||
let lastTime = 0
|
||||
const prefixes = 'webkit moz ms o'.split(' ')
|
||||
|
||||
let requestAnimationFrame
|
||||
let cancelAnimationFrame
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
if (isServer) {
|
||||
requestAnimationFrame = function () {}
|
||||
cancelAnimationFrame = function () {}
|
||||
} else {
|
||||
requestAnimationFrame = window.requestAnimationFrame
|
||||
cancelAnimationFrame = window.cancelAnimationFrame
|
||||
let prefix
|
||||
for (const prefix_ of prefixes) {
|
||||
if (requestAnimationFrame && cancelAnimationFrame) {
|
||||
break
|
||||
}
|
||||
prefix = prefix_
|
||||
requestAnimationFrame =
|
||||
requestAnimationFrame || window[`${prefix}RequestAnimationFrame`]
|
||||
cancelAnimationFrame =
|
||||
cancelAnimationFrame ||
|
||||
window[`${prefix}CancelAnimationFrame`] ||
|
||||
window[`${prefix}CancelRequestAnimationFrame`]
|
||||
}
|
||||
|
||||
if (!requestAnimationFrame || !cancelAnimationFrame) {
|
||||
requestAnimationFrame = function (callback) {
|
||||
const currTime = Date.now()
|
||||
const timeToCall = Math.max(0, 16 - (currTime - lastTime))
|
||||
const id = window.setTimeout(() => {
|
||||
callback(currTime + timeToCall)
|
||||
}, timeToCall)
|
||||
lastTime = currTime + timeToCall
|
||||
return id
|
||||
}
|
||||
|
||||
cancelAnimationFrame = function (id) {
|
||||
window.clearTimeout(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { requestAnimationFrame, cancelAnimationFrame }
|
||||
145
front-end/src/plugins/VabDialog/index.vue
Normal file
145
front-end/src/plugins/VabDialog/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
appendToBody: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lockScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '50%',
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showFullscreen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialogVisible = useVModel(props, 'modelValue', emit)
|
||||
const isFullscreen = ref(false)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
const setFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vab-dialog">
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
v-bind="$attrs"
|
||||
:append-to-body="appendToBody"
|
||||
:draggable="draggable"
|
||||
:fullscreen="isFullscreen"
|
||||
:lock-scroll="lockScroll"
|
||||
:show-close="false"
|
||||
:width="width"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header">
|
||||
<span class="el-dialog__title">{{ title }}</span>
|
||||
</slot>
|
||||
<div class="vab-dialog__headerbtn">
|
||||
<button
|
||||
v-if="showFullscreen"
|
||||
aria-label="fullscreen"
|
||||
type="button"
|
||||
@click="setFullscreen"
|
||||
>
|
||||
<vab-icon
|
||||
v-if="isFullscreen"
|
||||
icon="fullscreen-exit-line"
|
||||
/>
|
||||
<vab-icon v-else icon="fullscreen-line" />
|
||||
</button>
|
||||
<button
|
||||
v-if="showClose"
|
||||
aria-label="close"
|
||||
type="button"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<vab-icon icon="close-circle-line" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-dialog {
|
||||
&__headerbtn {
|
||||
position: absolute;
|
||||
top: var(--el-dialog-padding-primary);
|
||||
right: var(--el-dialog-padding-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
margin-left: 15px;
|
||||
font-size: var(--el-message-close-size, 16px);
|
||||
color: var(--el-color-info);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
transition: $base-transition;
|
||||
|
||||
&:hover i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
&.is-fullscreen {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding-bottom: 10px;
|
||||
border-top: 1px solid var(--el-border-color-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
128
front-end/src/plugins/VabFormTable/index.vue
Normal file
128
front-end/src/plugins/VabFormTable/index.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="vab-form-table">
|
||||
<vab-query-form>
|
||||
<vab-query-form-top-panel :span="12">
|
||||
<el-button
|
||||
:icon="Plus"
|
||||
type="primary"
|
||||
@click="handleAdd($event)"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</vab-query-form-top-panel>
|
||||
</vab-query-form>
|
||||
<el-table :key="toggleIndex" ref="tableRef" border :data="data">
|
||||
<el-table-column
|
||||
v-if="drag"
|
||||
align="center"
|
||||
label="操作"
|
||||
width="120"
|
||||
>
|
||||
<template #default>
|
||||
<vab-icon
|
||||
class="vab-rank"
|
||||
icon="drag-move-2-line"
|
||||
style="cursor: move"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<slot></slot>
|
||||
<el-table-column align="center" label="操作" width="120">
|
||||
<template #default="{ $index, row }">
|
||||
<el-button
|
||||
:icon="Delete"
|
||||
plain
|
||||
type="danger"
|
||||
@click="handleDelete(row, $index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty class="vab-data-empty" description="暂无数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VabFormTable',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
rowTemplate: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
tableRef: null,
|
||||
data: [],
|
||||
toggleIndex: 0,
|
||||
})
|
||||
|
||||
const rowDrop = () => {
|
||||
const tbody = state.tableRef.$el.querySelector(
|
||||
'.el-table__body-wrapper tbody'
|
||||
)
|
||||
Sortable.create(tbody, {
|
||||
handle: '.vab-rank',
|
||||
animation: 300,
|
||||
onEnd({ newIndex, oldIndex }) {
|
||||
const tableData = state.data
|
||||
const currRow = tableData.splice(oldIndex, 1)[0]
|
||||
tableData.splice(newIndex, 0, currRow)
|
||||
state.toggleIndex += 1
|
||||
nextTick(() => {
|
||||
rowDrop()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleAdd = () => {
|
||||
state.data.push(JSON.parse(JSON.stringify(props.rowTemplate)))
|
||||
}
|
||||
const handleDelete = (row, index) => {
|
||||
state.data.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
state.data = props.modelValue
|
||||
if (props.drag) rowDrop()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => (state.data = props.modelValue)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.data,
|
||||
() => emit('update:modelValue', state.data)
|
||||
)
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
rowDrop,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
Delete,
|
||||
Plus,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
116
front-end/src/plugins/VabIconSelector/index.vue
Normal file
116
front-end/src/plugins/VabIconSelector/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { getIconList } from '@/api/defaultIcon'
|
||||
|
||||
const emit = defineEmits(['handle-icon'])
|
||||
const state: any = reactive({
|
||||
icon: '24-hours-fill',
|
||||
layout: 'total, prev, next',
|
||||
total: 0,
|
||||
background: true,
|
||||
height: 0,
|
||||
selectRows: '',
|
||||
queryIcon: [],
|
||||
queryForm: {
|
||||
pageNo: 1,
|
||||
pageSize: 16,
|
||||
title: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleSizeChange: any = (val: string) => {
|
||||
state.queryForm.pageSize = val
|
||||
fetchData()
|
||||
}
|
||||
const handleCurrentChange: any = (val: string) => {
|
||||
state.queryForm.pageNo = val
|
||||
fetchData()
|
||||
}
|
||||
const queryData: any = () => {
|
||||
state.queryForm.pageNo = 1
|
||||
fetchData()
|
||||
}
|
||||
const fetchData: any = async () => {
|
||||
const {
|
||||
data: { list, total },
|
||||
} = await getIconList(state.queryForm)
|
||||
state.queryIcon = list
|
||||
state.total = total
|
||||
}
|
||||
const handleIcon: any = (item: any) => {
|
||||
state.icon = item
|
||||
emit('handle-icon', item)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<vab-query-form>
|
||||
<vab-query-form-top-panel>
|
||||
<el-form inline label-width="0" @submit.prevent>
|
||||
<el-form-item label="">
|
||||
<el-input v-model="state.queryForm.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label-width="0">
|
||||
<el-button
|
||||
native-type="submit"
|
||||
type="primary"
|
||||
@click="queryData"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</vab-query-form-top-panel>
|
||||
</vab-query-form>
|
||||
</el-col>
|
||||
|
||||
<el-col v-for="(item, index) in state.queryIcon" :key="index" :span="6">
|
||||
<vab-card @click="handleIcon(item)">
|
||||
<vab-icon :icon="item" />
|
||||
</vab-card>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-pagination
|
||||
:background="state.background"
|
||||
:current-page="state.queryForm.pageNo"
|
||||
:layout="state.layout"
|
||||
:page-size="state.queryForm.pageSize"
|
||||
:total="state.total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-selector-popper {
|
||||
.el-card__body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
vertical-align: middle;
|
||||
color: var(--el-color-grey);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
148
front-end/src/plugins/VabPrint.ts
Normal file
148
front-end/src/plugins/VabPrint.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
const Print: any = function (this: any, dom: any, options: any) {
|
||||
if (!(this instanceof Print)) return new Print(dom, options)
|
||||
|
||||
this.options = this.extend(
|
||||
{
|
||||
noPrint: '.no-print',
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
if (typeof dom === 'string') {
|
||||
try {
|
||||
this.dom = document.querySelector(dom)
|
||||
} catch {
|
||||
const createDom = document.createElement('div')
|
||||
createDom.innerHTML = dom
|
||||
this.dom = createDom
|
||||
}
|
||||
} else {
|
||||
this.isDOM(dom)
|
||||
this.dom = this.isDOM(dom) ? dom : dom.$el
|
||||
}
|
||||
|
||||
this.init()
|
||||
}
|
||||
Print.prototype = {
|
||||
init() {
|
||||
const content = this.getStyle() + this.getHtml()
|
||||
this.writeIframe(content)
|
||||
},
|
||||
extend(obj: { [x: string]: any }, obj2: { [x: string]: any }) {
|
||||
for (const k in obj2) {
|
||||
obj[k] = obj2[k]
|
||||
}
|
||||
return obj
|
||||
},
|
||||
|
||||
getStyle() {
|
||||
let str = ''
|
||||
const styles = document.querySelectorAll('style,link')
|
||||
for (const style of styles) {
|
||||
str += style.outerHTML
|
||||
}
|
||||
str += `<style>${this.options.noPrint ? this.options.noPrint : '.no-print'}{display:none;}</style>`
|
||||
str += '<style>html,body{background-color:#fff;}</style>'
|
||||
return str
|
||||
},
|
||||
|
||||
getHtml() {
|
||||
const inputs = document.querySelectorAll('input')
|
||||
const textareas = document.querySelectorAll('textarea')
|
||||
const selects = document.querySelectorAll('select')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.type == 'checkbox' || input.type == 'radio') {
|
||||
if (input.checked == true) {
|
||||
input.setAttribute('checked', 'checked')
|
||||
} else {
|
||||
input.removeAttribute('checked')
|
||||
}
|
||||
} else if (input.type == 'text') {
|
||||
input.setAttribute('value', input.value)
|
||||
} else {
|
||||
input.setAttribute('value', input.value)
|
||||
}
|
||||
}
|
||||
|
||||
for (const textarea of textareas) {
|
||||
if (textarea.type == 'textarea') textarea.innerHTML = textarea.value
|
||||
}
|
||||
|
||||
for (const select of selects) {
|
||||
if (select.type == 'select-one') {
|
||||
const child: any = select.children
|
||||
for (const i in child) {
|
||||
if (child[i].tagName == 'OPTION') {
|
||||
if (child[i].selected == true)
|
||||
child[i].setAttribute('selected', 'selected')
|
||||
else child[i].removeAttribute('selected')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.dom.outerHTML
|
||||
},
|
||||
|
||||
writeIframe(content: string) {
|
||||
const iframe: any = document.createElement('iframe')
|
||||
const f: any = document.body.appendChild(iframe)
|
||||
iframe.id = 'myIframe'
|
||||
iframe.setAttribute(
|
||||
'style',
|
||||
'position:absolute;width:0;height:0;top:-10px;left:-10px;'
|
||||
)
|
||||
const w: any = f.contentWindow || f.contentDocument
|
||||
const doc: any = f.contentDocument || f.contentWindow.document
|
||||
doc.open()
|
||||
doc.write(content)
|
||||
doc.close()
|
||||
const _this = this
|
||||
iframe.addEventListener('load', () => {
|
||||
_this.toPrint(w)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe)
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
|
||||
toPrint(frameWindow: {
|
||||
focus: () => void
|
||||
document: {
|
||||
execCommand: (arg0: string, arg1: boolean, arg2: null) => any
|
||||
}
|
||||
print: () => void
|
||||
close: () => void
|
||||
}) {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
frameWindow.focus()
|
||||
try {
|
||||
if (!frameWindow.document.execCommand('print', false, null))
|
||||
frameWindow.print()
|
||||
} catch {
|
||||
frameWindow.print()
|
||||
}
|
||||
frameWindow.close()
|
||||
}, 10)
|
||||
} catch (error) {
|
||||
console.log('err', error)
|
||||
}
|
||||
},
|
||||
isDOM:
|
||||
typeof HTMLElement === 'object'
|
||||
? function (obj: any) {
|
||||
return obj instanceof HTMLElement
|
||||
}
|
||||
: function (obj: { nodeType: number; nodeName: any }) {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
obj.nodeType === 1 &&
|
||||
typeof obj.nodeName === 'string'
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default Print
|
||||
1
front-end/src/plugins/VabQrCode.ts
Normal file
1
front-end/src/plugins/VabQrCode.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'vue-qr/src/packages/vue-qr.vue'
|
||||
157
front-end/src/plugins/VabUpdate/index.vue
Normal file
157
front-end/src/plugins/VabUpdate/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from '@/store/modules/settings'
|
||||
|
||||
const $sub: any = inject('$sub')
|
||||
const $baseMessage: any = inject('$baseMessage')
|
||||
|
||||
const { getTitle } = useSettingsStore()
|
||||
|
||||
const state = reactive({
|
||||
title: getTitle,
|
||||
version: __APP_INFO__['version'],
|
||||
updateTime: __APP_INFO__['lastBuildTime'],
|
||||
dialogVisible: false,
|
||||
loading: false,
|
||||
button: '立即升级',
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
$sub('vab-update', () => {
|
||||
state.dialogVisible = true
|
||||
setTimeout(() => {
|
||||
save()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
state.dialogVisible = false
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
state.button = '正在更新'
|
||||
state.loading = true
|
||||
$baseMessage(
|
||||
'正在更新,预计10S后更新完成',
|
||||
'success',
|
||||
'vab-hey-message-success'
|
||||
)
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.button = '更新完成'
|
||||
}, 1000 * 6)
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000 * 7)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="state.dialogVisible"
|
||||
append-to-body
|
||||
class="vab-update"
|
||||
width="410px"
|
||||
@close="close"
|
||||
>
|
||||
<div class="vab-update-icon">
|
||||
<vab-icon icon="upload-cloud-2-fill" />
|
||||
</div>
|
||||
<vab-icon class="vab-update-cup" icon="cup-line" />
|
||||
<h3>版本更新:</h3>
|
||||
<p>
|
||||
{{ state.title }}
|
||||
V{{ state.version }}
|
||||
</p>
|
||||
<p>
|
||||
更新时间:最近更新
|
||||
<!-- {{ updateTime }} -->
|
||||
</p>
|
||||
<p
|
||||
v-text="
|
||||
`${'如遇更' + '新失败' + '请手' + '动点击' + 'Ctr' + 'l + F' + '5' + '重试'}`
|
||||
"
|
||||
></p>
|
||||
<template #footer>
|
||||
<el-button
|
||||
v-loading="state.loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="save"
|
||||
>
|
||||
{{ state.button }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vab-update {
|
||||
position: relative;
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
text-align: center;
|
||||
background: linear-gradient(
|
||||
50deg,
|
||||
var(--el-color-primary),
|
||||
var(--el-color-primary-light-7)
|
||||
);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
i {
|
||||
font-size: 50px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&-cup {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 70px;
|
||||
font-size: 80px;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(
|
||||
var(--el-color-primary-light-7),
|
||||
var(--el-color-primary-light-9)
|
||||
);
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vab-update {
|
||||
&.el-dialog {
|
||||
margin-top: 30vh !important;
|
||||
border-radius: 15px;
|
||||
|
||||
.el-dialog__body {
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
text-align: center !important;
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(
|
||||
50deg,
|
||||
var(--el-color-primary-light-3),
|
||||
var(--el-color-primary)
|
||||
);
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
front-end/src/plugins/VabUpload/index.vue
Normal file
270
front-end/src/plugins/VabUpload/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogFormVisible"
|
||||
:before-close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
:title="title"
|
||||
width="909px"
|
||||
>
|
||||
<div class="upload">
|
||||
<el-alert
|
||||
:closable="false"
|
||||
:title="`支持jpg、jpeg、png格式,单次可最多选择${limit}张图片,每张不可大于${size}M,如果大于${size}M会自动为您过滤`"
|
||||
type="info"
|
||||
/>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
accept="image/png, image/jpeg"
|
||||
:action="action"
|
||||
:auto-upload="false"
|
||||
class="upload-content"
|
||||
:close-on-click-modal="false"
|
||||
:data="data"
|
||||
:file-list="fileList"
|
||||
:headers="headers"
|
||||
:limit="limit"
|
||||
list-type="picture-card"
|
||||
:multiple="true"
|
||||
:name="name"
|
||||
:on-change="handleChange"
|
||||
:on-error="handleError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-preview="handlePreview"
|
||||
:on-progress="handleProgress"
|
||||
:on-remove="handleRemove"
|
||||
:on-success="handleSuccess"
|
||||
>
|
||||
<template #trigger>
|
||||
<vab-icon icon="add-line" />
|
||||
</template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
append-to-body
|
||||
title="查看大图"
|
||||
>
|
||||
<div>
|
||||
<el-image :src="dialogImageUrl" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-upload>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="show"
|
||||
style="position: absolute; top: 10px; left: 15px; color: #999"
|
||||
>
|
||||
正在上传中... 当前上传成功数:{{ imgSuccessNum }}张
|
||||
当前上传失败数:{{ imgErrorNum }}张
|
||||
</div>
|
||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||
<el-button
|
||||
:loading="loading"
|
||||
style="margin-left: 10px"
|
||||
type="success"
|
||||
@click="submitUpload"
|
||||
>
|
||||
开始上传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VabUpload',
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '/upload',
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file',
|
||||
required: true,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore()
|
||||
const { token } = storeToRefs(userStore)
|
||||
const $baseMessage = inject('$baseMessage')
|
||||
|
||||
const state = reactive({
|
||||
uploadRef: null,
|
||||
show: false,
|
||||
loading: false,
|
||||
dialogVisible: false,
|
||||
dialogImageUrl: '',
|
||||
action: '',
|
||||
headers: {},
|
||||
fileList: [],
|
||||
picture: 'picture',
|
||||
imgNum: 0,
|
||||
imgSuccessNum: 0,
|
||||
imgErrorNum: 0,
|
||||
typeList: null,
|
||||
title: '上传',
|
||||
dialogFormVisible: false,
|
||||
data: {},
|
||||
})
|
||||
|
||||
const submitUpload = () => {
|
||||
state.uploadRef.submit()
|
||||
}
|
||||
const handleProgress = () => {
|
||||
state.loading = true
|
||||
state.show = true
|
||||
}
|
||||
const handleChange = (file, fileList) => {
|
||||
if (fileList && fileList.length > 0) {
|
||||
if (file.size > 1048576 * state.size) {
|
||||
fileList.filter((item) => item !== file)
|
||||
state.fileList = fileList
|
||||
} else {
|
||||
state.allImgNum = fileList.length
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleSuccess = (response, file, fileList) => {
|
||||
state.imgNum = state.imgNum + 1
|
||||
state.imgSuccessNum = state.imgSuccessNum + 1
|
||||
if (fileList.length === state.imgNum) {
|
||||
setTimeout(() => {
|
||||
$baseMessage(
|
||||
`上传完成! 共上传${fileList.length}张图片`,
|
||||
'success',
|
||||
'vab-hey-message-success'
|
||||
)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.show = false
|
||||
}, 1000)
|
||||
}
|
||||
const handleError = (err, file) => {
|
||||
state.imgNum = state.imgNum + 1
|
||||
state.imgErrorNum = state.imgErrorNum + 1
|
||||
$baseMessage(
|
||||
`文件[${file.raw.name}]上传失败,文件大小为${_.round(file.raw.size / 1024, 0)}KB`,
|
||||
'error',
|
||||
'vab-hey-message-error'
|
||||
)
|
||||
setTimeout(() => {
|
||||
state.loading = false
|
||||
state.show = false
|
||||
}, 1000)
|
||||
}
|
||||
const handleRemove = () => {
|
||||
state.imgNum = state.imgNum - 1
|
||||
state.allNum = state.allNum - 1
|
||||
}
|
||||
const handlePreview = (file) => {
|
||||
state.dialogImageUrl = file.url
|
||||
state.dialogVisible = true
|
||||
}
|
||||
const handleExceed = (files) => {
|
||||
$baseMessage(
|
||||
`当前限制选择 ${state.limit} 个文件,本次选择了
|
||||
${files.length}
|
||||
个文件`,
|
||||
'error',
|
||||
'vab-hey-message-error'
|
||||
)
|
||||
}
|
||||
const handleShow = (data) => {
|
||||
state.title = '上传'
|
||||
state.data = data
|
||||
state.dialogFormVisible = true
|
||||
}
|
||||
const handleClose = () => {
|
||||
state.fileList = []
|
||||
state.picture = 'picture'
|
||||
state.allImgNum = 0
|
||||
state.imgNum = 0
|
||||
state.imgSuccessNum = 0
|
||||
state.imgErrorNum = 0
|
||||
state.headers['Authorization'] = `Bearer ${token}`
|
||||
state.dialogFormVisible = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
state.headers['Authorization'] = `Bearer ${token}`
|
||||
state.action = props.url
|
||||
})
|
||||
|
||||
const percentage = computed(() => {
|
||||
if (state.allImgNum === 0) return 0
|
||||
return _.round(state.imgNum / state.allImgNum, 2) * 100
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
submitUpload,
|
||||
handleProgress,
|
||||
handleChange,
|
||||
handleSuccess,
|
||||
handleError,
|
||||
handleRemove,
|
||||
handlePreview,
|
||||
handleExceed,
|
||||
handleShow,
|
||||
handleClose,
|
||||
percentage,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload {
|
||||
height: 500px;
|
||||
|
||||
.upload-content {
|
||||
.el-upload__tip {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep() {
|
||||
.el-upload--picture-card {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin: 3px 8px 8px;
|
||||
border: 2px dashed #c0ccda;
|
||||
|
||||
.ri-add-line {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload-list--picture {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-upload-list--picture-card {
|
||||
.el-upload-list__item {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin: 3px 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user