- 后端
- 前端
- 音视频
【免费下载链接】Auto_Bangumi
AutoBangumi - 全自动追番工具
本文基于 AutoBangumi 仓库内的设计文档 docs/plans/2026-01-25-search-panel-redesign.md,结合前端 WebUI 与后端搜索链路的实际源码,完整梳理搜索面板的交互重构方案与落地实现。读者可以从中掌握:原搜索栏的痛点、模态搜索面板的结构与行为规范、四维过滤芯片系统的数据流、订阅确认模态的设计,以及 SSE 流式搜索在前端如何与后端解析器协作。
AutoBangumi(AB)的种子搜索功能自 v3.1 起内置在顶栏中(详见 docs/feature/search.md),用于快速查找番剧。2026 年初的设计文档将搜索组件从"下拉列表"重构为"全模态 + 高级过滤"的搜索体验。本文将这份设计文档作为主线,逐节拆解设计意图,并用 webui/src/components/search/ 下的四个组件、webui/src/store/search.ts 的状态管理以及后端 backend/src/module/api/search.py 的 SSE 数据源来佐证落地细节。
原实现的四大痛点
设计文档首先列出现有下拉式搜索栏的四个问题,它们共同构成了重构的直接动机:
- 点击外部即清空一切:
v-on-click-outside="clearSearch"会在用户误点空白处时清空关键词与全部结果,造成结果意外丢失; - 结果展示能力有限:绝对定位(absolute)的下拉列表没有滚动容器,结果一多就无法浏览;
- 缺少显式关闭控制:用户除了点击外部之外,没有其他有意的关闭方式;
- 完全没有过滤能力:同一番剧的不同字幕组、不同季度的结果混在一起,难以快速定位目标版本。
对应地,设计目标被明确为四条:防止搜索结果被意外关闭、支持大量结果的可滚动展示、支持按字幕组 / 分辨率 / 字幕类型 / 季度过滤、以及订阅前提供确认步骤。
触发与切换行为
设计文档规定了一套明确的开关语义,实际在 webui/src/components/ab-search-bar.vue 与 webui/src/components/search/ab-search-modal.vue 中实现:
- 点击顶栏搜索框:模态未开则打开,已开则关闭(
toggleModal()); - 按
Escape关闭模态; - 模态头部有可见的
×关闭按钮(ab-icon-button+Close图标,handleClose()触发); - 点击背板(backdrop)不会关闭:模板中背板元素
modal-backdrop本身不挂点击事件,只有模态容器自身的@click.self="handleClose"会响应——点击遮罩空白处不会清空结果,这正对应设计文档"防止意外丢失结果"的目标。
一个值得注意的实现细节是Escape监听器(onKeyStroke('Escape', ...)):ab-search-modal.vue是常驻挂载的组件,因此监听器内部必须先判断showModal,否则在整个应用的任何页面按Escape都会触发模态切换;在确认模态打开时,第一次Escape只清空selectedResult回到搜索结果,第二次才关闭搜索模态。
模态布局结构
设计文档给出了完整布局示意,核心结构为:固定头部(搜索输入 + 资源站选择器 + 关闭按钮)、粘性过滤芯片区、可滚动结果网格:
┌─────────────────────────────────────────────────────────┐ │ 🔍 [Search input] [provider ▼] [×] │ ├─────────────────────────────────────────────────────────┤ │ 字幕组: [喵萌奶茶屋] [ANi] [桜都] [LoliHouse] [+3] │ │ 分辨率: [1080p] [720p] [4K] │ │ 字幕语言: [简中 CHS] [繁中 CHT] [双语] [内嵌] [外挂] │ │ 季度: [S1] [S2] [剧场版] │ │ [清除筛选] 8/24 结果 │ ├─────────────────────────────────────────────────────────┤ │ (scrollable grid of result cards, 3 columns on desktop) │ └─────────────────────────────────────────────────────────┘实际实现(ab-search-modal.vue模板)与文档略有演进:模态通过<Teleport to="body">挂载到body,modal-container使用position: fixed+ 居中弹性布局,modal-content最大宽度 1100px、最大高度calc(100dvh - 100px)并启用内部滚动。头部左侧是带放大镜按钮(搜索中替换为NSpin)的输入框,中间是资源站下拉(provider-select,aria-haspopup="listbox"),右侧是关闭按钮。资源站列表来自useSearchStore().getProviders(),即后端/api/v1/search/provider返回的可用站点(默认mikan、anibt、dmhy、nyaa)。
过滤芯片系统
四个过滤维度
| 维度 | 说明 | 典型取值 |
|---|---|---|
| 字幕组 | 字幕组名称 | 喵萌奶茶屋、ANi、桜都、LoliHouse |
| 分辨率 | 视频分辨率 | 720p、1080p、4K/2160p |
| 字幕语言 | 字幕类型 | CHS(简中)、CHT(繁中)、双语、内嵌、外挂 ASS/SRT |
| 季度 | 季度/类型 | S1、S2、剧场版/Movie、OVA |
自动生成的过滤选项
设计文档要求"结果流式到达时,逐维度提取唯一值,芯片随新值动态出现"。这在 webui/src/store/search.ts 的groupedResults与模态内的filterOptionscomputed 中实现:groupedResults以official_title || title_raw为 key 将流式结果分组成GroupedBangumi { key, official_title, poster_link, year, variants },随后filterOptions遍历所有 group 的 variants,用Set收集四个维度的去重取值。
为让零散的原始元数据收敛成可过滤的规范取值,源码提供三个归一化函数:
normalizeResolution(raw):4k/2160/uhd → 4K,1080/fhd/1920 → FHD,720/hd → HD,480/sd → SD,未命中时原样返回;normalizeSubtitle(raw):先判断双语(含"双语/dual/简+繁/CHS+CHT"),再分别归一化简中(简/chs/sc)、繁中(繁/cht/tc)、日文(日/jp/ja)、内嵌(内嵌/内封)、外挂(外挂/ass/srt),并且"简 + 内嵌"会组合成简/内嵌这种复合值;normalizeSeason(raw):S\d+原样大写,纯数字提取为S{n},剧场/movie/劇場 → 剧场版,ova → OVA,sp/special → SP。
各维度的排序规则也在 computed 中定义:分辨率按4K → FHD → HD → SD的固定序,未知项排末尾;字幕按简 → 繁 → 双语 → 简/内嵌 → 繁/内嵌 → 内嵌 → 外挂 → 日;季度按S1、S2、S3…数字升序后再排特殊类型。这些规范化的取值随后渲染为芯片,标题旁的类别图标来自@icon-park/vue-next(字幕组PeoplesTwo、分辨率Monitor、字幕Translate、季度Calendar)。
过滤行为与组合逻辑
- 芯片可切换:
toggleFilter(type, value)在对应维度数组中增删取值; - 多选可同时生效;
- 同类别内 AND、跨类别 OR:
variantMatchesFilters()逐一检查四个维度,任一维度有激活取值时,变体必须命中该维度集合,且四个维度条件同时成立才算匹配; - 激活芯片实心高亮(
.active填充主色),未激活为描边样式; - 存在激活筛选时显示"清除筛选"(
clearFilters()清空全部四个数组); - 结果计数实时更新:"筛选中 8 / 24 个结果"由
filteredVariantCount / totalVariantCount两个 computed 提供。
实际实现还额外引入了两个设计文档未明确、但体验价值很高的能力:
- 无效选项禁用:
wouldProduceResults(type, value)会结合当前其他维度的激活值,预判"如果再点这个芯片是否还能得到结果",不能产生结果时芯片置灰(disabled),避免用户走进"零结果死胡同"; - 已选芯片摘要:
selectedFilterTags把四个维度的激活值汇总成可逐个删除的小标签(带×),与"清除筛选"按钮并存,方便精确回退单个筛选条件。
溢出处理
设计文档规定"某类选项超过 5 个时显示前 4 个 +[+N more]展开芯片,每行可独立折叠"。实现中常量MAX_VISIBLE_CHIPS = 6,getVisibleOptions()在未展开时截取前 6 个,hasOverflow()/getOverflowCount()计算剩余数量;点击展开按钮在expandedCategories(Set<'group' | 'resolution' | 'subtitle' | 'season'>)中增删对应类别,已展开时按钮文案切换为"收起"。每个类别的展开状态彼此独立。
结果卡片与展示形态
设计文档给出的卡片原型是紧凑网格项:海报、中文 + 罗马音双标题、字幕组徽标、分辨率与字幕标签、季度 + 集数:
┌──────────────────────────┐ │ ┌──────┐ 葬送的芙莉莲 │ │ │poster│ Frieren │ │ │ │ ───────────── │ │ └──────┘ 喵萌奶茶屋 │ │ 1080p · 简中 │ │ S1 · 全28集 │ └──────────────────────────┘实际落地的展示形态采用了设计稿演化后的"海报 + 变体芯片"行布局(源码注释标记为 "Original Prototype 4"):每个番剧分组一行,左侧是海报(无图时显示占位标题块),右侧是该分组的所有变体按钮。每个变体芯片展示tag-group(字幕组,未知时显示Unknown)、tag-res(归一化分辨率)、tag-sub(归一化字幕)、tag-season(归一化季度)四枚标签。点击某个变体芯片即选中该版本进入确认模态。
为保证大量变体不至于撑爆界面,单组变体同样有上限MAX_VISIBLE_VARIANTS = 12,超出部分通过+N展开按钮在expandedVariants中按组展开/收起。而 webui/src/components/search/ab-search-card.vue 中的search-card(海报 + 变体数角标variant-badge+ 标题)则是这一行布局之外保留的卡片语义,可在分组视角下继续演进。
流式动画与空态/加载态
设计文档要求结果卡片淡入上滑(opacity: 0 → 1、translateY: 8px → 0),相邻卡片延迟 50ms,并给出基于 Vue<TransitionGroup>的 CSS 示例:
.card-enter-active { transition: all 0.3s ease; transition-delay: calc(var(--index) * 50ms); } .card-enter-from { opacity: 0; transform: translateY(8px); }模态本身使用overlay/modal两套过渡实现背板与面板的淡入淡出,确认模态则用modal-in关键帧(scale(0.95) translateY(10px) → scale(1))入场。
四种空态在模板中按优先级依次判定,文案来自 webui/src/i18n/zh-CN.json 的search命名空间:
| 状态 | 判定条件 | 显示文案 |
|---|---|---|
| 初始 | 无输入且无结果 | "输入关键词开始搜索" |
| 搜索中 | SSE 连接建立中 | 输入框内 Spinner,结果逐个流入 |
| 无结果 | 有输入但结果为空 | "未找到相关结果,试试其他关键词" |
| 搜索失败 | searchFailed为真 | "搜索失败,请检查连接后重试。" |
| 筛选后为零 | 筛选不匹配 | 由无效选项禁用 + 结果计数联动处理 |
订阅确认模态
点击结果变体后,webui/src/components/search/ab-search-confirm.vue 弹出一层嵌套确认模态(z-index: calc(var(--z-modal) + 10)),设计布局如下:
┌─────────────────────────────────────────────────────┐ │ 添加订阅 [×] │ ├─────────────────────────────────────────────────────┤ │ ┌────────┐ 葬送的芙莉莲 │ │ │ poster │ Sousou no Frieren │ │ │ │ ★ 9.2 · 2023年秋 · 全28集 │ │ └────────┘ │ ├─────────────────────────────────────────────────────┤ │ RSS 源: [当前选择的RSS链接] [复制] │ │ 字幕组: 喵萌奶茶屋 │ │ 分辨率: 1080p │ │ 字幕类型: 简体中文 (内嵌) │ ├─────────────────────────────────────────────────────┤ │ 高级设置 ▼ │ │ 过滤规则 / 保存路径 / 重命名 │ ├─────────────────────────────────────────────────────┤ │ [取消] [确认订阅 ✓] │ └─────────────────────────────────────────────────────┘实际实现包含以下值得展开的行为:
- 本地深拷贝:
localBangumi = JSON.parse(JSON.stringify(props.bangumi)),后续编辑不污染 store 中的原始对象;watch(() => props.bangumi, ..., { deep: true })在切换选中项时重新同步并触发偏移检测; - 元信息标签:
infoTags按季度/分辨率/字幕/字幕组生成四色标签(季节用主色、分辨率用强调色、字幕用成功色、字幕组用警告色); - RSS 链接一键复制:
navigator.clipboard.writeText(rssLink),成功后按钮短暂变为CheckOne勾选态(2 秒后复原); - 高级设置默认折叠:包含三块——过滤规则用
NDynamicTags编辑filter数组(对应后端 webui/types/bangumi.ts 中BangumiRule.filter: string[])、季度偏移season_offset与集数偏移episode_offset(可调用apiBangumi.suggestOffset自动检测); - 偏移不匹配检查:挂载时调用
apiBangumi.detectOffset({ title, parsed_season, parsed_episode: 1 }),若has_mismatch为真,弹出ab-offset-mismatch-dialog展示 TMDB 对照信息与建议值,用户可选择应用(写入season_offset/episode_offset)、保留或取消; - 确认与取消语义:
取消仅关闭确认模态(clearSelectedResult),搜索结果原样保留;确认订阅调用handleConfirm→apiDownload.subscribe(bangumi, rss),成功后提示"订阅成功"并刷新番剧列表、关闭两层模态。
handleConfirm中的订阅对象构造也值得注意:它把选中的番剧数据打包为{ id: 0, name: official_title, url: rss_link[0], aggregate: false, parser: provider, enabled: true, ... }的 RSS 记录,即搜索订阅本质上等同于添加一条指向该资源站搜索结果的关键词 RSS。
键盘导航
| 按键 | 行为 |
|---|---|
Enter(搜索框内) | 触发搜索(@keyup.enter="onSearch") |
Escape | 先关闭确认模态(回到结果),再关闭搜索模态 |
Tab | 依次经过过滤芯片 → 结果卡片 |
Enter(聚焦卡片上) | 打开确认模态 |
| 方向键 | 网格导航(可选增强) |
onSearch()在 webui/src/store/search.ts 中校验关键词非空后调用openSearch()开启 SSE 流;模态内搜索中按钮变为 Spinner 且禁用,避免重复提交。
响应式设计
| 视口 | 网格列数 | 模态宽度 | 行为 |
|---|---|---|---|
| 桌面(>1024px) | 3 列 | 800px 居中 | 完整体验 |
| 平板(768–1024px) | 2 列 | 90% 宽 | 过滤收起为单行 |
| 移动端(<768px) | 1 列 | 全屏 | Bottom Sheet 样式 |
实际样式中,modal-container的padding: 60px 16px 16px在forDesktop媒体查询下提升为80px 24px 24px;模态max-width: 1100px(比文档的 800px 更宽,以容纳行式变体布局)。移动端语义对应"全屏 bottom sheet":模态撑满inset: 0,内容区自身可滚动。
组件结构与状态管理
设计文档规划的组件树为:
ab-search-modal.vue (new - main modal container) ├── ab-search-header.vue (search input + provider + close) ├── ab-search-filters.vue (new - filter chips) ├── ab-search-results.vue (new - scrollable grid) │ └── ab-search-card.vue (new - individual result card) └── ab-search-confirm.vue (new - confirmation modal)实际仓库中的 webui/src/components/search/ 收敛为四个文件:ab-search-modal.vue(主容器,内部内联实现 header、filter 区与结果列表)、ab-search-filters.vue(独立过滤组件,props 为filters/filterOptions/filteredCount/totalCount,emittoggle-filter与clear-filters)、ab-search-card.vue(卡片)、ab-search-confirm.vue(确认模态)。其中ab-search-filters.vue与模态内嵌实现是两套平行实现(常量上限分别为 8 与 6),说明过滤逻辑正处于组件化抽取的过渡阶段。
useSearchStore的状态设计比文档规划更内聚——showModal、selectedResult、groupedResults、providers、loading、searchFailed均收敛在 webui/src/store/search.ts 中,过滤状态(activeFilters、expandedCategories、expandedVariants)则由ab-search-modal.vue以组件内 ref 持有。toggleModal/openModal/closeModal/clearSearch/selectResult/clearSelectedResult构成完整的状态流转 API;closeModal会同时置空selectedResult并调用closeSearch()关闭 SSE 流。
实现要点:过滤解析、SSE 流式与后端数据链路
设计文档的三条实现要点在源码中均有对应:
- 过滤解析:元数据来自后端解析器对种子标题的解析结果,即
BangumiRule的group_name、dpi、subtitle、season_raw/season字段(类型定义见 webui/types/bangumi.ts,对应后端 backend/src/module/models/bangumi.py 的Bangumi模型),前端再经三个normalize*函数规整为过滤取值; - SSE 流式增量更新:前端 webui/src/api/search.ts 用原生
EventSource打开api/v1/search/bangumi?site={provider}&keywords={keyword}(withCredentials: true),每条onmessage解析BangumiAPI并拆分为filter: string[]与rss_link: string[],追加前先按rss_link[0] || title_raw去重。源码注释与 webui/src/api/tests/search.test.ts 中的FakeEventSource测试共同记录了关键生命周期治理:重搜索前必须关闭旧流,且旧流的onopen/onmessage/onerror均不得触碰共享状态(通过eventSource.value !== es同流判定),否则旧流会重复追加结果、甚至在新流建立后触发无限自动重连; - 新结果立即应用激活筛选:过滤逻辑基于
groupedResults的 computed 派生,流式追加数据后所有过滤与计数自动重算。
后端侧,backend/src/module/api/search.py 的GET /search/bangumi将关键词按空格拆分后交给SearchTorrent().analyse_keyword(),以sse_starlette的EventSourceResponse流式返回;backend/src/module/searcher/searcher.py 的analyse_keyword()逐条调用解析器torrent_to_data(..., fetch_poster=False)(交互搜索不做逐条 Mikan 主页抓取与海报下载以保持响应速度),按special_url去重后,再通过 TMDB 缓存补齐本地化标题与海报,最后以json.dumps(bangumi.dict())逐条yield。/search/provider端点则返回SEARCH_CONFIG的全部站点 key,前端据此渲染资源站下拉。搜索源的管理(URL 模板必须包含%s占位符、默认源 mikan/nyaa/dmhy 不可删除)参见 docs/feature/search.md。
小结
从设计文档到落地代码,AutoBangumi 的搜索面板完成了一次完整的交互升级:以模态取代下拉、以显式关闭取代点击外部清空、以四维过滤芯片解决同番多版本难以定位的问题、以订阅确认模态降低误订阅成本。前端通过归一化函数与组合过滤规则把解析器输出的原始元数据收敛为可操作的筛选维度,后端则以 SSE 流式输出保证首屏结果即时可见。若读者希望深入,可从 webui/src/components/search/ab-search-modal.vue 与 webui/src/store/search.ts 入手,结合 backend/src/module/searcher/searcher.py 的流式生成器与 webui/src/api/tests/search.test.ts 的 EventSource 生命周期测试,即可完整还原这条从关键词到订阅的全链路。
- 后端
- 前端
- 音视频
【免费下载链接】Auto_Bangumi
AutoBangumi - 全自动追番工具
相关推荐
Airweave搜索界面:实时搜索与过滤的交互设计
Airweave搜索界面:实时搜索与过滤的交互设计 引言:智能搜索的现代挑战 在当今数据爆炸的时代,如何从海量信息中快速准确地找到所需内容成为企业和开发者的核心
人工智能RAGAI AgentMCP 服务后端Handsontable 过滤与搜索实战:外部搜索框、`<mark>` 高亮匹配与多列过滤面板
Handsontable 过滤与搜索实战:外部搜索框、 <mark 高亮匹配与多列过滤面板 Handsontable 的 Search 插件与 Filters
前端UI组件ARIAKIT Combobox 搜索过滤实战:用 setValue 与 startTransition 构建响应式搜索下拉
ARIAKIT Combobox 搜索过滤实战:用 setValue 与 startTransition 构建响应式搜索下拉 本文基于 ARIAKIT 仓库中的
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考