✨ feat(vscode): 看板顶部加筛选框,按标题 / 工单号 / PR 号过滤卡片

工单多了以后靠肉眼在四列里翻太慢。顶部输入框按空白分词逐词 AND 匹配
标题、工单号、PR 号,`#427` 与 `427` 等价,纯数字按包含匹配,记不全
编号也能搜到。筛选只收窄渲染,拖拽与依赖锁定仍按全量列表算;列头角标
在筛选时显示「可见 / 总数」。
This commit is contained in:
cruldra committed 2026-09-20 17:24:54 +08:00
1 parent 73f2c3ec66
commit 9e255f517b
6 files changed
+167 -8

No files matched your search

@@ -0,0 +1,60 @@
import type { Issue } from '../types'
import { describe, expect, it } from 'vitest'
import { filterIssues } from './issueFilter'
function issue(number: number, title: string, extra: Partial<Issue> = {}): Issue {
return {
id: `repo#${number}`,
number,
title,
column: 'todo',
htmlUrl: '',
...extra,
}
}
const board = [
issue(426, '编程大赛:报名与提交作品'),
issue(427, '编程大赛-作品墙分类', { pr: '318' }),
issue(419, '管理后台统计各模型用量'),
issue(42, 'auto 模型:自动为用户会话选模型'),
]
function numbers(query: string): number[] {
return filterIssues(board, query).map(i => i.number)
}
describe('filterIssues', () => {
it('空查询放行全部,且原样返回同一个数组', () => {
expect(filterIssues(board, ' ')).toBe(board)
})
it('按标题子串匹配', () => {
expect(numbers('作品')).toEqual([426, 427])
})
it('标题匹配不区分大小写', () => {
expect(numbers('AUTO')).toEqual([42])
})
it('按工单号匹配,带不带 # 都行', () => {
expect(numbers('#427')).toEqual([427])
expect(numbers('427')).toEqual([427])
})
it('数字按包含匹配,短号能带出长号', () => {
expect(numbers('42')).toEqual([426, 427, 42])
})
it('按 PR 号匹配', () => {
expect(numbers('318')).toEqual([427])
})
it('多个词之间是 AND', () => {
expect(numbers('编程 分类')).toEqual([427])
})
it('没有命中时返回空', () => {
expect(numbers('不存在的东西')).toEqual([])
})
})
+34
View File
@@ -0,0 +1,34 @@
import type { Issue } from '../types'
/**
* 看板顶部筛选框的匹配规则。
*
* 查询按空白切成多个词,逐词 AND:工单要留在板上,每个词都得命中它的
* 标题、工单号或 PR 号之一。词首的 `#` 先去掉,`#427` 与 `427` 等价。
* 纯数字的词按「包含」匹配编号,`42` 既能带出 #42 也能带出 #427,
* 免得用户记不全编号就搜不到。
*/
export function parseFilterTerms(query: string): string[] {
return query.trim().toLowerCase().split(/\s+/).filter(Boolean)
}
function matchesTerm(issue: Issue, term: string): boolean {
if (issue.title.toLowerCase().includes(term))
return true
const digits = term.replace(/^#/, '')
if (digits === '' || !/^\d+$/.test(digits))
return false
return String(issue.number).includes(digits) || (issue.pr ?? '').includes(digits)
}
export function matchesIssueFilter(issue: Issue, terms: string[]): boolean {
return terms.every(term => matchesTerm(issue, term))
}
/** 空查询原样返回,调用方据此免掉一次无意义的数组拷贝。 */
export function filterIssues(issues: Issue[], query: string): Issue[] {
const terms = parseFilterTerms(query)
if (terms.length === 0)
return issues
return issues.filter(issue => matchesIssueFilter(issue, terms))
}