From d64f9ca0fd74b7c0cc54f4fc961e989e1d9b7be9 Mon Sep 17 00:00:00 2001 From: GavinXiao <2749984520@qq.com> Date: Thu, 2 Apr 2026 17:51:17 +0800 Subject: [PATCH 1/4] feat(TAP-10773): API audit metric optimization --- .gitignore | 4 +- apps/daas/src/i18n/langs/en.js | 15 +- apps/daas/src/i18n/langs/zh-CN.js | 16 +- apps/daas/src/i18n/langs/zh-TW.js | 15 +- .../daas/src/views/data-server-audit/Info.vue | 518 +++++++++++++++++- 5 files changed, 544 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 8e51bc962..4345dcdd9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ pnpm-debug.log* output.js webpack.confi.production.js -build/deploy.sh \ No newline at end of file +build/deploy.sh + +daas-components.d.ts \ No newline at end of file diff --git a/apps/daas/src/i18n/langs/en.js b/apps/daas/src/i18n/langs/en.js index 21d7ddb00..e9b387fd9 100644 --- a/apps/daas/src/i18n/langs/en.js +++ b/apps/daas/src/i18n/langs/en.js @@ -405,6 +405,8 @@ export default { apiaudit_visitor: 'Client Name', apiaudit_ip: 'Visitor IP', apiaudit_interview_time: 'Access Time', + apiaudit_interview_time_req: 'Request Time', + apiaudit_interview_time_db: 'Database Time', apiaudit_interview_time_start: 'Access start time', apiaudit_interview_time_end: 'Access end time', apiaudit_visit_result: 'Result', @@ -412,11 +414,13 @@ export default { apiaudit_log_info: 'Log details', apiaudit_parameter: 'parameter', apiaudit_link: 'Link', + apiaudit_access_records_byte: 'Response Size', apiaudit_access_records: 'Return the number of rows', apiaudit_total_records: 'Match rows', - apiaudit_average_access_rate: 'API access rate', - apiaudit_access_time: 'Request access time', - apiaudit_average_response_time: 'Database response time', + apiaudit_average_access_db_rate: 'Database Response Rate', + apiaudit_average_access_time: 'API Response Time', + apiaudit_access_time: 'Total Request Time', + apiaudit_average_response_time: 'Database Time Consumption', apiaudit_success: 'success', apiaudit_placeholder: 'Please enter name/ID', apiaudit_client_name_placeholder: 'Please enter client name', @@ -429,6 +433,11 @@ export default { connection_reload_schema_confirm_msg: 'If there are too many schemas in this library, it may take a long time. Make sure to refresh the schema of the data source', connection_reload_schema_fail: 'Schema load failed', + apiaudit_time_line:"Time Line", + apiaudit_req_start_point: "Request start", + apiaudit_req_end_point: "Request to end", + apiaudit_db_start_point: "Database response starts", + apiaudit_db_end_point: "Database response completed", //Dag //Task edit缓存节点提示 task_list_status_all: 'All status', diff --git a/apps/daas/src/i18n/langs/zh-CN.js b/apps/daas/src/i18n/langs/zh-CN.js index 36e6a33dd..4ea0e0a8c 100644 --- a/apps/daas/src/i18n/langs/zh-CN.js +++ b/apps/daas/src/i18n/langs/zh-CN.js @@ -391,6 +391,8 @@ export default { apiaudit_visitor: '客户端名称', apiaudit_ip: '访问人员IP', apiaudit_interview_time: '访问时间', + apiaudit_interview_time_req: '请求时间', + apiaudit_interview_time_db: '数据库时间', apiaudit_interview_time_start: '访问开始时间', apiaudit_interview_time_end: '访问结束时间', apiaudit_visit_result: '访问结果', @@ -399,13 +401,21 @@ export default { apiaudit_parameter: '参数', apiaudit_link: '链接', apiaudit_access_records: '返回行数', + apiaudit_access_records_byte: '响应大小', apiaudit_total_records: '匹配行数', - apiaudit_average_access_rate: 'API 访问速率', - apiaudit_access_time: '请求访问耗时', - apiaudit_average_response_time: '数据库响应时间', + apiaudit_average_access_db_rate: '数据库响应速率', + apiaudit_average_access_time: 'API响应耗时', + apiaudit_access_time: '请求总耗时', + apiaudit_average_response_time: '数据库耗时', apiaudit_success: '成功', apiaudit_placeholder: '请输入名称/ID', apiaudit_client_name_placeholder: '请输入客户端名称', + apiaudit_time_line:"时间线", + apiaudit_req_start_point: "请求开始", + apiaudit_req_end_point: "请求结束", + apiaudit_db_start_point: "数据库响应开始", + apiaudit_db_end_point: "数据库响应结束", + // 连接 connection_list_form_database_type: '数据库类型', connection_list_name: '连接名', diff --git a/apps/daas/src/i18n/langs/zh-TW.js b/apps/daas/src/i18n/langs/zh-TW.js index 3a81b1147..d05dc2f93 100644 --- a/apps/daas/src/i18n/langs/zh-TW.js +++ b/apps/daas/src/i18n/langs/zh-TW.js @@ -392,6 +392,8 @@ export default { apiaudit_visitor: '客戶端名稱', apiaudit_ip: '訪問人員IP', apiaudit_interview_time: '訪問時間', + apiaudit_interview_time_req: '請求時間', + apiaudit_interview_time_db: '資料庫時間', apiaudit_interview_time_start: '訪問開始時間', apiaudit_interview_time_end: '訪問結束時間', apiaudit_visit_result: '訪問結果', @@ -400,13 +402,20 @@ export default { apiaudit_parameter: '參數', apiaudit_link: '鏈接', apiaudit_access_records: '返回行數', + apiaudit_access_records_byte: '響應大小', apiaudit_total_records: '匹配行數', - apiaudit_average_access_rate: 'API 訪問速率', - apiaudit_access_time: '請求訪問耗時', - apiaudit_average_response_time: '數據庫回應時間', + apiaudit_average_access_db_rate: '數據庫響應速率', + apiaudit_average_access_time: 'API響應耗時', + apiaudit_access_time: '請求總耗時', + apiaudit_average_response_time: '資料庫耗時', apiaudit_success: '成功', apiaudit_placeholder: '請輸入名稱/ID', apiaudit_client_name_placeholder: '請輸入客戶端名稱', + apiaudit_time_line:"時間線", + apiaudit_req_start_point: "請求開始", + apiaudit_req_end_point: "請求結束", + apiaudit_db_start_point: "資料庫響應開始", + apiaudit_db_end_point: "資料庫響應結束", // 連接 connection_list_form_database_type: '數據庫類型', connection_list_name: '連接名', diff --git a/apps/daas/src/views/data-server-audit/Info.vue b/apps/daas/src/views/data-server-audit/Info.vue index 610d5b2d7..6be794a44 100644 --- a/apps/daas/src/views/data-server-audit/Info.vue +++ b/apps/daas/src/views/data-server-audit/Info.vue @@ -13,31 +13,216 @@ export default { return { auditData: null, loading: true, + hoverTimelineSeg: null, + timelineTooltipPopperOptions: { + modifiers: [ + { + name: 'offset', + options: { offset: [0, 8] }, + }, + { + name: 'flip', + options: { + fallbackPlacements: [ + 'top-start', + 'top-end', + 'bottom', + 'bottom-start', + 'bottom-end', + ], + }, + }, + { + name: 'preventOverflow', + options: { padding: 8 }, + }, + ], + }, list: [ + { + label: this.$t('apiaudit_total_records'), + key: 'totalRows', + value: 0, + }, { label: this.$t('apiaudit_access_records'), key: 'visitTotalCount', value: 0, }, { - label: this.$t('apiaudit_total_records'), - key: 'totalRows', + label: this.$t('apiaudit_access_records_byte'), + key: 'responseBytes', value: 0, }, + { label: this.$t('apiaudit_access_time'), key: 'latency', value: 0 }, { - label: this.$t('apiaudit_average_access_rate'), - key: 'speed', + label: this.$t('apiaudit_average_access_time'), + key: 'httpTime', value: 0, }, - { label: this.$t('apiaudit_access_time'), key: 'latency', value: 0 }, { label: this.$t('apiaudit_average_response_time'), key: 'dataQueryTotalTime', value: 0, }, + { + label: this.$t('apiaudit_average_access_db_rate'), + key: 'dbRate', + value: 0, + }, ], } }, + computed: { + timelinePoints() { + if (!this.auditData) return [] + + const callStart = Number(this.auditData.callStart) + const callEnd = Number(this.auditData.callEnd) + const dbStart = Number(this.auditData.dataQueryFromTime) + const dbEnd = Number(this.auditData.dataQueryEndTime) + + const reqOk = + Number.isFinite(callStart) && + Number.isFinite(callEnd) && + callEnd >= callStart + const dbOk = + reqOk && + Number.isFinite(dbStart) && + Number.isFinite(dbEnd) && + dbEnd >= dbStart && + dbStart >= callStart && + dbEnd <= callEnd + + const reqPoints = [ + { + key: 'callStart', + label: this.$t('apiaudit_req_start_point'), + ts: this.auditData.callStart, + text: this.auditData.callStartTime || '-', + type: 'req', + }, + { + key: 'callEnd', + label: this.$t('apiaudit_req_end_point'), + ts: this.auditData.callEnd, + text: this.auditData.callEndTime || '-', + type: 'req', + }, + ] + + if (!dbOk) { + return [ + { ...reqPoints[0], left: 0 }, + { ...reqPoints[1], left: 100 }, + ] + } + + const points = [ + reqPoints[0], + { + key: 'dataQueryFromTime', + label: this.$t('apiaudit_db_start_point'), + ts: this.auditData.dataQueryFromTime, + text: this.auditData.dataQueryFrom || '-', + type: 'db', + }, + { + key: 'dataQueryEndTime', + label: this.$t('apiaudit_db_end_point'), + ts: this.auditData.dataQueryEndTime, + text: this.auditData.dataQueryEnd || '-', + type: 'db', + }, + reqPoints[1], + ] + + const d0 = Math.max(0, dbStart - callStart) + const d1 = Math.max(0, dbEnd - dbStart) + const d2 = Math.max(0, callEnd - dbEnd) + const total = d0 + d1 + d2 + + let w0 = 33.3333 + let w1 = 33.3333 + if (total > 0) { + w0 = (d0 / total) * 100 + w1 = (d1 / total) * 100 + } + + const lefts = [0, w0, w0 + w1, 100] + return points.map((p, i) => ({ ...p, left: lefts[i] })) + }, + timelineSegments() { + if (!this.auditData) return [] + + const callStart = Number(this.auditData.callStart) + const dbStart = Number(this.auditData.dataQueryFromTime) + const dbEnd = Number(this.auditData.dataQueryEndTime) + const callEnd = Number(this.auditData.callEnd) + + const reqOk = + Number.isFinite(callStart) && + Number.isFinite(callEnd) && + callEnd >= callStart + const dbOk = + reqOk && + Number.isFinite(dbStart) && + Number.isFinite(dbEnd) && + dbEnd >= dbStart && + dbStart >= callStart && + dbEnd <= callEnd + + if (!dbOk) { + return [ + { + key: 'req', + type: reqOk ? 'req' : 'other', + width: 100, + startText: this.auditData.callStartTime || '-', + endText: this.auditData.callEndTime || '-', + }, + ] + } + + let w0 = 33.3333 + let w1 = 33.3333 + let w2 = 33.3334 + + const d0 = Math.max(0, dbStart - callStart) + const d1 = Math.max(0, dbEnd - dbStart) + const d2 = Math.max(0, callEnd - dbEnd) + const total = d0 + d1 + d2 + if (total > 0) { + w0 = (d0 / total) * 100 + w1 = (d1 / total) * 100 + w2 = 100 - w0 - w1 + } + + return [ + { + key: 'req-pre', + type: 'req', + width: w0, + startText: this.auditData.callStartTime || '-', + endText: this.auditData.dataQueryFrom || '-', + }, + { + key: 'db', + type: 'db', + width: w1, + startText: this.auditData.dataQueryFrom || '-', + endText: this.auditData.dataQueryEnd || '-', + }, + { + key: 'req-post', + type: 'req', + width: w2, + startText: this.auditData.dataQueryEnd || '-', + endText: this.auditData.callEndTime || '-', + }, + ] + }, + }, created() { this.getData() }, @@ -54,8 +239,23 @@ export default { this.auditData.createAt = data.createAt ? dayjs(data.createAt).format('YYYY-MM-DD HH:mm:ss') : '-' - this.auditData.reqTime = this.auditData.reqTime - ? dayjs(this.auditData.reqTime).format('YYYY-MM-DD HH:mm:ss') + this.auditData.callStartTime = this.auditData.callStart + ? dayjs(this.auditData.callStart).format( + 'YYYY-MM-DD HH:mm:ss.SSS', + ) + : '-' + this.auditData.callEndTime = this.auditData.callEnd + ? dayjs(this.auditData.callEnd).format('YYYY-MM-DD HH:mm:ss.SSS') + : '-' + this.auditData.dataQueryFrom = this.auditData.dataQueryFromTime + ? dayjs(this.auditData.dataQueryFromTime).format( + 'YYYY-MM-DD HH:mm:ss.SSS', + ) + : '-' + this.auditData.dataQueryEnd = this.auditData.dataQueryEndTime + ? dayjs(this.auditData.dataQueryEndTime).format( + 'YYYY-MM-DD HH:mm:ss.SSS', + ) : '-' const jsonData = this.auditData.body ? this.auditData.body @@ -71,7 +271,7 @@ export default { this.auditData.jsonParam.json = jsonData this.auditData.jsonParam.validation = true } catch (error) { - console.log(`parseJsonData error: ${error}`) + console.error(`parseJsonData error: ${error}`) } this.list.forEach((item) => { @@ -135,6 +335,38 @@ export default { console.error('JSON处理失败:', error) } }, + isTimelinePointActive(pointIndex) { + if ( + this.hoverTimelineSeg === null || + this.hoverTimelineSeg === undefined + ) { + return false + } + return ( + pointIndex === this.hoverTimelineSeg || + pointIndex === this.hoverTimelineSeg + 1 + ) + }, + timelineTooltipPlacement(pointIndex) { + if ( + this.hoverTimelineSeg === null || + this.hoverTimelineSeg === undefined || + !this.isTimelinePointActive(pointIndex) + ) { + return 'top' + } + + const startIndex = this.hoverTimelineSeg + const endIndex = this.hoverTimelineSeg + 1 + const startLeft = Number(this.timelinePoints?.[startIndex]?.left) + const endLeft = Number(this.timelinePoints?.[endIndex]?.left) + const segWidth = endLeft - startLeft + const shouldSplit = + Number.isFinite(segWidth) && segWidth >= 0 && segWidth <= 18 + + if (!shouldSplit) return 'top' + return pointIndex === endIndex ? 'bottom' : 'top' + }, }, } @@ -161,10 +393,6 @@ export default { >{{ $t('apiaudit_link') }}: {{ auditData.apiPath || '-' }} - {{ $t('apiaudit_interview_time') }}: - {{ auditData.reqTime }} {{ $t('apiaudit_ip') }}: {{ auditData.userIp }} + {{ $t('apiaudit_interview_time_req') }}: + + {{ auditData.callStartTime }} ~ {{ auditData.callEndTime }} + {{ $t('apiaudit_interview_time_db') }}: + + {{ auditData.dataQueryFrom }} ~ {{ auditData.dataQueryEnd }} + +
+
+
{{$t('apiaudit_time_line')}}
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+ +
+
{{ p.label }}
+
+
+
+
+
+
+
+
+
+
  • {{ formatDuring(item.value) }}
{{ item.value ? `${calcUnit(item.value, 'b')}/S` : '0 M/S' }}
+
+ {{ item.value ? `${calcUnit(item.value, 'B')}` : '0B' }} +
Date: Fri, 3 Apr 2026 10:15:56 +0800 Subject: [PATCH 2/4] fix(TAP-10773): The problem of overlapping dots due to the small proportion of time intervals, and the issue of not being able to display after exceeding 1 minute --- .../daas/src/views/data-server-audit/Info.vue | 498 +++++++++++++++--- .../daas/src/views/data-server-audit/List.vue | 23 +- 2 files changed, 436 insertions(+), 85 deletions(-) diff --git a/apps/daas/src/views/data-server-audit/Info.vue b/apps/daas/src/views/data-server-audit/Info.vue index 6be794a44..a1a8de6ca 100644 --- a/apps/daas/src/views/data-server-audit/Info.vue +++ b/apps/daas/src/views/data-server-audit/Info.vue @@ -14,6 +14,9 @@ export default { auditData: null, loading: true, hoverTimelineSeg: null, + timelineCoreWidth: 0, + timelinePointMinGapPx: 12, + timelineResizeObserver: null, timelineTooltipPopperOptions: { modifiers: [ { @@ -74,48 +77,113 @@ export default { } }, computed: { + timelineLabelMetaClassMap() { + const points = this.timelinePoints || [] + const width = Number(this.timelineCoreWidth) + const overlapPx = 160 + const map = {} + + const getX = (idx) => { + if (!Number.isFinite(width) || width <= 0) return Number.NaN + const left = Number(points?.[idx]?.left) + if (!Number.isFinite(left)) return Number.NaN + return (left / 100) * width + } + + const applyGroup = (type, side) => { + const idxs = points + .map((p, i) => ({ p, i })) + .filter((x) => x.p?.type === type) + .map((x) => x.i) + .sort( + (a, b) => + (Number(points[a]?.left) || 0) - (Number(points[b]?.left) || 0), + ) + + for (const i of idxs) { + map[points[i].key] = [`audit-timeline__meta--${side}`] + } + + if (idxs.length < 2) return + + for (let k = 0; k < idxs.length - 1; k++) { + const a = idxs[k] + const b = idxs[k + 1] + const ax = getX(a) + const bx = getX(b) + if (!Number.isFinite(ax) || !Number.isFinite(bx)) continue + if (Math.abs(bx - ax) < overlapPx) { + map[points[a].key] = [ + `audit-timeline__meta--${side}`, + 'audit-timeline__meta--align-right', + ] + map[points[b].key] = [ + `audit-timeline__meta--${side}`, + 'audit-timeline__meta--align-left', + ] + } + } + } + + applyGroup('req', 'top') + applyGroup('db', 'bottom') + return map + }, timelinePoints() { if (!this.auditData) return [] const callStart = Number(this.auditData.callStart) const callEnd = Number(this.auditData.callEnd) - const dbStart = Number(this.auditData.dataQueryFromTime) - const dbEnd = Number(this.auditData.dataQueryEndTime) + const dbStartRaw = Number(this.auditData.dataQueryFromTime) + const dbEndRaw = Number(this.auditData.dataQueryEndTime) const reqOk = Number.isFinite(callStart) && Number.isFinite(callEnd) && callEnd >= callStart + const reqInstant = reqOk && callStart === callEnd + const dbStart = Math.min(dbStartRaw, dbEndRaw) + const dbEnd = Math.max(dbStartRaw, dbEndRaw) const dbOk = reqOk && - Number.isFinite(dbStart) && - Number.isFinite(dbEnd) && - dbEnd >= dbStart && + Number.isFinite(dbStartRaw) && + Number.isFinite(dbEndRaw) && dbStart >= callStart && dbEnd <= callEnd + const formatTimelineTime = (ts) => + Number.isFinite(ts) ? dayjs(ts).format('YYYY-MM-DD HH:mm:ss.SSS') : '-' + const reqPoints = [ { key: 'callStart', label: this.$t('apiaudit_req_start_point'), - ts: this.auditData.callStart, + ts: callStart, text: this.auditData.callStartTime || '-', type: 'req', }, { key: 'callEnd', label: this.$t('apiaudit_req_end_point'), - ts: this.auditData.callEnd, + ts: callEnd, text: this.auditData.callEndTime || '-', type: 'req', }, ] if (!dbOk) { - return [ - { ...reqPoints[0], left: 0 }, - { ...reqPoints[1], left: 100 }, + const base = [ + { ...reqPoints[0], left: reqInstant ? 50 : 0, dotVariant: 'single' }, + { + ...reqPoints[1], + left: reqInstant ? 50 : 100, + dotVariant: 'single', + }, ] + if (!reqInstant) return base + base[0].dotVariant = 'triple' + base[1].dotVariant = 'triple' + return base } const points = [ @@ -123,52 +191,100 @@ export default { { key: 'dataQueryFromTime', label: this.$t('apiaudit_db_start_point'), - ts: this.auditData.dataQueryFromTime, - text: this.auditData.dataQueryFrom || '-', + ts: dbStart, + text: formatTimelineTime(dbStart), type: 'db', }, { key: 'dataQueryEndTime', label: this.$t('apiaudit_db_end_point'), - ts: this.auditData.dataQueryEndTime, - text: this.auditData.dataQueryEnd || '-', + ts: dbEnd, + text: formatTimelineTime(dbEnd), type: 'db', }, reqPoints[1], ] - const d0 = Math.max(0, dbStart - callStart) - const d1 = Math.max(0, dbEnd - dbStart) - const d2 = Math.max(0, callEnd - dbEnd) - const total = d0 + d1 + d2 + const segs = this.timelineSegments + const w0 = Number(segs?.[0]?.width) || 0 + const w1 = Number(segs?.[1]?.width) || 0 + const lefts = reqInstant ? [50, 50, 50, 50] : [0, w0, w0 + w1, 100] + const result = points.map((p, i) => ({ + ...p, + left: lefts[i], + dotVariant: 'single', + })) - let w0 = 33.3333 - let w1 = 33.3333 - if (total > 0) { - w0 = (d0 / total) * 100 - w1 = (d1 / total) * 100 + const EPS = 1e-6 + const order = result + .map((p, i) => ({ i, left: Number(p.left) })) + .filter((x) => Number.isFinite(x.left)) + .sort((a, b) => a.left - b.left) + + const groups = [] + let current = [] + for (const item of order) { + if (!current.length) { + current = [item] + continue + } + const prev = current.at(-1) + if (Math.abs(item.left - prev.left) <= EPS) current.push(item) + else { + groups.push(current) + current = [item] + } } + if (current.length) groups.push(current) + + for (const g of groups) { + if (g.length < 2) continue + const idxs = g.map((x) => x.i) + const hasReq = idxs.some((i) => result[i]?.type === 'req') + const hasDb = idxs.some((i) => result[i]?.type === 'db') + + if (hasReq && hasDb) { + for (const i of idxs) { + if (result[i]?.type === 'req') result[i].dotVariant = 'double-outer' + if (result[i]?.type === 'db') result[i].dotVariant = 'double-inner' + } + continue + } - const lefts = [0, w0, w0 + w1, 100] - return points.map((p, i) => ({ ...p, left: lefts[i] })) + for (const i of idxs) result[i].dotVariant = 'triple' + } + + return result }, timelineSegments() { if (!this.auditData) return [] const callStart = Number(this.auditData.callStart) - const dbStart = Number(this.auditData.dataQueryFromTime) - const dbEnd = Number(this.auditData.dataQueryEndTime) + const dbStartRaw = Number(this.auditData.dataQueryFromTime) + const dbEndRaw = Number(this.auditData.dataQueryEndTime) const callEnd = Number(this.auditData.callEnd) const reqOk = Number.isFinite(callStart) && Number.isFinite(callEnd) && callEnd >= callStart + if (reqOk && callStart === callEnd) { + return [ + { + key: 'req', + type: 'req', + width: 100, + startText: this.auditData.callStartTime || '-', + endText: this.auditData.callEndTime || '-', + }, + ] + } + const dbStart = Math.min(dbStartRaw, dbEndRaw) + const dbEnd = Math.max(dbStartRaw, dbEndRaw) const dbOk = reqOk && - Number.isFinite(dbStart) && - Number.isFinite(dbEnd) && - dbEnd >= dbStart && + Number.isFinite(dbStartRaw) && + Number.isFinite(dbEndRaw) && dbStart >= callStart && dbEnd <= callEnd @@ -198,34 +314,61 @@ export default { w2 = 100 - w0 - w1 } - return [ + return this.applyTimelineSegmentMinWidth([ { key: 'req-pre', type: 'req', width: w0, + durationMs: d0, startText: this.auditData.callStartTime || '-', - endText: this.auditData.dataQueryFrom || '-', + endText: Number.isFinite(dbStart) + ? dayjs(dbStart).format('YYYY-MM-DD HH:mm:ss.SSS') + : '-', }, { key: 'db', type: 'db', width: w1, - startText: this.auditData.dataQueryFrom || '-', - endText: this.auditData.dataQueryEnd || '-', + durationMs: d1, + startText: Number.isFinite(dbStart) + ? dayjs(dbStart).format('YYYY-MM-DD HH:mm:ss.SSS') + : '-', + endText: Number.isFinite(dbEnd) + ? dayjs(dbEnd).format('YYYY-MM-DD HH:mm:ss.SSS') + : '-', }, { key: 'req-post', type: 'req', width: w2, - startText: this.auditData.dataQueryEnd || '-', + durationMs: d2, + startText: Number.isFinite(dbEnd) + ? dayjs(dbEnd).format('YYYY-MM-DD HH:mm:ss.SSS') + : '-', endText: this.auditData.callEndTime || '-', }, - ] + ]) + }, + }, + watch: { + auditData() { + this.$nextTick(() => { + this.tryInitTimelineResizeObserver() + }) }, }, created() { this.getData() }, + mounted() { + this.tryInitTimelineResizeObserver() + }, + beforeUnmount() { + if (this.timelineResizeObserver) { + this.timelineResizeObserver.disconnect() + this.timelineResizeObserver = null + } + }, methods: { dayjs, // 获取数据 @@ -288,17 +431,15 @@ export default { }) }, formatDuring(mss) { - let time = '' - const minutes = Number.parseInt((mss % (1000 * 60 * 60)) / (1000 * 60)) - const seconds = (mss % (1000 * 60)) / 1000 - if (minutes > 1) { - time = `${minutes.toFixed(2)}min` - } else if (minutes < 1 && seconds > 1) { - time = `${seconds.toFixed(2)}s` - } else if (minutes < 1 && seconds < 1 && mss > 0) { - time = `${mss}ms` - } - return time + const ms = Number(mss) + if (!Number.isFinite(ms) || ms <= 0) return '0ms' + + if (ms >= 24 * 60 * 60 * 1000) return '超过24h' + if (ms >= 60 * 60 * 1000) return `${(ms / (60 * 60 * 1000)).toFixed(2)}h` + if (ms >= 60 * 1000) return `${(ms / (60 * 1000)).toFixed(2)}min` + if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s` + + return `${Math.round(ms)}ms` }, formatMs(ms) { return formatMs(ms) @@ -347,6 +488,32 @@ export default { pointIndex === this.hoverTimelineSeg + 1 ) }, + isTimelinePointTooltipActive(pointIndex) { + if ( + this.hoverTimelineSeg === null || + this.hoverTimelineSeg === undefined || + !this.isTimelinePointActive(pointIndex) + ) { + return false + } + + const a = Number(this.hoverTimelineSeg) + const b = a + 1 + const p1 = this.timelinePoints?.[a] + const p2 = this.timelinePoints?.[b] + const l1 = Number(p1?.left) + const l2 = Number(p2?.left) + if (!Number.isFinite(l1) || !Number.isFinite(l2)) return true + if (Math.abs(l1 - l2) > 1e-6) return true + + const t1 = p1?.type + const t2 = p2?.type + if (t1 !== t2) { + const prefer = t1 === 'req' ? a : t2 === 'req' ? b : a + return pointIndex === prefer + } + return pointIndex === a + }, timelineTooltipPlacement(pointIndex) { if ( this.hoverTimelineSeg === null || @@ -356,16 +523,120 @@ export default { return 'top' } - const startIndex = this.hoverTimelineSeg - const endIndex = this.hoverTimelineSeg + 1 - const startLeft = Number(this.timelinePoints?.[startIndex]?.left) - const endLeft = Number(this.timelinePoints?.[endIndex]?.left) - const segWidth = endLeft - startLeft - const shouldSplit = - Number.isFinite(segWidth) && segWidth >= 0 && segWidth <= 18 + const pointType = this.timelinePoints?.[pointIndex]?.type + const side = pointType === 'req' ? 'bottom' : 'top' + + const a = Number(this.hoverTimelineSeg) + const b = a + 1 + const otherIndex = pointIndex === a ? b : a + if (!Number.isFinite(otherIndex)) return side - if (!shouldSplit) return 'top' - return pointIndex === endIndex ? 'bottom' : 'top' + const width = Number(this.timelineCoreWidth) + const p1Left = Number(this.timelinePoints?.[pointIndex]?.left) + const p2Left = Number(this.timelinePoints?.[otherIndex]?.left) + if ( + !Number.isFinite(width) || + width <= 0 || + !Number.isFinite(p1Left) || + !Number.isFinite(p2Left) + ) { + return side + } + + const x1 = (p1Left / 100) * width + const x2 = (p2Left / 100) * width + const tooltipOverlapPx = 240 + if (Math.abs(x1 - x2) >= tooltipOverlapPx) return side + + if (x1 < x2) return `${side}-end` + if (x1 > x2) return `${side}-start` + return side + }, + updateTimelineCoreWidth() { + const el = this.$refs.timelineCore + const width = el && el.clientWidth ? el.clientWidth : 0 + this.timelineCoreWidth = width + }, + tryInitTimelineResizeObserver() { + this.updateTimelineCoreWidth() + if (this.timelineResizeObserver) return + const el = this.$refs.timelineCore + if (!el || typeof ResizeObserver === 'undefined') return + this.timelineResizeObserver = new ResizeObserver(() => { + this.updateTimelineCoreWidth() + }) + this.timelineResizeObserver.observe(el) + }, + applyTimelinePointMinGap(points) { + const width = Number(this.timelineCoreWidth) + if (!Number.isFinite(width) || width <= 0) return points + if (!points || points.length <= 2) return points + + const n = points.length + const maxGap = width / (n - 1) + const minGap = Math.max(0, Math.min(this.timelinePointMinGapPx, maxGap)) + + const xs = points.map((p) => (Number(p.left) / 100) * width) + xs[0] = 0 + xs[n - 1] = width + + for (let i = 1; i < n - 1; i++) { + xs[i] = Math.max(xs[i], xs[i - 1] + minGap) + } + for (let i = n - 2; i >= 1; i--) { + xs[i] = Math.min(xs[i], xs[i + 1] - minGap) + } + for (let i = 1; i < n - 1; i++) { + xs[i] = Math.max(xs[i], xs[i - 1] + minGap) + } + + return points.map((p, i) => ({ + ...p, + left: width ? (xs[i] / width) * 100 : p.left, + })) + }, + applyTimelineSegmentMinWidth(segments) { + const width = Number(this.timelineCoreWidth) + if (!Number.isFinite(width) || width <= 0) return segments + if (!segments || segments.length <= 1) return segments + + const minGapPx = Number(this.timelinePointMinGapPx) || 12 + + const basePx = segments.map((s) => (Number(s.width) / 100) * width) + const minPxList = segments.map((s) => { + const duration = Number(s.durationMs) + if (!Number.isFinite(duration) || duration <= 0) return 0 + return minGapPx + }) + + const totalMin = minPxList.reduce((sum, v) => sum + v, 0) + const scale = totalMin > width && totalMin > 0 ? width / totalMin : 1 + const scaledMinPx = minPxList.map((v) => v * scale) + + const px = basePx.map((v, i) => Math.max(v, scaledMinPx[i])) + let excess = px.reduce((sum, v) => sum + v, 0) - width + + if (excess > 0.0001) { + const order = px + .map((v, i) => ({ i, adjustable: v - scaledMinPx[i] })) + .filter((x) => x.adjustable > 0.0001) + .sort((a, b) => b.adjustable - a.adjustable) + + for (const item of order) { + if (excess <= 0) break + const delta = Math.min(item.adjustable, excess) + px[item.i] -= delta + excess -= delta + } + } + + const totalPx = px.reduce((sum, v) => sum + v, 0) + const fixScale = totalPx > 0 ? width / totalPx : 1 + + return segments.map((s, i) => ({ + ...s, + width: ((px[i] * fixScale) / width) * 100, + })) }, }, } @@ -447,11 +718,13 @@ export default {
-
{{$t('apiaudit_time_line')}}
+
+ {{ $t('apiaudit_time_line') }} +
-
+
-
+
-
+
{{ p.label }}
@@ -754,14 +1046,15 @@ export default { top: 0; left: 0; transform: translate(-50%, -50%); - width: 10px; - height: 10px; + width: var(--dot-size, 10px); + height: var(--dot-size, 10px); z-index: 5; } .audit-timeline__dot { - width: 10px; - height: 10px; + position: relative; + width: 100%; + height: 100%; border-radius: 50%; background: var(--el-bg-color); border: 2px solid var(--el-border-color); @@ -772,14 +1065,80 @@ export default { box-shadow 240ms ease; } + .audit-timeline__point--double-outer, + .audit-timeline__point--double-inner, + .audit-timeline__point--triple { + --dot-size: 12px; + } + + .audit-timeline__point--triple { + --dot-middle-size: 8px; + --dot-inner-size: 4px; + --timeline-inner-accent: var(--timeline-accent); + } + + .audit-timeline__point--double-inner { + --dot-size: 6px; + } + + .audit-timeline__dot--double-inner { + background: var(--timeline-accent); + border: none; + } + + .audit-timeline__dot--triple::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: var(--dot-middle-size, 8px); + height: var(--dot-middle-size, 8px); + border-radius: 50%; + transform: translate(-50%, -50%); + background: var(--el-bg-color); + border: 2px solid var(--el-border-color); + box-sizing: border-box; + } + + .audit-timeline__dot--triple::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: var(--dot-inner-size, 4px); + height: var(--dot-inner-size, 4px); + border-radius: 50%; + transform: translate(-50%, -50%); + background: var(--timeline-inner-accent); + } + .audit-timeline__meta { position: absolute; - top: 10px; + top: auto; left: 0; transform: translateX(-50%); width: 150px; text-align: center; white-space: normal; + pointer-events: none; + } + + .audit-timeline__meta--align-right { + transform: translateX(-100%); + text-align: right; + } + + .audit-timeline__meta--align-left { + transform: translateX(0); + text-align: left; + } + + .audit-timeline__meta--top { + bottom: 18px; + } + + .audit-timeline__meta--bottom { + top: 14px; } .audit-timeline__point--req { @@ -797,10 +1156,6 @@ export default { --timeline-accent-shadow: rgba(144, 147, 153, 0.15); } - .audit-timeline__point--db .audit-timeline__meta { - top: 28px; - } - .audit-timeline__point--req .audit-timeline__dot, .audit-timeline__point--db .audit-timeline__dot, .audit-timeline__point--other .audit-timeline__dot { @@ -812,6 +1167,7 @@ export default { line-height: 14px; color: var(--text-light); margin-bottom: 2px; + white-space: nowrap; transition: color 240ms ease, font-weight 240ms ease; diff --git a/apps/daas/src/views/data-server-audit/List.vue b/apps/daas/src/views/data-server-audit/List.vue index 6fef20fd8..16abebd40 100644 --- a/apps/daas/src/views/data-server-audit/List.vue +++ b/apps/daas/src/views/data-server-audit/List.vue @@ -1,5 +1,4 @@