|
182 | 182 | <header class="footer-panel-head"> |
183 | 183 | <span class="footer-panel-title">{{ footerPanelTitle }}</span> |
184 | 184 | <div class="footer-panel-actions"> |
| 185 | + <RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="下载原始线索" @click="downloadRawOutput"> |
| 186 | + <Download :size="12" :stroke-width="2" /> |
| 187 | + </RippleButton> |
185 | 188 | <RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="复制原始线索" @click="copyRawOutput"> |
186 | 189 | <Copy :size="12" :stroke-width="2" /> |
187 | 190 | </RippleButton> |
|
270 | 273 | ArrowUp, |
271 | 274 | Ban, |
272 | 275 | Copy, |
| 276 | + Download, |
273 | 277 | ExternalLink, |
274 | 278 | FileCode, |
275 | 279 | Flag, |
|
808 | 812 | } |
809 | 813 | } |
810 | 814 |
|
| 815 | + const sanitizeFilenameSegment = (input: string) => { |
| 816 | + const cleaned = String(input || '') |
| 817 | + .trim() |
| 818 | + .replace(/[\s/\\:*?"<>|]+/g, '-') |
| 819 | + .replace(/^-+|-+$/g, '') |
| 820 | + return cleaned.slice(0, 64) |
| 821 | + } |
| 822 | +
|
| 823 | + const buildRawDownloadFilename = () => { |
| 824 | + let host = 'page' |
| 825 | + try { |
| 826 | + const target = state.result?.url || state.rawResult?.url || '' |
| 827 | + if (target) host = new URL(target).hostname.replace(/^www\./, '') |
| 828 | + } catch { |
| 829 | + // ignore parse error, fall back to default host |
| 830 | + } |
| 831 | + const ctx = rawSourceContext.value |
| 832 | + const parts = ['stackprism', host || 'page'] |
| 833 | + if (ctx) { |
| 834 | + const techPart = sanitizeFilenameSegment(ctx.tech?.name || '') |
| 835 | + const sourcePart = sanitizeFilenameSegment(ctx.source || '') |
| 836 | + if (techPart) parts.push(techPart) |
| 837 | + if (sourcePart) parts.push(sourcePart) |
| 838 | + } |
| 839 | + parts.push(new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '')) |
| 840 | + return `${parts.filter(Boolean).join('_')}.json` |
| 841 | + } |
| 842 | +
|
| 843 | + const downloadRawOutput = () => { |
| 844 | + const text = rawOutputText.value |
| 845 | + if (!text || text === RAW_PLACEHOLDER || text === RAW_LOADING_TEXT) { |
| 846 | + setStatus('暂无可下载的原始线索。', 'error') |
| 847 | + return |
| 848 | + } |
| 849 | + try { |
| 850 | + const blob = new Blob([text], { type: 'application/json;charset=utf-8' }) |
| 851 | + const objectUrl = URL.createObjectURL(blob) |
| 852 | + const anchor = document.createElement('a') |
| 853 | + anchor.href = objectUrl |
| 854 | + anchor.download = buildRawDownloadFilename() |
| 855 | + document.body.appendChild(anchor) |
| 856 | + anchor.click() |
| 857 | + anchor.remove() |
| 858 | + setTimeout(() => URL.revokeObjectURL(objectUrl), 1000) |
| 859 | + setStatus(rawSourceContext.value ? '已下载来源详情。' : '已下载完整原始线索。', 'ok') |
| 860 | + } catch (error: any) { |
| 861 | + setStatus(`下载失败:${String(error?.message || error)}`, 'error') |
| 862 | + } |
| 863 | + } |
| 864 | +
|
811 | 865 | const getRawResult = async () => { |
812 | 866 | if (state.rawLoaded) return state.rawResult |
813 | 867 | const tabId = state.currentTabId || (await getActiveTabId()) |
|
0 commit comments