feat(grid): enhance field search and filter interaction

This commit is contained in:
zipg authored and GitHub committed 2026-08-07 13:43:55 +08:00
1 parent 13878a1a30
commit f9e93b356f
26 files changed
+491 -35

No files matched your search

@@ -0,0 +1,82 @@
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
/**
* Scores identifier matches for completion and field pickers.
* Higher scores are better; -1 means there is no match.
*/
export function identifierMatchScore(candidate: string, query: string): number {
if (!query) return 1;
const candidateLower = candidate.toLowerCase();
const queryLower = query.toLowerCase();
if (candidateLower === queryLower) return 3000 - candidateLower.length;
if (candidateLower.startsWith(queryLower)) return 2000 - candidateLower.length;
const initials = identifierInitials(candidate);
if (initials && initials.startsWith(queryLower)) {
const exactInitialsBonus = initials === queryLower ? 400 : 0;
return 2400 + exactInitialsBonus - candidateLower.length;
}
if (/^[a-z0-9]+$/.test(queryLower) && containsHan(candidateLower)) {
const pinyinInitials = pinyinFirstLetters(candidateLower);
if (pinyinInitials.startsWith(queryLower)) {
const exactInitialsBonus = pinyinInitials === queryLower ? 300 : 0;
return 2300 + exactInitialsBonus - candidateLower.length;
}
const subsequence = orderedSubsequenceSpan(pinyinInitials, queryLower);
if (subsequence) {
return 1600 - subsequence.first * 30 - (subsequence.span - queryLower.length) * 10 - candidateLower.length;
}
}
const substringIndex = candidateLower.indexOf(queryLower);
if (substringIndex >= 0) {
const boundaryBonus = isIdentifierBoundary(candidate, substringIndex) ? 400 : Math.max(0, 180 - substringIndex * 12);
return 900 + boundaryBonus - candidateLower.length;
}
let candidateIndex = 0;
let totalGap = 0;
let firstMatchPosition = -1;
let boundaryBonus = 0;
for (const character of queryLower) {
const nextPosition = candidateLower.indexOf(character, candidateIndex);
if (nextPosition === -1) return -1;
if (firstMatchPosition === -1) firstMatchPosition = nextPosition;
if (isIdentifierBoundary(candidate, nextPosition)) boundaryBonus += 40;
totalGap += nextPosition - candidateIndex;
candidateIndex = nextPosition + 1;
}
const earlyMatchBonus = Math.max(0, 700 - firstMatchPosition * 35) + boundaryBonus;
if (totalGap >= queryLower.length) {
return 400 + earlyMatchBonus * 0.3 - totalGap * 20 - candidateLower.length;
}
return 1200 + earlyMatchBonus - totalGap * 10 - candidateLower.length;
}
export function matchesIdentifierSearch(candidate: string, query: string): boolean {
return identifierMatchScore(candidate, query.trim()) >= 0;
}
function identifierWords(candidate: string): string[] {
return candidate
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean);
}
function identifierInitials(candidate: string): string {
return identifierWords(candidate)
.map((part) => part[0])
.join("");
}
function isIdentifierBoundary(candidate: string, index: number): boolean {
if (index <= 0) return true;
const previous = candidate[index - 1] ?? "";
const current = candidate[index] ?? "";
return /[^A-Za-z0-9]/.test(previous) || (/[a-z0-9]/.test(previous) && /[A-Z]/.test(current));
}