|
@@ -481,6 +481,19 @@
|
|
|
<pre class="otp-detail-code" v-html="detailJsonHtml"></pre>
|
|
<pre class="otp-detail-code" v-html="detailJsonHtml"></pre>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="otp-detail-foot">
|
|
|
|
|
+ <div class="otp-detail-foot-left">
|
|
|
|
|
+ <span>数据大小: {{ detailByteSize }} 字节</span>
|
|
|
|
|
+ <span>行数: {{ detailLineCount }}</span>
|
|
|
|
|
+ <span v-if="detailSearchKeyword" class="otp-detail-foot-search">
|
|
|
|
|
+ 搜索: "{{ detailSearchKeyword }}" ({{ detailSearchMatchCount }} 个匹配)
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="detailIsSlow" class="otp-detail-foot-slow">
|
|
|
|
|
+ <icon-svg name="yellow_alert" size="12px" />
|
|
|
|
|
+ <span>响应较慢</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
</div>
|
|
</div>
|
|
@@ -1035,6 +1048,35 @@ const highlightJson = (jsonText) => {
|
|
|
return html;
|
|
return html;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+/** 在已着色 HTML 的文本节点中高亮关键词 */
|
|
|
|
|
+const highlightSearchInHtml = (html, keyword) => {
|
|
|
|
|
+ const kw = String(keyword || "").trim();
|
|
|
|
|
+ if (!kw) return html;
|
|
|
|
|
+ const escapedKw = escapeHtml(kw).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
|
+ const re = new RegExp(escapedKw, "gi");
|
|
|
|
|
+ return String(html)
|
|
|
|
|
+ .split(/(<[^>]+>)/g)
|
|
|
|
|
+ .map((part) => {
|
|
|
|
|
+ if (!part || part.startsWith("<")) return part;
|
|
|
|
|
+ return part.replace(re, (matched) => `<mark class="otp-json-search-hit">${matched}</mark>`);
|
|
|
|
|
+ })
|
|
|
|
|
+ .join("");
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const countSearchMatches = (text, keyword) => {
|
|
|
|
|
+ const kw = String(keyword || "").trim();
|
|
|
|
|
+ if (!kw) return 0;
|
|
|
|
|
+ const source = String(text || "").toLowerCase();
|
|
|
|
|
+ const needle = kw.toLowerCase();
|
|
|
|
|
+ let count = 0;
|
|
|
|
|
+ let index = 0;
|
|
|
|
|
+ while ((index = source.indexOf(needle, index)) !== -1) {
|
|
|
|
|
+ count += 1;
|
|
|
|
|
+ index += needle.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ return count;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const responseJsonHtml = computed(() => highlightJson(responseJson.value));
|
|
const responseJsonHtml = computed(() => highlightJson(responseJson.value));
|
|
|
|
|
|
|
|
const hasTestResult = computed(() => searchResult.value != null);
|
|
const hasTestResult = computed(() => searchResult.value != null);
|
|
@@ -1042,23 +1084,29 @@ const hasTestResult = computed(() => searchResult.value != null);
|
|
|
const resultDetailVisible = ref(false);
|
|
const resultDetailVisible = ref(false);
|
|
|
const resultSearch = ref("");
|
|
const resultSearch = ref("");
|
|
|
|
|
|
|
|
-const detailDisplayJson = computed(() => {
|
|
|
|
|
|
|
+const detailSearchKeyword = computed(() => resultSearch.value.trim());
|
|
|
|
|
+
|
|
|
|
|
+const detailByteSize = computed(() =>
|
|
|
|
|
+ new TextEncoder().encode(responseJson.value || "").length,
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const detailLineCount = computed(() => {
|
|
|
const text = responseJson.value || "";
|
|
const text = responseJson.value || "";
|
|
|
- const keyword = resultSearch.value.trim();
|
|
|
|
|
- if (!keyword) return text;
|
|
|
|
|
- const lower = keyword.toLowerCase();
|
|
|
|
|
- return text
|
|
|
|
|
- .split("\n")
|
|
|
|
|
- .filter((line) => line.toLowerCase().includes(lower))
|
|
|
|
|
- .join("\n");
|
|
|
|
|
|
|
+ return text ? text.split("\n").length : 0;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const detailLineCount = computed(() => {
|
|
|
|
|
- const text = detailDisplayJson.value || "";
|
|
|
|
|
- return text ? text.split("\n").length : 1;
|
|
|
|
|
|
|
+const detailSearchMatchCount = computed(() =>
|
|
|
|
|
+ countSearchMatches(responseJson.value, detailSearchKeyword.value),
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const detailIsSlow = computed(() => {
|
|
|
|
|
+ const time = resultMeta.value?.time;
|
|
|
|
|
+ return time != null && Number(time) >= 1000;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const detailJsonHtml = computed(() => highlightJson(detailDisplayJson.value));
|
|
|
|
|
|
|
+const detailJsonHtml = computed(() =>
|
|
|
|
|
+ highlightSearchInHtml(highlightJson(responseJson.value), detailSearchKeyword.value),
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
const openResultDetail = () => {
|
|
const openResultDetail = () => {
|
|
|
resultSearch.value = "";
|
|
resultSearch.value = "";
|
|
@@ -1335,6 +1383,7 @@ const runTest = async () => {
|
|
|
.otp-side {
|
|
.otp-side {
|
|
|
flex: 1 1 0;
|
|
flex: 1 1 0;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
padding: 16px;
|
|
padding: 16px;
|
|
|
background: #FFFFFF;
|
|
background: #FFFFFF;
|
|
@@ -1424,6 +1473,7 @@ const runTest = async () => {
|
|
|
.otp-main {
|
|
.otp-main {
|
|
|
flex: 3 3 0;
|
|
flex: 3 3 0;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
gap: 16px;
|
|
gap: 16px;
|
|
@@ -1806,13 +1856,15 @@ const runTest = async () => {
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
- width: 100%;
|
|
|
|
|
|
|
+ width: auto;
|
|
|
|
|
+ max-width: 100%;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
min-height: 36px;
|
|
min-height: 36px;
|
|
|
padding: 6px 12px;
|
|
padding: 6px 12px;
|
|
|
background: #F8FAFC;
|
|
background: #F8FAFC;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
border: 1px solid #E2E8F0;
|
|
border: 1px solid #E2E8F0;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.otp-token-get-btn {
|
|
.otp-token-get-btn {
|
|
@@ -1844,7 +1896,7 @@ const runTest = async () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.otp-token-text {
|
|
.otp-token-text {
|
|
|
- flex: 1;
|
|
|
|
|
|
|
+ flex: 1 1 auto;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
font-family: PingFang SC, PingFang SC;
|
|
font-family: PingFang SC, PingFang SC;
|
|
|
font-weight: 400;
|
|
font-weight: 400;
|
|
@@ -1852,13 +1904,16 @@ const runTest = async () => {
|
|
|
color: #1E293B;
|
|
color: #1E293B;
|
|
|
line-height: 20px;
|
|
line-height: 20px;
|
|
|
text-align: left;
|
|
text-align: left;
|
|
|
- word-break: break-all;
|
|
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.otp-token-actions {
|
|
.otp-token-actions {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
|
|
+ margin-left: 12px;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2350,6 +2405,7 @@ const runTest = async () => {
|
|
|
.otp-result {
|
|
.otp-result {
|
|
|
flex: 1.5 1.5 0;
|
|
flex: 1.5 1.5 0;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.otp-result-card {
|
|
.otp-result-card {
|
|
@@ -2769,6 +2825,13 @@ const runTest = async () => {
|
|
|
color: #86EFAC;
|
|
color: #86EFAC;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.otp-json-search-hit {
|
|
|
|
|
+ background: #FACC15;
|
|
|
|
|
+ color: #0F172A;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ padding: 0 1px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.otp-result-dialog.el-dialog {
|
|
.otp-result-dialog.el-dialog {
|
|
|
max-width: 960px;
|
|
max-width: 960px;
|
|
|
padding: 0;
|
|
padding: 0;
|
|
@@ -3013,4 +3076,44 @@ const runTest = async () => {
|
|
|
white-space: pre-wrap;
|
|
white-space: pre-wrap;
|
|
|
word-break: break-word;
|
|
word-break: break-word;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.otp-detail-foot {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-top: -6px; /* 抵消父级 gap:16px,使与代码框间距为 10px */
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+ border-top: 1px solid #E2E8F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.otp-detail-foot-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #64748B;
|
|
|
|
|
+ line-height: 18px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.otp-detail-foot-search {
|
|
|
|
|
+ color: #1678FF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.otp-detail-foot-slow {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ font-family: PingFang SC, PingFang SC;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #F59E0B;
|
|
|
|
|
+ line-height: 18px;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|