From 7e931a4372492082509cae2c67ae1b69c35e2370 Mon Sep 17 00:00:00 2001 From: "yinjun.chen1" Date: Fri, 18 Sep 2026 10:02:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5favicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- popup.css | 8 ++++++++ src/popup/bookmark-import.ts | 1 + src/popup/config-edit-modal.ts | 3 ++- src/popup/config-form.ts | 3 ++- src/popup/config-list.ts | 15 ++++++++++++++- src/popup/transfer.ts | 1 + src/types.d.ts | 1 + src/ui-utils.ts | 27 +++++++++++++++++++++++++++ 8 files changed, 56 insertions(+), 3 deletions(-) diff --git a/popup.css b/popup.css index 06d0608..84c92d5 100644 --- a/popup.css +++ b/popup.css @@ -808,6 +808,14 @@ h2 { min-width: 0; } +.config-favicon { + width: 16px; + height: 16px; + flex-shrink: 0; + border-radius: 3px; + margin-right: 2px; +} + .config-domain { font-weight: 600; color: #e94560; diff --git a/src/popup/bookmark-import.ts b/src/popup/bookmark-import.ts index fd71ec5..37280f6 100644 --- a/src/popup/bookmark-import.ts +++ b/src/popup/bookmark-import.ts @@ -139,6 +139,7 @@ export function initBookmarkImport(ctx: PopupCtx) { submitSelector: null, enabled: true, delayMs: null, + favicon: null, }); added++; } diff --git a/src/popup/config-edit-modal.ts b/src/popup/config-edit-modal.ts index 495ce18..dea680a 100644 --- a/src/popup/config-edit-modal.ts +++ b/src/popup/config-edit-modal.ts @@ -3,7 +3,7 @@ * 通过 ctx.openEditModal(cfg | null) 打开:cfg 为 null 表示新增。 */ import { t } from "../i18n.js"; -import { resolveAlias } from "../ui-utils.js"; +import { resolveAlias, fetchFaviconByDomain } from "../ui-utils.js"; import type { DomainConfig } from "../types.js"; import { $ } from "./dom"; import type { PopupCtx } from "./context"; @@ -92,6 +92,7 @@ export function initEditConfigModal(ctx: PopupCtx) { submitSelector: editSubmitSelectorInput.value.trim() || null, enabled: editEnabledCheckbox.checked, delayMs: editDelayMsInput.value ? Number(editDelayMsInput.value) : null, + favicon: (await fetchFaviconByDomain(domain)) || null, }; const configs = await EnvStore.loadConfigs(); diff --git a/src/popup/config-form.ts b/src/popup/config-form.ts index e7be06c..c364bb5 100644 --- a/src/popup/config-form.ts +++ b/src/popup/config-form.ts @@ -3,7 +3,7 @@ * 添加按钮提交、"填入当前域名"按钮。 */ import { t } from "../i18n.js"; -import { resolveAlias } from "../ui-utils.js"; +import { resolveAlias, fetchFaviconByDomain } from "../ui-utils.js"; import type { DomainConfig } from "../types.js"; import { $ } from "./dom"; import type { PopupCtx } from "./context"; @@ -120,6 +120,7 @@ export function initConfigForm(ctx: PopupCtx) { submitSelector: submitSelectorInput.value.trim() || null, enabled: enabledCheckbox.checked, delayMs: delayMsInput.value ? Number(delayMsInput.value) : null, + favicon: (await fetchFaviconByDomain(domain)) || null, }; const configs = await EnvStore.loadConfigs(); diff --git a/src/popup/config-list.ts b/src/popup/config-list.ts index 312a351..031e06d 100644 --- a/src/popup/config-list.ts +++ b/src/popup/config-list.ts @@ -3,7 +3,7 @@ * 空状态与"添加区/列表区"位置互换。 */ import { t } from "../i18n.js"; -import { closeAllMenus, openConfigUrl } from "../ui-utils.js"; +import { closeAllMenus, openConfigUrl, faviconFallbackUrl } from "../ui-utils.js"; import { $ } from "./dom"; import type { PopupCtx } from "./context"; @@ -137,6 +137,19 @@ export function initConfigList(ctx: PopupCtx) {
${displayName} ${enabledTag} ${authTag}
`; + // favicon + const faviconSrc = cfg.favicon || faviconFallbackUrl(cfg.domain); + if (faviconSrc) { + const favicon = document.createElement("img"); + favicon.className = "config-favicon"; + favicon.src = faviconSrc; + favicon.alt = ""; + favicon.addEventListener("error", () => { + favicon.style.display = "none"; + }); + item.appendChild(favicon); + } + if (needsAuth) { const tag = info.querySelector(".tag-unauthorized"); if (tag) { diff --git a/src/popup/transfer.ts b/src/popup/transfer.ts index 31b3931..b0ac387 100644 --- a/src/popup/transfer.ts +++ b/src/popup/transfer.ts @@ -148,6 +148,7 @@ export function initTransfer(ctx: PopupCtx) { submitSelector: cfg.submitSelector || null, enabled: cfg.enabled !== false, delayMs: cfg.delayMs || null, + favicon: cfg.favicon || null, }); // 分支 A:导入 v3 环境结构 —— 同名环境合并配置(按 domain 去重),新环境创建 diff --git a/src/types.d.ts b/src/types.d.ts index 4168b08..29356df 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -11,6 +11,7 @@ export interface DomainConfig { submitSelector: string | null; enabled: boolean; delayMs: number | null; + favicon: string | null; } /** 环境 */ diff --git a/src/ui-utils.ts b/src/ui-utils.ts index 28b94c9..f100858 100644 --- a/src/ui-utils.ts +++ b/src/ui-utils.ts @@ -38,3 +38,30 @@ export async function resolveAlias(domain: string): Promise { export function closeAllMenus() { document.querySelectorAll(".config-menu.open").forEach((m) => m.classList.remove("open")); } +/** + * 从已打开的标签页里找匹配域名的页面,取其 favIconUrl。 + * 找不到时返回 null,渲染时用 faviconFallbackUrl 兜底。 + */ +export async function fetchFaviconByDomain(domain: string): Promise { + try { + const base = /^https?:\/\//i.test(domain) ? domain : "*://" + domain; + const patterns = [base]; + if (!base.endsWith("*")) patterns.push(base + "/*"); + const tabs = await chrome.tabs.query({ url: patterns }); + for (const tab of tabs) { + if (tab.url && tab.favIconUrl && isDomainMatch(tab.url, domain)) return tab.favIconUrl; + } + } catch (e) {} + return null; +} + +/** 从 domain 构造 /favicon.ico 兜底 URL */ +export function faviconFallbackUrl(domain: string): string | null { + try { + const withProto = /^https?:\/\//i.test(domain) ? domain : "https://" + domain; + const u = new URL(withProto); + return u.origin + "/favicon.ico"; + } catch (e) { + return null; + } +}