|
51 | 51 | <span>{{ animatedTotal }}</span> |
52 | 52 | <label>技术</label> |
53 | 53 | </div> |
54 | | - <div> |
| 54 | + <button |
| 55 | + type="button" |
| 56 | + :class="['summary-tile', { active: footerPanel === 'resources' }]" |
| 57 | + title="查看抓取到的资源列表" |
| 58 | + @click="openSummaryDetail('resources')" |
| 59 | + > |
55 | 60 | <span>{{ animatedResource }}</span> |
56 | 61 | <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 | + > |
59 | 69 | <span>{{ animatedHeader }}</span> |
60 | 70 | <label class="summary-label"> |
61 | 71 | 响应头 |
62 | 72 | <Loader2 v-if="isDetecting" class="detection-spinner" :size="12" :stroke-width="2" /> |
63 | 73 | </label> |
64 | | - </div> |
| 74 | + </button> |
65 | 75 | </section> |
66 | 76 |
|
67 | 77 | <nav class="filter-bar" aria-label="技术分类过滤"> |
|
166 | 176 | </Transition> |
167 | 177 |
|
168 | 178 | <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"> |
170 | 180 | <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> |
174 | 182 | <div class="footer-panel-actions"> |
175 | 183 | <RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="复制原始线索" @click="copyRawOutput"> |
176 | 184 | <Copy :size="12" :stroke-width="2" /> |
|
205 | 213 | <div v-else-if="footerPanel === 'raw'" class="footer-panel-body"> |
206 | 214 | <pre>{{ rawOutputText }}</pre> |
207 | 215 | </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> |
208 | 238 | </section> |
209 | 239 | </Transition> |
210 | 240 |
|
|
299 | 329 | }) |
300 | 330 | const rawOutputText = ref(RAW_PLACEHOLDER) |
301 | 331 | const theme = ref<ThemeMode>('auto') |
302 | | - const footerPanel = ref<'search' | 'raw' | null>(null) |
| 332 | + const footerPanel = ref<'search' | 'raw' | 'resources' | 'headers' | null>(null) |
303 | 333 | const rawSourceContext = ref<{ tech: any; source: string } | null>(null) |
304 | 334 | const sectionsScroller = ref<HTMLElement | null>(null) |
305 | 335 | const showScrollTop = ref(false) |
| 336 | + const detailLoading = ref(false) |
| 337 | + const detailResources = ref<string[]>([]) |
| 338 | + const detailHeaders = ref<Record<string, string>>({}) |
306 | 339 |
|
307 | 340 | const rawPanelTitle = computed(() => { |
308 | 341 | if (footerPanel.value !== 'raw') return '' |
|
311 | 344 | return `原始线索 · ${ctx.tech?.name || ''} · ${ctx.source}` |
312 | 345 | }) |
313 | 346 |
|
| 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 | +
|
314 | 355 | const toggleFooterPanel = (name: 'search' | 'raw') => { |
315 | 356 | if (footerPanel.value === name && !rawSourceContext.value) { |
316 | 357 | footerPanel.value = null |
|
334 | 375 | renderRawOutput().catch(() => {}) |
335 | 376 | } |
336 | 377 |
|
| 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 | +
|
337 | 411 | const onSectionsScroll = (event: Event) => { |
338 | 412 | const target = event.currentTarget as HTMLElement |
339 | 413 | showScrollTop.value = target.scrollTop > 240 |
|
529 | 603 | clearCacheRefreshTimer() |
530 | 604 | if (attempt >= CACHE_REFRESH_DELAYS.length) return |
531 | 605 | state.cacheRefreshTimer = window.setTimeout(() => { |
| 606 | + state.cacheRefreshTimer = 0 |
532 | 607 | refreshCachedResultIfReady(tabId, previousUpdatedAt, attempt).catch(() => {}) |
533 | 608 | }, CACHE_REFRESH_DELAYS[attempt]) |
534 | 609 | } |
|
1151 | 1226 | padding: 14px 16px; |
1152 | 1227 | } |
1153 | 1228 |
|
1154 | | - .summary > div { |
| 1229 | + .summary > div, |
| 1230 | + .summary-tile { |
1155 | 1231 | align-items: baseline; |
| 1232 | + background: transparent; |
| 1233 | + border: 0; |
| 1234 | + color: inherit; |
| 1235 | + cursor: default; |
1156 | 1236 | display: flex; |
| 1237 | + font: inherit; |
1157 | 1238 | 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); |
1158 | 1265 | } |
1159 | 1266 |
|
1160 | 1267 | .summary span { |
|
1753 | 1860 | padding: 12px; |
1754 | 1861 | } |
1755 | 1862 |
|
| 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 | +
|
1756 | 1952 | .footer-panel-enter-active, |
1757 | 1953 | .footer-panel-leave-active { |
1758 | 1954 | transition: |
|
0 commit comments