Compare commits
9 Commits
7353c4eede
...
c550369a4b
| Author | SHA1 | Date | |
|---|---|---|---|
| c550369a4b | |||
| a9c2a499ce | |||
| 24dd0c17da | |||
| 913e9e63ad | |||
| 3abdc187fb | |||
| 809dc883f5 | |||
| 7c505f8169 | |||
| 3654ccb9db | |||
| 195aa7ccff |
@@ -1,24 +0,0 @@
|
|||||||
import { AxiosProgressEvent } from 'axios'
|
|
||||||
import { request } from '@/api/request'
|
|
||||||
|
|
||||||
/** 上传文件 */
|
|
||||||
const FtsUpload = (data: FormData, onUploadProgress?: (progress: number) => void) => {
|
|
||||||
data.append('provider', 'local')
|
|
||||||
data.append('bucket', 'visual')
|
|
||||||
|
|
||||||
return request.post(`/Assets/v1/fts/uploader`, data, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
onUploadProgress: onUploadProgress
|
|
||||||
? (progressEvent: AxiosProgressEvent) => {
|
|
||||||
if (progressEvent.total) {
|
|
||||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
|
||||||
onUploadProgress(percentCompleted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FtsUpload
|
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
import { request } from '@/api/request'
|
import { request } from '@/api/request'
|
||||||
|
|
||||||
|
export interface Floor {
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
datacenter_id?: number
|
||||||
|
floor_number?: number
|
||||||
|
status?: string
|
||||||
|
area?: number
|
||||||
|
height?: number
|
||||||
|
load_bearing?: number
|
||||||
|
enabled?: boolean
|
||||||
|
layout_plan?: string
|
||||||
|
layout_plan_file_id?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveFloorPayload = Omit<Floor, 'layout_plan'>
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
details: T
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取楼层列表(分页) */
|
/** 获取楼层列表(分页) */
|
||||||
export const fetchFloorList = (data?: { page?: number; page_size?: number; keyword?: string; datacenter_id?: number; status?: string }) => {
|
export const fetchFloorList = (data?: { page?: number; page_size?: number; keyword?: string; datacenter_id?: number; status?: string }) => {
|
||||||
return request.post('/Assets/v1/floor/list', data || {})
|
return request.post('/Assets/v1/floor/list', data || {})
|
||||||
@@ -16,13 +39,13 @@ export const fetchFloorListByDatacenter = (datacenterId: number, params?: { name
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 创建楼层 */
|
/** 创建楼层 */
|
||||||
export const createFloor = (data: any) => {
|
export const createFloor = (data: SaveFloorPayload) => {
|
||||||
return request.post('/Assets/v1/floor/create', data)
|
return request.post<ApiResponse<Floor>>('/Assets/v1/floor/create', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新楼层 */
|
/** 更新楼层 */
|
||||||
export const updateFloor = (data: any) => {
|
export const updateFloor = (data: SaveFloorPayload) => {
|
||||||
return request.put('/Assets/v1/floor/update', data)
|
return request.put<ApiResponse<Floor>>('/Assets/v1/floor/update', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除楼层 */
|
/** 删除楼层 */
|
||||||
|
|||||||
48
src/api/ops/floorFile.ts
Normal file
48
src/api/ops/floorFile.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { request } from '@/api/request'
|
||||||
|
|
||||||
|
export interface PendingUpload {
|
||||||
|
file_id: string
|
||||||
|
object_key: string
|
||||||
|
upload: {
|
||||||
|
method: string
|
||||||
|
url: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
expires_at: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
details: T
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化楼层布局文件并直传 OSS。 */
|
||||||
|
export async function uploadFloorLayout(file: File): Promise<PendingUpload> {
|
||||||
|
const response = await request.post<ApiResponse<PendingUpload>>('/Assets/v1/floor/layout/init', {
|
||||||
|
filename: file.name,
|
||||||
|
size: file.size,
|
||||||
|
content_type: file.type || 'application/octet-stream',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(response.message || '初始化楼层布局文件上传失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingUpload = response.details
|
||||||
|
if (!pendingUpload?.file_id || !pendingUpload.upload?.method || !pendingUpload.upload?.url) {
|
||||||
|
throw new Error('楼层布局文件上传参数不完整')
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadResponse = await fetch(pendingUpload.upload.url, {
|
||||||
|
method: pendingUpload.upload.method,
|
||||||
|
headers: pendingUpload.upload.headers,
|
||||||
|
body: file,
|
||||||
|
credentials: 'omit',
|
||||||
|
})
|
||||||
|
if (!uploadResponse.ok) {
|
||||||
|
throw new Error(`上传楼层布局文件失败: HTTP ${uploadResponse.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingUpload
|
||||||
|
}
|
||||||
@@ -23,17 +23,6 @@ export interface GoViewProjectData {
|
|||||||
createUserId: string
|
createUserId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoViewFile {
|
|
||||||
id: string
|
|
||||||
fileName: string
|
|
||||||
fileSize: number
|
|
||||||
fileSuffix: string
|
|
||||||
virtualKey: string
|
|
||||||
relativePath: string
|
|
||||||
absolutePath: string
|
|
||||||
createTime: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OssInfo {
|
export interface OssInfo {
|
||||||
BucketName: string
|
BucketName: string
|
||||||
bucketURL: string
|
bucketURL: string
|
||||||
@@ -106,15 +95,6 @@ export const publishProject = (data: { identity: string; state: -1 | 1 }) => {
|
|||||||
return request.put<GoViewResponse<{ identity: string; state: number }>>('/Visual/v1/project/publish', data)
|
return request.put<GoViewResponse<{ identity: string; state: number }>>('/Visual/v1/project/publish', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传文件 */
|
|
||||||
export const uploadFile = (file: File) => {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('object', file)
|
|
||||||
return request.post<GoViewResponse<GoViewFile>>('/Visual/v1/project/upload', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 保存项目数据 */
|
/** 保存项目数据 */
|
||||||
export const saveProjectData = (projectId: string, content: string) => {
|
export const saveProjectData = (projectId: string, content: string) => {
|
||||||
const formData = new URLSearchParams()
|
const formData = new URLSearchParams()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { request } from "@/api/request"
|
import { request } from '@/api/request'
|
||||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage"
|
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||||
|
|
||||||
// ============ 通用响应类型 ============
|
// ============ 通用响应类型 ============
|
||||||
|
|
||||||
@@ -43,6 +43,8 @@ export interface ReportRecord {
|
|||||||
file_path?: string
|
file_path?: string
|
||||||
file_size?: number
|
file_size?: number
|
||||||
file_mime?: string
|
file_mime?: string
|
||||||
|
started_at?: string
|
||||||
|
finished_at?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,13 @@ export interface PageResult<T> {
|
|||||||
data: T[]
|
data: T[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReportExportFormat = 'csv' | 'xlsx' | 'pdf'
|
||||||
|
|
||||||
|
export interface DownloadedReport {
|
||||||
|
blob: Blob
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 报表生成参数接口 ============
|
// ============ 报表生成参数接口 ============
|
||||||
|
|
||||||
export interface TrafficReportParams {
|
export interface TrafficReportParams {
|
||||||
@@ -127,9 +136,7 @@ export interface NetworkDeviceReportParams {
|
|||||||
|
|
||||||
export interface StatisticsReportParams {
|
export interface StatisticsReportParams {
|
||||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware'
|
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware'
|
||||||
/** 与 metric_name 二选一;网络等指标优先用 metric_id */
|
metric_id: string
|
||||||
metric_id?: string
|
|
||||||
metric_name?: string
|
|
||||||
target_identities: string[]
|
target_identities: string[]
|
||||||
start_time: string
|
start_time: string
|
||||||
end_time: string
|
end_time: string
|
||||||
@@ -152,16 +159,13 @@ export interface HistoryReportParams {
|
|||||||
|
|
||||||
export interface TopNReportParams {
|
export interface TopNReportParams {
|
||||||
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware' | 'alert'
|
data_source: 'dc-host' | 'dc-network' | 'dc-database' | 'dc-middleware' | 'alert'
|
||||||
metric_id?: string
|
metric_id: string
|
||||||
metric_name?: string
|
|
||||||
target_identities: string[]
|
target_identities: string[]
|
||||||
start_time: string
|
start_time: string
|
||||||
end_time: string
|
end_time: string
|
||||||
n?: number
|
n?: number
|
||||||
rank_aggregate?: 'avg' | 'max' | 'min' | 'last'
|
rank_aggregate?: 'avg' | 'max' | 'min' | 'last'
|
||||||
order?: 'desc' | 'asc'
|
order?: 'desc' | 'asc'
|
||||||
metric_type?: 'cpu' | 'disk' | 'io' | 'memory' | 'network'
|
|
||||||
collector_identity?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 证据导出事件行 */
|
/** 证据导出事件行 */
|
||||||
@@ -230,7 +234,8 @@ export const fetchReportMetricsAvailable = (params: {
|
|||||||
identities?: string
|
identities?: string
|
||||||
keyword?: string
|
keyword?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
}) => request.get<ApiResponse<{ data_source: string; items: any[]; registry: any[] }>>('/DC-Control/v1/reports/metrics/available', { params })
|
}) =>
|
||||||
|
request.get<ApiResponse<{ data_source: string; items: any[]; registry: any[] }>>('/DC-Control/v1/reports/metrics/available', { params })
|
||||||
|
|
||||||
/** 逻辑指标目录(Registry) */
|
/** 逻辑指标目录(Registry) */
|
||||||
export const fetchReportMetricsRegistry = (params: { data_source: string }) =>
|
export const fetchReportMetricsRegistry = (params: { data_source: string }) =>
|
||||||
@@ -239,8 +244,8 @@ export const fetchReportMetricsRegistry = (params: { data_source: string }) =>
|
|||||||
/** 查看报表内容 */
|
/** 查看报表内容 */
|
||||||
export const fetchReportContent = (id: number) => request.get<ApiResponse<Record<string, any>>>(`/DC-Control/v1/reports/${id}/content`)
|
export const fetchReportContent = (id: number) => request.get<ApiResponse<Record<string, any>>>(`/DC-Control/v1/reports/${id}/content`)
|
||||||
|
|
||||||
/** 原始 ArrayBuffer → 下载用 Blob;xlsx 校验 ZIP 魔数 PK */
|
/** 原始 ArrayBuffer → 下载用 Blob;xlsx 校验 ZIP 魔数 PK,PDF 校验 %PDF- */
|
||||||
function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType: string | null): Blob {
|
function exportBufferToBlob(ab: ArrayBuffer, format: ReportExportFormat, contentType: string | null): Blob {
|
||||||
const u8 = new Uint8Array(ab)
|
const u8 = new Uint8Array(ab)
|
||||||
if (format === 'xlsx') {
|
if (format === 'xlsx') {
|
||||||
const okZip = u8.length >= 4 && u8[0] === 0x50 && u8[1] === 0x4b
|
const okZip = u8.length >= 4 && u8[0] === 0x50 && u8[1] === 0x4b
|
||||||
@@ -265,16 +270,62 @@ function exportBufferToBlob(ab: ArrayBuffer, format: 'csv' | 'xlsx', contentType
|
|||||||
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
return new Blob([ab], { type: mime })
|
return new Blob([ab], { type: mime })
|
||||||
}
|
}
|
||||||
const mime =
|
if (format === 'pdf') {
|
||||||
contentType && /csv|text|plain/i.test(contentType) ? contentType : 'text/csv;charset=utf-8'
|
const okPdf = u8.length >= 5 && u8[0] === 0x25 && u8[1] === 0x50 && u8[2] === 0x44 && u8[3] === 0x46 && u8[4] === 0x2d
|
||||||
|
if (!okPdf) {
|
||||||
|
throw new Error('导出失败:服务器返回的不是有效的 PDF。')
|
||||||
|
}
|
||||||
|
const mime = contentType && /pdf/i.test(contentType) ? contentType : 'application/pdf'
|
||||||
return new Blob([ab], { type: mime })
|
return new Blob([ab], { type: mime })
|
||||||
}
|
}
|
||||||
|
const mime = contentType && /csv|text|plain/i.test(contentType) ? contentType : 'text/csv;charset=utf-8'
|
||||||
|
return new Blob([ab], { type: mime })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContentDispositionFileName(value: string | null): string | undefined {
|
||||||
|
if (!value) return undefined
|
||||||
|
|
||||||
|
const utf8FileName = value.match(/filename\*=UTF-8''([^;]+)/i)
|
||||||
|
if (utf8FileName) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(utf8FileName[1])
|
||||||
|
} catch {
|
||||||
|
// 编码异常时继续读取普通文件名
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.match(/filename="([^"]+)"/i)?.[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReportFileName(fileName: string | undefined, id: number, format: ReportExportFormat): string {
|
||||||
|
const defaultName = `report_${id}.${format}`
|
||||||
|
if (!fileName) return defaultName
|
||||||
|
|
||||||
|
let name = fileName.split(/[\\/]/).pop() || ''
|
||||||
|
name = name
|
||||||
|
.replace(/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '')
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '_')
|
||||||
|
.replace(/[ .]+$/, '')
|
||||||
|
|
||||||
|
const extensionIndex = name.lastIndexOf('.')
|
||||||
|
if (extensionIndex > 0) {
|
||||||
|
name = name.slice(0, extensionIndex).replace(/[ .]+$/, '')
|
||||||
|
}
|
||||||
|
if (!name) return defaultName
|
||||||
|
|
||||||
|
const reservedName = /^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])$/i
|
||||||
|
if (reservedName.test(name.split('.')[0])) {
|
||||||
|
name = `_${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name}.${format}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 报表文件导出:使用 fetch + arrayBuffer,绕过 axios 拦截器与 Blob 中间态,
|
* 报表文件导出:使用 fetch + arrayBuffer,绕过 axios 拦截器与 Blob 中间态,
|
||||||
* 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。
|
* 避免网络面板里已是合法 xlsx(PK…)但落盘文件损坏的情况。
|
||||||
*/
|
*/
|
||||||
export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'): Promise<Blob> => {
|
export const exportReport = async (id: number, format: ReportExportFormat = 'csv'): Promise<DownloadedReport> => {
|
||||||
const base = String(import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
|
const base = String(import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
|
||||||
const url = `${base}/DC-Control/v1/reports/${id}/export?format=${encodeURIComponent(format)}`
|
const url = `${base}/DC-Control/v1/reports/${id}/export?format=${encodeURIComponent(format)}`
|
||||||
const token = SafeStorage.get(AppStorageKey.TOKEN)
|
const token = SafeStorage.get(AppStorageKey.TOKEN)
|
||||||
@@ -296,7 +347,10 @@ export const exportReport = async (id: number, format: 'csv' | 'xlsx' = 'csv'):
|
|||||||
}
|
}
|
||||||
throw new Error(msg)
|
throw new Error(msg)
|
||||||
}
|
}
|
||||||
return exportBufferToBlob(ab, format, r.headers.get('content-type'))
|
return {
|
||||||
|
blob: exportBufferToBlob(ab, format, r.headers.get('content-type')),
|
||||||
|
fileName: normalizeReportFileName(parseContentDispositionFileName(r.headers.get('content-disposition')), id, format),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 监测指标类接口(旧版兼容) ============
|
// ============ 监测指标类接口(旧版兼容) ============
|
||||||
|
|||||||
@@ -60,12 +60,6 @@
|
|||||||
list-type="picture-card"
|
list-type="picture-card"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
>
|
>
|
||||||
<!-- <template #upload-button>
|
|
||||||
<div class="upload-btn">
|
|
||||||
<icon-plus />
|
|
||||||
<div class="upload-text">上传图片</div>
|
|
||||||
</div>
|
|
||||||
</template> -->
|
|
||||||
</a-upload>
|
</a-upload>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -77,27 +71,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { computed } from 'vue'
|
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
import { createFloor, fetchDatacenterList, updateFloor } from '@/api/ops/floor'
|
||||||
import { createFloor, updateFloor } from '@/api/ops/floor'
|
import type { Floor, SaveFloorPayload } from '@/api/ops/floor'
|
||||||
import { fetchDatacenterList } from '@/api/ops/floor'
|
import { uploadFloorLayout } from '@/api/ops/floorFile'
|
||||||
import FtsUpload from '@/api/common/fts'
|
|
||||||
|
|
||||||
interface Floor {
|
|
||||||
id?: number
|
|
||||||
name?: string
|
|
||||||
datacenter_id?: number
|
|
||||||
floor_number?: number
|
|
||||||
status?: string
|
|
||||||
area?: number
|
|
||||||
height?: number
|
|
||||||
load_bearing?: number
|
|
||||||
enabled?: boolean
|
|
||||||
layout_plan?: string
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -117,6 +95,7 @@ const loadingDatacenters = ref(false)
|
|||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const datacenterList = ref<any[]>([])
|
const datacenterList = ref<any[]>([])
|
||||||
const fileList = ref<any[]>([])
|
const fileList = ref<any[]>([])
|
||||||
|
const localPreviewURL = ref('')
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@@ -129,12 +108,20 @@ const form = ref({
|
|||||||
load_bearing: undefined as number | undefined,
|
load_bearing: undefined as number | undefined,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
layout_plan: '',
|
layout_plan: '',
|
||||||
|
layout_plan_file_id: '',
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否为编辑模式
|
// 是否为编辑模式
|
||||||
const isEdit = computed(() => !!props.floor?.id)
|
const isEdit = computed(() => !!props.floor?.id)
|
||||||
|
|
||||||
|
const releaseLocalPreviewURL = () => {
|
||||||
|
if (localPreviewURL.value) {
|
||||||
|
URL.revokeObjectURL(localPreviewURL.value)
|
||||||
|
localPreviewURL.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载数据中心列表
|
// 加载数据中心列表
|
||||||
const loadDatacenterList = async () => {
|
const loadDatacenterList = async () => {
|
||||||
loadingDatacenters.value = true
|
loadingDatacenters.value = true
|
||||||
@@ -153,22 +140,16 @@ const loadDatacenterList = async () => {
|
|||||||
// 处理文件上传
|
// 处理文件上传
|
||||||
const handleUpload = async (option: any) => {
|
const handleUpload = async (option: any) => {
|
||||||
try {
|
try {
|
||||||
const file = option.fileItem.file
|
const file = option.fileItem.file as File
|
||||||
const formData = new FormData()
|
const pendingUpload = await uploadFloorLayout(file)
|
||||||
formData.append('file', file)
|
|
||||||
|
|
||||||
// 使用fts接口上传文件
|
releaseLocalPreviewURL()
|
||||||
const res: any = await FtsUpload(formData)
|
localPreviewURL.value = URL.createObjectURL(file)
|
||||||
|
option.fileItem.url = localPreviewURL.value
|
||||||
if (res.code === 0) {
|
form.value.layout_plan = ''
|
||||||
// 上传成功,设置文件URL
|
form.value.layout_plan_file_id = pendingUpload.file_id
|
||||||
form.value.layout_plan = res.data?.result_url || ''
|
option.onSuccess(pendingUpload)
|
||||||
option.onSuccess(res)
|
|
||||||
Message.success('上传成功')
|
Message.success('上传成功')
|
||||||
} else {
|
|
||||||
option.onError(new Error('上传失败'))
|
|
||||||
Message.error('上传失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('文件上传失败:', error)
|
console.error('文件上传失败:', error)
|
||||||
option.onError(error)
|
option.onError(error)
|
||||||
@@ -179,6 +160,11 @@ const handleUpload = async (option: any) => {
|
|||||||
// 处理文件变化
|
// 处理文件变化
|
||||||
const handleFileChange = (files: any[]) => {
|
const handleFileChange = (files: any[]) => {
|
||||||
fileList.value = files
|
fileList.value = files
|
||||||
|
if (files.length === 0) {
|
||||||
|
releaseLocalPreviewURL()
|
||||||
|
form.value.layout_plan = ''
|
||||||
|
form.value.layout_plan_file_id = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听对话框显示状态
|
// 监听对话框显示状态
|
||||||
@@ -186,6 +172,7 @@ watch(
|
|||||||
() => props.visible,
|
() => props.visible,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
|
releaseLocalPreviewURL()
|
||||||
if (props.floor && isEdit.value) {
|
if (props.floor && isEdit.value) {
|
||||||
// 编辑模式:填充表单
|
// 编辑模式:填充表单
|
||||||
form.value = {
|
form.value = {
|
||||||
@@ -198,6 +185,7 @@ watch(
|
|||||||
load_bearing: props.floor.load_bearing,
|
load_bearing: props.floor.load_bearing,
|
||||||
enabled: props.floor.enabled !== undefined ? props.floor.enabled : true,
|
enabled: props.floor.enabled !== undefined ? props.floor.enabled : true,
|
||||||
layout_plan: props.floor.layout_plan || '',
|
layout_plan: props.floor.layout_plan || '',
|
||||||
|
layout_plan_file_id: props.floor.layout_plan_file_id || '',
|
||||||
description: props.floor.description || '',
|
description: props.floor.description || '',
|
||||||
}
|
}
|
||||||
// 如果有布局图,设置文件列表
|
// 如果有布局图,设置文件列表
|
||||||
@@ -224,11 +212,12 @@ watch(
|
|||||||
load_bearing: undefined,
|
load_bearing: undefined,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
layout_plan: '',
|
layout_plan: '',
|
||||||
|
layout_plan_file_id: '',
|
||||||
description: '',
|
description: '',
|
||||||
}
|
}
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
}
|
}
|
||||||
}
|
} else releaseLocalPreviewURL()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -239,7 +228,7 @@ const handleOk = async () => {
|
|||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const data: any = {
|
const data: SaveFloorPayload = {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
datacenter_id: form.value.datacenter_id,
|
datacenter_id: form.value.datacenter_id,
|
||||||
floor_number: form.value.floor_number,
|
floor_number: form.value.floor_number,
|
||||||
@@ -248,7 +237,7 @@ const handleOk = async () => {
|
|||||||
height: form.value.height,
|
height: form.value.height,
|
||||||
load_bearing: form.value.load_bearing,
|
load_bearing: form.value.load_bearing,
|
||||||
enabled: form.value.enabled,
|
enabled: form.value.enabled,
|
||||||
layout_plan: form.value.layout_plan,
|
layout_plan_file_id: form.value.layout_plan_file_id,
|
||||||
description: form.value.description,
|
description: form.value.description,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +252,9 @@ const handleOk = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
|
const savedFloor = res.details
|
||||||
|
form.value.layout_plan = savedFloor?.layout_plan || ''
|
||||||
|
form.value.layout_plan_file_id = savedFloor?.layout_plan_file_id || ''
|
||||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||||
emit('success')
|
emit('success')
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
@@ -279,11 +271,13 @@ const handleOk = async () => {
|
|||||||
|
|
||||||
// 取消
|
// 取消
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
releaseLocalPreviewURL()
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理对话框可见性变化
|
// 处理对话框可见性变化
|
||||||
const handleVisibleChange = (visible: boolean) => {
|
const handleVisibleChange = (visible: boolean) => {
|
||||||
|
if (!visible) releaseLocalPreviewURL()
|
||||||
emit('update:visible', visible)
|
emit('update:visible', visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +285,8 @@ const handleVisibleChange = (visible: boolean) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDatacenterList()
|
loadDatacenterList()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(releaseLocalPreviewURL)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -298,19 +294,3 @@ export default {
|
|||||||
name: 'FloorFormDialog',
|
name: 'FloorFormDialog',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.upload-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-text {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -211,12 +213,13 @@ import {
|
|||||||
fetchReportList,
|
fetchReportList,
|
||||||
generateReport,
|
generateReport,
|
||||||
fetchReportContent,
|
fetchReportContent,
|
||||||
exportReport,
|
|
||||||
ReportType,
|
ReportType,
|
||||||
type ReportRecord,
|
type ReportRecord,
|
||||||
type NetworkDeviceReportParams,
|
type NetworkDeviceReportParams,
|
||||||
} from '@/api/ops/report'
|
} from '@/api/ops/report'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
import { useReportNetworkDevicePickOptions } from '../useReportNetworkDevicePickOptions'
|
import { useReportNetworkDevicePickOptions } from '../useReportNetworkDevicePickOptions'
|
||||||
|
|
||||||
@@ -560,7 +563,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -575,18 +578,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -235,13 +237,14 @@ import {
|
|||||||
fetchReportList,
|
fetchReportList,
|
||||||
generateReport,
|
generateReport,
|
||||||
fetchReportContent,
|
fetchReportContent,
|
||||||
exportReport,
|
|
||||||
exportEvidenceReport,
|
exportEvidenceReport,
|
||||||
ReportType,
|
ReportType,
|
||||||
type ReportRecord,
|
type ReportRecord,
|
||||||
type FaultReportParams,
|
type FaultReportParams,
|
||||||
} from '@/api/ops/report'
|
} from '@/api/ops/report'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { useFaultReportServiceIdentityOptions } from '../useReportTargetIdentityOptions'
|
import { useFaultReportServiceIdentityOptions } from '../useReportTargetIdentityOptions'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -619,7 +622,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -634,18 +637,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,38 +1,353 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrap">
|
<div class="container">
|
||||||
<a-result
|
<search-table
|
||||||
status="info"
|
:form-model="formModel"
|
||||||
title="历史报表入口已下线"
|
:form-items="formItems"
|
||||||
sub-title="多指标、多目标时序请使用「统计报告」:output_mode=timeseries,并按接口文档配置 interval 与 bucket_aggregation。旧类型记录仍可在各报表列表中按 report_type 筛选查看(若库中有数据)。"
|
:data="tableData"
|
||||||
|
:columns="tableColumns"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
:title="pageTitle"
|
||||||
|
@update:form-model="handleFormModelUpdate"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleReset"
|
||||||
|
@refresh="handleRefresh"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@page-size-change="handlePageSizeChange"
|
||||||
>
|
>
|
||||||
<template #extra>
|
<template #form-items>
|
||||||
<a-space>
|
<a-col :span="8">
|
||||||
<a-button type="primary" @click="goStatistics">前往统计报告</a-button>
|
<a-form-item label="创建时间" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
|
||||||
<a-button @click="goTopn">前往 TopN</a-button>
|
<a-range-picker
|
||||||
</a-space>
|
v-model="formModel.timeRange"
|
||||||
|
show-time
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
</template>
|
</template>
|
||||||
</a-result>
|
|
||||||
|
<template #reportType="{ record }">
|
||||||
|
{{ reportTypeLabel[record.report_type] || record.report_type || '—' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #status="{ record }">
|
||||||
|
<a-tag :color="reportStatusColor(record.status)">
|
||||||
|
{{ reportStatusLabel[record.status] || record.status || '—' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #failureReason="{ record }">
|
||||||
|
{{ record.error_message || '—' }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #operations="{ record }">
|
||||||
|
<a-button
|
||||||
|
v-if="canDownloadPDF(record)"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
:loading="downloadingIds.has(record.id)"
|
||||||
|
@click="handleDownloadPDF(record)"
|
||||||
|
>
|
||||||
|
下载 PDF
|
||||||
|
</a-button>
|
||||||
|
<span v-else-if="record.status === 'failed'" class="operation-note">生成失败</span>
|
||||||
|
<span v-else-if="record.status === 'pending' || record.status === 'running'" class="operation-note">生成中</span>
|
||||||
|
<span v-else class="operation-note">无 PDF</span>
|
||||||
|
</template>
|
||||||
|
</search-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
|
import { fetchReportList, ReportStatus, ReportType, type ReportListParams, type ReportRecord } from '@/api/ops/report'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
|
|
||||||
const router = useRouter()
|
const pageTitle = '历史报告'
|
||||||
|
|
||||||
const goStatistics = () => {
|
const reportTypeLabel: Record<string, string> = {
|
||||||
router.push('/report/statistics')
|
topn: 'TopN报表',
|
||||||
|
statistics: '统计报告',
|
||||||
|
traffic: '流量统计报告',
|
||||||
|
fault: '故障报告',
|
||||||
|
server: '服务器报告',
|
||||||
|
network_device: '网络设备报告',
|
||||||
|
evidence: '证据导出',
|
||||||
}
|
}
|
||||||
|
|
||||||
const goTopn = () => {
|
const pdfReportTypes = new Set([
|
||||||
router.push('/report/topn')
|
ReportType.TOPN,
|
||||||
|
ReportType.STATISTICS,
|
||||||
|
ReportType.TRAFFIC,
|
||||||
|
ReportType.FAULT,
|
||||||
|
ReportType.SERVER,
|
||||||
|
ReportType.NETWORK_DEVICE,
|
||||||
|
])
|
||||||
|
|
||||||
|
const formModel = ref<{
|
||||||
|
report_type: ReportType | ''
|
||||||
|
status: ReportStatus | ''
|
||||||
|
keyword: string
|
||||||
|
timeRange?: string[]
|
||||||
|
}>({
|
||||||
|
report_type: '',
|
||||||
|
status: '',
|
||||||
|
keyword: '',
|
||||||
|
timeRange: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const formItems = computed<FormItem[]>(() => [
|
||||||
|
{
|
||||||
|
field: 'report_type',
|
||||||
|
label: '报表类型',
|
||||||
|
type: 'select',
|
||||||
|
span: 8,
|
||||||
|
placeholder: '请选择',
|
||||||
|
options: [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: ReportType.TOPN, label: reportTypeLabel.topn },
|
||||||
|
{ value: ReportType.STATISTICS, label: reportTypeLabel.statistics },
|
||||||
|
{ value: ReportType.TRAFFIC, label: reportTypeLabel.traffic },
|
||||||
|
{ value: ReportType.FAULT, label: reportTypeLabel.fault },
|
||||||
|
{ value: ReportType.SERVER, label: reportTypeLabel.server },
|
||||||
|
{ value: ReportType.NETWORK_DEVICE, label: reportTypeLabel.network_device },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: '状态',
|
||||||
|
type: 'select',
|
||||||
|
span: 8,
|
||||||
|
placeholder: '请选择',
|
||||||
|
options: [
|
||||||
|
{ value: '', label: '全部' },
|
||||||
|
{ value: ReportStatus.PENDING, label: reportStatusLabel.pending },
|
||||||
|
{ value: ReportStatus.RUNNING, label: reportStatusLabel.running },
|
||||||
|
{ value: ReportStatus.SUCCESS, label: reportStatusLabel.success },
|
||||||
|
{ value: ReportStatus.FAILED, label: reportStatusLabel.failed },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'keyword',
|
||||||
|
label: '标题',
|
||||||
|
type: 'input',
|
||||||
|
span: 8,
|
||||||
|
placeholder: '请输入标题关键字',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const downloadingIds = reactive(new Set<number>())
|
||||||
|
const tableData = ref<ReportRecord[]>([])
|
||||||
|
let latestRequestId = 0
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
latestRequestId += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
showTotal: true,
|
||||||
|
showJumper: true,
|
||||||
|
showPageSize: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableColumns = computed(() => [
|
||||||
|
{
|
||||||
|
title: '报告编号',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '报表类型',
|
||||||
|
dataIndex: 'report_type',
|
||||||
|
width: 150,
|
||||||
|
slotName: 'reportType',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 100,
|
||||||
|
slotName: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '完成时间',
|
||||||
|
dataIndex: 'finished_at',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '失败原因',
|
||||||
|
dataIndex: 'error_message',
|
||||||
|
width: 220,
|
||||||
|
slotName: 'failureReason',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'operations',
|
||||||
|
width: 120,
|
||||||
|
slotName: 'operations',
|
||||||
|
fixed: 'right' as const,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const canDownloadPDF = (record: ReportRecord) =>
|
||||||
|
record.status === ReportStatus.SUCCESS && pdfReportTypes.has(record.report_type as ReportType)
|
||||||
|
|
||||||
|
const fetchList = async (): Promise<boolean> => {
|
||||||
|
const requestId = ++latestRequestId
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const reportType = formModel.value.report_type
|
||||||
|
const status = formModel.value.status
|
||||||
|
const keyword = formModel.value.keyword.trim()
|
||||||
|
const timeRange = formModel.value.timeRange
|
||||||
|
const page = pagination.current
|
||||||
|
const size = pagination.pageSize
|
||||||
|
const params: ReportListParams = {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportType) {
|
||||||
|
params.report_type = reportType
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
params.status = status
|
||||||
|
}
|
||||||
|
if (keyword) {
|
||||||
|
params.keyword = keyword
|
||||||
|
}
|
||||||
|
if (timeRange?.length === 2 && timeRange[0] && timeRange[1]) {
|
||||||
|
params.created_from = timeRange[0]
|
||||||
|
params.created_to = timeRange[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const res = await fetchReportList(params)
|
||||||
|
if (requestId !== latestRequestId) return false
|
||||||
|
|
||||||
|
if (res.code !== 0 || !res.details) {
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
Message.error(res.message || '获取报表列表失败')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = res.details.total || 0
|
||||||
|
const maximumPage = Math.max(1, Math.ceil(total / size))
|
||||||
|
if ((params.page || 1) > maximumPage) {
|
||||||
|
pagination.current = maximumPage
|
||||||
|
params.page = maximumPage
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData.value = normalizeReportRows(res.details.data || [])
|
||||||
|
pagination.total = total
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (requestId !== latestRequestId) return false
|
||||||
|
|
||||||
|
tableData.value = []
|
||||||
|
pagination.total = 0
|
||||||
|
console.error('获取报表列表失败:', error)
|
||||||
|
Message.error(error.message || '获取报表列表失败')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
if (requestId === latestRequestId) {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormModelUpdate = (value: Record<string, any>) => {
|
||||||
|
formModel.value = {
|
||||||
|
...formModel.value,
|
||||||
|
...value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
formModel.value = {
|
||||||
|
report_type: '',
|
||||||
|
status: '',
|
||||||
|
keyword: '',
|
||||||
|
timeRange: [],
|
||||||
|
}
|
||||||
|
pagination.current = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
if (await fetchList()) {
|
||||||
|
Message.success('数据已刷新')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (current: number) => {
|
||||||
|
pagination.current = current
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (pageSize: number) => {
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
pagination.current = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadPDF = async (record: ReportRecord) => {
|
||||||
|
downloadingIds.add(record.id)
|
||||||
|
try {
|
||||||
|
await downloadReportFile(record, 'pdf')
|
||||||
|
Message.success('PDF 下载成功')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('PDF 下载失败:', error)
|
||||||
|
Message.error(error.message || 'PDF 下载失败')
|
||||||
|
} finally {
|
||||||
|
downloadingIds.delete(record.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchList()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'ReportHistory',
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.wrap {
|
.container {
|
||||||
padding: 48px 24px;
|
padding: 20px;
|
||||||
max-width: 720px;
|
|
||||||
margin: 0 auto;
|
.operation-note {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -211,12 +213,13 @@ import {
|
|||||||
fetchReportList,
|
fetchReportList,
|
||||||
generateReport,
|
generateReport,
|
||||||
fetchReportContent,
|
fetchReportContent,
|
||||||
exportReport,
|
|
||||||
ReportType,
|
ReportType,
|
||||||
type ReportRecord,
|
type ReportRecord,
|
||||||
type ServerReportParams,
|
type ServerReportParams,
|
||||||
} from '@/api/ops/report'
|
} from '@/api/ops/report'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { useReportServerPickOptions } from '../useReportServerPickOptions'
|
import { useReportServerPickOptions } from '../useReportServerPickOptions'
|
||||||
|
|
||||||
const { serverIdentityOptions, serverOptionsLoading, loadServerPickOptions } = useReportServerPickOptions()
|
const { serverIdentityOptions, serverOptionsLoading, loadServerPickOptions } = useReportServerPickOptions()
|
||||||
@@ -548,7 +551,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -563,18 +566,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -248,12 +250,13 @@ import {
|
|||||||
fetchReportList,
|
fetchReportList,
|
||||||
generateReport,
|
generateReport,
|
||||||
fetchReportContent,
|
fetchReportContent,
|
||||||
exportReport,
|
|
||||||
ReportType,
|
ReportType,
|
||||||
type ReportRecord,
|
type ReportRecord,
|
||||||
type StatisticsReportParams,
|
type StatisticsReportParams,
|
||||||
} from '@/api/ops/report'
|
} from '@/api/ops/report'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
||||||
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
|
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
@@ -634,7 +637,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -649,18 +652,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -97,11 +99,7 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item
|
<a-form-item label="指标" field="metric_id" :rules="[{ required: true, message: '请选择指标' }]">
|
||||||
label="指标"
|
|
||||||
field="metric_id"
|
|
||||||
:rules="[{ required: true, message: '请选择指标' }]"
|
|
||||||
>
|
|
||||||
<a-select
|
<a-select
|
||||||
v-model="generateForm.metric_id"
|
v-model="generateForm.metric_id"
|
||||||
allow-clear
|
allow-clear
|
||||||
@@ -116,11 +114,7 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<a-form-item
|
<a-form-item label="目标标识" field="target_identities" :rules="[{ required: true, message: '请选择目标标识' }]">
|
||||||
label="目标标识"
|
|
||||||
field="target_identities"
|
|
||||||
:rules="[{ required: true, message: '请选择目标标识' }]"
|
|
||||||
>
|
|
||||||
<a-select
|
<a-select
|
||||||
v-model="generateForm.target_identities"
|
v-model="generateForm.target_identities"
|
||||||
multiple
|
multiple
|
||||||
@@ -128,9 +122,7 @@
|
|||||||
allow-search
|
allow-search
|
||||||
:loading="targetOptionsLoading"
|
:loading="targetOptionsLoading"
|
||||||
:options="targetIdentityOptions"
|
:options="targetIdentityOptions"
|
||||||
:placeholder="
|
:placeholder="generateForm.data_source ? '请选择或搜索目标(可多选)' : '请先选择数据源'"
|
||||||
generateForm.data_source ? '请选择或搜索目标(可多选)' : '请先选择数据源'
|
|
||||||
"
|
|
||||||
:disabled="!generateForm.data_source || generateForm.data_source === 'alert'"
|
:disabled="!generateForm.data_source || generateForm.data_source === 'alert'"
|
||||||
:max-tag-count="3"
|
:max-tag-count="3"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -179,28 +171,6 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<!-- 主机数据源额外参数 -->
|
|
||||||
<template v-if="generateForm.data_source === 'dc-host'">
|
|
||||||
<a-row :gutter="16">
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item label="指标类型" field="metric_type">
|
|
||||||
<a-select v-model="generateForm.metric_type" placeholder="请选择" style="width: 100%" allow-clear>
|
|
||||||
<a-option value="cpu">CPU</a-option>
|
|
||||||
<a-option value="disk">磁盘</a-option>
|
|
||||||
<a-option value="io">IO</a-option>
|
|
||||||
<a-option value="memory">内存</a-option>
|
|
||||||
<a-option value="network">网络</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="12">
|
|
||||||
<a-form-item label="采集器标识" field="collector_identity">
|
|
||||||
<a-input v-model="generateForm.collector_identity" placeholder="可选,手填标识" style="width: 100%" />
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<a-form-item label="报表标题" field="title">
|
<a-form-item label="报表标题" field="title">
|
||||||
<a-input v-model="generateForm.title" placeholder="可选,不填自动生成" style="width: 100%" />
|
<a-input v-model="generateForm.title" placeholder="可选,不填自动生成" style="width: 100%" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -217,12 +187,7 @@
|
|||||||
<div ref="chartRef" class="chart-container"></div>
|
<div ref="chartRef" class="chart-container"></div>
|
||||||
|
|
||||||
<!-- 排名表格(引擎字段为 identity + value,见 dc-control genTopN) -->
|
<!-- 排名表格(引擎字段为 identity + value,见 dc-control genTopN) -->
|
||||||
<a-table
|
<a-table :data="normalizedRankingRows" :columns="rankingTableColumns" :pagination="false" stripe />
|
||||||
:data="normalizedRankingRows"
|
|
||||||
:columns="rankingTableColumns"
|
|
||||||
:pagination="false"
|
|
||||||
stripe
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<a-empty v-else description="暂无数据" />
|
<a-empty v-else description="暂无数据" />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -234,22 +199,15 @@ import { ref, reactive, computed, nextTick, watch } from 'vue'
|
|||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import SearchTable from '@/components/search-table/index.vue'
|
import SearchTable from '@/components/search-table/index.vue'
|
||||||
import type { FormItem } from '@/components/search-form/types'
|
import type { FormItem } from '@/components/search-form/types'
|
||||||
import {
|
import { fetchReportList, generateReport, fetchReportContent, ReportType, type ReportRecord, type TopNReportParams } from '@/api/ops/report'
|
||||||
fetchReportList,
|
|
||||||
generateReport,
|
|
||||||
fetchReportContent,
|
|
||||||
exportReport,
|
|
||||||
ReportType,
|
|
||||||
type ReportRecord,
|
|
||||||
type TopNReportParams,
|
|
||||||
} from '@/api/ops/report'
|
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
||||||
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
|
import { useReportMetricRegistryOptions } from '../useReportMetricRegistryOptions'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
|
|
||||||
const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } =
|
const { targetIdentityOptions, targetOptionsLoading, loadTargetIdentityOptions } = useReportTargetIdentityOptions()
|
||||||
useReportTargetIdentityOptions()
|
|
||||||
|
|
||||||
const { metricOptions, metricOptionsLoading, loadMetricRegistryOptions } = useReportMetricRegistryOptions()
|
const { metricOptions, metricOptionsLoading, loadMetricRegistryOptions } = useReportMetricRegistryOptions()
|
||||||
|
|
||||||
@@ -359,8 +317,6 @@ const generateForm = ref<{
|
|||||||
n: number
|
n: number
|
||||||
rank_aggregate: string
|
rank_aggregate: string
|
||||||
order: string
|
order: string
|
||||||
metric_type: string
|
|
||||||
collector_identity: string
|
|
||||||
title: string
|
title: string
|
||||||
}>({
|
}>({
|
||||||
data_source: '',
|
data_source: '',
|
||||||
@@ -370,8 +326,6 @@ const generateForm = ref<{
|
|||||||
n: 10,
|
n: 10,
|
||||||
rank_aggregate: 'avg',
|
rank_aggregate: 'avg',
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
metric_type: '',
|
|
||||||
collector_identity: '',
|
|
||||||
title: '',
|
title: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -386,7 +340,7 @@ watch(
|
|||||||
}
|
}
|
||||||
loadTargetIdentityOptions(ds)
|
loadTargetIdentityOptions(ds)
|
||||||
loadMetricRegistryOptions(ds)
|
loadMetricRegistryOptions(ds)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 查看内容弹窗
|
// 查看内容弹窗
|
||||||
@@ -509,8 +463,6 @@ const handleOpenGenerateModal = () => {
|
|||||||
n: 10,
|
n: 10,
|
||||||
rank_aggregate: 'avg',
|
rank_aggregate: 'avg',
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
metric_type: '',
|
|
||||||
collector_identity: '',
|
|
||||||
title: '',
|
title: '',
|
||||||
}
|
}
|
||||||
generateModalVisible.value = true
|
generateModalVisible.value = true
|
||||||
@@ -567,16 +519,6 @@ const handleGenerate = async () => {
|
|||||||
params.order = generateForm.value.order as any
|
params.order = generateForm.value.order as any
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主机数据源额外参数
|
|
||||||
if (generateForm.value.data_source === 'dc-host') {
|
|
||||||
if (generateForm.value.metric_type) {
|
|
||||||
params.metric_type = generateForm.value.metric_type as any
|
|
||||||
}
|
|
||||||
if (generateForm.value.collector_identity) {
|
|
||||||
params.collector_identity = generateForm.value.collector_identity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await generateReport({
|
const res = await generateReport({
|
||||||
report_type: ReportType.TOPN,
|
report_type: ReportType.TOPN,
|
||||||
title: generateForm.value.title || undefined,
|
title: generateForm.value.title || undefined,
|
||||||
@@ -639,7 +581,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -654,18 +596,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -704,9 +635,7 @@ const renderChart = (ranking: any[]) => {
|
|||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: ranking
|
data: ranking.map((item: any) => item.target ?? item.identity ?? item.target_identity ?? '').reverse(),
|
||||||
.map((item: any) => item.target ?? item.identity ?? item.target_identity ?? '')
|
|
||||||
.reverse(),
|
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
<a-doption @click="handleExport('csv')">导出 CSV</a-doption>
|
||||||
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
<a-doption @click="handleExport('xlsx')">导出 Excel</a-doption>
|
||||||
|
<a-doption @click="handleExport('pdf')">导出 PDF</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleViewContent(record)">查看</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('csv', record)">CSV</a-button>
|
||||||
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('xlsx', record)">Excel</a-button>
|
||||||
|
<a-button v-if="record.status === 'success'" type="text" size="small" @click="handleExport('pdf', record)">PDF</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</search-table>
|
</search-table>
|
||||||
@@ -323,12 +325,13 @@ import {
|
|||||||
fetchReportList,
|
fetchReportList,
|
||||||
generateReport,
|
generateReport,
|
||||||
fetchReportContent,
|
fetchReportContent,
|
||||||
exportReport,
|
|
||||||
ReportType,
|
ReportType,
|
||||||
type ReportRecord,
|
type ReportRecord,
|
||||||
type TrafficReportParams,
|
type TrafficReportParams,
|
||||||
} from '@/api/ops/report'
|
} from '@/api/ops/report'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import { downloadReportFile } from '../useReportExport'
|
||||||
|
import type { ReportExportFormat } from '@/api/ops/report'
|
||||||
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
import { normalizeReportRows, reportStatusColor, reportStatusLabel } from '../useReportListRow'
|
||||||
import { useReportTopologyOptions } from '../useReportTopologyOptions'
|
import { useReportTopologyOptions } from '../useReportTopologyOptions'
|
||||||
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
import { useReportTargetIdentityOptions } from '../useReportTargetIdentityOptions'
|
||||||
@@ -815,7 +818,7 @@ const handleViewContent = async (record?: ReportRecord) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出报表
|
// 导出报表
|
||||||
const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
const handleExport = async (format: ReportExportFormat, record?: ReportRecord) => {
|
||||||
const targetRecord = record || selectedRecord.value
|
const targetRecord = record || selectedRecord.value
|
||||||
if (!targetRecord) {
|
if (!targetRecord) {
|
||||||
Message.warning('请选择要导出的报表')
|
Message.warning('请选择要导出的报表')
|
||||||
@@ -830,18 +833,7 @@ const handleExport = async (format: 'csv' | 'xlsx', record?: ReportRecord) => {
|
|||||||
exporting.value = true
|
exporting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await exportReport(targetRecord.id, format)
|
await downloadReportFile(targetRecord, format)
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `report_${targetRecord.id}.${format}`
|
|
||||||
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
Message.success('导出成功')
|
Message.success('导出成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
16
src/views/ops/pages/report/useReportExport.ts
Normal file
16
src/views/ops/pages/report/useReportExport.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { exportReport, type ReportExportFormat, type ReportRecord } from '@/api/ops/report'
|
||||||
|
|
||||||
|
export async function downloadReportFile(record: ReportRecord, format: ReportExportFormat): Promise<void> {
|
||||||
|
const { blob, fileName } = await exportReport(record.id, format)
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
try {
|
||||||
|
link.href = url
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
} finally {
|
||||||
|
link.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user