export type StoreCategoryLike = { id?: string; name?: string; parentId?: string | null; parent?: { name?: string } | null; }; export type StoreCategoryTreeNode = { id: string; name: string; children?: { id: string; name: string }[]; }; export function storeStarCount(rating?: number | string | null): number { const n = Number(rating); if (!Number.isFinite(n) || n <= 0) return 5; return Math.min(5, Math.max(1, Math.round(n))); } export type StoreAddressParts = { province?: string | null; cityName?: string | null; city?: string | null; district?: string | null; address?: string | null; }; function addressPart(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } /** 省 + 市 + 区 + 详细地址原样拼接;详细地址已含省市区时不去重 */ export function fullStoreAddress(store: StoreAddressParts, fallback = '地址待完善'): string { const province = addressPart(store.province); const city = addressPart(store.cityName) || addressPart(store.city); const district = addressPart(store.district); const detail = addressPart(store.address); const text = `${province}${city}${district}${detail}`; return text || fallback; } export function storeCategoryTags( store: { tags?: unknown; categoryId?: string | null; category?: StoreCategoryLike | null; categories?: StoreCategoryLike[] | null; }, tree: StoreCategoryTreeNode[] = [], ): string[] { const fromJson = Array.isArray(store.tags) ? store.tags.map((t) => String(t).trim()).filter(Boolean) : []; if (fromJson.length) return fromJson; const multi = Array.isArray(store.categories) ? store.categories : []; if (multi.length) { return groupCategoryTags(multi, tree); } if (store.category || store.categoryId) { const grouped = groupCategoryTags( store.category ? [store.category] : [], tree, store.categoryId, ); if (grouped.length) return grouped; } const storeCatId = String(store.categoryId || ''); if (storeCatId) { for (const root of tree) { for (const child of root.children ?? []) { if (child.id === storeCatId) { return [`${root.name}|${child.name}`]; } } } } return []; } /** 按大类分组:餐饮|火锅·江浙菜、娱乐|KTV */ function groupCategoryTags( categories: StoreCategoryLike[], tree: StoreCategoryTreeNode[] = [], fallbackCategoryId?: string | null, ): string[] { if (!categories.length && fallbackCategoryId) { return groupCategoryTags([{ id: String(fallbackCategoryId) }], tree); } const groups = new Map(); const order: string[] = []; for (const cat of categories) { const resolved = resolveCategoryParts(cat, tree); if (!resolved) continue; const { parentKey, parentName, childName } = resolved; if (!groups.has(parentKey)) { groups.set(parentKey, { parentName, children: [] }); order.push(parentKey); } const group = groups.get(parentKey)!; if (childName && childName !== parentName && !group.children.includes(childName)) { group.children.push(childName); } } return order .map((key) => { const group = groups.get(key)!; if (group.parentName && group.children.length) { return `${group.parentName}|${group.children.join('·')}`; } if (group.children.length) return group.children.join('·'); return group.parentName; }) .filter(Boolean); } function resolveCategoryParts( cat: StoreCategoryLike, tree: StoreCategoryTreeNode[] = [], ): { parentKey: string; parentName: string; childName: string } | null { const childName = String(cat.name || '').trim(); let parentName = String(cat.parent?.name || '').trim(); const catId = String(cat.id || ''); const parentId = String(cat.parentId || ''); if (!parentName && (parentId || catId)) { for (const root of tree) { if (parentId && root.id === parentId) { parentName = root.name; break; } for (const child of root.children ?? []) { if (catId && child.id === catId) { parentName = root.name; if (!childName) return { parentKey: root.id, parentName, childName: child.name }; break; } } } } if (!childName && !parentName) return null; const parentKey = parentId || parentName || catId; if (!parentName && catId) { for (const root of tree) { for (const child of root.children ?? []) { if (child.id === catId) { return { parentKey: root.id, parentName: root.name, childName: child.name }; } } } } return { parentKey, parentName: parentName || childName, childName: childName || parentName, }; } export function storeLeafCategoryIds(store: { categoryId?: string | null; category?: StoreCategoryLike | null; categories?: StoreCategoryLike[] | null; }): string[] { if (Array.isArray(store.categories) && store.categories.length) { return store.categories.map((c) => String(c.id || '')).filter(Boolean); } const id = String(store.categoryId || store.category?.id || ''); return id ? [id] : []; } /** C 端是否渲染「核销N次」;开关关闭或次数为 0 时不展示 */ export function shouldShowStoreRedeemCount( enabled: boolean | undefined, count?: number | null, ): boolean { if (enabled === false) return false; return Number(count) > 0; }