Compare commits
4 Commits
3dbcf80402
...
c733dc5ffe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c733dc5ffe | ||
|
|
f5676bac4f | ||
|
|
1714927ecb | ||
|
|
d6bee84581 |
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/monitor
|
VITE_HOME_PATH = /dashboard
|
||||||
|
|
||||||
# 本地存储前缀
|
# 本地存储前缀
|
||||||
VITE_STORAGE_PREFIX =
|
VITE_STORAGE_PREFIX =
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
:loading="confirmLoading"
|
:loading="confirmLoading"
|
||||||
|
:disabled="confirmDisabled"
|
||||||
@click="handleConfirm"
|
@click="handleConfirm"
|
||||||
>
|
>
|
||||||
{{ confirmText }}
|
{{ confirmText }}
|
||||||
@ -83,6 +84,8 @@ interface IDialogProps {
|
|||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
/** 确认按钮加载状态 */
|
/** 确认按钮加载状态 */
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
/** 确认按钮禁用状态 */
|
||||||
|
confirmDisabled?: boolean
|
||||||
/** 隐藏底部按钮 */
|
/** 隐藏底部按钮 */
|
||||||
footerHidden?: boolean
|
footerHidden?: boolean
|
||||||
/** 显示确认按钮 */
|
/** 显示确认按钮 */
|
||||||
@ -108,6 +111,7 @@ 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: '/' })
|
next({ path: import.meta.env.VITE_HOME_PATH || '/dashboard' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,4 @@
|
|||||||
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 || '/',
|
path: query.redirect || import.meta.env.VITE_HOME_PATH || '/dashboard',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -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/monitor',
|
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
title: '',
|
title: '',
|
||||||
|
|||||||
@ -1,423 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
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,6 +196,7 @@
|
|||||||
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"
|
||||||
>
|
>
|
||||||
@ -276,7 +277,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, onMounted, ref } from 'vue'
|
import { computed, 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'
|
||||||
@ -345,6 +346,34 @@ 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',
|
||||||
@ -749,7 +778,20 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
|||||||
onProgress({ percent: 10 })
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
// 调用上传API,传递选择的存储类型
|
// 调用上传API,传递选择的存储类型
|
||||||
await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
|
const result = 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 })
|
||||||
@ -762,6 +804,12 @@ 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,6 +317,7 @@
|
|||||||
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"
|
||||||
>
|
>
|
||||||
@ -405,7 +406,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, onMounted, ref } from 'vue'
|
import { computed, 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'
|
||||||
@ -480,6 +481,34 @@ 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',
|
||||||
@ -883,19 +912,24 @@ 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 - 使用选择的分类和存储类型
|
||||||
// console.log('调用上传API:', { pictureType: uploadForm.value.pictureType, pictureService: uploadForm.value.pictureService, fileSize: 2 })
|
const result = await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
|
||||||
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 })
|
||||||
@ -910,6 +944,12 @@ 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