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_HOME_PATH = /dashboard
|
||||
VITE_HOME_PATH = /dashboard/monitor
|
||||
|
||||
# 本地存储前缀
|
||||
VITE_STORAGE_PREFIX =
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
type="primary"
|
||||
size="medium"
|
||||
:loading="confirmLoading"
|
||||
:disabled="confirmDisabled"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
@ -84,8 +83,6 @@ interface IDialogProps {
|
||||
fullscreen?: boolean
|
||||
/** 确认按钮加载状态 */
|
||||
loading?: boolean
|
||||
/** 确认按钮禁用状态 */
|
||||
confirmDisabled?: boolean
|
||||
/** 隐藏底部按钮 */
|
||||
footerHidden?: boolean
|
||||
/** 显示确认按钮 */
|
||||
@ -111,7 +108,6 @@ const props = withDefaults(defineProps<IDialogProps>(), {
|
||||
cancelText: '取消',
|
||||
fullscreen: false,
|
||||
loading: false,
|
||||
confirmDisabled: false,
|
||||
footerHidden: false,
|
||||
showConfirm: true,
|
||||
showCancel: true,
|
||||
|
||||
@ -96,7 +96,7 @@ export function setupRouterGuard(router: Router) {
|
||||
|
||||
// 如果用户已登录且访问login页面,重定向到首页
|
||||
if (to.name === 'login' && isLogin) {
|
||||
next({ path: import.meta.env.VITE_HOME_PATH || '/dashboard' })
|
||||
next({ path: '/' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
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',
|
||||
// path: '/personal-center',
|
||||
|
||||
@ -124,7 +124,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
coiMsgSuccess('登录成功!')
|
||||
|
||||
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 = {
|
||||
path: '/appRoot',
|
||||
name: 'appRoot',
|
||||
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard',
|
||||
redirect: import.meta.env.VITE_HOME_PATH || '/dashboard/monitor',
|
||||
component: Layout,
|
||||
meta: {
|
||||
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"
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
:confirm-disabled="isConfirmDisabled"
|
||||
@coi-confirm="handleUploadConfirm"
|
||||
@coi-cancel="handleUploadCancel"
|
||||
>
|
||||
@ -277,7 +276,7 @@
|
||||
</template>
|
||||
|
||||
<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 type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||
@ -346,34 +345,6 @@ const searchForm = ref<SysFileSearchForm>({
|
||||
const uploadFileList = ref<UploadFileInfo[]>([])
|
||||
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({
|
||||
fileService: 'LOCAL',
|
||||
@ -778,20 +749,7 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
||||
onProgress({ percent: 10 })
|
||||
|
||||
// 调用上传API,传递选择的存储类型
|
||||
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
|
||||
}
|
||||
await uploadFile(fileObj, folderName, 2, '-1', uploadForm.value.fileService)
|
||||
|
||||
// 设置完成进度
|
||||
onProgress({ percent: 100 })
|
||||
@ -804,12 +762,6 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
||||
catch (error: any) {
|
||||
onError()
|
||||
|
||||
// 从文件列表中移除失败的文件
|
||||
const index = uploadFileList.value.findIndex(item => item.id === file.id)
|
||||
if (index !== -1) {
|
||||
uploadFileList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 解析后端错误信息
|
||||
let errorMessage = `文件 ${file.file.name} 上传失败`
|
||||
|
||||
|
||||
@ -317,7 +317,6 @@
|
||||
height="auto"
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
:confirm-disabled="isConfirmDisabled"
|
||||
@coi-confirm="handleUploadConfirm"
|
||||
@coi-cancel="handleUploadCancel"
|
||||
>
|
||||
@ -406,7 +405,7 @@
|
||||
</template>
|
||||
|
||||
<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 type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
||||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||
@ -481,34 +480,6 @@ const searchForm = ref<SysPictureSearchForm>({
|
||||
const uploadFileList = ref<UploadFileInfo[]>([])
|
||||
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({
|
||||
pictureService: 'LOCAL',
|
||||
@ -912,24 +883,19 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
||||
try {
|
||||
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 })
|
||||
|
||||
// 调用上传API - 使用选择的分类和存储类型
|
||||
const result = 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
|
||||
}
|
||||
// console.log('调用上传API:', { pictureType: uploadForm.value.pictureType, pictureService: uploadForm.value.pictureService, fileSize: 2 })
|
||||
await uploadPicture(fileObj, uploadForm.value.pictureType, 2, uploadForm.value.pictureService)
|
||||
|
||||
// 设置完成进度
|
||||
onProgress({ percent: 100 })
|
||||
@ -944,12 +910,6 @@ async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
||||
|
||||
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} 上传失败`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user