feat: 初始化项目结构并添加基础配置

添加前后端基础项目结构,包括.gitignore、package.json等配置文件
实现前端基础功能模块,包括路由、状态管理、API请求封装等
添加前端UI组件库和样式体系
配置开发环境Mock系统和构建工具链
This commit is contained in:
2026-03-18 14:03:35 +08:00
parent fc53f5620e
commit 9a387f3eec
504 changed files with 80629 additions and 0 deletions

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

View 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

View File

@@ -0,0 +1 @@
export { default } from 'vue-qr/src/packages/vue-qr.vue'

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

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