Skip to content

Commit cb3719e

Browse files
committed
feat: 技术 chip 拉 simpleicons CDN 图标,2s 超时回落首字母色块
新建 TechChip 组件,从 cdn.simpleicons.org/<slug> 异步加载技术品牌 SVG(slug 走 lowercase + `.` → dot / `+` → plus / `&` → and / 其他特殊字符删除规则);开 popup 后 2s 内 onload 拿到就直接换上图标,onerror / 超时则保留本地哈希色块 + 首字母,不阻塞展示。隐私政策补一条说明这类请求只带公开技术名 slug、不含用户浏览数据。 将版本号提升到 1.3.65。
1 parent 358ddb1 commit cb3719e

4 files changed

Lines changed: 165 additions & 87 deletions

File tree

PRIVACY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ StackPrism / 栈棱镜(以下简称「本扩展」)是一款基于 Chrome / Edge
4040

4141
- 本扩展**不会**向任何远程服务器上传您的浏览数据、识别结果、个人信息或任何其他数据
4242
- 没有遥测、没有广告 SDK、没有第三方分析、没有错误上报到云端
43-
- 扩展唯一发起的网络请求是异步抓取**您当前访问页面自身已经加载的**少量 JS 文件首段(走浏览器缓存,不增加额外网络流量),用于在本地扫描该 JS 中的版权注释和 OAuth 入口 URL —— 这是浏览器原本就已下载的内容,扩展只是再读一遍,****向第三方域名传输任何信息
43+
- 扩展只发起两类网络请求,**均不**携带您的任何身份信息或浏览数据:
44+
1. 异步抓取**您当前访问页面自身已经加载的**少量 JS 文件首段(走浏览器缓存,不增加额外网络流量),用于在本地扫描该 JS 中的版权注释和 OAuth 入口 URL
45+
2.`cdn.simpleicons.org` 拉取技术品牌图标(请求体只包含一个公开技术名 slug,例如 react / vuedotjs / docker,不含您正在访问的页面 URL、识别结果或任何个人信息);图标如果 2 秒内未加载成功,扩展会自动回落到本地生成的首字母色块,绝不阻塞识别流程
4446

