refactor: organize service dto contracts
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import '@/assets/styles/vue-cropper.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { getUploadErrorMessage } from '@/utils/uploadResponse'
|
||||
|
||||
@@ -137,6 +137,10 @@
|
||||
<el-switch v-model="wrapLines" aria-label="自动换行" />
|
||||
自动换行
|
||||
</label>
|
||||
<div class="log-view-mode" role="group" aria-label="日志显示模式">
|
||||
<button type="button" :class="{ active: viewMode === 'structured' }" @click="viewMode = 'structured'">结构化</button>
|
||||
<button type="button" :class="{ active: viewMode === 'raw' }" @click="viewMode = 'raw'">原始</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="mr-2 hidden rounded-full bg-muted px-2.5 py-1 text-[11px] text-muted-foreground sm:inline">{{ contentMeta }}</span>
|
||||
@@ -187,7 +191,41 @@
|
||||
class="h-full overflow-auto font-mono text-xs leading-6"
|
||||
:class="hasMore ? 'pt-12' : 'pt-2'"
|
||||
>
|
||||
<div v-if="displayLines.length" :class="wrapLines ? 'min-w-0' : 'min-w-max'">
|
||||
<div v-if="displayLines.length && viewMode === 'structured'" class="log-structured-list">
|
||||
<article
|
||||
v-for="line in displayLines"
|
||||
:key="line.sourceIndex"
|
||||
:data-log-line-index="line.sourceIndex"
|
||||
class="log-entry"
|
||||
:class="logEntryTone(line.json)"
|
||||
>
|
||||
<div v-if="line.json" class="log-entry-main">
|
||||
<div class="log-entry-heading">
|
||||
<span class="log-line-number">#{{ line.number }}</span>
|
||||
<span class="log-level" :class="`level-${String(line.json.level || 'info').toLowerCase()}`">{{ String(line.json.level || 'info').toUpperCase() }}</span>
|
||||
<time v-if="line.json.ts" class="log-time">{{ formatLogTimestamp(line.json.ts) }}</time>
|
||||
<span v-if="line.json.mod" class="log-module">{{ line.json.mod }}</span>
|
||||
<span v-if="line.json.caller" class="log-caller">{{ line.json.caller }}</span>
|
||||
</div>
|
||||
<div class="log-message">{{ line.json.msg || '未命名日志' }}</div>
|
||||
<div v-if="isSqlLog(line.json)" class="log-sql">{{ line.json.sql }}</div>
|
||||
<div v-if="isHttpLog(line.json)" class="log-metrics">
|
||||
<span v-if="line.json.http_method || line.json.http_path"><b>{{ line.json.http_method || '-' }}</b> {{ line.json.http_path || '-' }}</span>
|
||||
<span v-if="line.json.http_status != null" :class="httpStatusTone(line.json.http_status)">HTTP {{ line.json.http_status }}</span>
|
||||
<span v-if="line.json.latency_ms != null">耗时 {{ line.json.latency_ms }} ms</span>
|
||||
<span v-if="line.json.bytes_out != null">响应 {{ formatFileSize(Number(line.json.bytes_out)) }}</span>
|
||||
</div>
|
||||
<div v-if="structuredFields(line).length" class="log-fields">
|
||||
<span v-for="field in structuredFields(line)" :key="field.key" class="log-field">
|
||||
<b>{{ field.label }}</b><span :title="field.value">{{ field.value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre v-else class="log-fallback" :class="wrapLines ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'">{{ line.text }}</pre>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-else-if="displayLines.length" :class="wrapLines ? 'min-w-0' : 'min-w-max'">
|
||||
<div
|
||||
v-for="line in displayLines"
|
||||
:key="line.sourceIndex"
|
||||
@@ -248,6 +286,7 @@ const limitedByBytes = ref(false)
|
||||
const windowLimited = ref(false)
|
||||
const searchText = ref('')
|
||||
const wrapLines = ref(false)
|
||||
const viewMode = ref('structured')
|
||||
const loadingDates = ref(false)
|
||||
const loadingFiles = ref(false)
|
||||
const loadingContent = ref(false)
|
||||
@@ -292,7 +331,12 @@ const allLines = computed(() => {
|
||||
if (!content.value) return []
|
||||
const lines = content.value.split('\n')
|
||||
if (lines[lines.length - 1] === '') lines.pop()
|
||||
return lines.map((text, index) => ({ number: index + 1, sourceIndex: index, text }))
|
||||
return lines.map((text, index) => ({
|
||||
number: index + 1,
|
||||
sourceIndex: index,
|
||||
text,
|
||||
json: parseLogLine(text)
|
||||
}))
|
||||
})
|
||||
const displayLines = computed(() => {
|
||||
const keyword = searchText.value.trim().toLowerCase()
|
||||
@@ -340,6 +384,96 @@ function lineTone(text) {
|
||||
return 'text-container'
|
||||
}
|
||||
|
||||
function parseLogLine(text) {
|
||||
const value = text.trim()
|
||||
if (!value.startsWith('{') || !value.endsWith('}')) return null
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function logEntryTone(log) {
|
||||
const level = log?.level
|
||||
if (/^(error|dpanic|panic|fatal)$/i.test(level || '')) return 'entry-error'
|
||||
if (/^warn$/i.test(level || '')) return 'entry-warn'
|
||||
if (/^debug$/i.test(level || '')) return 'entry-debug'
|
||||
return 'entry-info'
|
||||
}
|
||||
|
||||
function formatLogTimestamp(value) {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3,
|
||||
hour12: false
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function isSqlLog(log) {
|
||||
return Boolean(log?.sql)
|
||||
}
|
||||
|
||||
function isHttpLog(log) {
|
||||
return Boolean(log && (log.mod === 'http' || log.http_method || log.http_status != null))
|
||||
}
|
||||
|
||||
function httpStatusTone(status) {
|
||||
const value = Number(status)
|
||||
if (value >= 500) return 'text-error'
|
||||
if (value >= 400) return 'text-warning'
|
||||
if (value >= 300) return 'text-info'
|
||||
return 'text-success'
|
||||
}
|
||||
|
||||
const fieldLabels = {
|
||||
request_id: '请求 ID',
|
||||
trace_id: 'Trace ID',
|
||||
span_id: 'Span ID',
|
||||
client_ip: '客户端 IP',
|
||||
ip: 'IP',
|
||||
user_id: '用户',
|
||||
authority_id: '角色',
|
||||
rows: '影响行数',
|
||||
elapsed_ms: 'SQL 耗时',
|
||||
provider: '渠道',
|
||||
trade_no: '订单号',
|
||||
error: '错误',
|
||||
device_id: '设备'
|
||||
}
|
||||
|
||||
const hiddenStructuredFields = new Set([
|
||||
'level', 'ts', 'caller', 'msg', 'mod', 'sql', 'gorm_logger',
|
||||
'http_method', 'http_path', 'http_route', 'http_status', 'latency_ms',
|
||||
'bytes_in', 'bytes_out', 'ua', 'req_headers', 'req_body', 'resp_data',
|
||||
'service.id', 'service.name', 'service.version', 'node', 'app_id', 'env'
|
||||
])
|
||||
|
||||
function displayFieldValue(value) {
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function structuredFields(line) {
|
||||
if (!line.json) return []
|
||||
return Object.entries(line.json)
|
||||
.filter(([key, value]) => !hiddenStructuredFields.has(key) && value !== '' && value != null)
|
||||
.slice(0, 10)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
label: fieldLabels[key] || key,
|
||||
value: displayFieldValue(value)
|
||||
}))
|
||||
}
|
||||
|
||||
function highlightedSegments(text) {
|
||||
const keyword = searchText.value.trim()
|
||||
if (!keyword) return [{ text, match: false }]
|
||||
@@ -656,4 +790,179 @@ onBeforeUnmount(() => window.removeEventListener('resize', updateRootHeight))
|
||||
color: rgb(var(--primary-color));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-view-mode {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid rgb(var(--border-color));
|
||||
border-radius: 7px;
|
||||
background: rgb(var(--muted-color) / 0.35);
|
||||
}
|
||||
|
||||
.log-view-mode button {
|
||||
height: 26px;
|
||||
padding: 0 9px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: rgb(var(--muted-foreground-color));
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.log-view-mode button:hover,
|
||||
.log-view-mode button.active {
|
||||
background: rgb(var(--container-bg-color));
|
||||
color: rgb(var(--primary-color));
|
||||
}
|
||||
|
||||
.log-structured-list {
|
||||
min-width: 760px;
|
||||
padding: 8px 12px 28px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
position: relative;
|
||||
margin: 0 0 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(var(--container-bg-color) / 0.18);
|
||||
border-left: 3px solid rgb(var(--muted-foreground-color) / 0.45);
|
||||
border-radius: 7px;
|
||||
background: rgb(var(--container-bg-color) / 0.06);
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background: rgb(var(--container-bg-color) / 0.11);
|
||||
}
|
||||
|
||||
.log-entry.entry-error { border-left-color: rgb(var(--error-color)); }
|
||||
.log-entry.entry-warn { border-left-color: rgb(var(--warning-color)); }
|
||||
.log-entry.entry-debug { border-left-color: rgb(var(--info-color)); }
|
||||
.log-entry.entry-info { border-left-color: rgb(var(--success-color) / 0.7); }
|
||||
|
||||
.log-entry-main {
|
||||
padding: 10px 12px 11px;
|
||||
}
|
||||
|
||||
.log-entry-heading,
|
||||
.log-metrics,
|
||||
.log-fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
}
|
||||
|
||||
.log-entry-heading {
|
||||
min-height: 20px;
|
||||
color: rgb(var(--muted-foreground-color));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-line-number {
|
||||
color: rgb(var(--muted-foreground-color) / 0.7);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.level-error, .level-dpanic, .level-panic, .level-fatal { background: rgb(var(--error-color) / .14); color: rgb(var(--error-color)); }
|
||||
.level-warn { background: rgb(var(--warning-color) / .16); color: rgb(var(--warning-color)); }
|
||||
.level-debug { background: rgb(var(--info-color) / .14); color: rgb(var(--info-color)); }
|
||||
.level-info { background: rgb(var(--success-color) / .14); color: rgb(var(--success-color)); }
|
||||
|
||||
.log-time,
|
||||
.log-caller,
|
||||
.log-module {
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.log-module {
|
||||
color: rgb(var(--primary-color));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
margin-top: 5px;
|
||||
color: rgb(var(--base-text-color));
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.log-sql {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgb(var(--primary-color) / .12);
|
||||
border-radius: 5px;
|
||||
background: rgb(var(--primary-color) / .06);
|
||||
color: rgb(var(--base-text-color));
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.log-metrics {
|
||||
margin-top: 8px;
|
||||
color: rgb(var(--muted-foreground-color));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-metrics span {
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgb(var(--muted-color) / .52);
|
||||
}
|
||||
|
||||
.log-metrics b {
|
||||
color: rgb(var(--base-text-color));
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log-fields {
|
||||
margin-top: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.log-field {
|
||||
display: inline-flex;
|
||||
max-width: 320px;
|
||||
gap: 5px;
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgb(var(--muted-color) / .38);
|
||||
color: rgb(var(--muted-foreground-color));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-field b {
|
||||
color: rgb(var(--base-text-color) / .78);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-field span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-fallback {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
color: rgb(var(--base-text-color));
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user