Compare commits
No commits in common. "c733dc5ffe2e2d507ee416d9cb900bf75c2aa13b" and "3dbcf804029e4d22e8d54ba9b830da11c39e1b07" have entirely different histories.
c733dc5ffe
...
3dbcf80402
2
.env
2
.env
@ -11,7 +11,7 @@ VITE_ROUTE_MODE = web
|
|||||||
VITE_ROUTE_LOAD_MODE = dynamic
|
VITE_ROUTE_LOAD_MODE = dynamic
|
||||||
|
|
||||||
# 设置登陆后跳转地址
|
# 设置登陆后跳转地址
|
||||||
VITE_HOME_PATH = /dashboard
|
VITE_HOME_PATH = /dashboard/monitor
|
||||||
|
|
||||||
# 本地存储前缀
|
# 本地存储前缀
|
||||||
VITE_STORAGE_PREFIX =
|
VITE_STORAGE_PREFIX =
|
||||||
|
|||||||
@ -52,7 +52,6 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
:loading="confirmLoading"
|
:loading="confirmLoading"
|
||||||
:disabled="confirmDisabled"
|
|
||||||
@click="handleConfirm"
|
@click="handleConfirm"
|
||||||
>
|
>
|
||||||
{{ confirmText }}
|
{{ confirmText }}
|
||||||
@ -84,8 +83,6 @@ interface IDialogProps {
|
|||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
/** 确认按钮加载状态 */
|
/** 确认按钮加载状态 */
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
/** 确认按钮禁用状态 */
|
|
||||||
confirmDisabled?: boolean
|
|
||||||
/** 隐藏底部按钮 */
|
/** 隐藏底部按钮 */
|
||||||
footerHidden?: boolean
|
footerHidden?: boolean
|
||||||
/** 显示确认按钮 */
|
/** 显示确认按钮 */
|
||||||
@ -111,7 +108,6 @@ const props = withDefaults(defineProps<IDialogProps>(), {
|
|||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
confirmDisabled: false,
|
|
||||||
footerHidden: false,
|
footerHidden: false,
|
||||||
showConfirm: true,
|
showConfirm: true,
|
||||||
showCancel: true,
|
showCancel: true,
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export function setupRouterGuard(router: Router) {
|
|||||||
|
|
||||||
// 如果用户已登录且访问login页面,重定向到首页
|
// 如果用户已登录且访问login页面,重定向到首页
|
||||||
if (to.name === 'login' && isLogin) {
|
if (to.name === 'login' && isLogin) {
|
||||||
next({ path: import.meta.env.VITE_HOME_PATH || '/dashboard' })
|
next({ path: '/' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,16 @@
|
|||||||
export const staticRoutes: AppRoute.RowRoute[] = [
|
export const staticRoutes: AppRoute.RowRoute[] = [
|
||||||
|
{
|
||||||
|
name: 'monitor',
|
||||||
|
path: '/dashboard/monitor',
|
||||||
|
title: '仪表盘',
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: 'icon-park-outline:dashboard-one',
|
||||||
|
menuType: '2',
|
||||||
|
componentPath: '/dashboard/monitor/index',
|
||||||
|
id: 3,
|
||||||
|
pid: null,
|
||||||
|
pinTab: true,
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// name: 'personal-center',
|
// name: 'personal-center',
|
||||||
// path: '/personal-center',
|
// path: '/personal-center',
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export const useAuthStore = defineStore('auth-store', {
|
|||||||
coiMsgSuccess('登录成功!')
|
coiMsgSuccess('登录成功!')
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
path: query.redirect || import.meta.env.VITE_HOME_PATH || '/dashboard',
|
path: query.redirect || '/',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -140,7 +140,7 @@ export function createRoutes(routeData: (AppRoute.BackendRoute | AppRoute.RowRou
|
|||||||
const appRootRoute: RouteRecordRaw = {
|
const appRootRoute: RouteRecordRaw = {
|
||||||
path: '/appRoot',
|
path: '/appRoot',
|
||||||
name: 'appRoot',
|
name: 'appRoot',
|
||||||
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard',
|
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard/monitor',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
title: '',
|
title: '',
|
||||||
|
|||||||
423
src/views/dashboard/monitor/components/OperationPieChart.vue
Normal file
423
src/views/dashboard/monitor/components/OperationPieChart.vue
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
<template>
|
||||||
|
<div class="operation-pie-chart">
|
||||||
|
<!-- 图表容器 -->
|
||||||
|
<div class="chart-container">
|
||||||
|
<svg
|
||||||
|
class="pie-svg"
|
||||||
|
:width="chartSize"
|
||||||
|
:height="chartSize"
|
||||||
|
:viewBox="`0 0 ${chartSize} ${chartSize}`"
|
||||||
|
>
|
||||||
|
<!-- 饼图片段 -->
|
||||||
|
<g class="pie-segments" :transform="`translate(${center}, ${center})`">
|
||||||
|
<path
|
||||||
|
v-for="(segment, index) in pieSegments"
|
||||||
|
:key="index"
|
||||||
|
:d="segment.path"
|
||||||
|
:fill="segment.color"
|
||||||
|
class="pie-segment" :class="[{ active: hoveredIndex === index }]"
|
||||||
|
@mouseenter="hoverSegment(index, $event)"
|
||||||
|
@mouseleave="unhoverSegment"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 中心文字 -->
|
||||||
|
<g class="center-text" :transform="`translate(${center}, ${center})`">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="-10"
|
||||||
|
text-anchor="middle"
|
||||||
|
class="center-title"
|
||||||
|
>
|
||||||
|
总操作数
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="15"
|
||||||
|
text-anchor="middle"
|
||||||
|
class="center-value"
|
||||||
|
>
|
||||||
|
{{ totalOperations.toLocaleString() }}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- 工具提示 -->
|
||||||
|
<div
|
||||||
|
v-if="tooltip.visible"
|
||||||
|
class="chart-tooltip"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<div class="tooltip-header">
|
||||||
|
<span class="tooltip-dot" :style="{ backgroundColor: tooltip.color }" />
|
||||||
|
<span class="tooltip-type">{{ tooltip.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-stats">
|
||||||
|
<div>数量: {{ tooltip.count?.toLocaleString() }}</div>
|
||||||
|
<div>占比: {{ tooltip.percentage }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图例 -->
|
||||||
|
<div class="chart-legend">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in chartData"
|
||||||
|
:key="index"
|
||||||
|
class="legend-item"
|
||||||
|
:class="{ active: hoveredIndex === index }"
|
||||||
|
@mouseenter="hoverSegment(index)"
|
||||||
|
@mouseleave="unhoverSegment"
|
||||||
|
>
|
||||||
|
<div class="legend-color" :style="{ backgroundColor: item.color }" />
|
||||||
|
<div class="legend-content">
|
||||||
|
<div class="legend-type">
|
||||||
|
{{ item.type }}
|
||||||
|
</div>
|
||||||
|
<div class="legend-stats">
|
||||||
|
<span class="legend-count">{{ item.count.toLocaleString() }}</span>
|
||||||
|
<span class="legend-percentage">{{ item.percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
|
||||||
|
// Props定义
|
||||||
|
interface OperationData {
|
||||||
|
type: string
|
||||||
|
count: number
|
||||||
|
percentage: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: OperationData[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const hoveredIndex = ref<number | null>(null)
|
||||||
|
const chartSize = 240
|
||||||
|
const center = chartSize / 2
|
||||||
|
const radius = 80
|
||||||
|
const innerRadius = 30
|
||||||
|
|
||||||
|
// 工具提示状态
|
||||||
|
const tooltip = reactive({
|
||||||
|
visible: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
type: '',
|
||||||
|
count: 0,
|
||||||
|
percentage: 0,
|
||||||
|
color: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const chartData = computed(() => props.data || [])
|
||||||
|
|
||||||
|
const totalOperations = computed(() => {
|
||||||
|
return chartData.value.reduce((sum, item) => sum + item.count, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 饼图片段计算
|
||||||
|
const pieSegments = computed(() => {
|
||||||
|
let currentAngle = -Math.PI / 2 // 从顶部开始
|
||||||
|
|
||||||
|
return chartData.value.map((item, index) => {
|
||||||
|
const angleSize = (item.percentage / 100) * 2 * Math.PI
|
||||||
|
const startAngle = currentAngle
|
||||||
|
const endAngle = currentAngle + angleSize
|
||||||
|
|
||||||
|
// 计算路径
|
||||||
|
const path = createArcPath(0, 0, innerRadius, radius, startAngle, endAngle)
|
||||||
|
|
||||||
|
currentAngle = endAngle
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
color: item.color,
|
||||||
|
data: item,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建圆弧路径
|
||||||
|
function createArcPath(
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
innerRadius: number,
|
||||||
|
outerRadius: number,
|
||||||
|
startAngle: number,
|
||||||
|
endAngle: number,
|
||||||
|
): string {
|
||||||
|
const startOuterX = centerX + outerRadius * Math.cos(startAngle)
|
||||||
|
const startOuterY = centerY + outerRadius * Math.sin(startAngle)
|
||||||
|
const endOuterX = centerX + outerRadius * Math.cos(endAngle)
|
||||||
|
const endOuterY = centerY + outerRadius * Math.sin(endAngle)
|
||||||
|
|
||||||
|
const startInnerX = centerX + innerRadius * Math.cos(endAngle)
|
||||||
|
const startInnerY = centerY + innerRadius * Math.sin(endAngle)
|
||||||
|
const endInnerX = centerX + innerRadius * Math.cos(startAngle)
|
||||||
|
const endInnerY = centerY + innerRadius * Math.sin(startAngle)
|
||||||
|
|
||||||
|
const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0
|
||||||
|
|
||||||
|
return [
|
||||||
|
'M',
|
||||||
|
startOuterX,
|
||||||
|
startOuterY,
|
||||||
|
'A',
|
||||||
|
outerRadius,
|
||||||
|
outerRadius,
|
||||||
|
0,
|
||||||
|
largeArcFlag,
|
||||||
|
1,
|
||||||
|
endOuterX,
|
||||||
|
endOuterY,
|
||||||
|
'L',
|
||||||
|
startInnerX,
|
||||||
|
startInnerY,
|
||||||
|
'A',
|
||||||
|
innerRadius,
|
||||||
|
innerRadius,
|
||||||
|
0,
|
||||||
|
largeArcFlag,
|
||||||
|
0,
|
||||||
|
endInnerX,
|
||||||
|
endInnerY,
|
||||||
|
'Z',
|
||||||
|
].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具提示样式
|
||||||
|
const tooltipStyle = computed(() => ({
|
||||||
|
left: `${tooltip.x}px`,
|
||||||
|
top: `${tooltip.y}px`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 悬停处理
|
||||||
|
function hoverSegment(index: number, event?: MouseEvent) {
|
||||||
|
hoveredIndex.value = index
|
||||||
|
const item = chartData.value[index]
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
const rect = (event.target as Element).closest('.chart-container')?.getBoundingClientRect()
|
||||||
|
if (rect) {
|
||||||
|
tooltip.visible = true
|
||||||
|
tooltip.x = event.clientX - rect.left + 10
|
||||||
|
tooltip.y = event.clientY - rect.top - 40
|
||||||
|
tooltip.type = item.type
|
||||||
|
tooltip.count = item.count
|
||||||
|
tooltip.percentage = item.percentage
|
||||||
|
tooltip.color = item.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhoverSegment() {
|
||||||
|
hoveredIndex.value = null
|
||||||
|
tooltip.visible = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.operation-pie-chart {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-segment {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-segment:hover,
|
||||||
|
.pie-segment.active {
|
||||||
|
transform: scale(1.05);
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text .center-title {
|
||||||
|
font-size: 12px;
|
||||||
|
fill: var(--text-color-2);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text .center-value {
|
||||||
|
font-size: 16px;
|
||||||
|
fill: var(--text-color-1);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--popover-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
color: var(--text-color-2);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item:hover,
|
||||||
|
.legend-item.active {
|
||||||
|
background: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-type {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-percentage {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.operation-pie-chart {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-type {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-count {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
.pie-segment {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: fadeInScale 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为每个片段添加延迟动画 */
|
||||||
|
.pie-segment:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.pie-segment:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.pie-segment:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
.pie-segment:nth-child(4) { animation-delay: 0.4s; }
|
||||||
|
.pie-segment:nth-child(5) { animation-delay: 0.5s; }
|
||||||
|
</style>
|
||||||
24
src/views/dashboard/monitor/components/chart.vue
Normal file
24
src/views/dashboard/monitor/components/chart.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-placeholder h-200px w-full flex-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-lg font-bold text-gray-400 mb-2">
|
||||||
|
监控图表
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
图表功能已简化
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 简化的图表占位组件
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-placeholder {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/views/dashboard/monitor/components/chart2.vue
Normal file
24
src/views/dashboard/monitor/components/chart2.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-placeholder h-200px w-full flex-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-lg font-bold text-gray-400 mb-2">
|
||||||
|
柱状图
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
图表功能已简化
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 简化的图表占位组件
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-placeholder {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/views/dashboard/monitor/components/chart3.vue
Normal file
24
src/views/dashboard/monitor/components/chart3.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart-placeholder h-200px w-full flex-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-lg font-bold text-gray-400 mb-2">
|
||||||
|
饼图
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
图表功能已简化
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 简化的图表占位组件
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-placeholder {
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/views/dashboard/monitor/mockData.ts
Normal file
116
src/views/dashboard/monitor/mockData.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import type { DashboardData } from './types'
|
||||||
|
|
||||||
|
// 生成模拟仪表盘数据
|
||||||
|
export function generateMockDashboardData(): DashboardData {
|
||||||
|
return {
|
||||||
|
// 用户统计数据
|
||||||
|
userStats: {
|
||||||
|
totalUsers: 1286,
|
||||||
|
todayNewUsers: 23,
|
||||||
|
activeUsers: 856,
|
||||||
|
onlineUsers: 142,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 登录统计数据
|
||||||
|
loginStats: {
|
||||||
|
todayLogins: 468,
|
||||||
|
totalLogins: 45672,
|
||||||
|
loginTrend: generateLoginTrendData(),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 存储统计数据
|
||||||
|
storageStats: {
|
||||||
|
totalFiles: 8924,
|
||||||
|
totalImages: 3420,
|
||||||
|
totalSize: '2.3 GB',
|
||||||
|
todayUploads: 67,
|
||||||
|
storageUsage: 67.5,
|
||||||
|
availableSpace: '1.2 GB',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 今日活跃数据
|
||||||
|
dailyActivityStats: {
|
||||||
|
todayVisits: 1247,
|
||||||
|
todayOperations: 856,
|
||||||
|
activeUsers: 142,
|
||||||
|
newContent: 23,
|
||||||
|
apiCalls: 3420,
|
||||||
|
avgResponseTime: 235,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 系统状态
|
||||||
|
systemStatus: {
|
||||||
|
diskUsage: 67.5,
|
||||||
|
memoryUsage: 43.2,
|
||||||
|
cpuUsage: 28.7,
|
||||||
|
systemHealth: 'good',
|
||||||
|
uptime: '15天 8小时 23分钟',
|
||||||
|
lastBackup: '2024-01-15 02:30:00',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成登录趋势数据(最近7天)
|
||||||
|
function generateLoginTrendData() {
|
||||||
|
const trendData = []
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const date = new Date(today)
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
|
||||||
|
const dateStr = date.toISOString().split('T')[0]
|
||||||
|
const label = date.toLocaleDateString('zh-CN', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成随机但合理的登录数量
|
||||||
|
const baseCount = 300
|
||||||
|
const randomVariation = Math.floor(Math.random() * 200) - 100
|
||||||
|
const weekendMultiplier
|
||||||
|
= date.getDay() === 0 || date.getDay() === 6 ? 0.6 : 1
|
||||||
|
const count = Math.max(
|
||||||
|
50,
|
||||||
|
Math.floor((baseCount + randomVariation) * weekendMultiplier),
|
||||||
|
)
|
||||||
|
|
||||||
|
trendData.push({
|
||||||
|
date: dateStr,
|
||||||
|
count,
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return trendData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机颜色
|
||||||
|
export function generateRandomColor(): string {
|
||||||
|
const colors = [
|
||||||
|
'#18A058',
|
||||||
|
'#2080F0',
|
||||||
|
'#F0A020',
|
||||||
|
'#D03050',
|
||||||
|
'#722ED1',
|
||||||
|
'#13C2C2',
|
||||||
|
'#52C41A',
|
||||||
|
'#1890FF',
|
||||||
|
'#FAAD14',
|
||||||
|
'#F5222D',
|
||||||
|
]
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
export function getStatusColor(status: string): string {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
success: 'var(--success-color)',
|
||||||
|
failed: 'var(--error-color)',
|
||||||
|
warning: 'var(--warning-color)',
|
||||||
|
info: 'var(--info-color)',
|
||||||
|
good: 'var(--success-color)',
|
||||||
|
critical: 'var(--error-color)',
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'var(--text-color-3)'
|
||||||
|
}
|
||||||
@ -196,7 +196,6 @@
|
|||||||
height="auto"
|
height="auto"
|
||||||
confirm-text="确定"
|
confirm-text="确定"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
:confirm-disabled="isConfirmDisabled"
|
|
||||||
@coi-confirm="handleUploadConfirm"
|
@coi-confirm="handleUploadConfirm"
|
||||||
@coi-cancel="handleUploadCancel"
|
@coi-cancel="handleUploadCancel"
|
||||||
>
|
>
|
||||||
@ -277,7 +276,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, ref } from 'vue'
|
import { h, onMounted, ref } from 'vue'
|
||||||
import { NButton, NIcon, NP, NPopconfirm, NSpace, NTag, NText, NUpload, NUploadDragger } from 'naive-ui'
|
import { NButton, NIcon, NP, NPopconfirm, NSpace, NTag, NText, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
import type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
import type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
||||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||||
@ -346,34 +345,6 @@ const searchForm = ref<SysFileSearchForm>({
|
|||||||
const uploadFileList = ref<UploadFileInfo[]>([])
|
const uploadFileList = ref<UploadFileInfo[]>([])
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
|
|
||||||
// 计算属性:判断确定按钮是否应该禁用
|
|
||||||
const isConfirmDisabled = computed(() => {
|
|
||||||
const fileList = uploadFileList.value
|
|
||||||
|
|
||||||
// 没有文件时禁用
|
|
||||||
if (fileList.length === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有文件正在上传时禁用(pending 或 uploading 状态)
|
|
||||||
const uploadingFiles = fileList.filter(file =>
|
|
||||||
file.status === 'pending' || file.status === 'uploading',
|
|
||||||
)
|
|
||||||
if (uploadingFiles.length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有文件上传失败时禁用(error 状态)
|
|
||||||
const errorFiles = fileList.filter(file => file.status === 'error')
|
|
||||||
if (errorFiles.length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有所有文件都上传成功时才启用
|
|
||||||
const finishedFiles = fileList.filter(file => file.status === 'finished')
|
|
||||||
return finishedFiles.length === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 上传表单数据
|
// 上传表单数据
|
||||||
const uploadForm = ref({
|
const uploadForm = ref({
|
||||||
fileService: 'LOCAL',
|
fileService: 'LOCAL',
|
||||||
@ -778,20 +749,7 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
onProgress({ percent: 10 })
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
// 调用上传API,传递选择的存储类型
|
// 调用上传API,传递选择的存储类型
|
||||||
const result = await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
|
await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
|
||||||
|
|
||||||
// 检查业务逻辑是否成功
|
|
||||||
if (result.isSuccess === false) {
|
|
||||||
// 业务错误:HTTP状态码200但业务逻辑失败
|
|
||||||
// 注意:HTTP拦截器已经显示了错误信息,这里只需要标记为失败
|
|
||||||
onError()
|
|
||||||
// 从文件列表中移除失败的文件
|
|
||||||
const index = uploadFileList.value.findIndex(item => item.id === file.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
uploadFileList.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置完成进度
|
// 设置完成进度
|
||||||
onProgress({ percent: 100 })
|
onProgress({ percent: 100 })
|
||||||
@ -804,12 +762,6 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
onError()
|
onError()
|
||||||
|
|
||||||
// 从文件列表中移除失败的文件
|
|
||||||
const index = uploadFileList.value.findIndex(item => item.id === file.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
uploadFileList.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析后端错误信息
|
// 解析后端错误信息
|
||||||
let errorMessage = `文件 ${file.file.name} 上传失败`
|
let errorMessage = `文件 ${file.file.name} 上传失败`
|
||||||
|
|
||||||
|
|||||||
@ -317,7 +317,6 @@
|
|||||||
height="auto"
|
height="auto"
|
||||||
confirm-text="确定"
|
confirm-text="确定"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
:confirm-disabled="isConfirmDisabled"
|
|
||||||
@coi-confirm="handleUploadConfirm"
|
@coi-confirm="handleUploadConfirm"
|
||||||
@coi-cancel="handleUploadCancel"
|
@coi-cancel="handleUploadCancel"
|
||||||
>
|
>
|
||||||
@ -406,7 +405,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, ref } from 'vue'
|
import { h, onMounted, ref } from 'vue'
|
||||||
import { NButton, NIcon, NP, NPopconfirm, NSpace, NSpin, NTag, NText, NTooltip, NUpload, NUploadDragger } from 'naive-ui'
|
import { NButton, NIcon, NP, NPopconfirm, NSpace, NSpin, NTag, NText, NTooltip, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
import type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
import type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
||||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||||
@ -481,34 +480,6 @@ const searchForm = ref<SysPictureSearchForm>({
|
|||||||
const uploadFileList = ref<UploadFileInfo[]>([])
|
const uploadFileList = ref<UploadFileInfo[]>([])
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
|
|
||||||
// 计算属性:判断确定按钮是否应该禁用
|
|
||||||
const isConfirmDisabled = computed(() => {
|
|
||||||
const fileList = uploadFileList.value
|
|
||||||
|
|
||||||
// 没有文件时禁用
|
|
||||||
if (fileList.length === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有文件正在上传时禁用(pending 或 uploading 状态)
|
|
||||||
const uploadingFiles = fileList.filter(file =>
|
|
||||||
file.status === 'pending' || file.status === 'uploading',
|
|
||||||
)
|
|
||||||
if (uploadingFiles.length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有文件上传失败时禁用(error 状态)
|
|
||||||
const errorFiles = fileList.filter(file => file.status === 'error')
|
|
||||||
if (errorFiles.length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有所有文件都上传成功时才启用
|
|
||||||
const finishedFiles = fileList.filter(file => file.status === 'finished')
|
|
||||||
return finishedFiles.length === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 上传表单数据
|
// 上传表单数据
|
||||||
const uploadForm = ref({
|
const uploadForm = ref({
|
||||||
pictureService: 'LOCAL',
|
pictureService: 'LOCAL',
|
||||||
@ -912,24 +883,19 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
try {
|
try {
|
||||||
const fileObj = file.file as File
|
const fileObj = file.file as File
|
||||||
|
|
||||||
|
// 添加上传开始日志
|
||||||
|
// console.log('customUpload 开始上传:', {
|
||||||
|
// fileName: fileObj.name,
|
||||||
|
// fileSize: fileObj.size,
|
||||||
|
// fileSizeMB: (fileObj.size / 1024 / 1024).toFixed(2),
|
||||||
|
// })
|
||||||
|
|
||||||
// 设置进度
|
// 设置进度
|
||||||
onProgress({ percent: 10 })
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
// 调用上传API - 使用选择的分类和存储类型
|
// 调用上传API - 使用选择的分类和存储类型
|
||||||
const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
|
// console.log('调用上传API:', { pictureType: uploadForm.value.pictureType, pictureService: uploadForm.value.pictureService, fileSize: 2 })
|
||||||
|
await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
|
||||||
// 检查业务逻辑是否成功
|
|
||||||
if (result.isSuccess === false) {
|
|
||||||
// 业务错误:HTTP状态码200但业务逻辑失败
|
|
||||||
// 注意:HTTP拦截器已经显示了错误信息,这里只需要标记为失败
|
|
||||||
onError()
|
|
||||||
// 从文件列表中移除失败的文件
|
|
||||||
const index = uploadFileList.value.findIndex(item => item.id === file.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
uploadFileList.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置完成进度
|
// 设置完成进度
|
||||||
onProgress({ percent: 100 })
|
onProgress({ percent: 100 })
|
||||||
@ -944,12 +910,6 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
|
|
||||||
console.error('图片上传失败:', error)
|
console.error('图片上传失败:', error)
|
||||||
|
|
||||||
// 从文件列表中移除失败的文件
|
|
||||||
const index = uploadFileList.value.findIndex(item => item.id === file.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
uploadFileList.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析后端返回的错误信息
|
// 解析后端返回的错误信息
|
||||||
let errorMessage = `图片 ${file.file.name} 上传失败`
|
let errorMessage = `图片 ${file.file.name} 上传失败`
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user