| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- export type PolicyListFilterOption = "所有分类" | "只看免申即享" | "非免申即享";
- /**
- * 由「被点击的条目」反查它在 `policies` 数组里的下标;查不到返回 -1。
- *
- * 🐛 为什么需要这个函数(2026-09-17 修的真实 bug):
- * 同一条政策下的**多个申报事项**会各生成一张卡片,但适配层把卡片的
- * `title` 与 `declaration_item` 都取自 `card.name.text`(**只有政策名**),
- * 而后端又没有下发 `id` / `policy_id`(实测恒为 undefined)。
- * 于是这些卡片在「按字段比对」这条路上**完全无法区分**——
- * 原先的 findIndex 只要任一条件命中就返回,导致点第二张卡片也跳到第一张,
- * 表现为「两张卡片的详情内容一模一样」。
- *
- * 匹配顺序(从可靠到不可靠):
- * 1. **对象同一性**:卡片区传入的就是 policies 里的那个对象,这一条是精确的
- * 2. 字段比对(id / policy_id / declaration_item):**只有唯一命中才采用**。
- * 多命中说明这组数据本就区分不了(例如同一政策的多个申报事项),
- * 此时返回 -1 交给调用方用「直接展示被点条目自己」的兜底路径 ——
- * 宁可走兜底,也不要张冠李戴地展示另一条的内容。
- */
- export const resolvePolicyDetailIndex = <T extends { originalData?: any; declaration_item?: string }>(
- policies: T[] | null | undefined,
- item: { originalData?: any; declaration_item?: string } | null | undefined
- ): number => {
- const list = Array.isArray(policies) ? policies : [];
- if (!list.length || !item) return -1;
- // 1. 对象同一性
- const exact = list.findIndex((policy) => policy?.originalData === item.originalData);
- if (exact >= 0) return exact;
- // 2. 字段比对(仅在唯一命中时采信)
- const targetId = item.originalData?.id || item.originalData?.policy_id;
- const targetDeclaration = item.originalData?.declaration_item;
- const matched: number[] = [];
- list.forEach((policy, index) => {
- const current = policy?.originalData;
- const sameId =
- !!targetId && (current?.id === targetId || current?.policy_id === targetId);
- const sameDeclaration =
- !!targetDeclaration && policy?.declaration_item === targetDeclaration;
- if (sameId || sameDeclaration) matched.push(index);
- });
- return matched.length === 1 ? matched[0] : -1;
- };
- // ─── 「更多」→ 政策列表 侧边栏用的纯逻辑 ────────────────────────────────
- //
- // 侧边栏列的是**本条回答里出现的政策**(用户 2026-09-18 调整;此前一度做成
- // 「整个会话跨消息聚合」,已撤回)。所以这里只留去重 / 排序 / 关键词过滤这几个
- // 通用零件,数据源由 PolicyMatch 自己提供(props.policyData / policyContent)。
- /**
- * 卡片去重键。
- *
- * 主键用 `declaration_item`:自 2026-09-17 起它是「政策名 - 申报事项」,每条唯一
- * (之前它与政策名同值,同政策的多个申报事项无法区分 —— 见 policy-match-utils
- * 里 resolvePolicyDetailIndex 的说明)。
- *
- * 精简格式(minimal)的 `declaration_item` 恒为占位符「查看政策详情」,
- * 用它去重会把不同卡片错并成一条,故退化为「标题|匹配度|匹配理由|id」组合键。
- */
- // 入参刻意用 any:进来的是从消息里解析出的无类型 JSON(也可能是已 normalize 的 PolicyItem),
- // 收窄成具体类型反而会让 dedupePolicyItems 的泛型推断跑偏(会把 T 推成 Record<string, any>)
- export const buildPolicyCardDedupKey = (item: any): string => {
- const declaration = String(item?.declaration_item ?? "").trim();
- if (declaration && declaration !== "查看政策详情") {
- return `decl:${declaration}`;
- }
- const id = String(
- item?.id ?? item?.policy_id ?? item?.originalData?.id ?? item?.originalData?.policy_id ?? ""
- ).trim();
- return `fb:${String(item?.title ?? "")}|${String(item?.match_degree ?? "")}|${String(
- item?.match_reason ?? ""
- ).slice(0, 60)}|${id}`;
- };
- /** 按 key 去重,**首见优先**(保证聚合结果与会话内出现顺序一致、可预期) */
- export const dedupePolicyItems = <T>(items: T[] | null | undefined, keyOf: (item: T) => string): T[] => {
- const seen = new Set<string>();
- return (Array.isArray(items) ? items : []).filter((item) => {
- const key = keyOf(item);
- if (seen.has(key)) return false;
- seen.add(key);
- return true;
- });
- };
- /** 按 match_score 降序;同分保持原有相对顺序(稳定排序,避免顺序抖动) */
- export const sortPolicyCardsByMatchScore = <T extends { match_score?: number | string }>(
- items: T[] | null | undefined
- ): T[] =>
- (Array.isArray(items) ? items : [])
- .map((item, index) => ({ item, index }))
- .sort(
- (a, b) =>
- (Number(b.item?.match_score) || 0) - (Number(a.item?.match_score) || 0) || a.index - b.index
- )
- .map(({ item }) => item);
- /**
- * 卡片关键词匹配。检索面与旧列表 buildRecordPolicyList 保持一致
- * (title / desc / declaration_item / match_reason / source / enterprise_benefit)。
- * 空关键词恒真;大小写不敏感。
- */
- export const matchesPolicyCardKeyword = (
- item: {
- title?: unknown;
- desc?: unknown;
- declaration_item?: unknown;
- match_reason?: unknown;
- source?: unknown;
- enterprise_benefit?: unknown;
- } | null
- | undefined,
- keyword: string
- ): boolean => {
- const kw = String(keyword ?? "").trim().toLowerCase();
- if (!kw) return true;
- const haystack = [
- item?.title,
- item?.desc,
- item?.declaration_item,
- item?.match_reason,
- item?.source,
- item?.enterprise_benefit,
- ]
- .map((value) => String(value ?? ""))
- .join(" ")
- .toLowerCase();
- return haystack.includes(kw);
- };
|