refactor: organize service dto contracts
This commit is contained in:
@@ -25,6 +25,14 @@ export const refundPaymentOrder = (data) => service({
|
||||
})
|
||||
|
||||
export const testPaymentProvider = (provider) => service({
|
||||
// The payment test route is registered at /payment/... (without the
|
||||
// frontend /api proxy prefix). When the built UI is opened directly from
|
||||
// the backend port, bypass axios' default /api baseURL for this request
|
||||
// only. Vite/Nginx deployments continue to use the normal proxy path.
|
||||
...(typeof window !== 'undefined' &&
|
||||
String(window.location.port) === String(import.meta.env.VITE_SERVER_PORT || '8000')
|
||||
? { baseURL: `${window.location.origin}/` }
|
||||
: {}),
|
||||
url: `/payment/providers/${encodeURIComponent(provider)}/test`,
|
||||
method: 'post'
|
||||
})
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.vue-cropper[data-v-a742df44]{position:relative;width:100%;height:100%;box-sizing:border-box;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;direction:ltr;touch-action:none;text-align:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-box[data-v-a742df44],.cropper-box-canvas[data-v-a742df44],.cropper-drag-box[data-v-a742df44],.cropper-crop-box[data-v-a742df44],.cropper-face[data-v-a742df44]{position:absolute;top:0;right:0;bottom:0;left:0;-webkit-user-select:none;user-select:none}.cropper-box-canvas img[data-v-a742df44]{position:relative;text-align:left;-webkit-user-select:none;user-select:none;transform:none;max-width:none;max-height:none}.cropper-box[data-v-a742df44]{overflow:hidden}.cropper-move[data-v-a742df44]{cursor:move}.cropper-crop[data-v-a742df44]{cursor:crosshair}.cropper-modal[data-v-a742df44]{background:rgba(0,0,0,.5)}.cropper-view-box[data-v-a742df44]{display:block;overflow:hidden;width:100%;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;-webkit-user-select:none;user-select:none}.cropper-view-box img[data-v-a742df44]{-webkit-user-select:none;user-select:none;text-align:left;max-width:none;max-height:none}.cropper-face[data-v-a742df44]{top:0;left:0;background-color:#fff;opacity:.1}.crop-info[data-v-a742df44]{position:absolute;left:0;min-width:65px;text-align:center;color:#fff;line-height:20px;background-color:#000c;font-size:12px}.crop-line[data-v-a742df44]{position:absolute;display:block;width:100%;height:100%;opacity:.1}.line-w[data-v-a742df44]{top:-3px;left:0;height:5px;cursor:n-resize}.line-a[data-v-a742df44]{top:0;left:-3px;width:5px;cursor:w-resize}.line-s[data-v-a742df44]{bottom:-3px;left:0;height:5px;cursor:s-resize}.line-d[data-v-a742df44]{top:0;right:-3px;width:5px;cursor:e-resize}.crop-point[data-v-a742df44]{position:absolute;width:8px;height:8px;opacity:.75;background-color:#39f;border-radius:100%}.point1[data-v-a742df44]{top:-4px;left:-4px;cursor:nw-resize}.point2[data-v-a742df44]{top:-5px;left:50%;margin-left:-3px;cursor:n-resize}.point3[data-v-a742df44]{top:-4px;right:-4px;cursor:ne-resize}.point4[data-v-a742df44]{top:50%;left:-4px;margin-top:-3px;cursor:w-resize}.point5[data-v-a742df44]{top:50%;right:-4px;margin-top:-3px;cursor:e-resize}.point6[data-v-a742df44]{bottom:-5px;left:-4px;cursor:sw-resize}.point7[data-v-a742df44]{bottom:-5px;left:50%;margin-left:-3px;cursor:s-resize}.point8[data-v-a742df44]{bottom:-5px;right:-4px;cursor:se-resize}@media screen and (max-width: 500px){.crop-point[data-v-a742df44]{position:absolute;width:20px;height:20px;opacity:.45;background-color:#39f;border-radius:100%}.point1[data-v-a742df44]{top:-10px;left:-10px}.point2[data-v-a742df44],.point4[data-v-a742df44],.point5[data-v-a742df44],.point7[data-v-a742df44]{display:none}.point3[data-v-a742df44]{top:-10px;right:-10px}.point4[data-v-a742df44]{top:0;left:0}.point6[data-v-a742df44]{bottom:-10px;left:-10px}.point8[data-v-a742df44]{bottom:-10px;right:-10px}}
|
||||
File diff suppressed because one or more lines are too long
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
import '@/assets/styles/wangeditor.css'
|
||||
|
||||
const basePath = import.meta.env.VITE_BASE_API
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
import '@/assets/styles/wangeditor.css'
|
||||
|
||||
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||||
import { Editor } from '@wangeditor/editor-for-vue'
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
import { ref, getCurrentInstance } from 'vue'
|
||||
import { 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'
|
||||
|
||||
@@ -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