4547
## 第三方共享
4648

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.3.64",
4+
"version": "1.3.65",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/ui/components/TechChip.vue

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<template>
2+
<span :class="['tech-chip', chipClass]" aria-hidden="true">
3+
<img v-show="iconState === 'loaded'" class="tech-chip-img" :src="iconUrl" alt="" @load="onLoad" @error="onError" />
4+
<span v-show="iconState !== 'loaded'" class="tech-chip-initial">{{ initial }}</span>
5+
</span>
6+
</template>
7+
8+
<script setup lang="ts">
9+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
10+
11+
const props = defineProps<{
12+
name: string
13+
large?: boolean
14+
}>()
15+
16+
const TIMEOUT_MS = 2000
17+
18+
const PALETTE = [
19+
'tech-chip-blue',
20+
'tech-chip-emerald',
21+
'tech-chip-amber',
22+
'tech-chip-rose',
23+
'tech-chip-violet',
24+
'tech-chip-cyan',
25+
'tech-chip-slate'
26+
] as const
27+
28+
const hashName = (name: string): number => {
29+
let hash = 0
30+
for (let i = 0; i < name.length; i++) {
31+
hash = (hash * 31 + name.charCodeAt(i)) | 0
32+
}
33+
return Math.abs(hash)
34+
}
35+
36+
const chipClass = computed(() => {
37+
const cls = PALETTE[hashName(props.name) % PALETTE.length]
38+
return props.large ? `${cls} tech-chip-large` : cls
39+
})
40+
41+
const initial = computed(() => {
42+
const raw = String(props.name || '').trim()
43+
if (!raw) return '?'
44+
if (/^[぀-ヿ㐀-鿿]/.test(raw)) return raw.charAt(0)
45+
const letter = raw.replace(/^[^a-zA-Z0-9]+/, '').charAt(0)
46+
return (letter || raw.charAt(0)).toUpperCase()
47+
})
48+
49+
// 走 cdn.simpleicons.org/<slug> 拉 SVG 图标。
50+
// SimpleIcons slug 规则:小写、空格/特殊字符删掉、`.` → `dot`、`+` → `plus`、`&` → `and`。
51+
// 没收录的(中文名 / 站点自家脚本 / 兜底「疑似前端库」)slug 为空或 404,2s 超时回落文字色块
52+
const toSlug = (raw: string): string => {
53+
const baseName = raw.split('/')[0].trim()
54+
return baseName
55+
.toLowerCase()
56+
.replace(/\./g, 'dot')
57+
.replace(/\+/g, 'plus')
58+
.replace(/&/g, 'and')
59+
.replace(/[^a-z0-9]/g, '')
60+
}
61+
62+
const iconUrl = computed(() => {
63+
const slug = toSlug(String(props.name || ''))
64+
if (!slug) return ''
65+
return `https://cdn.simpleicons.org/${slug}`
66+
})
67+
68+
const iconState = ref<'pending' | 'loaded' | 'failed'>(iconUrl.value ? 'pending' : 'failed')
69+
70+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null
71+
72+
const clearTimer = () => {
73+
if (timeoutHandle != null) {
74+
clearTimeout(timeoutHandle)
75+
timeoutHandle = null
76+
}
77+
}
78+
79+
const onLoad = () => {
80+
clearTimer()
81+
iconState.value = 'loaded'
82+
}
83+
84+
const onError = () => {
85+
clearTimer()
86+
iconState.value = 'failed'
87+
}
88+
89+
onMounted(() => {
90+
if (iconState.value !== 'pending') return
91+
timeoutHandle = setTimeout(() => {
92+
timeoutHandle = null
93+
if (iconState.value === 'pending') iconState.value = 'failed'
94+
}, TIMEOUT_MS)
95+
})
96+
97+
onBeforeUnmount(clearTimer)
98+
</script>
99+
100+
<style lang="scss" scoped>
101+
.tech-chip {
102+
align-items: center;
103+
background: var(--tech-chip-bg, var(--accent));
104+
border-radius: 4px;
105+
color: #fff;
106+
display: inline-flex;
107+
flex-shrink: 0;
108+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
109+
font-size: 10px;
110+
font-weight: 600;
111+
height: 18px;
112+
justify-content: center;
113+
line-height: 1;
114+
overflow: hidden;
115+
position: relative;
116+
width: 18px;
117+
118+
&.tech-chip-blue {
119+
--tech-chip-bg: #4f7ab8;
120+
}
121+
&.tech-chip-emerald {
122+
--tech-chip-bg: #3f8f6b;
123+
}
124+
&.tech-chip-amber {
125+
--tech-chip-bg: #b58435;
126+
}
127+
&.tech-chip-rose {
128+
--tech-chip-bg: #b95a6a;
129+
}
130+
&.tech-chip-violet {
131+
--tech-chip-bg: #7a6cb5;
132+
}
133+
&.tech-chip-cyan {
134+
--tech-chip-bg: #4a8a9b;
135+
}
136+
&.tech-chip-slate {
137+
--tech-chip-bg: #6b7280;
138+
}
139+
}
140+
141+
.tech-chip-large {
142+
border-radius: 8px;
143+
font-size: 16px;
144+
height: 36px;
145+
width: 36px;
146+
}
147+
148+
// favicon 拉到后,img 充满 chip 容器覆盖文字
149+
.tech-chip-img {
150+
height: 100%;
151+
object-fit: contain;
152+
width: 100%;
153+
}
154+
155+
.tech-chip-initial {
156+
position: relative;
157+
}
158+
</style>

src/ui/popup/Popup.vue

