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')}}
+
+
+
+
-
{{ 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') }}
+
-
+
-
@@ -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 @@