Skip to content

Commit 9c8c335

Browse files
committed
feat: 资源/响应头块点击展开列表,修复刷新后 loading 不消失
scheduleCachedResultRefresh 的 setTimeout 触发后未把 state.cacheRefreshTimer 清零,attempt 用完也不再覆盖,导致 isDetecting 永真、响应头旁的 spinner 一直转。setTimeout 回调入口立即将 timer 清零,spinner 在检测窗口结束后自动隐藏。summary 区域的资源与响应头改为可点击的 button,点击切到对应底部面板:资源面板列出抓取到的 URL 列表(行点击在新 tab 打开),响应头面板用 dl/dt/dd 展示主文档 KV,数据从 raw 接口懒加载。footerPanelTitle computed 统一 4 种面板的标题。将版本号提升到 1.2.94。
1 parent 07e580b commit 9c8c335

2 files changed

Lines changed: 207 additions & 11 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.2.93",
4+
"version": "1.2.94",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/ui/popup/Popup.vue

Lines changed: 206 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,27 @@
5151
<span>{{ animatedTotal }}</span>
5252
<label>技术</label>
5353
</div>
54-
<div>
54+
<button
55+
type="button"
56+
:class="['summary-tile', { active: footerPanel === 'resources' }]"
57+
title="查看抓取到的资源列表"
58+
@click="openSummaryDetail('resources')"
59+
>
5560
<span>{{ animatedResource }}</span>
5661
<label>资源</label>
57-
</div>
58-
<div>
62+
</button>
63+
<button
64+
type="button"
65+
:class="['summary-tile', { active: footerPanel === 'headers' }]"
66+
title="查看主文档响应头"
67+
@click="openSummaryDetail('headers')"
68+
>
5969
<span>{{ animatedHeader }}</span>
6070
<label class="summary-label">
6171
响应头
6272
<Loader2 v-if="isDetecting" class="detection-spinner" :size="12" :stroke-width="2" />
6373
</label>
64-
</div>
74+
</button>
6575
</section>
6676

6777
<nav class="filter-bar" aria-label="技术分类过滤">
@@ -166,11 +176,9 @@
166176
</Transition>
167177

168178
<Transition name="footer-panel">
169-
<section v-if="footerPanel" class="footer-panel" :aria-label="footerPanel === 'search' ? '网页源代码搜索' : rawPanelTitle">
179+
<section v-if="footerPanel" class="footer-panel" :aria-label="footerPanelTitle">
170180
<header class="footer-panel-head">
171-
<span class="footer-panel-title">
172-
{{ footerPanel === 'search' ? '网页源代码搜索' : rawPanelTitle }}
173-
</span>
181+
<span class="footer-panel-title">{{ footerPanelTitle }}</span>
174182
<div class="footer-panel-actions">
175183
<RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="复制原始线索" @click="copyRawOutput">
176184
<Copy :size="12" :stroke-width="2" />
@@ -205,6 +213,28 @@
205213
<div v-else-if="footerPanel === 'raw'" class="footer-panel-body">
206214
<pre>{{ rawOutputText }}</pre>
207215
</div>
216+
<div v-else-if="footerPanel === 'resources'" class="footer-panel-body detail-body">
217+
<div v-if="detailLoading" class="detail-empty">正在加载...</div>
218+
<div v-else-if="!detailResources.length" class="detail-empty">未抓取到资源。</div>
219+
<ul v-else class="resource-list">
220+
<li v-for="url in detailResources" :key="url">
221+
<button type="button" class="resource-link" :title="url" @click="openResourceLink(url)">
222+
<ExternalLink class="resource-link-icon" :size="11" :stroke-width="2" />
223+
<span>{{ url }}</span>
224+
</button>
225+
</li>
226+
</ul>
227+
</div>
228+
<div v-else-if="footerPanel === 'headers'" class="footer-panel-body detail-body">
229+
<div v-if="detailLoading" class="detail-empty">正在加载...</div>
230+
<div v-else-if="!Object.keys(detailHeaders).length" class="detail-empty">没有主文档响应头数据;可点击"刷新"重新抓取。</div>
231+
<dl v-else class="header-list">
232+
<template v-for="(value, key) in detailHeaders" :key="key">
233+
<dt>{{ key }}</dt>
234+
<dd>{{ value }}</dd>
235+
</template>
236+
</dl>
237+
</div>
208238
</section>
209239
</Transition>
210240

