更新仪表盘
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="关于项目" class="about-card">
|
||||
<p>高性能 / 精致 / 优雅。基于Vue3 + Ant Design Vue 的中后台前端解决方案,如果喜欢就点个星星支持一下。</p>
|
||||
<p>
|
||||
<a href="https://gitee.com/lolicode/scui" target="_blank">
|
||||
<img src="https://gitee.com/lolicode/scui/badge/star.svg?theme=dark" alt="star" style="vertical-align: middle;">
|
||||
</a>
|
||||
</p>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'AboutWidget',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.about-card {
|
||||
p {
|
||||
color: #999;
|
||||
margin-top: 10px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="实时收入">
|
||||
<a-spin :spinning="loading">
|
||||
<div ref="chartRef" style="width: 100%; height: 300px;"></div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'EchartsWidget',
|
||||
})
|
||||
|
||||
const chartRef = ref(null)
|
||||
const loading = ref(true)
|
||||
let chart = null
|
||||
let timer = null
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chart = echarts.init(chartRef.value)
|
||||
|
||||
// 生成初始数据
|
||||
const now = new Date()
|
||||
const xData = []
|
||||
const yData = []
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const time = new Date(now.getTime() - i * 2000)
|
||||
xData.unshift(time.toLocaleTimeString().replace(/^\D*/, ''))
|
||||
yData.push(Math.round(Math.random() * 0))
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
boundaryGap: false,
|
||||
type: 'category',
|
||||
data: xData,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '价格',
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: '#409EFF',
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: '#79bbff',
|
||||
},
|
||||
data: yData,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 模拟实时更新
|
||||
timer = setInterval(() => {
|
||||
const newTime = new Date().toLocaleTimeString().replace(/^\D*/, '')
|
||||
const newValue = Math.round(Math.random() * 100)
|
||||
|
||||
xData.shift()
|
||||
xData.push(newTime)
|
||||
|
||||
yData.shift()
|
||||
yData.push(newValue)
|
||||
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: xData,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: yData,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, 2100)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟加载延迟
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
initChart()
|
||||
}, 500)
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 样式根据需要添加
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { markRaw } from 'vue'
|
||||
const resultComps = {}
|
||||
const files = import.meta.glob('./*.vue', { eager: true })
|
||||
Object.keys(files).forEach((fileName) => {
|
||||
let comp = files[fileName]
|
||||
resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, '$1')] = comp.default
|
||||
})
|
||||
export default markRaw(resultComps)
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="系统信息" class="info-card">
|
||||
<a-spin :spinning="loading">
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
||||
{{ item.values }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import systemApi from '@/api/system'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'InfoWidget',
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const sysInfo = ref([])
|
||||
|
||||
const getSystemList = async () => {
|
||||
try {
|
||||
const res = await systemApi.info.get()
|
||||
if (res.code === 1) {
|
||||
sysInfo.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSystemList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.info-card {
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
width: 120px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="进度环">
|
||||
<div class="progress">
|
||||
<a-progress type="dashboard" :percent="85.5" :width="160">
|
||||
<template #format="percent">
|
||||
<div class="percentage-value">{{ percent }}%</div>
|
||||
<div class="percentage-label">当前进度</div>
|
||||
</template>
|
||||
</a-progress>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'ProgressWidget',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.progress {
|
||||
text-align: center;
|
||||
|
||||
.percentage-value {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.percentage-label {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<a-row :gutter="10">
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="今日数量">
|
||||
<a-statistic :value="count.today" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="昨日数量">
|
||||
<a-statistic :value="count.yesterday" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="本周数量">
|
||||
<a-statistic :value="count.week" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="上周数量">
|
||||
<a-statistic :value="count.last_week" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="今年数量">
|
||||
<a-statistic :value="count.year" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-card :bordered="true" title="总数量">
|
||||
<a-statistic :value="count.all" :formatter="formatNumber" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import systemApi from '@/api/system'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'SmsWidget',
|
||||
})
|
||||
|
||||
const count = ref({})
|
||||
|
||||
const getSmsCount = async () => {
|
||||
try {
|
||||
// 注意:API中可能没有短信统计接口,这里使用模拟数据
|
||||
// 如果有接口,请取消注释以下代码
|
||||
// const res = await systemApi.sms.count.get()
|
||||
// if (res.code === 1) {
|
||||
// count.value = res.data
|
||||
// }
|
||||
|
||||
// 模拟数据
|
||||
count.value = {
|
||||
today: 1234,
|
||||
yesterday: 5678,
|
||||
week: 45678,
|
||||
last_week: 43210,
|
||||
year: 567890,
|
||||
all: 1234567
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取短信统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (value) => {
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSmsCount()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.ant-card-head-title) {
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="时钟" class="time-card">
|
||||
<div class="time">
|
||||
<h2>{{ time }}</h2>
|
||||
<p>{{ day }}</p>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import tool from '@/utils/tool'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'TimeWidget',
|
||||
})
|
||||
|
||||
const time = ref('')
|
||||
const day = ref('')
|
||||
let timer = null
|
||||
|
||||
const showTime = () => {
|
||||
time.value = tool.dateFormat(new Date(), 'hh:mm:ss')
|
||||
day.value = tool.dateFormat(new Date(), 'yyyy年MM月dd日')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
showTime()
|
||||
timer = setInterval(() => {
|
||||
showTime()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.time-card {
|
||||
background: linear-gradient(to right, #8e54e9, #4776e6);
|
||||
color: #fff;
|
||||
|
||||
:deep(.ant-card-head-title) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.ant-card-head) {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
h2 {
|
||||
font-size: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin-top: 13px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="版本信息" class="ver-card">
|
||||
<a-spin :spinning="loading">
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
||||
{{ item.values }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import systemApi from '@/api/system'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'VerWidget',
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const sysInfo = ref([])
|
||||
|
||||
const getSystemList = async () => {
|
||||
try {
|
||||
const res = await systemApi.version.get()
|
||||
if (res.code === 1) {
|
||||
sysInfo.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取版本信息失败:', error)
|
||||
// 使用模拟数据作为fallback
|
||||
sysInfo.value = [
|
||||
{ label: '系统版本', values: '1.0.0' },
|
||||
{ label: '框架版本', values: 'Vue 3.x' },
|
||||
{ label: '构建时间', values: '2024-01-01' },
|
||||
]
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSystemList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ver-card {
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
width: 120px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<a-card :bordered="true" title="欢迎">
|
||||
<div class="welcome">
|
||||
<div class="logo">
|
||||
<img src="/favicon.ico" alt="logo">
|
||||
<h2>VueAdmin</h2>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<div class="tips-item">
|
||||
<div class="tips-item-icon"><MenuOutlined /></div>
|
||||
<div class="tips-item-message">这里是项目控制台,你可以点击右上方的"自定义"按钮来添加移除或者移动部件。</div>
|
||||
</div>
|
||||
<div class="tips-item">
|
||||
<div class="tips-item-icon"><RocketOutlined /></div>
|
||||
<div class="tips-item-message">在提高前端算力、减少带宽请求和代码执行力上多次优化,并且持续着。</div>
|
||||
</div>
|
||||
<div class="tips-item">
|
||||
<div class="tips-item-icon"><CoffeeOutlined /></div>
|
||||
<div class="tips-item-message">项目目的:让前端工作更快乐</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MenuOutlined, RocketOutlined, CoffeeOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'WelcomeWidget',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.welcome {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome .logo {
|
||||
text-align: center;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 30px;
|
||||
font-weight: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tips-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 7.5px 0;
|
||||
}
|
||||
|
||||
.tips-item-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 18px;
|
||||
margin-right: 20px;
|
||||
color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.tips-item-message {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,598 @@
|
||||
<template>
|
||||
<div :class="['widgets-home', customizing ? 'customizing' : '']" ref="main">
|
||||
<div class="widgets-content">
|
||||
<div class="widgets-top">
|
||||
<div class="widgets-top-title">控制台</div>
|
||||
<div class="widgets-top-actions">
|
||||
<a-button v-if="customizing" type="primary" shape="round" @click="handleSave">
|
||||
<template #icon><CheckOutlined /></template>
|
||||
完成
|
||||
</a-button>
|
||||
<a-button v-else type="primary" shape="round" @click="handleCustom">
|
||||
<template #icon><EditOutlined /></template>
|
||||
自定义
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="widgets" ref="widgetsRef">
|
||||
<div class="widgets-wrapper">
|
||||
<div v-if="nowCompsList.length <= 0" class="no-widgets">
|
||||
<a-empty description="没有部件啦" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
<a-row :gutter="15">
|
||||
<a-col
|
||||
v-for="(item, index) in grid.layout"
|
||||
:key="index"
|
||||
:md="item"
|
||||
:xs="24"
|
||||
>
|
||||
<div class="draggable-wrapper">
|
||||
<draggable
|
||||
v-model="grid.compsList[index]"
|
||||
item-key="key"
|
||||
:animation="200"
|
||||
handle=".customize-overlay"
|
||||
group="widgets"
|
||||
class="draggable-box"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="widgets-item">
|
||||
<component :is="allComps[element]" />
|
||||
<div v-if="customizing" class="customize-overlay">
|
||||
<a-button
|
||||
class="close"
|
||||
type="primary"
|
||||
ghost
|
||||
shape="circle"
|
||||
@click="removeComp(element)"
|
||||
>
|
||||
<template #icon><CloseOutlined /></template>
|
||||
</a-button>
|
||||
<label>
|
||||
<component :is="allComps[element].icon" />
|
||||
{{ allComps[element].title }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义侧边栏 -->
|
||||
<a-drawer
|
||||
v-if="customizing"
|
||||
:open="customizing"
|
||||
:width="360"
|
||||
placement="right"
|
||||
:closable="false"
|
||||
:mask="false"
|
||||
class="widgets-drawer"
|
||||
>
|
||||
<template #title>
|
||||
<div class="widgets-aside-title">
|
||||
<PlusCircleOutlined /> 添加部件
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-button type="text" @click="handleClose">
|
||||
<template #icon><CloseOutlined /></template>
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 布局选择 -->
|
||||
<div class="select-layout">
|
||||
<h3>选择布局</h3>
|
||||
<div class="select-layout-options">
|
||||
<div
|
||||
class="select-layout-item item01"
|
||||
:class="{ active: grid.layout.join(',') === '12,6,6' }"
|
||||
@click="setLayout([12, 6, 6])"
|
||||
>
|
||||
<a-row :gutter="2">
|
||||
<a-col :span="12"><span></span></a-col>
|
||||
<a-col :span="6"><span></span></a-col>
|
||||
<a-col :span="6"><span></span></a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div
|
||||
class="select-layout-item item02"
|
||||
:class="{ active: grid.layout.join(',') === '24,16,8' }"
|
||||
@click="setLayout([24, 16, 8])"
|
||||
>
|
||||
<a-row :gutter="2">
|
||||
<a-col :span="24"><span></span></a-col>
|
||||
<a-col :span="16"><span></span></a-col>
|
||||
<a-col :span="8"><span></span></a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div
|
||||
class="select-layout-item item03"
|
||||
:class="{ active: grid.layout.join(',') === '24' }"
|
||||
@click="setLayout([24])"
|
||||
>
|
||||
<a-row :gutter="2">
|
||||
<a-col :span="24"><span></span></a-col>
|
||||
<a-col :span="24"><span></span></a-col>
|
||||
<a-col :span="24"><span></span></a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 部件列表 -->
|
||||
<div class="widgets-list">
|
||||
<h3>可用部件</h3>
|
||||
<div v-if="myCompsList.length <= 0" class="widgets-list-nodata">
|
||||
<a-empty description="没有部件啦" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
<div v-for="item in myCompsList" :key="item.key" class="widgets-list-item">
|
||||
<div class="item-logo">
|
||||
<component :is="item.icon" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a-button type="primary" @click="addComp(item)">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleResetDefault">恢复默认</a-button>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { Empty } from 'ant-design-vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import {
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
CloseOutlined,
|
||||
PlusCircleOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import allComps from './components'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'WidgetsPage',
|
||||
})
|
||||
|
||||
// 定义组件元数据
|
||||
allComps.welcome.icon = 'GiftOutlined'
|
||||
allComps.welcome.title = '欢迎'
|
||||
allComps.welcome.description = '项目特色以及文档链接'
|
||||
|
||||
allComps.info.icon = 'MonitorOutlined'
|
||||
allComps.info.title = '系统信息'
|
||||
allComps.info.description = '当前项目系统信息'
|
||||
|
||||
allComps.about.icon = 'SettingOutlined'
|
||||
allComps.about.title = '关于项目'
|
||||
allComps.about.description = '点个星星支持一下'
|
||||
|
||||
allComps.echarts.icon = 'LineChartOutlined'
|
||||
allComps.echarts.title = '实时收入'
|
||||
allComps.echarts.description = 'Echarts组件演示'
|
||||
|
||||
allComps.progress.icon = 'DashboardOutlined'
|
||||
allComps.progress.title = '进度环'
|
||||
allComps.progress.description = '进度环原子组件演示'
|
||||
|
||||
allComps.time.icon = 'ClockCircleOutlined'
|
||||
allComps.time.title = '时钟'
|
||||
allComps.time.description = '演示部件效果'
|
||||
|
||||
allComps.sms.icon = 'MessageOutlined'
|
||||
allComps.sms.title = '短信统计'
|
||||
allComps.sms.description = '短信统计'
|
||||
|
||||
allComps.ver.icon = 'FileTextOutlined'
|
||||
allComps.ver.title = '版本信息'
|
||||
allComps.ver.description = '当前项目版本信息'
|
||||
|
||||
// 导入图标组件
|
||||
import {
|
||||
GiftOutlined,
|
||||
MonitorOutlined,
|
||||
SettingOutlined as SettingIcon,
|
||||
LineChartOutlined,
|
||||
DashboardOutlined,
|
||||
ClockCircleOutlined,
|
||||
MessageOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
GiftOutlined,
|
||||
MonitorOutlined,
|
||||
SettingOutlined: SettingIcon,
|
||||
LineChartOutlined,
|
||||
DashboardOutlined,
|
||||
ClockCircleOutlined,
|
||||
MessageOutlined,
|
||||
FileTextOutlined,
|
||||
}
|
||||
|
||||
// 替换组件中的icon引用
|
||||
Object.keys(allComps).forEach((key) => {
|
||||
if (allComps[key].icon && typeof allComps[key].icon === 'string') {
|
||||
allComps[key].icon = iconMap[allComps[key].icon] || GiftOutlined
|
||||
}
|
||||
})
|
||||
|
||||
const customizing = ref(false)
|
||||
const widgetsRef = ref(null)
|
||||
const defaultGrid = {
|
||||
layout: [12, 6, 6],
|
||||
compsList: [['welcome', 'info'], ['echarts', 'progress'], ['time', 'sms']],
|
||||
}
|
||||
const grid = reactive({ layout: [], compsList: [] })
|
||||
|
||||
// 初始化
|
||||
const initGrid = () => {
|
||||
const savedGrid = localStorage.getItem('widgetsGrid')
|
||||
if (savedGrid) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedGrid)
|
||||
grid.layout = parsed.layout
|
||||
grid.compsList = parsed.compsList
|
||||
} catch (e) {
|
||||
resetToDefault()
|
||||
}
|
||||
} else {
|
||||
resetToDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
grid.layout = [...defaultGrid.layout]
|
||||
grid.compsList = defaultGrid.compsList.map((arr) => [...arr])
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const allCompsList = computed(() => {
|
||||
const list = []
|
||||
for (const key in allComps) {
|
||||
list.push({
|
||||
key,
|
||||
title: allComps[key].title,
|
||||
icon: allComps[key].icon,
|
||||
description: allComps[key].description,
|
||||
})
|
||||
}
|
||||
const myCompKeys = grid.compsList.flat()
|
||||
list.forEach((comp) => {
|
||||
comp.disabled = myCompKeys.includes(comp.key)
|
||||
})
|
||||
return list
|
||||
})
|
||||
|
||||
const myCompsList = computed(() => {
|
||||
return allCompsList.value.filter((item) => !item.disabled)
|
||||
})
|
||||
|
||||
const nowCompsList = computed(() => {
|
||||
return grid.compsList.flat()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleCustom = () => {
|
||||
customizing.value = true
|
||||
const oldWidth = widgetsRef.value?.offsetWidth || 0
|
||||
nextTick(() => {
|
||||
if (widgetsRef.value) {
|
||||
const scale = widgetsRef.value.offsetWidth / oldWidth
|
||||
widgetsRef.value.style.setProperty('transform', `scale(${scale})`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setLayout = (layout) => {
|
||||
grid.layout = layout
|
||||
if (layout.join(',') === '24') {
|
||||
grid.compsList[0] = [...grid.compsList[0], ...grid.compsList[1], ...grid.compsList[2]]
|
||||
grid.compsList[1] = []
|
||||
grid.compsList[2] = []
|
||||
}
|
||||
}
|
||||
|
||||
const addComp = (item) => {
|
||||
grid.compsList[0].push(item.key)
|
||||
}
|
||||
|
||||
const removeComp = (key) => grid.compsList.forEach((list, index) => {
|
||||
grid.compsList[index] = list.filter((k) => k !== key)
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
customizing.value = false
|
||||
if (widgetsRef.value) {
|
||||
widgetsRef.value.style.removeProperty('transform')
|
||||
}
|
||||
localStorage.setItem('widgetsGrid', JSON.stringify(grid))
|
||||
emit('on-mounted')
|
||||
}
|
||||
|
||||
const handleResetDefault = () => {
|
||||
customizing.value = false
|
||||
if (widgetsRef.value) {
|
||||
widgetsRef.value.style.removeProperty('transform')
|
||||
}
|
||||
resetToDefault()
|
||||
localStorage.removeItem('widgetsGrid')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
customizing.value = false
|
||||
if (widgetsRef.value) {
|
||||
widgetsRef.value.style.removeProperty('transform')
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initGrid()
|
||||
emit('on-mounted')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-mounted'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.widgets-home {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.widgets-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.widgets-top {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.widgets-top-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widgets {
|
||||
transform-origin: top left;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.customizing .widgets-wrapper {
|
||||
margin-right: -360px;
|
||||
}
|
||||
|
||||
.customizing .widgets-wrapper .ant-col {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.customizing .widgets-wrapper .draggable-wrapper {
|
||||
border: 1px dashed #1890ff;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.customizing .widgets-wrapper .no-widgets {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.customizing .widgets-item {
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.customize-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.customize-overlay:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.customize-overlay label {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
height: 40px;
|
||||
padding: 0 30px;
|
||||
border-radius: 40px;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
margin-top: 8px;
|
||||
|
||||
.anticon {
|
||||
margin-right: 15px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.customize-overlay .close {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.widgets-list {
|
||||
margin-top: 24px;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.widgets-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.widgets-list-item .item-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(180, 180, 180, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
margin-right: 15px;
|
||||
color: #6a8bad;
|
||||
}
|
||||
|
||||
.widgets-list-item .item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.widgets-list-item .item-info h2 {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
cursor: default;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.widgets-list-item .item-info p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.widgets-wrapper .sortable-ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.select-layout {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-layout-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.select-layout-item {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px solid #d9d9d9;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
background: #d9d9d9;
|
||||
height: 46px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.item02 span {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&.item02 .ant-col:nth-child(1) span {
|
||||
height: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&.item03 span {
|
||||
height: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #1890ff;
|
||||
|
||||
span {
|
||||
background: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widgets-drawer {
|
||||
:deep(.ant-drawer-body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.widgets-aside-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.anticon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.customizing .widgets {
|
||||
transform: scale(1) !important;
|
||||
}
|
||||
.customizing .widgets-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
.customizing .widgets-wrapper {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user