Lines changed: 3 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
:title="`查看 ${tech.name} 详情`"
129129
@click="openTechDetail(tech)"
130130
>
131-
<span :class="['tech-chip', techChipClass(tech)]" aria-hidden="true">{{ techInitial(tech) }}</span>
131+
<TechChip :name="tech.name" />
132132
<span class="tech-row-name">{{ tech.name }}</span>
133133
</button>
134134
</div>
@@ -214,9 +214,7 @@
214214
</div>
215215
<div v-else-if="footerPanel === 'tech' && selectedTech" class="footer-panel-body tech-detail-body">
216216
<div class="tech-detail-head">
217-
<span :class="['tech-chip', 'tech-chip-large', techChipClass(selectedTech)]" aria-hidden="true">
218-
{{ techInitial(selectedTech) }}
219-
</span>
217+
<TechChip :name="selectedTech.name" large />
220218
<div class="tech-detail-meta">
221219
<span class="tech-detail-category">{{ selectedTech.category }}</span>
222220
<span class="tech-detail-name">{{ selectedTech.name }}</span>
@@ -316,6 +314,7 @@
316314
import Checkbox from '@/ui/components/Checkbox.vue'
317315
import Input from '@/ui/components/Input.vue'
318316
import RippleButton from '@/ui/components/RippleButton.vue'
317+
import TechChip from '@/ui/components/TechChip.vue'
319318
import { categoryIndex, confidenceClass, confidenceRank } from '@/utils/category-order'
320319
import { applyCustomCss } from '@/utils/apply-custom-css'
321320
import { normalizeSettings } from '@/utils/normalize-settings'
@@ -386,41 +385,6 @@
386385
return ''
387386
})
388387
389-
// 技术列表条目的颜色块:基于名字哈希到一组克制的中性色,避免外部 favicon 请求泄露用户行为
390-
const TECH_CHIP_PALETTE = [
391-
'tech-chip-blue',
392-
'tech-chip-emerald',
393-
'tech-chip-amber',
394-
'tech-chip-rose',
395-
'tech-chip-violet',
396-
'tech-chip-cyan',
397-
'tech-chip-slate'
398-
] as const
399-
400-
const hashTechName = (name: string): number => {
401-
let hash = 0
402-
for (let i = 0; i < name.length; i++) {
403-
hash = (hash * 31 + name.charCodeAt(i)) | 0
404-
}
405-
return Math.abs(hash)
406-
}
407-
408-
const techChipClass = (tech: any) => {
409-
const name = String(tech?.name || '')
410-
if (!name) return TECH_CHIP_PALETTE[0]
411-
return TECH_CHIP_PALETTE[hashTechName(name) % TECH_CHIP_PALETTE.length]
412-
}
413-
414-
const techInitial = (tech: any) => {
415-
const raw = String(tech?.name || '').trim()
416-
if (!raw) return '?'
417-
// 中文 / 日文 / 韩文取第一个字符
418-
if (/^[぀-ヿ㐀-鿿]/.test(raw)) return raw.charAt(0)
419-
// 跳过常见前缀(@、/),取第一个字母
420-
const letter = raw.replace(/^[^a-zA-Z0-9]+/, '').charAt(0)
421-
return (letter || raw.charAt(0)).toUpperCase()
422-
}
423-
424388
const openTechDetail = (tech: any) => {
425389
selectedTech.value = tech
426390
rawSourceContext.value = null
@@ -1664,52 +1628,6 @@
16641628
white-space: nowrap;
16651629
}
16661630
1667-
// 圆角小色块,首字母在中间。哈希到 7 个调度过的中性色,避免饱和过高
1668-
.tech-chip {
1669-
align-items: center;
1670-
background: var(--tech-chip-bg, var(--accent));
1671-
border-radius: 4px;
1672-
color: #fff;
1673-
display: inline-flex;
1674-
flex-shrink: 0;
1675-
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
1676-
font-size: 10px;
1677-
font-weight: 600;
1678-
height: 18px;
1679-
justify-content: center;
1680-
line-height: 1;
1681-
width: 18px;
1682-
1683-
&.tech-chip-blue {
1684-
--tech-chip-bg: #4f7ab8;
1685-
}
1686-
&.tech-chip-emerald {
1687-
--tech-chip-bg: #3f8f6b;
1688-
}
1689-
&.tech-chip-amber {
1690-
--tech-chip-bg: #b58435;
1691-
}
1692-
&.tech-chip-rose {
1693-
--tech-chip-bg: #b95a6a;
1694-
}
1695-
&.tech-chip-violet {
1696-
--tech-chip-bg: #7a6cb5;
1697-
}
1698-
&.tech-chip-cyan {
1699-
--tech-chip-bg: #4a8a9b;
1700-
}
1701-
&.tech-chip-slate {
1702-
--tech-chip-bg: #6b7280;
1703-
}
1704-
}
1705-
1706-
.tech-chip-large {
1707-
border-radius: 8px;
1708-
font-size: 16px;
1709-
height: 36px;
1710-
width: 36px;
1711-
}
1712-
17131631
// 详情面板里仍保留的彩色置信度徽章(详情视图里信息密度低,徽章撑得开)
17141632
.confidence {
17151633
border-radius: 4px;

0 commit comments

Comments
 (0)