import type { ReactNode } from 'react'; import type { PisellLookupProps } from '@pisell/materials'; import type { BaseResponsiveProps } from '../../responsive'; /** * 业务域唯一 key * * 内置 6 个常用 key 仅为提示,业务可自由扩展为任意字符串。 */ export declare type GlobalSearchProviderKey = 'reservation' | 'order' | 'resource' | 'client' | 'product' | 'device' | (string & {}); /** * Provider 多语言标签 * * 使用对象形式而不是 i18n key,是因为 Provider 通常由业务模块自己注册, * 业务模块自己持有文案;GlobalSearch 只负责按当前语言挑一条。 */ export interface GlobalSearchProviderLabel { en: string; 'zh-CN': string; 'zh-HK': string; ja?: string; pt?: string; } /** * 全局搜索的"视角/模式"。 * * - 'admin' 管理模式(如管理员视角,可看全局数据) * - 'business' 业务模式(如门店/业务员视角,受业务范围约束) * * 该值由 PisellGlobalSearch 内的模式单选驱动,作为 search 参数原样传给 Provider; * **PisellGlobalSearch 本身不基于它做任何额外逻辑**(不切 tab / 不变 UI / 不过滤 provider)。 * 是否使用、如何使用,由 Provider.search 的实现决定(接口入参、权限判定等)。 */ export declare type GlobalSearchMode = 'admin' | 'business'; /** * Provider 搜索调用参数 */ export interface GlobalSearchSearchParams { /** 搜索关键词(用户在搜索框输入的文本) */ keyword: string; /** 期望条数;All 模式下通常较小(如 5),单 Tab 模式下较大(如 20) */ pageSize?: number; /** 分页偏移,单 Tab 模式下使用 */ offset?: number; /** 由 GlobalSearch 统一注入的取消信号,Provider 应透传给 fetch/axios */ signal?: AbortSignal; /** * 当前所选视角;由 PisellGlobalSearch 的模式单选驱动。 * Provider 可选择性使用(如拼接到接口请求里);不读取也完全合规。 */ mode: GlobalSearchMode; /** * 当前搜索作用域: * - `'all'`:在「全部」范围内被聚合调度(与其它 Provider 并发); * - `'single'`:在该 Provider 自己的单 tab 内被单独调度。 * * Provider 可据此切换接口(如订单:All 走聚合 `/shop/es/search`、单 tab 走 * 单模块 `/shop/es/search/sales`);不读取也完全合规。 */ scope: 'all' | 'single'; /** * Provider 可选实时增量回写入口。 * 仅用于类似订单搜索这种 OS Server delta callback:调用方只会 patch 当前已加载列表中的已有项。 * * @example * onRealtimePatch?.([{ order_id: 1001, status: 'paid' }]) */ onRealtimePatch?: (items: any[]) => void; /** 当前 provider 本轮搜索可复用的实时订阅 id */ realtimeSubscriberId?: string; /** 注册实时订阅清理函数,useProviderSearch 会在重查/卸载时执行 */ onRealtimeCleanup?: (cleanup: () => void) => void; } /** * Provider 搜索结果 */ export interface GlobalSearchSearchResult { list: TItem[]; /** 总数;用于判断是否还有更多以及展示「共 N 条」 */ total: number; } /** * Provider renderItem / onSelect 上下文 */ export interface GlobalSearchItemContext { keyword: string; /** 关闭搜索面板 */ close: () => void; /** 路由跳转(由宿主注入,例如 history.push / window.location.assign) */ navigate?: (url: string) => void; /** * 就地打开详情(opt-in,由 GlobalSearch 注入)。 * * Provider(如订单表格)在自己的行点击里调用它,把「选中了哪条」回传给外层; * 外层据此在右侧详情面板(桌面)或抽屉(手机)内联展示详情,而不是跳转 / 开宿主抽屉。 * 整页形态会注入;其它形态(如 Modal)不注入时为 undefined,Provider 应回退到原有行为。 */ onOpenDetail?: (params: { providerKey: GlobalSearchProviderKey; id: string | number; /** 当前行/结果项原始数据;详情组件可用它做首屏预加载,避免再按 id 查询。 */ record?: unknown; }) => void; /** * Provider 自管实时订阅时,把 delta 原始项同步给外层,用于刷新已打开详情。 */ onRealtimePatchItems?: (providerKey: GlobalSearchProviderKey, items: unknown[]) => void; /** * Provider 自管取数(如管理模式订单表格)时,把当前查询总数回写给外层, * 用于分组头「共 N 条」/「View all」展示,从而省去 `search()` 里单独的「只取数量」计数请求。 */ onReportTotal?: (providerKey: GlobalSearchProviderKey, total: number) => void; /** * 详情刷新版本表。实时 delta 命中某条记录时,外层递增对应 key, * Provider 可把版本拼进详情组件 key/osKey 触发重新拉取。 */ detailRefreshVersions?: Record; } /** * Provider 权限上下文 */ export interface GlobalSearchPermissionContext { user?: unknown; permissions?: string[]; [key: string]: unknown; } /** * 统一的业务搜索 Provider 接口 * * 每个业务域实现一个 Provider 并注册到 PisellGlobalSearch, * 全局搜索本身不感知业务字段、不感知接口。 */ export interface GlobalSearchProvider { /** 唯一 key;用于 Tab 切换、历史分类等 */ key: GlobalSearchProviderKey; /** Tab 显示名(多语言) */ label: GlobalSearchProviderLabel; /** Tab 图标(可选) */ icon?: ReactNode; /** Tab 排序,从小到大;不传按注册顺序 */ order?: number; /** * 搜索实现;必须支持 AbortSignal。 * * GlobalSearch 会保证: * - 同一 Provider 的旧请求被 abort 后才发起新请求; * - 切换 Tab 时所有 in-flight 请求都会被 abort。 */ search: (params: GlobalSearchSearchParams) => Promise>; /** * 单条结果渲染。 * * - 由业务自己控制卡片样式,保证与业务详情页一致; * - 推荐复用 PisellCards 下的卡片; * - 不要在这里发请求。 * * 注:当 Provider 同时提供 `renderResults` 时,`renderItem` 会被忽略; * 但仍建议保留一个简洁实现作为降级(renderResults 出错时的兜底)。 */ renderItem: (item: TItem, ctx: GlobalSearchItemContext) => ReactNode; /** * 整组结果渲染(可选)。 * * 提供此函数后,PisellGlobalSearch 将**完全交由 Provider** 渲染该业务域的 * 结果区域(不再使用 renderItem 逐条 map)。适用于: * * - 想要表格 / 网格 / 自定义 layout 而不是简单列表(如订单复用 PisellSalesManagement); * - 需要 Provider 内部自己管分页 / 过滤 / 选择 / 实时订阅; * - 结果区域结构跟"卡片列表"差异很大。 * * 注意事项: * - 该函数**完全替换**默认的 loading / empty / list 渲染; * 如果你需要这些状态,请自行处理; * - 调用方仍然会调用 `search()` 获取 `{ list, total }` 写入 state, * 方便分组头展示 "共 N 条";如果你的 renderResults 不依赖这份 list, * `search` 可以返回 `{ list: [], total: 0 }`(例如业务模式单 tab 由 OrderBusinessList * 自管数据,state.total 不可用于该 tab 的结果计数); * - All 模式下仍然会使用 `state.total > list.length` 来决定要不要展示 "View all", * 想关闭这个,只需 `total === list.length`。 */ renderResults?: (args: { state: GlobalSearchProviderState; keyword: string; /** 当前模式(与 search 参数一致),方便 renderResults 把它继续透传给下游 */ mode: GlobalSearchMode; /** * 当前渲染位置: * - `'single'`:该 Provider 自己的单 tab(完整结果区,可做无限加载 / 虚拟滚动); * - `'grouped'`:All 模式下的分组预览(通常只展示前若干条 + 「View all」跳转)。 * * Provider 可据此切换重交互形态(如订单业务列表:单 tab 走虚拟滚动 + 触底翻页, * 分组里仅做轻量预览);不读取也完全合规。 */ layout: 'single' | 'grouped'; }, ctx: GlobalSearchItemContext) => ReactNode; /** * 选中某条结果时触发;用于跳转或就地打开详情 Modal。 * * - 如果在 renderItem 内部已经处理了点击,可以不实现 onSelect。 * - 推荐写法:业务在 renderItem 里读取 ctx.close / ctx.navigate。 */ onSelect?: (item: TItem, ctx: GlobalSearchItemContext) => void; /** * 是否启用;通常用于权限控制。 * 返回 false 时不出现在 Tab 列表中。 */ enabledWhen?: (ctx: GlobalSearchPermissionContext) => boolean; /** * 自定义空态文案;不传时使用全局默认。 */ emptyText?: GlobalSearchProviderLabel; } /** * "All" 这个特殊 tab 的 key */ export declare const ALL_TAB_KEY: "__all__"; export declare type AllTabKey = typeof ALL_TAB_KEY; /** * 当前激活的 tab key */ export declare type GlobalSearchActiveTab = AllTabKey | GlobalSearchProviderKey; /** * 单个 Provider 在 GlobalSearch 内的运行时状态 */ export interface GlobalSearchProviderState { loading: boolean; error?: Error; list: TItem[]; total: number; /** 最近一次成功搜索对应的 keyword,用于过期结果丢弃 */ resolvedKeyword?: string; } /** * 全部 Provider 的运行时状态映射 */ export declare type GlobalSearchStateMap = Record; /** * PisellGlobalSearch Ref */ export interface PisellGlobalSearchRef { /** 打开搜索面板 */ open: () => void; /** 关闭搜索面板 */ close: () => void; /** 设置关键词(会触发搜索) */ setKeyword: (keyword: string) => void; /** 获取当前关键词 */ getKeyword: () => string; /** 切换 tab */ setActiveTab: (tab: GlobalSearchActiveTab) => void; /** 设置模式(会触发搜索) */ setMode: (mode: GlobalSearchMode) => void; /** 获取当前模式 */ getMode: () => GlobalSearchMode; /** 主动刷新当前 tab */ refresh: () => void; } /** * PisellGlobalSearch Props * * 只暴露与全局搜索相关的参数;其余 PisellLookup 的细节通过 lookupProps 透传。 */ export interface PisellGlobalSearchProps extends BaseResponsiveProps { /** * 注册的业务 Provider 列表 * * 通常由业务模块各自提供,再由宿主聚合: * ```ts * * ``` */ providers: GlobalSearchProvider[]; /** * 权限/用户上下文,传给 Provider.enabledWhen */ permissionContext?: GlobalSearchPermissionContext; /** * 受控的 activeTab */ activeTab?: GlobalSearchActiveTab; /** * activeTab 变化回调 */ onActiveTabChange?: (tab: GlobalSearchActiveTab) => void; /** * All 模式下,每个 Provider 展示的最大条数(点击 "View all" 后切到对应 Tab 看完整结果) * @default 5 */ allModePageSize?: number; /** * 单 Tab 模式下,每页条数 * @default 20 */ singleModePageSize?: number; /** * @deprecated 已忽略;组件始终展示 All 范围(与各 Provider 一起露出), * 传入此 prop 不再生效,仅为旧 schema/调用方的类型兼容而保留。 */ showAllTab?: boolean; /** * "All" tab 文案 */ allTabLabel?: GlobalSearchProviderLabel; /** * 路由跳转函数;会注入到 Provider 的 ctx.navigate */ navigate?: (url: string) => void; /** * 默认模式;不传时为 `'business'`。 */ defaultMode?: GlobalSearchMode; /** * 受控的模式。 */ mode?: GlobalSearchMode; /** * 模式变更回调。 */ onModeChange?: (mode: GlobalSearchMode) => void; /** * 是否在结果区顶部显示模式切换单选。 * @default true */ showModeSwitch?: boolean; /** * 搜索提交模式: * * - `'instant'`(默认):输入即触发搜索,与 PisellLookup 原生行为一致; * - `'submit'`:只在用户**按下回车 / 点击搜索按钮**后才把 input 值写入真正的 keyword * 并触发搜索。适合"整页全局搜索"这种"准备 + 提交"语义更强的场景。 * * @default 'instant' */ submitMode?: 'instant' | 'submit'; /** * 自定义模式选项的展示文案(覆盖内置 i18n 文案)。 * 不传时使用内置:管理模式 / 业务模式(中英繁)。 */ modeOptions?: { admin?: GlobalSearchProviderLabel; business?: GlobalSearchProviderLabel; }; /** * 透传给底层 PisellLookup 的参数; * 用于自定义 trigger / displayType / modalWidth / placeholder / history 等。 * * 注意:onSearch / resultRender / value / onChange 会被 PisellGlobalSearch 接管, * 即使传入也会被覆盖。 */ lookupProps?: Omit; /** * 自定义类名 */ className?: string; /** * 自定义样式 */ style?: React.CSSProperties; }