From 5bcd2435661a61126e596167e34644293f843f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 8 Sep 2026 16:42:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20=E5=AE=89=E8=A3=85=E9=A1=B5?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=97=B6=E6=8C=89=E5=B7=AE=E5=BC=82=E5=91=88?= =?UTF-8?q?=E7=8E=B0=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新页此前与全新安装共用同一份无基线的权限渲染,权限一项没改也全量重复 上次已确认过的内容(#1716)。已安装脚本的 metadata 其实早已取到,只是被 丢弃,因此计算差异不需要任何新的跨进程调用。 以已安装版本为基线逐取值标注新增与移除:新增与移除常驻可见,未变动项收 进「未变动 N 项」折叠桶,有变动的类别排到最前、未变动的类别塌成单行。变 动状态只由字重、描边与 +/− 记号表达,chip 底色继续只表达风险等级——否则 新增的 @connect * 会被染成「新增色」,把最该报警的情况伪装成安全的。每个 带状态的 chip 另附 sr-only 文案,使变动不只靠颜色传达。 某类别在新版本被清空但旧版本非空时仍然成行,否则「不再请求网络访问」这类 变化会整行消失。全新安装没有基线,呈现一字不改。 Ref #1716 --- src/locales/de-DE/install.json | 7 + src/locales/en-US/install.json | 7 + src/locales/ja-JP/install.json | 7 + src/locales/ko-KR/install.json | 7 + src/locales/pt-BR/install.json | 7 + src/locales/ru-RU/install.json | 7 + src/locales/tr-TR/install.json | 7 + src/locales/vi-VN/install.json | 7 + src/locales/zh-CN/install.json | 7 + src/locales/zh-TW/install.json | 7 + src/pages/install/App.test.tsx | 34 +++++ src/pages/install/App.tsx | 5 +- .../components/PermissionCard.test.tsx | 63 +++++++- .../install/components/PermissionCard.tsx | 80 ++++++++-- .../install/components/PermissionRow.test.tsx | 54 +++++++ .../install/components/PermissionRow.tsx | 140 ++++++++++++++---- src/pages/install/permissions.test.ts | 72 ++++++++- src/pages/install/permissions.ts | 105 +++++++++---- src/pages/install/useInstallData.test.ts | 36 +++++ src/pages/install/useInstallData.ts | 11 +- 20 files changed, 590 insertions(+), 80 deletions(-) diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json index 485602949..5c98d791b 100644 --- a/src/locales/de-DE/install.json +++ b/src/locales/de-DE/install.json @@ -13,6 +13,13 @@ "perm_card_title": "Dieses Skript erhält die folgenden Berechtigungen", "perm_card_hint": "Bitte vor der Installation bestätigen", "perm_card_empty": "Dieses Skript fordert keine besonderen Berechtigungen an", + "perm_card_title_changed": "Berechtigungsänderungen", + "perm_card_compare": "Verglichen mit installierter Version {{version}}", + "perm_no_change": "Keine Änderung", + "perm_change_added": "Hinzugefügt", + "perm_change_removed": "Entfernt", + "perm_change_unchanged": "Unverändert", + "perm_unchanged_more": "{{count}} unverändert", "perm_match_label": "Läuft auf", "perm_match_summary": "Das Skript läuft auf diesen Websites und verändert die Seiten", "perm_connect_label": "Cross-Origin-Zugriff", diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json index a2619096a..18f58f659 100644 --- a/src/locales/en-US/install.json +++ b/src/locales/en-US/install.json @@ -13,6 +13,13 @@ "perm_card_title": "This script will be granted the following permissions", "perm_card_hint": "Please confirm before installing", "perm_card_empty": "This script does not request any special permissions", + "perm_card_title_changed": "Permission changes", + "perm_card_compare": "Compared with installed {{version}}", + "perm_no_change": "No change", + "perm_change_added": "Added", + "perm_change_removed": "Removed", + "perm_change_unchanged": "Unchanged", + "perm_unchanged_more": "{{count}} unchanged", "perm_match_label": "Runs on", "perm_match_summary": "The script runs on these websites and modifies the pages", "perm_connect_label": "Cross-origin access", diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json index 89547e531..1a9f034a5 100644 --- a/src/locales/ja-JP/install.json +++ b/src/locales/ja-JP/install.json @@ -13,6 +13,13 @@ "perm_card_title": "このスクリプトには以下の権限が付与されます", "perm_card_hint": "インストール前に確認してください", "perm_card_empty": "このスクリプトは特別な権限を要求しません", + "perm_card_title_changed": "権限の変更", + "perm_card_compare": "インストール済み {{version}} との比較", + "perm_no_change": "変更なし", + "perm_change_added": "追加", + "perm_change_removed": "削除", + "perm_change_unchanged": "変更なし", + "perm_unchanged_more": "変更なし {{count}} 件", "perm_match_label": "実行サイト", "perm_match_summary": "スクリプトはこれらのサイトで動作し、ページを変更します", "perm_connect_label": "クロスオリジンアクセス", diff --git a/src/locales/ko-KR/install.json b/src/locales/ko-KR/install.json index f11d0a884..a26e39daa 100644 --- a/src/locales/ko-KR/install.json +++ b/src/locales/ko-KR/install.json @@ -13,6 +13,13 @@ "perm_card_title": "이 스크립트에 다음 권한이 부여됩니다", "perm_card_hint": "설치하기 전에 확인해 주세요", "perm_card_empty": "이 스크립트는 특별한 권한을 요청하지 않습니다", + "perm_card_title_changed": "권한 변경", + "perm_card_compare": "설치된 {{version}}과 비교", + "perm_no_change": "변경 없음", + "perm_change_added": "추가됨", + "perm_change_removed": "제거됨", + "perm_change_unchanged": "변경 없음", + "perm_unchanged_more": "변경 없음 {{count}}개", "perm_match_label": "실행 사이트", "perm_match_summary": "스크립트가 다음 사이트에서 실행되며 페이지를 수정합니다", "perm_connect_label": "교차 출처 접근", diff --git a/src/locales/pt-BR/install.json b/src/locales/pt-BR/install.json index cb38a1aee..eda55b9ee 100644 --- a/src/locales/pt-BR/install.json +++ b/src/locales/pt-BR/install.json @@ -13,6 +13,13 @@ "perm_card_title": "A este script serão concedidas as seguintes permissões", "perm_card_hint": "Por favor, confirme antes de instalar", "perm_card_empty": "Este script não solicita nenhuma permissão especial", + "perm_card_title_changed": "Alterações de permissões", + "perm_card_compare": "Comparado com a versão instalada {{version}}", + "perm_no_change": "Sem alterações", + "perm_change_added": "Adicionado", + "perm_change_removed": "Removido", + "perm_change_unchanged": "Inalterado", + "perm_unchanged_more": "{{count}} inalterado(s)", "perm_match_label": "Executa em", "perm_match_summary": "O script é executado nestes sites e modifica as páginas", "perm_connect_label": "Acesso entre origens (CORS)", diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json index eb759a6f5..f0b623bb5 100644 --- a/src/locales/ru-RU/install.json +++ b/src/locales/ru-RU/install.json @@ -13,6 +13,13 @@ "perm_card_title": "Этому скрипту будут предоставлены следующие разрешения", "perm_card_hint": "Подтвердите перед установкой", "perm_card_empty": "Этот скрипт не запрашивает особых разрешений", + "perm_card_title_changed": "Изменения разрешений", + "perm_card_compare": "Сравнение с установленной {{version}}", + "perm_no_change": "Без изменений", + "perm_change_added": "Добавлено", + "perm_change_removed": "Удалено", + "perm_change_unchanged": "Без изменений", + "perm_unchanged_more": "Без изменений: {{count}}", "perm_match_label": "Запускается на", "perm_match_summary": "Скрипт работает на этих сайтах и изменяет страницы", "perm_connect_label": "Кросс-доменный доступ", diff --git a/src/locales/tr-TR/install.json b/src/locales/tr-TR/install.json index 9792ff3b9..11c6eff77 100644 --- a/src/locales/tr-TR/install.json +++ b/src/locales/tr-TR/install.json @@ -13,6 +13,13 @@ "perm_card_title": "Bu betiğe aşağıdaki izinler verilecek", "perm_card_hint": "Lütfen yüklemeden önce onaylayın", "perm_card_empty": "Bu betik herhangi bir özel izin istemiyor", + "perm_card_title_changed": "İzin değişiklikleri", + "perm_card_compare": "Yüklü {{version}} ile karşılaştırma", + "perm_no_change": "Değişiklik yok", + "perm_change_added": "Eklendi", + "perm_change_removed": "Kaldırıldı", + "perm_change_unchanged": "Değişmedi", + "perm_unchanged_more": "{{count}} değişmedi", "perm_match_label": "Çalıştırıldığı yer", "perm_match_summary": "Betik bu web sitelerinde çalışır ve sayfaları değiştirir", "perm_connect_label": "Kaynaklar arası erişim", diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json index ed556c9db..07ca4970e 100644 --- a/src/locales/vi-VN/install.json +++ b/src/locales/vi-VN/install.json @@ -13,6 +13,13 @@ "perm_card_title": "Script này sẽ được cấp các quyền sau", "perm_card_hint": "Vui lòng xác nhận trước khi cài đặt", "perm_card_empty": "Script này không yêu cầu bất kỳ quyền đặc biệt nào", + "perm_card_title_changed": "Thay đổi quyền", + "perm_card_compare": "So với {{version}} đã cài đặt", + "perm_no_change": "Không thay đổi", + "perm_change_added": "Đã thêm", + "perm_change_removed": "Đã xóa", + "perm_change_unchanged": "Không đổi", + "perm_unchanged_more": "{{count}} không đổi", "perm_match_label": "Chạy trên", "perm_match_summary": "Script chạy trên các website này và thay đổi trang", "perm_connect_label": "Truy cập khác nguồn gốc", diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json index 6e1eba734..52a71d2e4 100644 --- a/src/locales/zh-CN/install.json +++ b/src/locales/zh-CN/install.json @@ -13,6 +13,13 @@ "perm_card_title": "此脚本将获得以下权限", "perm_card_hint": "安装前请确认", "perm_card_empty": "此脚本不请求任何特殊权限", + "perm_card_title_changed": "权限变化", + "perm_card_compare": "对比已安装的 {{version}}", + "perm_no_change": "无变化", + "perm_change_added": "新增", + "perm_change_removed": "已移除", + "perm_change_unchanged": "未变动", + "perm_unchanged_more": "未变动 {{count}} 项", "perm_match_label": "运行网站", "perm_match_summary": "脚本会在这些网站上运行并修改页面", "perm_connect_label": "跨域访问", diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json index 4e16a8c38..454450165 100644 --- a/src/locales/zh-TW/install.json +++ b/src/locales/zh-TW/install.json @@ -13,6 +13,13 @@ "perm_card_title": "此腳本將取得以下權限", "perm_card_hint": "安裝前請確認", "perm_card_empty": "此腳本不要求任何特殊權限", + "perm_card_title_changed": "權限變更", + "perm_card_compare": "對比已安裝的 {{version}}", + "perm_no_change": "無變更", + "perm_change_added": "新增", + "perm_change_removed": "已移除", + "perm_change_unchanged": "未變更", + "perm_unchanged_more": "未變更 {{count}} 項", "perm_match_label": "執行網站", "perm_match_summary": "腳本會在這些網站上執行並修改頁面", "perm_connect_label": "跨域存取", diff --git a/src/pages/install/App.test.tsx b/src/pages/install/App.test.tsx index 260b34f49..7baa46421 100644 --- a/src/pages/install/App.test.tsx +++ b/src/pages/install/App.test.tsx @@ -323,3 +323,37 @@ describe("Install App 安装反馈", () => { expect(screen.getByTestId("install-error-bar-message")).toHaveTextContent("技能写入失败"); }); }); + +describe("Install App 权限卡的更新基线", () => { + it("更新场景把已安装版本作为权限对比基线传给权限卡", () => { + mockHook.mockReturnValue({ + ...baseHook(), + state: { + status: "ready", + view: readyView({ + isUpdate: true, + version: { kind: "update", oldVersion: "1.2.3", newVersion: "2.3.1", changed: true }, + permissions: [ + { + kind: "connect", + risk: "warn", + values: ["api.a.com"], + sensitive: [], + diff: { added: ["api.a.com"], removed: [] }, + }, + ], + }), + }, + }); + render(); + expect(screen.getByText("对比已安装的 1.2.3")).toBeInTheDocument(); + expect(screen.getByText("权限变化")).toBeInTheDocument(); + }); + + it("全新安装不传基线,权限卡维持安装态卡头", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } }); + render(); + expect(screen.getByText("此脚本将获得以下权限")).toBeInTheDocument(); + expect(screen.queryByText("权限变化")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index a0857f7d6..0958d8b00 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -235,7 +235,10 @@ export default function App() { {view.isSubscribe ? ( ) : ( - + )} p.risk === "danger")} diff --git a/src/pages/install/components/PermissionCard.test.tsx b/src/pages/install/components/PermissionCard.test.tsx index 753d88fa0..25d5cc07b 100644 --- a/src/pages/install/components/PermissionCard.test.tsx +++ b/src/pages/install/components/PermissionCard.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; -import { render, screen, cleanup } from "@testing-library/react"; +import { render, screen, cleanup, within, fireEvent } from "@testing-library/react"; import { initTestLanguage } from "@Tests/initTestLanguage"; let mobile = false; @@ -47,3 +47,64 @@ describe("PermissionCard 权限卡", () => { expect(screen.queryByText("https://a.com/*")).not.toBeInTheDocument(); }); }); + +describe("PermissionCard 更新差异态", () => { + const changedConnect: PermissionRow = { + kind: "connect", + risk: "danger", + values: ["*", "api.a.com"], + sensitive: [], + diff: { added: ["*"], removed: ["old.a.com"] }, + }; + const sameMatch: PermissionRow = { + kind: "match", + risk: "normal", + values: ["https://a.com/*"], + sensitive: [], + diff: { added: [], removed: [] }, + }; + + it("有变动时卡头显示变化标题、全卡增删计数与对比版本", () => { + render(); + expect(screen.getByText("权限变化")).toBeInTheDocument(); + expect(screen.getByTestId("permission-card-delta")).toHaveTextContent("+1"); + expect(screen.getByTestId("permission-card-delta")).toHaveTextContent("−1"); + expect(screen.getByText("对比已安装的 1.2.3")).toBeInTheDocument(); + }); + + it("有变动的类别展开,无变动的类别塌成单行", () => { + render(); + expect(screen.getAllByTestId("permission-row")).toHaveLength(1); + const collapsed = screen.getByTestId("permission-row-collapsed"); + expect(within(collapsed).getByText("运行网站")).toBeInTheDocument(); + expect(within(collapsed).getByText("无变化")).toBeInTheDocument(); + expect(screen.queryByText("https://a.com/*")).not.toBeInTheDocument(); + }); + + it("点开塌行后该类别取值可见", () => { + render(); + fireEvent.click(screen.getByTestId("permission-row-collapsed")); + expect(screen.getByText("https://a.com/*")).toBeInTheDocument(); + expect(screen.getAllByTestId("permission-row")).toHaveLength(2); + }); + + it("更新但一项未变时不显示变化标题与增删计数", () => { + render(); + expect(screen.queryByText("权限变化")).not.toBeInTheDocument(); + expect(screen.queryByTestId("permission-card-delta")).not.toBeInTheDocument(); + }); + + it("移动端默认展开有变动的类别,未变动的收起", () => { + mobile = true; + render(); + expect(screen.getByText("*")).toBeInTheDocument(); + expect(screen.queryByText("https://a.com/*")).not.toBeInTheDocument(); + }); + + it("全新安装(无 diff)卡头维持原样", () => { + render(); + expect(screen.getByText("此脚本将获得以下权限")).toBeInTheDocument(); + expect(screen.queryByTestId("permission-card-delta")).not.toBeInTheDocument(); + expect(screen.queryByTestId("permission-row-collapsed")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/PermissionCard.tsx b/src/pages/install/components/PermissionCard.tsx index 2115a21f6..c7803d186 100644 --- a/src/pages/install/components/PermissionCard.tsx +++ b/src/pages/install/components/PermissionCard.tsx @@ -1,15 +1,26 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ShieldCheck } from "lucide-react"; +import { ChevronDown, ShieldCheck } from "lucide-react"; import { cn } from "@App/pkg/utils/cn"; import { useIsMobile } from "@App/pages/components/use-is-mobile"; import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@App/pages/components/ui/accordion"; -import type { PermissionRow as PermissionRowData } from "../permissions"; -import { PermissionRow, PermissionChips, KIND_META, RISK_STYLE } from "./PermissionRow"; +import { isPermissionChanged, type PermissionRow as PermissionRowData } from "../permissions"; +import { PermissionRow, PermissionChips, PermissionDelta, KIND_META, RISK_STYLE } from "./PermissionRow"; -function MobilePermissions({ rows }: { rows: PermissionRowData[] }) { +/** 无变化标记;更新场景用来把「这一类你上次已经确认过」说出来 */ +function NoChangeTag() { const { t } = useTranslation(["install", "common"]); - // 默认仅展开高风险(danger)项 - const defaultValue = rows.filter((r) => r.risk === "danger").map((r) => r.kind); + return ( + + {t("install:perm_no_change")} + + ); +} + +function MobilePermissions({ rows, isUpdate }: { rows: PermissionRowData[]; isUpdate: boolean }) { + const { t } = useTranslation(["install", "common"]); + // 更新场景默认只展开有变动的类别,全新安装仍只展开高风险项 + const defaultValue = rows.filter((r) => (isUpdate ? isPermissionChanged(r) : r.risk === "danger")).map((r) => r.kind); return ( @@ -27,6 +38,8 @@ function MobilePermissions({ rows }: { rows: PermissionRowData[] }) { {row.values.length} + + {isUpdate && !isPermissionChanged(row) && } @@ -39,26 +52,71 @@ function MobilePermissions({ rows }: { rows: PermissionRowData[] }) { ); } -export function PermissionCard({ rows }: { rows: PermissionRowData[] }) { +/** 未变动类别的单行形态:名称、计数与「无变化」,点开即还原成完整权限行 */ +function CollapsedRow({ row }: { row: PermissionRowData }) { + const { t } = useTranslation(["install", "common"]); + const [open, setOpen] = useState(false); + const { icon: Icon, labelKey } = KIND_META[row.kind]; + const style = RISK_STYLE[row.risk]; + + if (open) return ; + + return ( + + ); +} + +export function PermissionCard({ rows, baselineVersion }: { rows: PermissionRowData[]; baselineVersion?: string }) { const { t } = useTranslation(["install", "common"]); const isMobile = useIsMobile(); + const isUpdate = baselineVersion !== undefined; + const changed = rows.filter(isPermissionChanged); + const added = changed.reduce((n, r) => n + r.diff!.added.length, 0); + const removed = changed.reduce((n, r) => n + r.diff!.removed.length, 0); return (
-

{t("install:perm_card_title")}

- {t("install:perm_card_hint")} +

+ {changed.length ? t("install:perm_card_title_changed") : t("install:perm_card_title")} +

+ {changed.length > 0 && ( + + {added > 0 && {`+${added}`}} + {removed > 0 && {`−${removed}`}} + + )} + {isUpdate && !changed.length && } + + {isUpdate ? t("install:perm_card_compare", { version: baselineVersion }) : t("install:perm_card_hint")} +
{rows.length === 0 ? (

{t("install:perm_card_empty")}

) : isMobile ? ( - + ) : ( rows.map((row, i) => (
0 && "border-t border-border")}> - + {isUpdate && !isPermissionChanged(row) ? : }
)) )} diff --git a/src/pages/install/components/PermissionRow.test.tsx b/src/pages/install/components/PermissionRow.test.tsx index 15a927e9d..de24f2a19 100644 --- a/src/pages/install/components/PermissionRow.test.tsx +++ b/src/pages/install/components/PermissionRow.test.tsx @@ -69,3 +69,57 @@ describe("PermissionRow 权限行", () => { expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument(); }); }); + +describe("PermissionRow 更新差异态", () => { + const changed = { + kind: "connect" as const, + risk: "danger" as const, + values: ["*", "api.a.com", "api.b.com"], + sensitive: [], + diff: { added: ["*"], removed: ["old.a.com"] }, + }; + + it("新增与移除的取值常驻可见,未变动的收进折叠桶", () => { + render(); + const row = screen.getByTestId("permission-row"); + expect(within(row).getByText("*")).toBeInTheDocument(); + expect(within(row).getByText("old.a.com")).toBeInTheDocument(); + expect(within(row).queryByText("api.a.com")).not.toBeInTheDocument(); + expect(within(row).getByTestId("permission-more")).toHaveTextContent("未变动 2 项"); + }); + + it("变动状态标在 chip 上,且新增/移除带可读状态文案而不只靠颜色", () => { + render(); + const row = screen.getByTestId("permission-row"); + expect(within(row).getByText("*").closest("[data-chip]")).toHaveAttribute("data-change", "added"); + expect(within(row).getByText("old.a.com").closest("[data-chip]")).toHaveAttribute("data-change", "removed"); + expect(within(row).getByText("新增")).toBeInTheDocument(); + expect(within(row).getByText("已移除")).toBeInTheDocument(); + }); + + it("展开折叠桶后未变动取值可见并标记为 unchanged", () => { + render(); + const row = screen.getByTestId("permission-row"); + fireEvent.click(within(row).getByTestId("permission-more")); + expect(within(row).getByText("api.a.com").closest("[data-chip]")).toHaveAttribute("data-change", "unchanged"); + expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument(); + }); + + it("行内变动计数按新增与移除分别呈现", () => { + render(); + const row = screen.getByTestId("permission-row"); + expect(within(row).getByTestId("permission-delta")).toHaveTextContent("+1"); + expect(within(row).getByTestId("permission-delta")).toHaveTextContent("−1"); + }); + + it("差异为空的行不渲染变动计数,取值全部按未变动呈现", () => { + render( + + ); + const row = screen.getByTestId("permission-row"); + expect(within(row).queryByTestId("permission-delta")).not.toBeInTheDocument(); + expect(within(row).getByText("a").closest("[data-chip]")).toHaveAttribute("data-change", "unchanged"); + }); +}); diff --git a/src/pages/install/components/PermissionRow.tsx b/src/pages/install/components/PermissionRow.tsx index bc18da4c2..74f7fa508 100644 --- a/src/pages/install/components/PermissionRow.tsx +++ b/src/pages/install/components/PermissionRow.tsx @@ -2,7 +2,12 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Globe, ArrowLeftRight, ChevronDown, KeyRound, Package, TriangleAlert, type LucideIcon } from "lucide-react"; import { cn } from "@App/pkg/utils/cn"; -import type { PermissionKind, PermissionRisk, PermissionRow as PermissionRowData } from "../permissions"; +import { + isPermissionChanged, + type PermissionKind, + type PermissionRisk, + type PermissionRow as PermissionRowData, +} from "../permissions"; export const KIND_META: Record = { match: { icon: Globe, labelKey: "install:perm_match_label", summaryKey: "install:perm_match_summary" }, @@ -35,7 +40,64 @@ export const RISK_STYLE: Record = { + added: "install:perm_change_added", + removed: "install:perm_change_removed", + unchanged: "install:perm_change_unchanged", +}; + +function Chip({ value, row, change }: { value: string; row: PermissionRowData; change?: ChangeState }) { + const { t } = useTranslation(["install", "common"]); + const isSensitive = row.sensitive.includes(value); + const base = isSensitive ? "border border-warning-fg bg-muted text-warning-fg" : RISK_STYLE[row.risk].chip; + + // 变动状态只用字重、描边与 +/− 记号表达,底色继续留给风险等级—— + // 否则新增的 @connect * 会被染成「新增色」,把最该报警的情况伪装成安全的。 + const changeClass = + change === "removed" + ? "border border-border bg-transparent text-muted-foreground line-through" + : change === "unchanged" + ? "bg-muted border border-border text-muted-foreground" + : change === "added" + ? cn(base, "font-semibold", row.risk === "danger" && !isSensitive ? "border-destructive" : "border-current") + : base; + + return ( + + {change === "added" && } + {change === "removed" && } + {isSensitive && change !== "removed" && } + {change && {t(CHANGE_LABEL_KEY[change])}} + {value} + + ); +} + +function MoreButton({ label, onClick }: { label: string; onClick: () => void }) { + return ( + + ); +} + +/** + * 权限取值 chip 列表;桌面行与移动 Accordion 共用。 + * 全新安装按可见项 + 折叠的 +N 呈现;更新场景把新增与移除常驻在前,未变动项收进「未变动 N 项」。 + */ export function PermissionChips({ row, maxVisible = DEFAULT_MAX_VISIBLE, @@ -43,46 +105,59 @@ export function PermissionChips({ row: PermissionRowData; maxVisible?: number; }) { - const style = RISK_STYLE[row.risk]; - const sensitive = new Set(row.sensitive); + const { t } = useTranslation(["install", "common"]); const [expanded, setExpanded] = useState(false); - const visible = expanded ? row.values : row.values.slice(0, maxVisible); - const hidden = row.values.length - visible.length; + + if (!row.diff) { + const visible = expanded ? row.values : row.values.slice(0, maxVisible); + const hidden = row.values.length - visible.length; + return ( +
+ {visible.map((v) => ( + + ))} + {hidden > 0 && setExpanded(true)} />} +
+ ); + } + + const { added, removed } = row.diff; + const addedSet = new Set(added); + const unchanged = row.values.filter((v) => !addedSet.has(v)); + // 无任何变动时没有可钉住的内容,折叠桶会变成一个必须点开才能看到全部的空壳,故直接摊开 + const showUnchanged = expanded || added.length + removed.length === 0; return (
- {visible.map((v) => { - const isSensitive = sensitive.has(v); - return ( - - {isSensitive && } - {v} - - ); - })} - {hidden > 0 && ( - + /> )}
); } +/** 行头的变动计数;沿用代码卡 +N −M 的记号,无变动时不渲染 */ +export function PermissionDelta({ row }: { row: PermissionRowData }) { + if (!isPermissionChanged(row)) return null; + const { added, removed } = row.diff!; + return ( + + {added.length > 0 && {`+${added.length}`}} + {removed.length > 0 && {`−${removed.length}`}} + + ); +} + export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; maxVisible?: number }) { const { t } = useTranslation(["install", "common"]); const { icon: Icon, labelKey, summaryKey } = KIND_META[row.kind]; @@ -97,6 +172,7 @@ export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; max
{t(labelKey)} {row.values.length} + {t(summaryKey)}
diff --git a/src/pages/install/permissions.test.ts b/src/pages/install/permissions.test.ts index 3484d7691..bdf0ed955 100644 --- a/src/pages/install/permissions.test.ts +++ b/src/pages/install/permissions.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import type { SCMetadata } from "@App/app/repo/metadata"; -import { derivePermissions } from "./permissions"; +import { derivePermissions, derivePermissionDiff } from "./permissions"; describe("derivePermissions 权限派生", () => { it("无任何权限元数据时返回空数组", () => { @@ -92,3 +92,73 @@ describe("derivePermissions 权限派生", () => { expect(rows.map((r) => r.risk)).toEqual(["danger", "warn", "normal", "normal"]); }); }); + +describe("derivePermissionDiff 权限差异派生", () => { + it("新增的取值被标进 diff.added,未变动的不进", () => { + const rows = derivePermissionDiff( + { connect: ["api.example.com"] }, + { connect: ["api.example.com", "cdn.example.com"] } + ); + const connect = rows.find((r) => r.kind === "connect")!; + expect(connect.diff!.added).toEqual(["cdn.example.com"]); + expect(connect.diff!.removed).toEqual([]); + }); + + it("旧版本有、新版本没有的取值进 diff.removed", () => { + const rows = derivePermissionDiff({ grant: ["GM_setValue", "GM_notification"] }, { grant: ["GM_setValue"] }); + const grant = rows.find((r) => r.kind === "grant")!; + expect(grant.diff!.removed).toEqual(["GM_notification"]); + expect(grant.values).toEqual(["GM_setValue"]); + }); + + it("权限完全一致时每行的 diff 增删都为空", () => { + const metadata: SCMetadata = { match: ["https://a.com/*"], grant: ["GM_setValue"] }; + const rows = derivePermissionDiff(metadata, metadata); + expect(rows).toHaveLength(2); + expect(rows.every((r) => r.diff!.added.length === 0 && r.diff!.removed.length === 0)).toBe(true); + }); + + it("类别计数取新版本取值,移除项不计入 values", () => { + const rows = derivePermissionDiff( + { grant: ["GM_setValue", "GM_notification"] }, + { grant: ["GM_setValue", "GM_cookie"] } + ); + const grant = rows.find((r) => r.kind === "grant")!; + expect(grant.values).toHaveLength(2); + expect(grant.values).not.toContain("GM_notification"); + }); + + it("类别在新版本被清空但旧版本非空时仍然成行,风险降为 normal", () => { + const rows = derivePermissionDiff({ connect: ["*"] }, {}); + const connect = rows.find((r) => r.kind === "connect")!; + expect(connect.values).toEqual([]); + expect(connect.diff!.removed).toEqual(["*"]); + expect(connect.risk).toBe("normal"); + }); + + it("新旧两版都没有的类别不成行", () => { + const rows = derivePermissionDiff({ match: ["https://a.com/*"] }, { match: ["https://a.com/*"] }); + expect(rows.map((r) => r.kind)).toEqual(["match"]); + }); + + it("有变动的类别排在未变动的高危类别之前", () => { + const rows = derivePermissionDiff( + { connect: ["*"], match: ["https://a.com/*"] }, + { connect: ["*"], match: ["https://a.com/*", "https://b.com/*"] } + ); + expect(rows.map((r) => r.kind)).toEqual(["match", "connect"]); + }); + + it("同为有变动时仍按风险排序", () => { + const rows = derivePermissionDiff({}, { connect: ["*"], match: ["https://a.com/*"] }); + expect(rows.map((r) => r.kind)).toEqual(["connect", "match"]); + }); + + it("新增的危险取值仍排在该行取值最前", () => { + const rows = derivePermissionDiff({ connect: ["api.example.com"] }, { connect: ["api.example.com", "*"] }); + const connect = rows.find((r) => r.kind === "connect")!; + expect(connect.values[0]).toBe("*"); + expect(connect.risk).toBe("danger"); + expect(connect.diff!.added).toEqual(["*"]); + }); +}); diff --git a/src/pages/install/permissions.ts b/src/pages/install/permissions.ts index 70b5c20ce..6962b79c1 100644 --- a/src/pages/install/permissions.ts +++ b/src/pages/install/permissions.ts @@ -4,12 +4,20 @@ export type PermissionRisk = "normal" | "warn" | "danger"; export type PermissionKind = "match" | "connect" | "grant" | "require"; +/** 相对已安装版本的取值增删;仅更新场景派生 */ +export interface PermissionDiff { + added: string[]; + removed: string[]; +} + export interface PermissionRow { kind: PermissionKind; risk: PermissionRisk; values: string[]; /** values 中被判定为敏感的子集(如 GM_cookie),用于额外高亮 */ sensitive: string[]; + /** 更新场景才有;全新安装为 undefined,此时整行按「全部都是新的」呈现 */ + diff?: PermissionDiff; } // 需要额外标记的敏感 GM 能力(可访问 Cookie 等隐私数据) @@ -20,6 +28,9 @@ const RISK_ORDER: Record = { normal: 2, }; +// 基础类别顺序:运行网站 → 跨域访问 → GM 能力 → 外部资源 +const KIND_ORDER: PermissionKind[] = ["match", "connect", "grant", "require"]; + function sortDangerConnectFirst(values: string[]): string[] { return [...values].sort((a, b) => Number(b === "*") - Number(a === "*")); } @@ -28,43 +39,75 @@ function sortSensitiveGrantFirst(values: string[]): string[] { return [...values].sort((a, b) => Number(SENSITIVE_GRANTS.has(b)) - Number(SENSITIVE_GRANTS.has(a))); } +/** 按类别摊平元数据中的权限取值,空类别保留为空数组以便与另一版本对齐比较 */ +function collectValues(metadata: SCMetadata): Record { + return { + match: [...(metadata.match || []), ...(metadata.include || [])], + connect: metadata.connect || [], + grant: (metadata.grant || []).filter((g) => g !== "none"), + require: [...(metadata.require || []), ...(metadata.resource || [])], + }; +} + +/** 由单个类别的取值构造权限行,决定行内排序、风险与敏感项 */ +function buildRow(kind: PermissionKind, values: string[]): PermissionRow { + // 取值为空意味着该类别什么都不请求,无论原本属于哪一档都不再构成风险 + if (!values.length) { + return { kind, risk: "normal", values: [], sensitive: [] }; + } + switch (kind) { + case "connect": { + const sorted = sortDangerConnectFirst(values); + return { kind, risk: sorted.includes("*") ? "danger" : "warn", values: sorted, sensitive: [] }; + } + case "grant": { + const sorted = sortSensitiveGrantFirst(values); + return { kind, risk: "warn", values: sorted, sensitive: sorted.filter((g) => SENSITIVE_GRANTS.has(g)) }; + } + default: + return { kind, risk: "normal", values, sensitive: [] }; + } +} + +/** 该行相对已安装版本是否发生了增删;全新安装(无 diff)恒为 false */ +export const isPermissionChanged = (row: PermissionRow) => + !!row.diff && (row.diff.added.length > 0 || row.diff.removed.length > 0); + /** * 把脚本元数据派生为「权限行」,作为安装页信任决策的核心呈现。 - * 高危/告警权限优先展示,同风险内保持基础类别顺序:运行网站 → 跨域访问 → GM 能力 → 外部资源。 + * 高危/告警权限优先展示,同风险内保持基础类别顺序。 */ export function derivePermissions(metadata: SCMetadata): PermissionRow[] { - const rows: PermissionRow[] = []; - - const match = [...(metadata.match || []), ...(metadata.include || [])]; - if (match.length) { - rows.push({ kind: "match", risk: "normal", values: match, sensitive: [] }); - } - - const connect = metadata.connect || []; - if (connect.length) { - const values = sortDangerConnectFirst(connect); - rows.push({ - kind: "connect", - risk: values.includes("*") ? "danger" : "warn", - values, - sensitive: [], - }); - } + const values = collectValues(metadata); + return KIND_ORDER.filter((kind) => values[kind].length) + .map((kind) => buildRow(kind, values[kind])) + .sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]); +} - const grant = sortSensitiveGrantFirst((metadata.grant || []).filter((g) => g !== "none")); - if (grant.length) { - rows.push({ - kind: "grant", - risk: "warn", - values: grant, - sensitive: grant.filter((g) => SENSITIVE_GRANTS.has(g)), - }); - } +/** + * 以已安装脚本为基线派生权限行,逐取值标注新增与移除。 + * 排序键为「有变动 → 风险 → 基础类别顺序」:更新页首先要回答的是这次变了什么, + * 未变动的高危项是用户上次安装时已经确认过的内容。 + * 某类别在新版本被清空但旧版本非空时仍然成行,否则「不再请求网络访问」这类变化会整行消失。 + */ +export function derivePermissionDiff(oldMetadata: SCMetadata, newMetadata: SCMetadata): PermissionRow[] { + const oldValues = collectValues(oldMetadata); + const newValues = collectValues(newMetadata); + const rows: PermissionRow[] = []; - const require = [...(metadata.require || []), ...(metadata.resource || [])]; - if (require.length) { - rows.push({ kind: "require", risk: "normal", values: require, sensitive: [] }); + for (const kind of KIND_ORDER) { + const next = newValues[kind]; + const prev = oldValues[kind]; + const nextSet = new Set(next); + const prevSet = new Set(prev); + const removed = prev.filter((v) => !nextSet.has(v)); + if (!next.length && !removed.length) continue; + const row = buildRow(kind, next); + row.diff = { added: row.values.filter((v) => !prevSet.has(v)), removed }; + rows.push(row); } - return rows.sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]); + return rows.sort( + (a, b) => Number(isPermissionChanged(b)) - Number(isPermissionChanged(a)) || RISK_ORDER[a.risk] - RISK_ORDER[b.risk] + ); } diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts index 99a28eea6..caee21fd2 100644 --- a/src/pages/install/useInstallData.test.ts +++ b/src/pages/install/useInstallData.test.ts @@ -284,6 +284,42 @@ describe("useInstallData 数据流编排", () => { expect(state.view.oldCode).toBe("// old code"); }); + it("uuid 更新时以已安装脚本的 metadata 为基线派生权限差异", async () => { + window.history.replaceState({}, "", "/install.html?uuid=u1"); + const metadata = { name: ["示例脚本"], version: ["2.0.0"], connect: ["api.e.com", "cdn.e.com"] }; + const oldMetadata = { name: ["示例脚本"], version: ["1.0.0"], connect: ["api.e.com"] }; + const info: ScriptInfo = { + url: "https://e.com/x.user.js", + code: "", + uuid: "u1", + userSubscribe: false, + metadata, + source: "user", + }; + (scriptClient.getInstallInfo as Mock).mockResolvedValue([true, info, {}]); + (getTempCode as Mock).mockResolvedValue("// new code"); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + oldScript: { metadata: oldMetadata } as unknown as Script, + oldScriptCode: "// old code", + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + const state = result.current.state; + if (state.status !== "ready") throw new Error("not ready"); + const connect = state.view.permissions.find((r) => r.kind === "connect")!; + expect(connect.diff).toEqual({ added: ["cdn.e.com"], removed: [] }); + }); + + it("全新安装时权限行不携带差异", async () => { + const result = await setupReady(); + const state = result.current.state; + if (state.status !== "ready") throw new Error("not ready"); + expect(state.view.isUpdate).toBe(false); + expect(state.view.permissions.every((r) => r.diff === undefined)).toBe(true); + }); + describe("安装成功后离开安装页:独立新标签应关闭,网页链接接管的原标签应返回上一页", () => { it("独立新标签 history.length 为 1 时应使用 window.close() 关闭", async () => { const result = await setupReady(); diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts index 080341ab2..64683a847 100644 --- a/src/pages/install/useInstallData.ts +++ b/src/pages/install/useInstallData.ts @@ -18,7 +18,7 @@ import { loadHandle } from "@App/pkg/utils/filehandle-db"; import { startFileTrack, unmountFileTrack, type FTInfo } from "@App/pkg/utils/file-tracker"; import { TempStorageDAO } from "@App/app/repo/tempStorage"; import { EnableAgent } from "@App/app/const"; -import { derivePermissions, type PermissionRow } from "./permissions"; +import { derivePermissions, derivePermissionDiff, type PermissionRow } from "./permissions"; import { deriveVersion, deriveAntifeatures, @@ -68,8 +68,10 @@ export function assembleInstallView(args: { code: string; oldVersion: string | null; oldCode?: string; + /** 已安装脚本的元数据,作为权限差异的基线;全新安装与订阅为 undefined */ + oldMetadata?: SCMetadata; }): InstallView { - const { isUpdate, inTrash, scriptInfo, action, code, oldVersion, oldCode } = args; + const { isUpdate, inTrash, scriptInfo, action, code, oldVersion, oldCode, oldMetadata } = args; const metadata = scriptInfo.metadata; const schedule = deriveScheduleInfo(metadata); return { @@ -82,7 +84,7 @@ export function assembleInstallView(args: { source: prettyUrl(scriptInfo.url), description: i18nDescription(action), version: deriveVersion(metadata.version?.[0], oldVersion), - permissions: derivePermissions(metadata), + permissions: oldMetadata ? derivePermissionDiff(oldMetadata, metadata) : derivePermissions(metadata), antifeatures: deriveAntifeatures(metadata), schedule, scheduleNextRun: schedule?.kind === "cron" ? nextTimeDisplay(schedule.expression) : undefined, @@ -253,6 +255,7 @@ export function useInstallData(): UseInstallData { let action: Script | Subscribe; let oldVersion: string | null; let oldCode: string | undefined; + let oldMetadata: SCMetadata | undefined; let inTrash = false; if (info.userSubscribe) { const p = await prepareSubscribeByCode(code, info.url); @@ -271,6 +274,7 @@ export function useInstallData(): UseInstallData { action = p.script; oldVersion = versionOf(p.oldScript); oldCode = p.oldScriptCode; + oldMetadata = p.oldScript?.metadata; inTrash = p.oldInTrash === true; } if (cancelled) return; @@ -288,6 +292,7 @@ export function useInstallData(): UseInstallData { code, oldVersion, oldCode, + oldMetadata, }), }); }; From 8c280c2d79cb566dc77af6c15395aa8bf2e5d80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 8 Sep 2026 16:48:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20=E6=9B=B4=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E6=9D=83=E9=99=90=E4=B8=80=E9=A1=B9=E6=B2=A1=E5=8F=98=E5=88=99?= =?UTF-8?q?=E6=95=B4=E5=8D=A1=E6=8A=98=E5=8F=A0=E4=B8=BA=E5=8D=95=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 零变化的更新页此前仍旧铺开整块权限卡,让用户在上次已经确认过的内容上重新 扫一遍(#1716 的截图即此)。 折叠的是注意力而不是信息:单行上给出基线版本、类别数与取值总数,点开即得 到与全新安装完全一致的全量清单,每个类别标「无变化」。桌面与窄屏同一形态。 Ref #1716 --- src/locales/de-DE/install.json | 4 ++ src/locales/en-US/install.json | 4 ++ src/locales/ja-JP/install.json | 4 ++ src/locales/ko-KR/install.json | 4 ++ src/locales/pt-BR/install.json | 4 ++ src/locales/ru-RU/install.json | 4 ++ src/locales/tr-TR/install.json | 4 ++ src/locales/vi-VN/install.json | 4 ++ src/locales/zh-CN/install.json | 4 ++ src/locales/zh-TW/install.json | 4 ++ .../components/PermissionCard.test.tsx | 60 +++++++++++++++++ .../install/components/PermissionCard.tsx | 65 +++++++++++++++---- .../install/components/PermissionRow.tsx | 11 ++++ 13 files changed, 164 insertions(+), 12 deletions(-) diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json index 5c98d791b..c6978f9e6 100644 --- a/src/locales/de-DE/install.json +++ b/src/locales/de-DE/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Entfernt", "perm_change_unchanged": "Unverändert", "perm_unchanged_more": "{{count}} unverändert", + "perm_card_title_short": "Berechtigungen", + "perm_card_same_as": "Identisch mit {{version}}", + "perm_card_summary": "{{kinds}} Kategorien · {{count}} Einträge", + "perm_card_expand": "Berechtigungsdetails anzeigen", "perm_match_label": "Läuft auf", "perm_match_summary": "Das Skript läuft auf diesen Websites und verändert die Seiten", "perm_connect_label": "Cross-Origin-Zugriff", diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json index 18f58f659..6cec86f80 100644 --- a/src/locales/en-US/install.json +++ b/src/locales/en-US/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Removed", "perm_change_unchanged": "Unchanged", "perm_unchanged_more": "{{count}} unchanged", + "perm_card_title_short": "Permissions", + "perm_card_same_as": "Same as {{version}}", + "perm_card_summary": "{{kinds}} categories · {{count}} items", + "perm_card_expand": "Show permission details", "perm_match_label": "Runs on", "perm_match_summary": "The script runs on these websites and modifies the pages", "perm_connect_label": "Cross-origin access", diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json index 1a9f034a5..06b0aae48 100644 --- a/src/locales/ja-JP/install.json +++ b/src/locales/ja-JP/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "削除", "perm_change_unchanged": "変更なし", "perm_unchanged_more": "変更なし {{count}} 件", + "perm_card_title_short": "権限", + "perm_card_same_as": "{{version}} と同じ", + "perm_card_summary": "{{kinds}} カテゴリ · {{count}} 件", + "perm_card_expand": "権限の詳細を表示", "perm_match_label": "実行サイト", "perm_match_summary": "スクリプトはこれらのサイトで動作し、ページを変更します", "perm_connect_label": "クロスオリジンアクセス", diff --git a/src/locales/ko-KR/install.json b/src/locales/ko-KR/install.json index a26e39daa..6f0093a3a 100644 --- a/src/locales/ko-KR/install.json +++ b/src/locales/ko-KR/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "제거됨", "perm_change_unchanged": "변경 없음", "perm_unchanged_more": "변경 없음 {{count}}개", + "perm_card_title_short": "권한", + "perm_card_same_as": "{{version}}과(와) 동일", + "perm_card_summary": "{{kinds}}개 분류 · {{count}}개 항목", + "perm_card_expand": "권한 세부 정보 표시", "perm_match_label": "실행 사이트", "perm_match_summary": "스크립트가 다음 사이트에서 실행되며 페이지를 수정합니다", "perm_connect_label": "교차 출처 접근", diff --git a/src/locales/pt-BR/install.json b/src/locales/pt-BR/install.json index eda55b9ee..e3ab2b90e 100644 --- a/src/locales/pt-BR/install.json +++ b/src/locales/pt-BR/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Removido", "perm_change_unchanged": "Inalterado", "perm_unchanged_more": "{{count}} inalterado(s)", + "perm_card_title_short": "Permissões", + "perm_card_same_as": "Iguais à {{version}}", + "perm_card_summary": "{{kinds}} categorias · {{count}} itens", + "perm_card_expand": "Mostrar detalhes das permissões", "perm_match_label": "Executa em", "perm_match_summary": "O script é executado nestes sites e modifica as páginas", "perm_connect_label": "Acesso entre origens (CORS)", diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json index f0b623bb5..a988359f4 100644 --- a/src/locales/ru-RU/install.json +++ b/src/locales/ru-RU/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Удалено", "perm_change_unchanged": "Без изменений", "perm_unchanged_more": "Без изменений: {{count}}", + "perm_card_title_short": "Разрешения", + "perm_card_same_as": "Совпадают с {{version}}", + "perm_card_summary": "Категорий: {{kinds}} · элементов: {{count}}", + "perm_card_expand": "Показать подробности разрешений", "perm_match_label": "Запускается на", "perm_match_summary": "Скрипт работает на этих сайтах и изменяет страницы", "perm_connect_label": "Кросс-доменный доступ", diff --git a/src/locales/tr-TR/install.json b/src/locales/tr-TR/install.json index 11c6eff77..756333045 100644 --- a/src/locales/tr-TR/install.json +++ b/src/locales/tr-TR/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Kaldırıldı", "perm_change_unchanged": "Değişmedi", "perm_unchanged_more": "{{count}} değişmedi", + "perm_card_title_short": "İzinler", + "perm_card_same_as": "{{version}} ile aynı", + "perm_card_summary": "{{kinds}} kategori · {{count}} öğe", + "perm_card_expand": "İzin ayrıntılarını göster", "perm_match_label": "Çalıştırıldığı yer", "perm_match_summary": "Betik bu web sitelerinde çalışır ve sayfaları değiştirir", "perm_connect_label": "Kaynaklar arası erişim", diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json index 07ca4970e..d6599ed1f 100644 --- a/src/locales/vi-VN/install.json +++ b/src/locales/vi-VN/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "Đã xóa", "perm_change_unchanged": "Không đổi", "perm_unchanged_more": "{{count}} không đổi", + "perm_card_title_short": "Quyền", + "perm_card_same_as": "Giống {{version}}", + "perm_card_summary": "{{kinds}} nhóm · {{count}} mục", + "perm_card_expand": "Hiện chi tiết quyền", "perm_match_label": "Chạy trên", "perm_match_summary": "Script chạy trên các website này và thay đổi trang", "perm_connect_label": "Truy cập khác nguồn gốc", diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json index 52a71d2e4..106787dc3 100644 --- a/src/locales/zh-CN/install.json +++ b/src/locales/zh-CN/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "已移除", "perm_change_unchanged": "未变动", "perm_unchanged_more": "未变动 {{count}} 项", + "perm_card_title_short": "权限", + "perm_card_same_as": "与 {{version}} 相同", + "perm_card_summary": "{{kinds}} 类 {{count}} 项", + "perm_card_expand": "展开权限详情", "perm_match_label": "运行网站", "perm_match_summary": "脚本会在这些网站上运行并修改页面", "perm_connect_label": "跨域访问", diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json index 454450165..55e80f63a 100644 --- a/src/locales/zh-TW/install.json +++ b/src/locales/zh-TW/install.json @@ -20,6 +20,10 @@ "perm_change_removed": "已移除", "perm_change_unchanged": "未變更", "perm_unchanged_more": "未變更 {{count}} 項", + "perm_card_title_short": "權限", + "perm_card_same_as": "與 {{version}} 相同", + "perm_card_summary": "{{kinds}} 類 {{count}} 項", + "perm_card_expand": "展開權限詳情", "perm_match_label": "執行網站", "perm_match_summary": "腳本會在這些網站上執行並修改頁面", "perm_connect_label": "跨域存取", diff --git a/src/pages/install/components/PermissionCard.test.tsx b/src/pages/install/components/PermissionCard.test.tsx index 25d5cc07b..11a318d73 100644 --- a/src/pages/install/components/PermissionCard.test.tsx +++ b/src/pages/install/components/PermissionCard.test.tsx @@ -108,3 +108,63 @@ describe("PermissionCard 更新差异态", () => { expect(screen.queryByTestId("permission-row-collapsed")).not.toBeInTheDocument(); }); }); + +describe("PermissionCard 更新零变化态", () => { + const same = (kind: PermissionRow["kind"], values: string[]): PermissionRow => ({ + kind, + risk: "normal", + values, + sensitive: [], + diff: { added: [], removed: [] }, + }); + const rows = [same("connect", ["api.a.com"]), same("match", ["https://a.com/*", "https://b.com/*"])]; + + it("零变化时整卡折叠为单行,四类取值都不在 DOM", () => { + render(); + expect(screen.getByTestId("permission-card-collapsed")).toBeInTheDocument(); + expect(screen.queryByText("api.a.com")).not.toBeInTheDocument(); + expect(screen.queryByText("https://a.com/*")).not.toBeInTheDocument(); + expect(screen.queryAllByTestId("permission-row")).toHaveLength(0); + }); + + it("折叠单行给出基线版本与类别数、取值总数", () => { + render(); + const line = screen.getByTestId("permission-card-collapsed"); + expect(within(line).getByText("与 1.2.3 相同")).toBeInTheDocument(); + expect(within(line).getByText("2 类 3 项")).toBeInTheDocument(); + expect(within(line).getByText("无变化")).toBeInTheDocument(); + }); + + it("点开后四类全量呈现,每类标无变化", () => { + render(); + fireEvent.click(screen.getByTestId("permission-card-collapsed")); + expect(screen.getAllByTestId("permission-row")).toHaveLength(2); + expect(screen.getByText("api.a.com")).toBeInTheDocument(); + expect(screen.getByText("https://a.com/*")).toBeInTheDocument(); + expect(screen.getAllByText("无变化")).toHaveLength(3); + }); + + it("有变动时不折叠整卡", () => { + const changedRow: PermissionRow = { + kind: "connect", + risk: "warn", + values: ["api.a.com"], + sensitive: [], + diff: { added: ["api.a.com"], removed: [] }, + }; + render(); + expect(screen.queryByTestId("permission-card-collapsed")).not.toBeInTheDocument(); + }); + + it("全新安装不折叠整卡", () => { + render(); + expect(screen.queryByTestId("permission-card-collapsed")).not.toBeInTheDocument(); + }); + + it("移动端零变化同样折叠为单行", () => { + mobile = true; + render(); + expect(screen.getByTestId("permission-card-collapsed")).toBeInTheDocument(); + expect(screen.queryByText("api.a.com")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/PermissionCard.tsx b/src/pages/install/components/PermissionCard.tsx index c7803d186..87193a4ab 100644 --- a/src/pages/install/components/PermissionCard.tsx +++ b/src/pages/install/components/PermissionCard.tsx @@ -5,17 +5,7 @@ import { cn } from "@App/pkg/utils/cn"; import { useIsMobile } from "@App/pages/components/use-is-mobile"; import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@App/pages/components/ui/accordion"; import { isPermissionChanged, type PermissionRow as PermissionRowData } from "../permissions"; -import { PermissionRow, PermissionChips, PermissionDelta, KIND_META, RISK_STYLE } from "./PermissionRow"; - -/** 无变化标记;更新场景用来把「这一类你上次已经确认过」说出来 */ -function NoChangeTag() { - const { t } = useTranslation(["install", "common"]); - return ( - - {t("install:perm_no_change")} - - ); -} +import { PermissionRow, PermissionChips, PermissionDelta, NoChangeTag, KIND_META, RISK_STYLE } from "./PermissionRow"; function MobilePermissions({ rows, isUpdate }: { rows: PermissionRowData[]; isUpdate: boolean }) { const { t } = useTranslation(["install", "common"]); @@ -79,13 +69,59 @@ function CollapsedRow({ row }: { row: PermissionRowData }) { ); } +/** + * 权限一项没变时的整卡单行形态。 + * 折叠的是注意力而不是信息:类别数与取值总数就在行上,点开即得到与全新安装一致的全量清单。 + */ +function CollapsedCard({ + rows, + baselineVersion, + onExpand, +}: { + rows: PermissionRowData[]; + baselineVersion: string; + onExpand: () => void; +}) { + const { t } = useTranslation(["install", "common"]); + const total = rows.reduce((n, r) => n + r.values.length, 0); + + return ( +
+ +
+ ); +} + export function PermissionCard({ rows, baselineVersion }: { rows: PermissionRowData[]; baselineVersion?: string }) { const { t } = useTranslation(["install", "common"]); const isMobile = useIsMobile(); + const [expanded, setExpanded] = useState(false); const isUpdate = baselineVersion !== undefined; const changed = rows.filter(isPermissionChanged); const added = changed.reduce((n, r) => n + r.diff!.added.length, 0); const removed = changed.reduce((n, r) => n + r.diff!.removed.length, 0); + const noChange = isUpdate && rows.length > 0 && changed.length === 0; + + if (noChange && !expanded) { + return setExpanded(true)} />; + } return (
@@ -116,7 +152,12 @@ export function PermissionCard({ rows, baselineVersion }: { rows: PermissionRowD ) : ( rows.map((row, i) => (
0 && "border-t border-border")}> - {isUpdate && !isPermissionChanged(row) ? : } + {/* 有变动时未变动的类别塌成单行让位;整卡零变化时用户是主动点开的,给全量 */} + {changed.length > 0 && !isPermissionChanged(row) ? ( + + ) : ( + + )}
)) )} diff --git a/src/pages/install/components/PermissionRow.tsx b/src/pages/install/components/PermissionRow.tsx index 74f7fa508..a6ec31d12 100644 --- a/src/pages/install/components/PermissionRow.tsx +++ b/src/pages/install/components/PermissionRow.tsx @@ -146,6 +146,16 @@ export function PermissionChips({ ); } +/** 无变化标记;更新场景用来把「这一类你上次已经确认过」说出来 */ +export function NoChangeTag() { + const { t } = useTranslation(["install", "common"]); + return ( + + {t("install:perm_no_change")} + + ); +} + /** 行头的变动计数;沿用代码卡 +N −M 的记号,无变动时不渲染 */ export function PermissionDelta({ row }: { row: PermissionRowData }) { if (!isPermissionChanged(row)) return null; @@ -173,6 +183,7 @@ export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; max {t(labelKey)} {row.values.length} + {row.diff && !isPermissionChanged(row) && } {t(summaryKey)}
From e0fdb266bb7e0e935345dc4296255dc8298da0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 8 Sep 2026 16:58:42 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=94=A5=20=E7=A7=BB=E9=99=A4=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=A1=B5=E5=AE=89=E5=85=A8=E8=AD=A6=E7=A4=BA=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 维护者决定:权限卡重设计后颜色与位置已足够显眼,常驻横幅每次安装/更新都 复读同一句,反而钝化了它。危险权限由权限 chip 的红底描边表达,反特性由身 份卡的琥珀 Tag 表达,两者都不依赖这条横幅。 这是一项有意的能力减少而非遗漏:横幅独有的「请从合法来源安装脚本」这句来 源可信度提示,移除后在安装页不再有承载位置——权限卡说的是脚本能做什么, 不说这个来源该不该信。 连带清理仅被该组件使用的 6 个文案键(10 个语言包共 60 条),并修正 terminology-ja-JP.md 中指向其中一个键的术语示例。 Ref #1716 --- docs/references/terminology-ja-JP.md | 2 +- src/locales/de-DE/install.json | 6 --- src/locales/en-US/install.json | 6 --- src/locales/ja-JP/install.json | 6 --- src/locales/ko-KR/install.json | 6 --- src/locales/pt-BR/install.json | 6 --- src/locales/ru-RU/install.json | 6 --- src/locales/tr-TR/install.json | 6 --- src/locales/vi-VN/install.json | 6 --- src/locales/zh-CN/install.json | 6 --- src/locales/zh-TW/install.json | 6 --- src/pages/install/App.test.tsx | 29 +++++++++++++ src/pages/install/App.tsx | 5 --- .../components/InstallWarning.test.tsx | 26 ------------ .../install/components/InstallWarning.tsx | 41 ------------------- 15 files changed, 30 insertions(+), 133 deletions(-) delete mode 100644 src/pages/install/components/InstallWarning.test.tsx delete mode 100644 src/pages/install/components/InstallWarning.tsx diff --git a/docs/references/terminology-ja-JP.md b/docs/references/terminology-ja-JP.md index 32b97e151..4747125c8 100644 --- a/docs/references/terminology-ja-JP.md +++ b/docs/references/terminology-ja-JP.md @@ -83,7 +83,7 @@ | directory / path | `ディレクトリ` / `パス` | ファイルシステムを扱う開発機能では、現行の `ディレクトリ` を維持します。一般向け機能へ拡大する場合は別途確認します。 | `open_directory`, `open_backup_dir`, `script_operation_description`, `watch_file_description` | | panel / console | `パネル` / `コンソール` | `panel` は `パネル`、開発者ツールの console は `コンソール` とし、相互に置換しません。 | `background_script_description`, `build_success_message` | | check / confirm | `チェック` / `確認` | 更新確認の UI には両表現が現存します。新しい文言では周辺 UI に合わせ、統一はレビュー後に行います。 | `check_update`, `updatepage.main_header`, `updatepage.status_checking_updates` | -| source | `ソース` / `インストール元` | 外部リソースや出所一般は `ソース`、導入元を明示する欄では `インストール元` を使用しています。 | `source`, `importpage.col_source`, `from_legitimate_sources_warning` | +| source | `ソース` / `インストール元` | 外部リソースや出所一般は `ソース`、導入元を明示する欄では `インストール元` を使用しています。 | `source`, `importpage.col_source` | | query / filter | `検索` / `絞り込み` / `クエリ` | ログ検索 UI の操作名は `検索`、結果を条件で狭める説明は `絞り込み` を優先します。API や問い合わせ言語など技術文脈では `クエリ` を使用できます。 | `query`, `enter_filter_conditions`, `filtered_logs` | | match / exclude | `対象` / `除外`、ラベルでは `対象サイト(@match)` / `除外サイト(@exclude)` | ユーザー向け操作では現行の `対象` / `除外` を優先します。`@match` / `@exclude` は識別子として保持します。現行の `マッチ` 文言は後日レビュー対象です。 | `add_match`, `add_exclude`, `website_match`, `website_exclude`, `match` | | storage access / operation | `ストレージにアクセス` / `ストレージ` | 許可ダイアログは一般のファイルストレージへのアクセス確認であり、同期機能に限定しません。タイトルでは対象への `アクセス` を明確にし、説明文で具体的操作を補います。 | `script_operation_title`, `script_storage`, `storage_api` | diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json index c6978f9e6..41c45de67 100644 --- a/src/locales/de-DE/install.json +++ b/src/locales/de-DE/install.json @@ -59,11 +59,6 @@ "watching_title": "Dateiänderungen werden überwacht", "watching_file_desc": "Beim Speichern von {{file}} wird das Skript automatisch neu installiert und synchronisiert", "watching_last_sync": "Zuletzt synchronisiert {{time}}", - "warning_title": "Bitte bestätige, dass das Skript aus einer vertrauenswürdigen Quelle stammt", - "warning_risk_connect": "es kann auf alle Domains zugreifen", - "warning_risk_antifeature": "es deklariert Anti-Features", - "warning_risk_join": " und ", - "warning_risk_tail": " — mit Vorsicht installieren.", "action_note_install": "Mit der Installation vertraust du der Quelle und dem Autor dieses Skripts", "action_note_update": "Mit der Aktualisierung akzeptierst du diese Code- und Berechtigungsänderung", "action_note_subscribe": "Mit der Installation vertraust du diesem Abonnement und seinem Autor", @@ -241,7 +236,6 @@ "invalid_page": "Ungültige Seite", "frame_blocked_title": "Einbettung blockiert", "frame_blocked_desc": "Aus Sicherheitsgründen kann die Installationsseite nicht in einem iframe eingebettet werden. Bitte öffnen Sie sie direkt in einem Browser-Tab.", - "from_legitimate_sources_warning": "Bitte installieren Sie Skripte aus legitimen Quellen! Unbekannte Skripte können Ihre Privatsphäre verletzen oder bösartige Operationen durchführen!", "referral_link_title": "Empfehlungslink", "ads_title": "Mit Werbung", "payment_title": "Bezahltes Skript", diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json index 6cec86f80..fc3aee00a 100644 --- a/src/locales/en-US/install.json +++ b/src/locales/en-US/install.json @@ -59,11 +59,6 @@ "watching_title": "Watching file changes", "watching_file_desc": "Saving {{file}} will reinstall and sync the script automatically", "watching_last_sync": "Last synced {{time}}", - "warning_title": "Please confirm the script comes from a trusted source", - "warning_risk_connect": "it can access all domains", - "warning_risk_antifeature": "it declares anti-features", - "warning_risk_join": " and ", - "warning_risk_tail": " — install with caution.", "action_note_install": "Installing means you trust this script's source and author", "action_note_update": "Updating means you accept this code and permission change", "action_note_subscribe": "Installing means you trust this subscription and its author", @@ -241,7 +236,6 @@ "invalid_page": "Invalid Page", "frame_blocked_title": "Embedding Blocked", "frame_blocked_desc": "For security reasons, the install page cannot be embedded in an iframe. Please open it directly in a browser tab.", - "from_legitimate_sources_warning": "Please install scripts from legitimate sources! Unknown scripts may invade your privacy or conduct malicious operations.", "referral_link_title": "Referral Link", "ads_title": "Ads", "payment_title": "Payment", diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json index 06b0aae48..8ac27fbff 100644 --- a/src/locales/ja-JP/install.json +++ b/src/locales/ja-JP/install.json @@ -59,11 +59,6 @@ "watching_title": "ファイルの変更を監視中", "watching_file_desc": "{{file}} を保存すると、自動的に再インストールしてスクリプトを同期します", "watching_last_sync": "最終同期 {{time}}", - "warning_title": "信頼できるソースのスクリプトか確認してください", - "warning_risk_connect": "すべてのドメインにアクセスできます", - "warning_risk_antifeature": "アンチフィーチャーが宣言されています", - "warning_risk_join": "、", - "warning_risk_tail": " — 慎重にインストールしてください。", "action_note_install": "インストールは、このスクリプトのソースと作者を信頼することを意味します", "action_note_update": "更新は、このコードと権限の変更を受け入れることを意味します", "action_note_subscribe": "インストールは、このサブスクリプションとその作者を信頼することを意味します", @@ -241,7 +236,6 @@ "invalid_page": "無効なページ", "frame_blocked_title": "埋め込みがブロックされました", "frame_blocked_desc": "セキュリティ上の理由により、インストールページを iframe に埋め込むことはできません。ブラウザのタブで直接開いてください。", - "from_legitimate_sources_warning": "正当なソースからスクリプトをインストールしてください!不明なスクリプトはプライバシーを侵害したり、悪意のある操作を行う可能性があります!", "referral_link_title": "紹介リンク", "ads_title": "広告付き", "payment_title": "有料スクリプト", diff --git a/src/locales/ko-KR/install.json b/src/locales/ko-KR/install.json index 6f0093a3a..09897161b 100644 --- a/src/locales/ko-KR/install.json +++ b/src/locales/ko-KR/install.json @@ -59,11 +59,6 @@ "watching_title": "파일 변경 감시 중", "watching_file_desc": "{{file}} 파일을 저장하면 스크립트가 자동으로 재설치되고 동기화됩니다", "watching_last_sync": "마지막 동기화 {{time}}", - "warning_title": "신뢰할 수 있는 출처의 스크립트인지 확인하세요", - "warning_risk_connect": "모든 도메인에 접근할 수 있음", - "warning_risk_antifeature": "안티피처(anti-feature)가 포함되어 있음", - "warning_risk_join": ", ", - "warning_risk_tail": " — 설치 시 주의하세요.", "action_note_install": "설치하면 이 스크립트의 출처와 제작자를 신뢰하는 것입니다", "action_note_update": "업데이트하면 이 코드와 권한 변경 사항을 승인하는 것입니다", "action_note_subscribe": "설치하면 이 구독과 그 제작자를 신뢰하는 것입니다", @@ -241,7 +236,6 @@ "invalid_page": "잘못된 페이지", "frame_blocked_title": "삽입이 차단됨", "frame_blocked_desc": "보안상의 이유로 설치 페이지는 iframe에 삽입할 수 없습니다. 브라우저 탭에서 직접 열어주세요.", - "from_legitimate_sources_warning": "반드시 정당한 출처에서 스크립트를 설치하세요! 출처가 불분명한 스크립트는 개인정보를 침해하거나 악의적인 동작을 수행할 수 있습니다.", "referral_link_title": "추천 링크", "ads_title": "광고", "payment_title": "유료", diff --git a/src/locales/pt-BR/install.json b/src/locales/pt-BR/install.json index e3ab2b90e..f70fc9007 100644 --- a/src/locales/pt-BR/install.json +++ b/src/locales/pt-BR/install.json @@ -59,11 +59,6 @@ "watching_title": "Monitorando alterações do arquivo", "watching_file_desc": "Salvar {{file}} reinstalará e sincronizará o script automaticamente", "watching_last_sync": "Última sincronização {{time}}", - "warning_title": "Por favor, confirme que o script vem de uma origem confiável", - "warning_risk_connect": "ele pode acessar todos os domínios", - "warning_risk_antifeature": "ele declara metadados @antifeature", - "warning_risk_join": " e ", - "warning_risk_tail": " — instale com cautela.", "action_note_install": "Instalar significa que você confia na origem e no autor deste script", "action_note_update": "Atualizar significa que você aceita esta alteração de código e permissão", "action_note_subscribe": "Instalar significa que você confia nesta assinatura e em seu autor", @@ -241,7 +236,6 @@ "invalid_page": "Página inválida", "frame_blocked_title": "Incorporação bloqueada", "frame_blocked_desc": "Por motivos de segurança, a página de instalação não pode ser incorporada em um iframe. Abra-a diretamente em uma aba do navegador.", - "from_legitimate_sources_warning": "Por favor, instale scripts de fontes legítimas! Scripts desconhecidos podem invadir sua privacidade ou realizar operações maliciosas.", "referral_link_title": "Link de indicação", "ads_title": "Anúncios", "payment_title": "Pagamento", diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json index a988359f4..0ce1ddf2b 100644 --- a/src/locales/ru-RU/install.json +++ b/src/locales/ru-RU/install.json @@ -59,11 +59,6 @@ "watching_title": "Отслеживание изменений файла", "watching_file_desc": "Сохранение {{file}} автоматически переустановит и синхронизирует скрипт", "watching_last_sync": "Последняя синхронизация {{time}}", - "warning_title": "Убедитесь, что скрипт получен из доверенного источника", - "warning_risk_connect": "он может получить доступ ко всем доменам", - "warning_risk_antifeature": "он объявляет антифункции", - "warning_risk_join": " и ", - "warning_risk_tail": " — устанавливайте с осторожностью.", "action_note_install": "Установка означает, что вы доверяете источнику и автору этого скрипта", "action_note_update": "Обновление означает, что вы принимаете изменения кода и разрешений", "action_note_subscribe": "Установка означает, что вы доверяете этой подписке и её автору", @@ -241,7 +236,6 @@ "invalid_page": "Недействительная страница", "frame_blocked_title": "Встраивание заблокировано", "frame_blocked_desc": "В целях безопасности страница установки не может быть встроена в iframe. Пожалуйста, откройте её напрямую во вкладке браузера.", - "from_legitimate_sources_warning": "Пожалуйста, устанавливайте скрипты только из законных источников! Неизвестные скрипты могут нарушить вашу конфиденциальность или выполнить злонамеренные операции!", "referral_link_title": "Реферальная ссылка", "ads_title": "Содержит рекламу", "payment_title": "Платный скрипт", diff --git a/src/locales/tr-TR/install.json b/src/locales/tr-TR/install.json index 756333045..f62730e91 100644 --- a/src/locales/tr-TR/install.json +++ b/src/locales/tr-TR/install.json @@ -59,11 +59,6 @@ "watching_title": "Dosya değişiklikleri izleniyor", "watching_file_desc": "{{file}} kaydedildiğinde betik otomatik olarak yeniden yüklenir ve eşitlenir", "watching_last_sync": "Son eşitleme: {{time}}", - "warning_title": "Lütfen betiğin güvenilir bir kaynaktan geldiğini onaylayın", - "warning_risk_connect": "tüm etki alanlarına erişebilir", - "warning_risk_antifeature": "istenmeyen riskli özellikler bildiriyor", - "warning_risk_join": " ve ", - "warning_risk_tail": " — dikkatli yükleyin.", "action_note_install": "Yüklemek, bu betiğin kaynağına ve yazarına güvendiğiniz anlamına gelir", "action_note_update": "Güncellemek, bu kodu ve izin değişikliğini kabul ettiğiniz anlamına gelir", "action_note_subscribe": "Yüklemek, bu aboneliğe ve yazarına güvendiğiniz anlamına gelir", @@ -241,7 +236,6 @@ "invalid_page": "Geçersiz Sayfa", "frame_blocked_title": "Gömme Engellendi", "frame_blocked_desc": "Güvenlik nedeniyle yükleme sayfası bir iframe içine gömülemez. Lütfen doğrudan bir tarayıcı sekmesinde açın.", - "from_legitimate_sources_warning": "Lütfen betikleri meşru kaynaklardan yükleyin! Bilinmeyen betikler gizliliğinizi ihlal edebilir veya kötü amaçlı işlemler gerçekleştirebilir.", "referral_link_title": "Yönlendirme Bağlantısı", "ads_title": "Reklamlar", "payment_title": "Ödeme", diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json index d6599ed1f..c8a7178fb 100644 --- a/src/locales/vi-VN/install.json +++ b/src/locales/vi-VN/install.json @@ -59,11 +59,6 @@ "watching_title": "Đang theo dõi thay đổi tệp", "watching_file_desc": "Lưu {{file}} sẽ tự động cài đặt lại và đồng bộ script", "watching_last_sync": "Đồng bộ lần cuối {{time}}", - "warning_title": "Vui lòng xác nhận script đến từ nguồn đáng tin cậy", - "warning_risk_connect": "nó có thể truy cập mọi tên miền", - "warning_risk_antifeature": "nó khai báo các tính năng đối nghịch", - "warning_risk_join": " và ", - "warning_risk_tail": " — hãy cài đặt thận trọng.", "action_note_install": "Cài đặt nghĩa là bạn tin tưởng nguồn và tác giả của script này", "action_note_update": "Cập nhật nghĩa là bạn chấp nhận thay đổi mã và quyền này", "action_note_subscribe": "Cài đặt nghĩa là bạn tin tưởng đăng ký này và tác giả của nó", @@ -241,7 +236,6 @@ "invalid_page": "Trang không hợp lệ", "frame_blocked_title": "Nhúng trang bị chặn", "frame_blocked_desc": "Vì lý do bảo mật, trang cài đặt không thể được nhúng trong iframe. Vui lòng mở trực tiếp trong một tab trình duyệt.", - "from_legitimate_sources_warning": "Vui lòng cài đặt script từ các nguồn hợp pháp! Script không rõ nguồn gốc có thể xâm phạm quyền riêng tư của bạn hoặc thực hiện các hoạt động độc hại.", "referral_link_title": "Liên kết giới thiệu", "ads_title": "Quảng cáo", "payment_title": "Thanh toán", diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json index 106787dc3..42d04707c 100644 --- a/src/locales/zh-CN/install.json +++ b/src/locales/zh-CN/install.json @@ -59,11 +59,6 @@ "watching_title": "正在监听文件变化", "watching_file_desc": "保存 {{file}} 会自动重新安装并同步脚本", "watching_last_sync": "最后更新 {{time}}", - "warning_title": "请确认脚本来自可信来源", - "warning_risk_connect": "它可以访问所有域名", - "warning_risk_antifeature": "它声明了反功能特性", - "warning_risk_join": " 且 ", - "warning_risk_tail": " — 请谨慎安装。", "action_note_install": "安装代表您信任该脚本的来源与作者", "action_note_update": "更新代表您接受此代码与权限变更", "action_note_subscribe": "安装代表您信任该订阅及其作者", @@ -241,7 +236,6 @@ "invalid_page": "无效页面", "frame_blocked_title": "禁止内嵌访问", "frame_blocked_desc": "出于安全考虑,安装页不允许在 iframe 中嵌入显示,请在浏览器标签页中直接打开。", - "from_legitimate_sources_warning": "请从合法来源安装脚本!未知的脚本可能会侵犯您的隐私或者做出恶意的操作!", "referral_link_title": "推荐链接", "ads_title": "附带广告", "payment_title": "付费脚本", diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json index 55e80f63a..91ce2b2d7 100644 --- a/src/locales/zh-TW/install.json +++ b/src/locales/zh-TW/install.json @@ -59,11 +59,6 @@ "watching_title": "正在監聽檔案變化", "watching_file_desc": "儲存 {{file}} 會自動重新安裝並同步腳本", "watching_last_sync": "最後更新 {{time}}", - "warning_title": "請確認腳本來自可信來源", - "warning_risk_connect": "它可以存取所有網域", - "warning_risk_antifeature": "它宣告了反功能特性", - "warning_risk_join": " 且 ", - "warning_risk_tail": " — 請謹慎安裝。", "action_note_install": "安裝代表您信任該腳本的來源與作者", "action_note_update": "更新代表您接受此程式碼與權限變更", "action_note_subscribe": "安裝代表您信任該訂閱及其作者", @@ -241,7 +236,6 @@ "invalid_page": "無效頁面", "frame_blocked_title": "禁止內嵌存取", "frame_blocked_desc": "基於安全考量,安裝頁不允許在 iframe 中嵌入顯示,請在瀏覽器分頁中直接開啟。", - "from_legitimate_sources_warning": "請從合法來源安裝腳本!未知的腳本可能會侵犯您的隱私或進行惡意操作!", "referral_link_title": "推薦連結", "ads_title": "附帶廣告", "payment_title": "付費腳本", diff --git a/src/pages/install/App.test.tsx b/src/pages/install/App.test.tsx index 7baa46421..48b874fd4 100644 --- a/src/pages/install/App.test.tsx +++ b/src/pages/install/App.test.tsx @@ -357,3 +357,32 @@ describe("Install App 权限卡的更新基线", () => { expect(screen.queryByText("权限变化")).not.toBeInTheDocument(); }); }); + +describe("Install App 不再渲染安全警示条", () => { + it("全新安装页没有安全警示条", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } }); + render(); + expect(screen.queryByTestId("install-warning-title")).not.toBeInTheDocument(); + expect(screen.queryByTestId("install-warning-desc")).not.toBeInTheDocument(); + }); + + it("声明了危险权限的更新页同样没有安全警示条", () => { + mockHook.mockReturnValue({ + ...baseHook(), + state: { + status: "ready", + view: readyView({ + isUpdate: true, + version: { kind: "update", oldVersion: "1.2.3", newVersion: "2.3.1", changed: true }, + antifeatures: ["ads"], + permissions: [ + { kind: "connect", risk: "danger", values: ["*"], sensitive: [], diff: { added: [], removed: [] } }, + ], + }), + }, + }); + render(); + expect(screen.queryByTestId("install-warning-title")).not.toBeInTheDocument(); + expect(screen.queryByTestId("install-warning-risk")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index 0958d8b00..b3629ba45 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -10,7 +10,6 @@ import { SubscribeScripts } from "./components/SubscribeScripts"; import { SkillInstallView } from "./components/SkillInstallView"; import { CodePreview } from "./components/CodePreview"; import { InstallActions } from "./components/InstallActions"; -import { InstallWarning } from "./components/InstallWarning"; import { InstallLoading, InstallError, InstallExpired } from "./components/InstallStates"; import { WatchingBanner } from "./components/WatchingBanner"; import { ExternalAccessBanner } from "./components/ExternalAccessBanner"; @@ -240,10 +239,6 @@ export default function App() { baselineVersion={view.version.kind === "update" ? view.version.oldVersion : undefined} /> )} - p.risk === "danger")} - hasAntifeature={view.antifeatures.length > 0} - /> initTestLanguage("zh-CN")); - -afterEach(cleanup); - -describe("InstallWarning 安全警示条", () => { - it("渲染警示标题与说明两段(对照设计稿)", () => { - render(); - expect(screen.getByTestId("install-warning-title")).toBeInTheDocument(); - expect(screen.getByTestId("install-warning-desc")).toBeInTheDocument(); - }); - - it("存在危险权限时附加风险提示分句", () => { - render(); - expect(screen.getByTestId("install-warning-risk")).toBeInTheDocument(); - }); - - it("无风险信号时不渲染风险提示分句", () => { - render(); - expect(screen.queryByTestId("install-warning-risk")).not.toBeInTheDocument(); - }); -}); diff --git a/src/pages/install/components/InstallWarning.tsx b/src/pages/install/components/InstallWarning.tsx deleted file mode 100644 index 11206e0d1..000000000 --- a/src/pages/install/components/InstallWarning.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { ShieldAlert } from "lucide-react"; - -export interface InstallWarningProps { - /** 是否声明了危险权限(如 @connect *) */ - hasDangerPermission: boolean; - /** 是否声明了已知反特性(推广链接/广告/挖矿等) */ - hasAntifeature: boolean; -} - -/** - * 安全警示条(对照设计稿 Alert Warning):标题「请确认脚本来自可信来源」+ 说明, - * 当声明了危险权限或反特性时,附加一句风险提示,引导用户谨慎安装。 - */ -export function InstallWarning({ hasDangerPermission, hasAntifeature }: InstallWarningProps) { - const { t } = useTranslation(["install", "common"]); - const showRisk = hasDangerPermission || hasAntifeature; - - return ( -
- -
- - {t("install:warning_title")} - - - {t("install:from_legitimate_sources_warning")} - {showRisk && ( - - {" "} - {hasDangerPermission && t("install:warning_risk_connect")} - {hasDangerPermission && hasAntifeature && t("install:warning_risk_join")} - {hasAntifeature && t("install:warning_risk_antifeature")} - {t("install:warning_risk_tail")} - - )} - -
-
- ); -} From 01298926e7e5656d915e0dbc3830473e8fe7d866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Tue, 8 Sep 2026 17:20:34 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20=E7=A9=BA=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8D=E5=86=8D=E8=BF=9B=E5=85=A5=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=E5=91=88=E7=8E=B0=EF=BC=8C=E6=8A=98=E5=8F=A0=E5=8D=95?= =?UTF-8?q?=E8=A1=8C=E4=B8=8D=E5=86=8D=E5=AF=B9=E8=BE=85=E5=8A=A9=E6=8A=80?= =?UTF-8?q?=E6=9C=AF=E5=93=91=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 规格写明「没有任何权限的更新沿用现有空态文案,不进入差异呈现」,但权限卡 只把空行排除在整卡折叠之外,卡头仍按更新态渲染「无变化」与「对比已安装的 X」——一张没有任何可比之物的卡片替用户报出了差异结论。改为把「有基线且 有权限行」作为进入差异呈现的唯一条件。 折叠单行上的 aria-label 会覆盖按钮内容参与可访问名计算,辅助技术只听得到 一句「展开权限详情」;而规格要求这一行呈现「无变化」标记、与已安装版本相 同的说明以及类别数与取值总数——折叠的本该是注意力而不是信息。改用 aria-expanded 声明收起态,可访问名回到可见内容;未变动类别的塌行同样补上 aria-expanded,让「展开控件」这一语义对辅助技术成立。随之清理仅被该 aria-label 使用的 perm_card_expand(10 个语言包)。 Ref #1716 --- src/locales/de-DE/install.json | 1 - src/locales/en-US/install.json | 1 - src/locales/ja-JP/install.json | 1 - src/locales/ko-KR/install.json | 1 - src/locales/pt-BR/install.json | 1 - src/locales/ru-RU/install.json | 1 - src/locales/tr-TR/install.json | 1 - src/locales/vi-VN/install.json | 1 - src/locales/zh-CN/install.json | 1 - src/locales/zh-TW/install.json | 1 - .../components/PermissionCard.test.tsx | 21 +++++++++++++++++++ .../install/components/PermissionCard.tsx | 10 ++++++--- 12 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json index 41c45de67..f22259290 100644 --- a/src/locales/de-DE/install.json +++ b/src/locales/de-DE/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "Berechtigungen", "perm_card_same_as": "Identisch mit {{version}}", "perm_card_summary": "{{kinds}} Kategorien · {{count}} Einträge", - "perm_card_expand": "Berechtigungsdetails anzeigen", "perm_match_label": "Läuft auf", "perm_match_summary": "Das Skript läuft auf diesen Websites und verändert die Seiten", "perm_connect_label": "Cross-Origin-Zugriff", diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json index fc3aee00a..64c24de4f 100644 --- a/src/locales/en-US/install.json +++ b/src/locales/en-US/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "Permissions", "perm_card_same_as": "Same as {{version}}", "perm_card_summary": "{{kinds}} categories · {{count}} items", - "perm_card_expand": "Show permission details", "perm_match_label": "Runs on", "perm_match_summary": "The script runs on these websites and modifies the pages", "perm_connect_label": "Cross-origin access", diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json index 8ac27fbff..0abd1017c 100644 --- a/src/locales/ja-JP/install.json +++ b/src/locales/ja-JP/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "権限", "perm_card_same_as": "{{version}} と同じ", "perm_card_summary": "{{kinds}} カテゴリ · {{count}} 件", - "perm_card_expand": "権限の詳細を表示", "perm_match_label": "実行サイト", "perm_match_summary": "スクリプトはこれらのサイトで動作し、ページを変更します", "perm_connect_label": "クロスオリジンアクセス", diff --git a/src/locales/ko-KR/install.json b/src/locales/ko-KR/install.json index 09897161b..a06879c34 100644 --- a/src/locales/ko-KR/install.json +++ b/src/locales/ko-KR/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "권한", "perm_card_same_as": "{{version}}과(와) 동일", "perm_card_summary": "{{kinds}}개 분류 · {{count}}개 항목", - "perm_card_expand": "권한 세부 정보 표시", "perm_match_label": "실행 사이트", "perm_match_summary": "스크립트가 다음 사이트에서 실행되며 페이지를 수정합니다", "perm_connect_label": "교차 출처 접근", diff --git a/src/locales/pt-BR/install.json b/src/locales/pt-BR/install.json index f70fc9007..93047d702 100644 --- a/src/locales/pt-BR/install.json +++ b/src/locales/pt-BR/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "Permissões", "perm_card_same_as": "Iguais à {{version}}", "perm_card_summary": "{{kinds}} categorias · {{count}} itens", - "perm_card_expand": "Mostrar detalhes das permissões", "perm_match_label": "Executa em", "perm_match_summary": "O script é executado nestes sites e modifica as páginas", "perm_connect_label": "Acesso entre origens (CORS)", diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json index 0ce1ddf2b..f452106b2 100644 --- a/src/locales/ru-RU/install.json +++ b/src/locales/ru-RU/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "Разрешения", "perm_card_same_as": "Совпадают с {{version}}", "perm_card_summary": "Категорий: {{kinds}} · элементов: {{count}}", - "perm_card_expand": "Показать подробности разрешений", "perm_match_label": "Запускается на", "perm_match_summary": "Скрипт работает на этих сайтах и изменяет страницы", "perm_connect_label": "Кросс-доменный доступ", diff --git a/src/locales/tr-TR/install.json b/src/locales/tr-TR/install.json index f62730e91..0cb0a6752 100644 --- a/src/locales/tr-TR/install.json +++ b/src/locales/tr-TR/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "İzinler", "perm_card_same_as": "{{version}} ile aynı", "perm_card_summary": "{{kinds}} kategori · {{count}} öğe", - "perm_card_expand": "İzin ayrıntılarını göster", "perm_match_label": "Çalıştırıldığı yer", "perm_match_summary": "Betik bu web sitelerinde çalışır ve sayfaları değiştirir", "perm_connect_label": "Kaynaklar arası erişim", diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json index c8a7178fb..6f99c54d7 100644 --- a/src/locales/vi-VN/install.json +++ b/src/locales/vi-VN/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "Quyền", "perm_card_same_as": "Giống {{version}}", "perm_card_summary": "{{kinds}} nhóm · {{count}} mục", - "perm_card_expand": "Hiện chi tiết quyền", "perm_match_label": "Chạy trên", "perm_match_summary": "Script chạy trên các website này và thay đổi trang", "perm_connect_label": "Truy cập khác nguồn gốc", diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json index 42d04707c..3947d3f7a 100644 --- a/src/locales/zh-CN/install.json +++ b/src/locales/zh-CN/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "权限", "perm_card_same_as": "与 {{version}} 相同", "perm_card_summary": "{{kinds}} 类 {{count}} 项", - "perm_card_expand": "展开权限详情", "perm_match_label": "运行网站", "perm_match_summary": "脚本会在这些网站上运行并修改页面", "perm_connect_label": "跨域访问", diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json index 91ce2b2d7..6998fe976 100644 --- a/src/locales/zh-TW/install.json +++ b/src/locales/zh-TW/install.json @@ -23,7 +23,6 @@ "perm_card_title_short": "權限", "perm_card_same_as": "與 {{version}} 相同", "perm_card_summary": "{{kinds}} 類 {{count}} 項", - "perm_card_expand": "展開權限詳情", "perm_match_label": "執行網站", "perm_match_summary": "腳本會在這些網站上執行並修改頁面", "perm_connect_label": "跨域存取", diff --git a/src/pages/install/components/PermissionCard.test.tsx b/src/pages/install/components/PermissionCard.test.tsx index 11a318d73..340d65799 100644 --- a/src/pages/install/components/PermissionCard.test.tsx +++ b/src/pages/install/components/PermissionCard.test.tsx @@ -88,6 +88,19 @@ describe("PermissionCard 更新差异态", () => { expect(screen.getAllByTestId("permission-row")).toHaveLength(2); }); + it("塌行把自己声明为收起的展开控件", () => { + render(); + expect(screen.getByTestId("permission-row-collapsed")).toHaveAttribute("aria-expanded", "false"); + }); + + it("没有任何权限的更新沿用空态卡头与文案,不进入差异呈现", () => { + render(); + expect(screen.getByText("此脚本不请求任何特殊权限")).toBeInTheDocument(); + expect(screen.getByText("安装前请确认")).toBeInTheDocument(); + expect(screen.queryByText("无变化")).not.toBeInTheDocument(); + expect(screen.queryByText("对比已安装的 1.2.3")).not.toBeInTheDocument(); + }); + it("更新但一项未变时不显示变化标题与增删计数", () => { render(); expect(screen.queryByText("权限变化")).not.toBeInTheDocument(); @@ -144,6 +157,14 @@ describe("PermissionCard 更新零变化态", () => { expect(screen.getAllByText("无变化")).toHaveLength(3); }); + it("折叠单行对辅助技术同样报出无变化与摘要,并声明自己是收起的展开控件", () => { + render(); + const line = screen.getByTestId("permission-card-collapsed"); + expect(line).toHaveAccessibleName(expect.stringContaining("无变化")); + expect(line).toHaveAccessibleName(expect.stringContaining("2 类 3 项")); + expect(line).toHaveAttribute("aria-expanded", "false"); + }); + it("有变动时不折叠整卡", () => { const changedRow: PermissionRow = { kind: "connect", diff --git a/src/pages/install/components/PermissionCard.tsx b/src/pages/install/components/PermissionCard.tsx index 87193a4ab..50b193704 100644 --- a/src/pages/install/components/PermissionCard.tsx +++ b/src/pages/install/components/PermissionCard.tsx @@ -55,6 +55,7 @@ function CollapsedRow({ row }: { row: PermissionRowData }) {