news 2026/9/5 1:40:27

用浏览器用户脚本实现原神共享造物屏蔽与批量删除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用浏览器用户脚本实现原神共享造物屏蔽与批量删除

原神玩家社区里存在大量共享造物内容,常见形态是卡片式列表:每张卡片包含作者、标题、预览图和访问入口。当这些内容越攒越多,会出现两类很常见的需求:一类是不想再看某个作者或某条造物,希望把它“屏蔽”掉,但页面往往没有现成入口;另一类是自己发布过的旧造物需要清理,却只能一条一条点击删除,操作重复、耗时长。把这两个需求交给前端来处理,本质上是两个不同问题:屏蔽是基于本地状态的内容过滤,删除是基于账户权限的远端数据变更。下面以“浏览器用户脚本 + 页面 DOM 操作”的方式,从页面结构分析、本地存储设计、屏蔽过滤实现、批量删除队列、验证与排错五个层面,把这个小工具完整做出来。

1. 先拆分需求:屏蔽与删除看似接近,工程实现完全不同

1.1 屏蔽解决的是“不想看到”,不改变远端数据

屏蔽某个共享造物,并不代表这条内容从服务器上消失,它只是不会再出现在“当前浏览器看到的列表”里。

可以屏蔽的对象有两种粒度:

  • 按造物屏蔽:某一条具体的共享内容,例如某个尘歌壶摹本码、某张同人图。屏蔽后只隐藏这一条。
  • 按作者屏蔽:某个用户发布的所有共享造物。屏蔽后,该作者在列表中出现的任何内容都会被过滤掉。

从实现上看,屏蔽是一种“视图过滤”。每次列表渲染或动态加载新卡片时,脚本都要查询本地保存的屏蔽状态;如果当前卡片的造物 ID 或作者 ID 命中屏蔽名单,就把这张卡隐藏起来。它不依赖任何接口权限,也不需要登录态,只影响当前浏览器环境。

1.2 一键删除解决的是“批量清理”,必须改远端数据

一键删除和屏蔽有本质区别。删除的对象只能是“当前账号自己发布的共享造物”,删除成功后,服务器上的记录会真的消失,其他玩家也会看不到。

因此一键删除不能只做本地隐藏,它必须触达服务端。实现上有两条路线:

  • 模拟用户操作:依次找到每条卡片上的删除按钮,点击后在确认弹窗中点击确认。风险最低,不需要单独处理接口签名。
  • 直接调用页面接口:用开发者工具观察删除请求的 URL、请求头和请求体,再通过 fetch 批量发送。效率高,但要处理 CSRF Token、Cookie、频率限制等问题。

对于小工具来说,第一条路线更安全。它复用页面自身的权限校验、弹窗提醒和失败兜底,脚本只是把成千上百次重复点击串起来。

1.3 数据边界与脚本边界要提前定义

动手写代码之前,必须先确定边界,否则功能会越做越偏。

核心边界有三条:

  • 屏蔽只影响本机:换浏览器、换设备、清除站点数据后,屏蔽列表会丢失。如果想实现账号级屏蔽,需要把状态同步到自己的后端服务。
  • 删除只允许操作自己的内容:脚本必须识别卡片上的“当前作者”标记,只把属于当前账号的卡片加入删除队列,绝不能去点别人的删除按钮。
  • 一键删除本质是把 N 次手工操作串行化:如果页面原本要求人工确认,脚本也应该等待确认弹窗出现再继续,不能通过修改页面逻辑跳过确认。
维度屏蔽一键删除
数据影响范围当前浏览器本地列表服务端数据记录
是否需要登录权限不需要必须使用当前账号权限
典型实现方式本地状态过滤 + DOM 隐藏触发页面删除按钮或调用同源接口
失败影响最多是过滤失效,不影响服务端可能删除失败或部分删除,需要重试机制
扩展复杂度中高,需处理弹窗、加载态、队列重试

注意:如果脚本运行在某个具体的游戏社区页面上,发布前要重新评估该平台的服务条款。示例代码的作用是展示通用实现思路,不代表所有页面都允许通过脚本批量操作。