@@ -299,10 +329,13 @@
299329
})
300330
const rawOutputText = ref(RAW_PLACEHOLDER)
301331
const theme = ref<ThemeMode>('auto')
302-
const footerPanel = ref<'search' | 'raw' | null>(null)
332+
const footerPanel = ref<'search' | 'raw' | 'resources' | 'headers' | null>(null)
303333
const rawSourceContext = ref<{ tech: any; source: string } | null>(null)
304334
const sectionsScroller = ref<HTMLElement | null>(null)
305335
const showScrollTop = ref(false)
336+
const detailLoading = ref(false)
337+
const detailResources = ref<string[]>([])
338+
const detailHeaders = ref<Record<string, string>>({})
306339
307340
const rawPanelTitle = computed(() => {
308341
if (footerPanel.value !== 'raw') return ''
@@ -311,6 +344,14 @@
311344
return `原始线索 · ${ctx.tech?.name || ''} · ${ctx.source}`
312345
})
313346
347+
const footerPanelTitle = computed(() => {
348+
if (footerPanel.value === 'search') return '网页源代码搜索'
349+
if (footerPanel.value === 'raw') return rawPanelTitle.value
350+
if (footerPanel.value === 'resources') return `资源列表(${detailResources.value.length})`
351+
if (footerPanel.value === 'headers') return `响应头(${Object.keys(detailHeaders.value).length})`
352+
return ''
353+
})
354+
314355
const toggleFooterPanel = (name: 'search' | 'raw') => {
315356
if (footerPanel.value === name && !rawSourceContext.value) {
316357
footerPanel.value = null
@@ -334,6 +375,39 @@
334375
renderRawOutput().catch(() => {})
335376
}
336377
378+
const loadDetailFromRaw = async () => {
379+
detailLoading.value = true
380+
try {
381+
const raw = state.rawResult || (state.currentTabId ? await requestPopupRawResult(state.currentTabId) : null)
382+
if (raw && !state.rawResult) {
383+
state.rawResult = raw
384+
state.rawLoaded = true
385+
}
386+
detailResources.value = Array.isArray(raw?.resources?.all) ? raw.resources.all : []
387+
detailHeaders.value = raw?.headers && typeof raw.headers === 'object' ? raw.headers : {}
388+
} catch {
389+
detailResources.value = []
390+
detailHeaders.value = {}
391+
} finally {
392+
detailLoading.value = false
393+
}
394+
}
395+
396+
const openSummaryDetail = (kind: 'resources' | 'headers') => {
397+
if (footerPanel.value === kind) {
398+
footerPanel.value = null
399+
return
400+
}
401+
rawSourceContext.value = null
402+
footerPanel.value = kind
403+
loadDetailFromRaw().catch(() => {})
404+
}
405+
406+
const openResourceLink = (url: string) => {
407+
if (!url) return
408+
chrome.tabs.create({ url })
409+
}
410+
337411
const onSectionsScroll = (event: Event) => {
338412
const target = event.currentTarget as HTMLElement
339413
showScrollTop.value = target.scrollTop > 240
@@ -529,6 +603,7 @@
529603
clearCacheRefreshTimer()
530604
if (attempt >= CACHE_REFRESH_DELAYS.length) return
531605
state.cacheRefreshTimer = window.setTimeout(() => {
606+
state.cacheRefreshTimer = 0
532607
refreshCachedResultIfReady(tabId, previousUpdatedAt, attempt).catch(() => {})
533608
}, CACHE_REFRESH_DELAYS[attempt])
534609
}
@@ -1151,10 +1226,42 @@
11511226
padding: 14px 16px;
11521227
}
11531228
1154-
.summary > div {
1229+
.summary > div,
1230+
.summary-tile {
11551231
align-items: baseline;
1232+
background: transparent;
1233+
border: 0;
1234+
color: inherit;
1235+
cursor: default;
11561236
display: flex;
1237+
font: inherit;
11571238
gap: 6px;
1239+
margin: 0;
1240+
padding: 0;
1241+
text-align: left;
1242+
}
1243+
1244+
.summary-tile {
1245+
border-radius: 4px;
1246+
cursor: pointer;
1247+
padding: 2px 6px;
1248+
margin: -2px -6px;
1249+
transition:
1250+
background 0.15s ease,
1251+
color 0.15s ease;
1252+
}
1253+
1254+
.summary-tile:hover {
1255+
background: var(--accent-soft);
1256+
}
1257+
1258+
.summary-tile.active {
1259+
background: var(--accent-soft);
1260+
color: var(--accent);
1261+
}
1262+
1263+
.summary-tile.active span {
1264+
color: var(--accent);
11581265
}
11591266
11601267
.summary span {
@@ -1753,6 +1860,95 @@
17531860
padding: 12px;
17541861
}
17551862
1863+
.detail-body {
1864+
font-size: 12px;
1865+
padding: 8px 0;
1866+
}
1867+
1868+
.detail-empty {
1869+
color: var(--muted);
1870+
padding: 12px 16px;
1871+
}
1872+
1873+
.resource-list {
1874+
list-style: none;
1875+
margin: 0;
1876+
padding: 0;
1877+
}
1878+
1879+
.resource-list li {
1880+
border-bottom: 1px solid var(--line);
1881+
}
1882+
1883+
.resource-list li:last-child {
1884+
border-bottom: 0;
1885+
}
1886+
1887+
.resource-link {
1888+
align-items: center;
1889+
background: transparent;
1890+
border: 0;
1891+
color: var(--text);
1892+
cursor: pointer;
1893+
display: flex;
1894+
font: inherit;
1895+
font-size: 12px;
1896+
gap: 6px;
1897+
overflow: hidden;
1898+
padding: 8px 16px;
1899+
text-align: left;
1900+
transition:
1901+
background 0.15s ease,
1902+
color 0.15s ease;
1903+
width: 100%;
1904+
}
1905+
1906+
.resource-link:hover {
1907+
background: var(--accent-soft);
1908+
color: var(--accent);
1909+
}
1910+
1911+
.resource-link span {
1912+
overflow: hidden;
1913+
text-overflow: ellipsis;
1914+
white-space: nowrap;
1915+
}
1916+
1917+
.resource-link-icon {
1918+
color: var(--muted);
1919+
flex-shrink: 0;
1920+
}
1921+
1922+
.resource-link:hover .resource-link-icon {
1923+
color: var(--accent);
1924+
}
1925+
1926+
.header-list {
1927+
display: grid;
1928+
gap: 4px 12px;
1929+
grid-template-columns: auto 1fr;
1930+
margin: 0;
1931+
padding: 4px 16px 12px;
1932+
}
1933+
1934+
.header-list dt {
1935+
color: var(--muted);
1936+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
1937+
font-size: 11px;
1938+
font-weight: 500;
1939+
padding-top: 4px;
1940+
word-break: break-all;
1941+
}
1942+
1943+
.header-list dd {
1944+
color: var(--text);
1945+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
1946+
font-size: 11px;
1947+
margin: 0;
1948+
padding-top: 4px;
1949+
word-break: break-all;
1950+
}
1951+
17561952
.footer-panel-enter-active,
17571953
.footer-panel-leave-active {
17581954
transition:

0 commit comments

Comments
 (0)