2. 页面分析与脚本骨架:先确认 DOM 再写功能

2.1 在浏览器中观察共享造物列表的 DOM

用户脚本直接操作页面 DOM,因此写功能前必须先知道列表里每一张卡片的固定结构。

打开页面后,在共享造物列表中右键某张卡片,选择“检查”,查看 Elements 面板。重点确认以下信息:

  • 卡片根节点是什么标签和 class。
  • 卡片根节点是否有 data 属性,例如><div class="creation-card">const CONFIG = { // 列表和卡片 listSelector: '#creation-list', cardSelector: '.creation-card', // 卡片内字段 creationIdAttr: 'data-creation-id', authorIdAttr: 'data-author-id', authorNameSelector: '.card-author', titleSelector: '.card-title', // 操作按钮 deleteBtnSelector: '.card-delete-btn', confirmOkSelector: '.confirm-box .confirm-ok', actionBoxSelector: '.card-actions', // 本地存储 key storageKey: 'genshin_shared_creation_tool_state_v1', // 当前登录用户 ID,需要从页面已有元素中读取 currentUserId: '' };

    这里的核心思路是:业务逻辑只读CONFIG,不直接写死 class 名。上线时如果页面改版,只需要维护配置对象。

    2.3 用户脚本头与基础能力接入

    以 Tampermonkey 为例,脚本头需要声明运行范围。共享造物列表可能由前端框架渲染,建议在document-idle阶段执行,避免过早操作还没有生成的卡片。

    // ==UserScript== // @name 原神共享造物屏蔽与一键删除助手 // @namespace example.tool.shared-creation // @version 0.1.0 // @description 为共享造物列表页增加本地屏蔽和一键删除功能 // @match https://example.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript==

    @match必须指向你要使用的社区页面域名。这里用https://example.com/*是占位,实际要根据目标页面改成真实域名。

    3. 本地状态存储:版本化结构比临时数组更可靠

    3.1 状态字段设计与版本号

    屏蔽状态不能只存一个临时数组,因为在页面刷新后可能还要判断“这条卡片当初是否被屏蔽”。建议把所有状态封装成一个带版本号的对象:

    { "version": 1, "blockedIds": ["creation_001", "creation_002"], "blockedAuthors": ["author_001"], "updatedAt": 1735722000000 }

    字段含义如下:

    字段作用
    version标识数据结构版本,后续字段变更时用于迁移或重建
    blockedIds被屏蔽的造物 ID 列表,只过滤单条
    blockedAuthors被屏蔽的作者 ID 列表,过滤该作者全部内容
    updatedAt最近一次更新时间的毫秒时间戳,便于排查同步问题

    为什么要加版本号?因为用户脚本会持续迭代。今天只有blockedIds,明天增加blockedAuthors,后天可能把 ID 改成字符串拼接的复合键。如果没有版本号,旧数据落到新代码里,很可能因为缺字段导致整个解析异常。

    3.2 读写示例

    可以使用localStorage保存状态。这个 API 简单、无权限提示,适合单个站点使用。

    function getDefaultState() { return { version: 1, blockedIds: [], blockedAuthors: [], updatedAt: Date.now() }; } function loadState() { try { const raw = localStorage.getItem(CONFIG.storageKey); if (!raw) return getDefaultState(); const parsed = JSON.parse(raw); if (!parsed || parsed.version !== 1) return getDefaultState(); return { version: 1, blockedIds: Array.isArray(parsed.blockedIds) ? parsed.blockedIds : [], blockedAuthors: Array.isArray(parsed.blockedAuthors) ? parsed.blockedAuthors : [], updatedAt: parsed.updatedAt || Date.now() }; } catch (error) { console.warn('[shared-creation-tool] 读取本地状态失败,已重置', error); return getDefaultState(); } } function saveState(state) { const clone = { version: state.version || 1, blockedIds: Array.from(new Set(state.blockedIds || [])), blockedAuthors: Array.from(new Set(state.blockedAuthors || [])), updatedAt: Date.now() }; localStorage.setItem(CONFIG.storageKey, JSON.stringify(clone)); return clone; }

    这里写入前用Set去重,避免在一次操作中反复 toggle 导致同一条 ID 出现两次。读取时如果 JSON 解析失败,直接打印警告并回到默认状态,不能让异常把整个脚本中断。

    3.3 存储异常被吞掉会造成屏蔽失效

    一个常见错误是:保存时只用裸的localStorage.setItem,不处理异常。浏览器处于隐私模式、站点限制了存储、或者存储空间已满时,写入会抛异常。如果脚本没有捕获,后面所有屏蔽逻辑都会中断,但用户很难察觉。

    因此在saveState的外层调用处也要做保护:

    function toggleBlockedId(creationId) { const state = loadState(); const index = state.blockedIds.indexOf(creationId); if (index >= 0) { state.blockedIds.splice(index, 1); } else { state.blockedIds.push(creationId); } try { saveState(state); } catch (error) { console.error('[shared-creation-tool] 状态保存失败', error); alert('屏蔽状态保存失败,请检查浏览器存储是否被限制'); } }

    注意:如果采用GM_setValue,需要把@grant GM_setValue@grant GM_getValue写在脚本头中,并注意本方法读写的键是一个字符串,不能直接存对象。

    4. 实现屏蔽:状态过滤、按钮注入与懒加载观察

    4.1 给卡片加屏蔽按钮

    先让卡片出现一个“屏蔽”按钮。按钮的位置尽量放在操作区,不建议用innerHTML拼接动态内容,因为作者昵称和标题都可能包含特殊字符,必须用textContent和原生 DOM 创建。

    function ensureBlockButton(cardRoot) { const meta = parseCard(cardRoot); if (!meta || !meta.creationId) return null; if (cardRoot.classList.contains('mc-blocked')) { return null; } let existing = cardRoot.querySelector('.mc-block-btn'); if (existing) return existing; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'mc-block-btn'; btn.textContent = '屏蔽'; const actionBox = cardRoot.querySelector(CONFIG.actionBoxSelector) || cardRoot; actionBox.appendChild(btn); return btn; } function parseCard(cardRoot) { return { creationId: cardRoot.getAttribute(CONFIG.creationIdAttr), authorId: cardRoot.getAttribute(CONFIG.authorIdAttr), title: cardRoot.querySelector(CONFIG.titleSelector)?.textContent?.trim() || '', authorName: cardRoot.querySelector(CONFIG.authorName
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 1:39:07

从角色设定到AI绘画:苹果嘉儿同人创作全流程指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 1:38:23

NAT 网关:以太网设备跨网段通信方案

一、产品概述在实际工业网络部署中&#xff0c;经常遇到这样的难题&#xff1a;新上位的监控系统、SCADA 或上位机软件与现场设备的 IP 不在同一网段&#xff0c;而设备出厂 IP 固定、无法修改&#xff0c;或修改成本高昂、影响原有系统。天津三格电子 SG-NAT 系列网关正是为解…

作者头像 李华
网站建设 2026/9/5 1:35:52

从冰感蚂蚁模型看产品开发:如何平衡视觉设计与工程实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 1:32:18

DeepSeek Harness 规模化落地:耗时、Token成本与失败排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 1:29:18

基于multisim的交通信号灯控制器设计45-25-5

设计一个交通信号灯控制器并用仿真软件测试&#xff0c;由一条主干道和一条支干道汇合成十字路口&#xff0c;在每个入口处设置红、绿、黄三色信号灯。红灯亮禁止通行&#xff0c;绿灯亮允许通行&#xff0c;黄灯亮则停止行驶&#xff08;给行驶中的车辆有时间停在禁行线以外&a…

作者头像 李华
网站建设 2026/9/5 1:28:49

珠海居民选择有保障眼科机构的3个实用参考指南

选择有保障眼科机构的常见认知误区很多珠海居民在选择眼科机构时&#xff0c;容易陷入一些认知偏差&#xff0c;反而可能影响就诊体验与诊疗效果。常见的误区主要有三类&#xff1a;第一类是将机构性质作为判断正规性的唯一标准&#xff0c;不少人默认公立机构一定更稳妥&#…

作者头像 李华