import { detectVisitorCountry, translationProvider } from "./visitor-region.js?v=20260814f"; import { renderActivities } from "./render-intelligence-activities.js?v=20260814e"; import { state } from "./render-intelligence-state.js?v=20260814e"; /** 家纺跨境情报前端渲染器。用户文案集中在 TEXT,便于后续接入完整 i18n。 */ const TEXT = { tabs: { today: "今日必看", standards: "趋势雷达", calendar: "备货日历", exhibitions: "展会通", policies: "政策预警", activities: "跨境活动", platforms: "平台渠道", }, allRegions: "全部区域", searchPlaceholder: "搜索国家、展会、政策或产品机会", sourceLink: "查看来源页", loadMore: "加载更多", empty: "当前筛选条件下没有情报", verified: "已核验", officialRegistration: "官方报名", exhibitorInfo: "参展信息", visitorRegistration: "观众登记", translatedVersion: "查看译文版", shareItem: "转发给同事", calendar: { important: "重点商机", all: "全部节点", allMarkets: "全部市场区域", allCountries: "全部国家/地区", allProducts: "全部产品方向", allPlatforms: "全部销售平台", platformFilterHint: "只显示该平台已有明确经营动作的节日与消费节点;不代表平台官方活动日历。", what: "这个节日是什么", markets: "影响国家/地区", languages: "主要语种", platforms: "重点销售渠道", platformActions: "平台怎么做", opportunity: "家纺与纺织机会", design: "设计与文化提醒", next: "现在该做什么", facts: "官方事实", analysis: "系统经营分析", cultureReference: "了解节日文化", visualReference: "Bing 找节日照片", addCalendar: "下载日历提醒(ICS)", }, }; const SECTION_LABELS = { calendar: "备货日历", exhibition: "展会通", policy: "政策预警", standard: "趋势雷达", platform: "平台渠道", activity: "跨境活动", }; const CALENDAR_PRODUCT_RULES = [ ["床品", /床品|四件套|被|床单|枕套|床垫|睡袋/], ["毛巾浴巾", /毛巾|浴巾|浴袍/], ["毯类", /毯|盖毯/], ["窗帘软装", /窗帘|抱枕|靠垫|软装|地毯/], ["餐桌厨房", /桌布|餐垫|餐桌|厨房/], ["酒店布草", /酒店|民宿|布草/], ["婴童用品", /婴|儿童|宝宝|护理/], ["面料与功能", /面料|凉感|防晒|阻燃|防水|功能/], ]; const PLATFORM_FAMILIES = ["Amazon", "Walmart", "TikTok Shop", "Temu", "Shopee", "Lazada", "Mercado Libre", "Ozon", "Wildberries", "Coupang", "乐天市场", "OTTO", "Cdiscount", "Etsy", "eBay", "bol.com", "Kaufland", "Shopify", "Alibaba.com", "SHEIN"]; const PLATFORM_LOGOS = { Amazon: "amazon", Walmart: "walmart", "TikTok Shop": "tiktok", Temu: "temu", Shopee: "shopee", Lazada: "lazada", "Mercado Libre": "mercado-libre", Ozon: "ozon", Wildberries: "wildberries", Coupang: "coupang", 乐天市场: "rakuten", OTTO: "otto", Cdiscount: "cdiscount", Etsy: "etsy", eBay: "ebay", "bol.com": "bol", Kaufland: "kaufland", Shopify: "shopify", "Alibaba.com": "alibaba", SHEIN: "shein", }; function platformFamily(value) { return PLATFORM_FAMILIES.find((family) => value === family || value.startsWith(`${family} `)) || value; } function makePlatformIdentity(name) { const identity = create("strong", "calendar-platform-identity"); const logoKey = PLATFORM_LOGOS[platformFamily(name)]; if (logoKey) { const logo = create("img", "calendar-platform-logo"); logo.src = `./assets/platform-logos/${logoKey}.svg`; logo.alt = ""; logo.width = 24; logo.height = 24; identity.appendChild(logo); } identity.appendChild(create("span", "", name)); return identity; } function calendarPlatformFamilies(item) { return [...new Set((item.platforms || []).map(platformFamily))]; } function create(tag, className = "", text = "") { const node = document.createElement(tag); if (className) node.className = className; if (text) node.textContent = text; return node; } function openLightbox(src, alt = "放大图片", { previewSrc = "" } = {}) { const previousFocus = document.activeElement; const overlay = create("div", "lightbox-overlay lightbox-brand-sansheng"); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-label", alt); const content = create("div", "lightbox-content-wrapper"); const stage = create("div", "lightbox-stage"); const close = create("button", "lightbox-close", "×"); close.type = "button"; close.setAttribute("aria-label", "关闭图片预览"); if (previewSrc) { const preview = create("img", "lightbox-preview"); preview.src = previewSrc; preview.alt = ""; preview.referrerPolicy = "no-referrer"; stage.appendChild(preview); } const loader = create("div", "lightbox-loader"); loader.setAttribute("role", "status"); loader.setAttribute("aria-live", "polite"); const logoWrap = create("span", "lightbox-loader-logo-wrap"); const logo = create("img", "lightbox-loader-logo"); logo.src = "./assets/logo.svg"; logo.alt = ""; logo.setAttribute("aria-hidden", "true"); logoWrap.appendChild(logo); const status = create("span", "lightbox-loader-status", "正在加载高清原图"); const retry = create("button", "lightbox-retry", "重新加载"); retry.type = "button"; retry.hidden = true; loader.append(logoWrap, status, retry); stage.appendChild(loader); const footer = create("div", "lightbox-footer"); const downloadLink = create("a", "lightbox-download"); downloadLink.href = src; downloadLink.target = "_blank"; downloadLink.rel = "noopener noreferrer"; downloadLink.download = ""; downloadLink.setAttribute("aria-label", `下载《${alt}》原图`); downloadLink.innerHTML = '下载原图'; downloadLink.addEventListener("click", async (event) => { event.preventDefault(); if (downloadLink.classList.contains("is-downloading")) return; const label = downloadLink.lastElementChild; downloadLink.classList.add("is-downloading"); downloadLink.setAttribute("aria-busy", "true"); label.textContent = "准备下载…"; try { const response = await fetch(src, { mode: "cors", credentials: "omit", referrerPolicy: "no-referrer" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blobUrl = URL.createObjectURL(await response.blob()); const save = create("a"); save.href = blobUrl; save.download = decodeURIComponent(new URL(src, document.baseURI).pathname.split("/").pop() || "image") .replace(/[\\/:*?"<>|]/g, "-"); document.body.appendChild(save); save.click(); save.remove(); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000); } catch (error) { console.error("原图下载失败,已打开原图地址", error); window.open(src, "_blank", "noopener,noreferrer"); } finally { downloadLink.classList.remove("is-downloading"); downloadLink.removeAttribute("aria-busy"); label.textContent = "下载原图"; } }); footer.appendChild(downloadLink); content.append(close, stage, footer); overlay.appendChild(content); document.body.appendChild(overlay); let loadId = 0; const loadOriginal = () => { const currentLoad = ++loadId; overlay.classList.remove("is-loaded", "is-error"); status.textContent = "正在加载高清原图"; retry.hidden = true; const image = create("img", "lightbox-img"); image.alt = alt; image.decoding = "async"; image.referrerPolicy = "no-referrer"; stage.insertBefore(image, loader); image.addEventListener("load", async () => { try { await image.decode(); } catch (error) { if (!image.naturalWidth) return; } if (currentLoad !== loadId || !overlay.isConnected) return; stage.querySelectorAll(".lightbox-img").forEach((node) => { if (node !== image) node.remove(); }); requestAnimationFrame(() => overlay.classList.add("is-loaded")); }, { once: true }); image.addEventListener("error", () => { if (currentLoad !== loadId || !overlay.isConnected) return; image.remove(); status.textContent = previewSrc ? "高清原图加载失败,当前显示预览图" : "高清原图加载失败"; retry.hidden = false; overlay.classList.add("is-error"); }, { once: true }); image.src = src; }; retry.addEventListener("click", loadOriginal); loadOriginal(); const closeLightbox = () => { document.removeEventListener("keydown", handleEscape); overlay.classList.remove("active"); window.setTimeout(() => { overlay.remove(); previousFocus?.focus?.(); }, 180); }; const handleEscape = (event) => { if (event.key === "Escape") closeLightbox(); }; overlay.addEventListener("click", (event) => { if (event.target === overlay || event.target === close) closeLightbox(); }); document.addEventListener("keydown", handleEscape); requestAnimationFrame(() => { overlay.classList.add("active"); close.focus(); }); } function sectionItems(data) { if (!data) return []; if (state.activeSection === "today") return Array.isArray(data.today) ? data.today : []; if (state.activeSection === "calendar") return Array.isArray(data.calendar_insights) ? data.calendar_insights : (data.calendar || []); if (state.activeSection === "standards") return [...(data.standards || []), ...trendItems(data)]; return Array.isArray(data[state.activeSection]) ? data[state.activeSection] : []; } function trendItems(data) { return Object.values(data?.trends?.categories || {}).flatMap((category) => category.items || []); } function normalized(value) { return String(value || "").trim().toLowerCase(); } function matchesFilters(item) { const itemRegions = [...(item.affected_regions || []), ...(item.affected_countries || [])]; if (state.region && item.region !== state.region && !itemRegions.includes(state.region)) return false; if (!state.query) return true; const haystack = normalized([ item.title, item.title_original, item.summary, item.source, item.region, item.opportunity, item.action, item.policy_type, item.what_it_is, (item.affected_markets || []).join(" "), (item.affected_countries || []).join(" "), (item.languages || []).join(" "), (item.platforms || []).join(" "), (item.product_suggestions || []).join(" "), (item.design_opportunities || []).join(" "), (item.event_focus || []).join(" "), (item.markets || []).join(" "), (item.fit_sellers || []).join(" "), item.cross_border, item.textile_category, item.category, item.category_key, item.activity_type, item.location, item.relevance, ].join(" ")); return haystack.includes(normalized(state.query)); } function formatDate(item) { const start = String(item.start_date || item.published_at || "").slice(0, 10); const end = String(item.end_date || "").slice(0, 10); if (start && end && start !== end) return `${start} 至 ${end}`; return start || "持续监测"; } function formatVerified(value) { const raw = String(value || ""); if (!raw) return ""; return raw.slice(0, 16).replace("T", " "); } function sectionOf(item) { if (item.section === "exhibition") return "exhibitions"; if (item.section === "policy") return "policies"; if (item.section === "standard") return "standards"; if (item.section === "activity") return "activities"; if (item.section === "platform") return "platforms"; return "calendar"; } function makeVisual(item) { const visual = create("div", "intelligence-card-visual"); if (item.image) { const img = create("img", "intelligence-card-image"); img.src = item.image; img.alt = item.title || "家纺情报配图"; img.loading = "lazy"; img.referrerPolicy = "no-referrer"; const usePoster = () => { visual.classList.add("is-poster"); img.remove(); }; img.addEventListener("load", () => { if (img.naturalWidth < 320 || img.naturalHeight < 180) usePoster(); }); img.addEventListener("error", usePoster); visual.appendChild(img); } else { visual.classList.add("is-poster"); } const poster = item.poster || {}; visual.style.setProperty("--poster-accent", poster.accent || "#0051d6"); visual.style.setProperty("--poster-ink", poster.ink || "#102a43"); visual.style.setProperty("--poster-paper", poster.paper || "#eef7ff"); const overlay = create("div", "intelligence-poster-copy"); overlay.append( create("span", "intelligence-poster-eyebrow", poster.eyebrow || SECTION_LABELS[item.section] || "家纺跨境情报"), create("strong", "intelligence-poster-title", item.title || "家纺情报"), create("span", "intelligence-poster-state.region", item.region || "全球"), ); visual.appendChild(overlay); return visual; } function makeBadge(text, className = "") { return create("span", `intelligence-badge ${className}`.trim(), text); } function makeExternalLink(label, href, className = "intelligence-source-link") { const link = create("a", className, label); link.href = href || "#"; link.target = "_blank"; link.rel = "noopener noreferrer nofollow"; if (/(?:wikipedia|wikimedia)\.org/i.test(href || "")) { link.classList.add("requires-cross-border-network"); link.title = "请注意:目标网址可能需要跨境网络才能访问"; link.appendChild(create("span", "external-network-badge", "需跨境网络")); } return link; } function sourceLinkLabel(item) { const authority = String(item.authority || ""); if (item.source_id === "calendar_tmall") return "打开规则检索入口"; if (authority.includes("结构化公共假日数据")) return "查看日期数据"; if (authority.includes("结构化宗教历法数据")) return "查看历法数据"; if (authority.includes("认证目录")) return "查看认证目录"; if (/官方|政府|主办方/.test(authority)) return "查看官方来源"; return TEXT.sourceLink; } function currentTranslationProvider() { return state.viewerCountryResolved ? translationProvider(state.viewerCountryCode) : ""; } function preferredTranslation(item, prefix = "") { const provider = currentTranslationProvider(); if (!provider) return { url: "", isFallback: false }; const key = (name) => prefix ? `${prefix}_${name}` : name; const googleUrl = item[key("translation_url")] || ""; const yandexUrl = item[key("translation_fallback_url")] || ""; const url = provider === "yandex" ? (yandexUrl || googleUrl) : (googleUrl || yandexUrl); return { url, isFallback: Boolean(url && url === yandexUrl) }; } function cultureReferenceUrl(item) { if (!state.viewerCountryResolved) return ""; return state.viewerCountryCode === "CN" ? (item.culture_reference_cn_url || item.culture_reference_url || "") : (item.culture_reference_url || ""); } function detectViewerCountry() { if (state.viewerCountryResolved) return Promise.resolve(state.viewerCountryCode); return detectVisitorCountry() .then((countryCode) => { state.viewerCountryCode = countryCode; state.viewerCountryResolved = true; state.activeIntelligenceRerender?.(); return state.viewerCountryCode; }) .catch(() => { state.viewerCountryResolved = true; state.activeIntelligenceRerender?.(); return ""; }); } function appendTranslationLinks(actions, item, className = "intelligence-source-link policy-translate") { if (!item.needs_translation) return; const translation = preferredTranslation(item); if (translation.url) actions.appendChild(makeExternalLink(TEXT.translatedVersion, translation.url, `${className}${translation.isFallback ? " is-fallback" : ""}`)); } function appendPrefixedTranslationLink(actions, item, prefix, className = "platform-action translate") { if (!item[`${prefix}_needs_translation`]) return; const translation = preferredTranslation(item, prefix); if (translation.url) actions.appendChild(makeExternalLink("辅助翻译入驻说明", translation.url, `${className}${translation.isFallback ? " is-fallback" : ""}`)); } function makePlatformActionGroup(label, url, item, prefix, className = "platform-action") { const group = create("span", "platform-action-group translation-action-group"); group.appendChild(makeExternalLink(label, url, className)); const actions = group; if (prefix === "seller") appendPrefixedTranslationLink(actions, item, "seller"); return group; } function itemShareUrl(item) { const section = sectionOf(item); return new URL(`/textile/intelligence/${section}/${item.id}/`, window.location.origin).href; } async function shareItem(item) { const shareData = { title: `${item.title}|家纺跨境情报`, text: item.summary || item.opportunity || "家纺跨境机会情报", url: itemShareUrl(item), }; try { if (navigator.share) { await navigator.share(shareData); return; } await navigator.clipboard.writeText(shareData.url); } catch (error) { if (error?.name !== "AbortError") console.error("[家纺跨境情报] 单条分享失败:", error); } } function makeShareButton(item) { const button = create("button", "intelligence-item-share is-tertiary"); button.type = "button"; button.setAttribute("aria-label", `分享${item.title}`); button.title = TEXT.shareItem; button.innerHTML = ''; button.addEventListener("click", () => shareItem(item)); return button; } function makeCard(item) { const article = create("article", `intelligence-card is-${sectionOf(item)}`); article.appendChild(makeVisual(item)); const body = create("div", "intelligence-card-body"); const meta = create("div", "intelligence-card-meta"); meta.appendChild(makeBadge(SECTION_LABELS[item.section] || "情报", `badge-${sectionOf(item)}`)); if (item.risk_level) meta.appendChild(makeBadge(`${item.risk_level}风险`, `risk-${item.risk_level}`)); if (item.impact) meta.appendChild(makeBadge(`${item.impact}影响`, `impact-${item.impact}`)); meta.appendChild(create("span", "intelligence-card-date", formatDate(item))); const title = create("h3", "intelligence-card-title", item.title || "未命名情报"); const summary = create("p", "intelligence-card-summary", item.summary || ""); body.append(meta, title); if (item.title_original && item.title_original !== item.title) { body.appendChild(create("p", "intelligence-card-original", item.title_original)); } if (summary.textContent) body.appendChild(summary); const relevance = create("div", "intelligence-card-relevance"); const affected = item.affected_countries || item.affected_markets || [item.region].filter(Boolean); const targets = item.product_suggestions || item.affected_products || item.event_focus || item.product_targets || []; relevance.append( create("span", "", affected.length ? `影响:${compactList(affected, 3)}` : `区域:${item.region || "全球"}`), create("span", "", targets.length ? `重点:${compactList(targets, 3)}` : "重点:打开详情核对适用商品"), ); body.appendChild(relevance); const action = item.today_action || item.next_action || item.opportunity || item.action || item.application; if (action) { const actionBox = create("div", "intelligence-card-action"); actionBox.append(create("span", "intelligence-card-action-label", "现在要做"), create("p", "", action)); body.appendChild(actionBox); } const footer = create("div", "intelligence-card-footer"); const proof = create("div", "intelligence-card-proof"); proof.append( create("strong", "", item.source || "权威来源"), create("span", "", `${item.authority || TEXT.verified} · ${formatVerified(item.verified_at)}`), ); const link = makeExternalLink(sourceLinkLabel(item), item.url || item.source_url); link.setAttribute("aria-label", `打开${item.source || "情报"}来源`); const footerActions = create("div", "intelligence-card-footer-actions"); footerActions.append(makeShareButton(item)); appendTranslationLinks(footerActions, item); footerActions.appendChild(link); footer.append(proof, footerActions); body.appendChild(footer); article.appendChild(body); return article; } function appendTags(container, values, className = "intelligence-tag") { (values || []).forEach((value) => container.appendChild(create("span", className, value))); } function formatMonthDay(value) { const raw = String(value || "").slice(0, 10); if (!raw) return { year: "", month: "", day: "", weekday: "" }; const parsed = new Date(`${raw}T00:00:00`); return { year: String(parsed.getFullYear()), month: `${parsed.getMonth() + 1}月`, day: String(parsed.getDate()).padStart(2, "0"), weekday: new Intl.DateTimeFormat("zh-CN", { weekday: "short" }).format(parsed), }; } function compactList(values, limit = 5) { const unique = [...new Set((values || []).filter(Boolean))]; if (unique.length <= limit) return unique.join("、") || "暂无专项信息"; return `${unique.slice(0, limit).join("、")} 等 ${unique.length} 项`; } function calendarProductCategories(item) { const haystack = (item.product_suggestions || []).join(" "); const matched = CALENDAR_PRODUCT_RULES.filter(([, pattern]) => pattern.test(haystack)).map(([label]) => label); return matched.length ? matched : ["其他产品"]; } function escapeIcs(value) { return String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;"); } function nextDateValue(value) { const parsed = new Date(`${value}T00:00:00Z`); parsed.setUTCDate(parsed.getUTCDate() + 1); return parsed.toISOString().slice(0, 10); } function makeCalendarDownloadButton(item) { const button = create("button", "intelligence-item-share calendar-download", TEXT.calendar.addCalendar); button.type = "button"; button.addEventListener("click", () => { try { const start = String(item.start_date || "").replace(/-/g, ""); const end = nextDateValue(item.end_date || item.start_date).replace(/-/g, ""); const description = `${item.what_it_is || item.summary || ""}\n备货窗口:${item.stocking_start || ""} 至 ${item.stocking_end || ""}\n行动:${item.next_action || item.opportunity || ""}`; const content = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//AI News Radar//Textile Intelligence//ZH-CN", "BEGIN:VEVENT", `UID:${escapeIcs(item.id)}@ai-news-radar`, `DTSTART;VALUE=DATE:${start}`, `DTEND;VALUE=DATE:${end}`, `SUMMARY:${escapeIcs(item.title)}`, `DESCRIPTION:${escapeIcs(description)}`, `URL:${escapeIcs(item.url || item.source_url || "")}`, "END:VEVENT", "END:VCALENDAR", ].join("\r\n"); const href = URL.createObjectURL(new Blob([content], { type: "text/calendar;charset=utf-8" })); const link = create("a"); link.href = href; link.download = `${item.start_date || "calendar"}-${String(item.title || "备货节点").replace(/[\\/:*?\"<>|]/g, "-")}.ics`; link.click(); URL.revokeObjectURL(href); } catch (error) { console.error("生成备货日历文件失败", error); } }); return button; } function calendarScopeText(item) { const details = item.country_details || []; if (!details.length) return "以官方来源公布范围为准"; const regional = details.filter((detail) => detail.scope && detail.scope !== "全国适用" && !detail.scope.includes("全境")); if (!regional.length) return `${details.length} 个国家/地区均为全国适用`; const nationalCount = details.length - regional.length; const regionalText = regional.map((detail) => `${detail.country}:${detail.scope}`).join(";"); return `${nationalCount ? `${nationalCount} 国全国适用;` : ""}${regionalText}`; } function makeCalendarFact(label, values) { const fact = create("div", "calendar-fact"); fact.append(create("span", "", label), create("strong", "", compactList(values))); return fact; } function makeLocalHolidayNames(item) { const details = (item.country_details || []) .filter((detail) => detail.local_name) .sort((a, b) => Number(b.country === state.calendarCountry) - Number(a.country === state.calendarCountry)); if (!details.length) return null; const section = create("details", "calendar-local-names"); const countryCount = new Set(details.map((detail) => detail.country_code || detail.country).filter(Boolean)).size || details.length; const summary = create("summary", "calendar-local-names-summary"); summary.append( create("span", "calendar-local-summary-name", item.title_english || item.title_original || item.title), create("span", "calendar-local-summary-count", `${countryCount} 个国家/地区`), ); section.appendChild(summary); const list = create("div", "calendar-local-name-list"); details.forEach((detail) => { const row = create("div", "calendar-local-name-row"); const country = create("strong", "calendar-country-name"); const flag = create("span", "calendar-country-flag", countryFlag(detail.country_code)); flag.setAttribute("aria-hidden", "true"); country.append(flag, create("span", "", `${detail.country || "目标市场"}${detail.country_english ? ` · ${detail.country_english}` : ""}`)); row.append( country, create("span", "calendar-local-name", detail.local_name), create("small", "", `${detail.local_language || compactList(detail.languages || [], 2)} · ${detail.scope || "以官方范围为准"}${(detail.dates || []).length ? ` · ${[...detail.dates].sort().join("、")}` : ""}`), ); list.appendChild(row); }); section.appendChild(list); return section; } function makeCalendarDateVariants(item) { const variants = item.date_variants || []; if (variants.length <= 1) return null; const section = create("section", "calendar-date-variants"); section.appendChild(create("span", "calendar-section-label", "各地日期差异")); if (item.date_variation_note) section.appendChild(create("p", "calendar-date-variant-note", item.date_variation_note)); const list = create("div", "calendar-date-variant-list"); variants.forEach((variant) => { const dateInfo = formatMonthDay(variant.date); const row = create("div", "calendar-date-variant-row"); row.append( create("strong", "", `${dateInfo.month}${Number(dateInfo.day)}日`), create("span", "", variant.label || "地区日期"), create("small", "", compactList(variant.countries || [], 4)), ); list.appendChild(row); }); section.appendChild(list); return section; } function countryFlag(code) { const value = String(code || "").toUpperCase(); return /^[A-Z]{2}$/.test(value) ? [...value].map((char) => String.fromCodePoint(127397 + char.charCodeAt(0))).join("") : "🌐"; } function isCalendarOpportunity(item, asOfDate = state.currentData?.as_of_date) { const stockingEnd = String(item.stocking_end || item.start_date || "").slice(0, 10); return ["high", "medium"].includes(item.business_relevance) && Boolean(item.stocking_start) && (item.product_suggestions || []).length > 0 && (item.affected_countries || []).length > 0 && (item.platforms || []).length > 0 && Boolean(stockingEnd) && stockingEnd >= String(asOfDate || "").slice(0, 10); } function makeCalendarCard(item) { const article = create("article", "calendar-event-card calendar-opportunity-card"); article.id = `calendar-event-${item.id}`; article.dataset.culture = item.culture_key || "central-europe"; article.dataset.eventType = item.event_type || "public-holiday"; article.dataset.relevance = item.business_relevance || "low"; const dateInfo = formatMonthDay(item.start_date); const dateBlock = create("div", "calendar-date-block"); dateBlock.append( create("span", "calendar-date-year", dateInfo.year), create("span", "calendar-date-month", dateInfo.month), create("strong", "calendar-date-day", dateInfo.day), create("span", "calendar-date-weekday", dateInfo.weekday), create("span", "calendar-culture-label", item.country_count > 1 ? `${item.country_count} 个国家/地区` : (item.region || "全球")), ); const content = create("div", "calendar-event-content"); const meta = create("div", "calendar-event-meta"); meta.append( makeBadge(item.decision_label || "经营观察", `calendar-decision is-${item.business_relevance || "low"}`), makeBadge(item.planning_stage || "规划期", "calendar-stage"), create("span", "calendar-event-state.region", item.market_group || item.region || "全球"), ); const title = create("h3", "calendar-event-title", item.title || "公共假日"); const localNames = makeLocalHolidayNames(item); const explanation = create("section", "calendar-explanation"); explanation.append(create("span", "calendar-section-label", TEXT.calendar.what), create("p", "", item.what_it_is || item.summary || "")); const facts = create("div", "calendar-fact-grid"); facts.append( makeCalendarFact(TEXT.calendar.markets, item.affected_countries || [item.region]), makeCalendarFact("适用范围", [calendarScopeText(item)]), makeCalendarFact(TEXT.calendar.languages, item.languages || []), makeCalendarFact(TEXT.calendar.platforms, item.platforms || []), ); const opportunity = create("section", "calendar-business-panel"); opportunity.append(create("span", "calendar-section-label", TEXT.calendar.opportunity)); const scenes = create("p", "calendar-consumption-scenes", compactList(item.consumption_scenes || [])); const products = create("div", "calendar-product-tags"); appendTags(products, item.product_suggestions || []); opportunity.append(scenes, products); const platformPanel = create("section", "calendar-platform-panel"); const platformRows = item.platform_actions || []; if (platformRows.length) { platformPanel.appendChild(create("span", "calendar-section-label", TEXT.calendar.platformActions)); platformRows.forEach((row) => { const action = create("div", "calendar-platform-action"); action.append(makePlatformIdentity(row.platform), create("p", "", row.action)); platformPanel.appendChild(action); }); } const guidance = create("div", "calendar-guidance-grid"); if ((item.design_opportunities || []).length) { const design = create("section", "calendar-guidance is-design"); design.append(create("span", "calendar-section-label", TEXT.calendar.design), create("p", "", compactList(item.design_opportunities, 3))); guidance.appendChild(design); } const actionBox = create("section", "calendar-action-box"); const windowTitle = create("div", "calendar-stock-window-title"); windowTitle.append(create("span", "", "建议备货窗口"), create("strong", "", `${item.stocking_start || ""} 至 ${item.stocking_end || ""}`)); const nextAction = create("div", "calendar-next-action"); nextAction.append(create("span", "calendar-section-label", TEXT.calendar.next), create("p", "", item.next_action || item.opportunity || "")); actionBox.append(windowTitle, nextAction); const footer = create("div", "calendar-event-footer"); const source = create("div", "calendar-event-source"); source.append( create("strong", "", `${TEXT.calendar.facts}:${item.source || "权威日期来源"}`), create("span", "", `${TEXT.calendar.analysis} · ${item.analysis_basis || "日期事实与经营建议分层展示"}`), ); const actions = create("div", "calendar-event-actions"); actions.append(makeCalendarDownloadButton(item), makeShareButton(item), makeExternalLink(sourceLinkLabel(item), item.url || item.source_url)); appendTranslationLinks(actions, item); const cultureUrl = cultureReferenceUrl(item); if (cultureUrl) actions.appendChild(makeExternalLink(TEXT.calendar.cultureReference, cultureUrl)); if (item.visual_reference_url) actions.appendChild(makeExternalLink(TEXT.calendar.visualReference, item.visual_reference_url)); footer.append(source, actions); content.append(meta, title); if (localNames) content.appendChild(localNames); const dateVariants = makeCalendarDateVariants(item); if (dateVariants) content.appendChild(dateVariants); content.append(explanation, facts, opportunity); if (platformPanel.childElementCount) content.appendChild(platformPanel); if (guidance.childElementCount) content.appendChild(guidance); const hasLiveAction = isCalendarOpportunity(item); if (hasLiveAction) content.appendChild(actionBox); content.appendChild(footer); article.append(dateBlock, content); return article; } function makeCalendarSelect(label, value, options, onChange) { const wrapper = create("label", "calendar-filter-field"); wrapper.appendChild(create("span", "", label)); const select = create("select", "calendar-filter-select"); options.forEach(([optionValue, optionLabel]) => { const option = create("option", "", optionLabel); option.value = optionValue; select.appendChild(option); }); select.value = value; select.addEventListener("change", (event) => onChange(event.target.value)); wrapper.appendChild(select); return wrapper; } function renderCalendarMonthGrid(year, month, items) { const section = create("section", "calendar-month-grid-wrap"); const heading = create("div", "calendar-month-grid-heading"); heading.append( create("h3", "", `${year}年${month}月`), create("span", "calendar-month-count", `本月 ${items.length} 条${state.calendarFocus === "opportunity" ? "备货机会" : "运营节点"}`), ); const grid = create("div", "calendar-month-grid"); ["一", "二", "三", "四", "五", "六", "日"].forEach((label) => grid.appendChild(create("span", "calendar-weekday", label))); const firstWeekday = (new Date(year, month - 1, 1).getDay() + 6) % 7; const days = new Date(year, month, 0).getDate(); for (let index = 0; index < firstWeekday; index += 1) grid.appendChild(create("span", "calendar-day is-empty")); for (let day = 1; day <= days; day += 1) { const cell = create("div", "calendar-day"); cell.appendChild(create("span", "calendar-day-number", String(day))); const date = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; items.flatMap((item) => { const variants = (item.date_variants || []).filter((variant) => variant.date === date); if (variants.length) return variants.map((variant) => ({ item, variant })); return String(item.start_date).slice(0, 10) === date ? [{ item, variant: null }] : []; }).forEach(({ item, variant }) => { const label = variant?.label === "平安夜" ? "平安夜" : item.title; const event = create("a", `calendar-day-event is-${item.business_relevance || "low"}`, label); event.href = `#calendar-event-${item.id}`; event.title = `${item.title} · ${compactList(item.affected_countries || [], 2)}`; cell.appendChild(event); }); grid.appendChild(cell); } section.append(heading, grid); return section; } function renderCalendar(data, container) { const sourceItems = Array.isArray(data.calendar_insights) ? data.calendar_insights : (data.calendar || []); const all = sourceItems.filter(matchesFilters); const defaultYear = Number(String(data.as_of_date || "").slice(0, 4)); const years = [...new Set(all.map((item) => Number(String(item.start_date || "").slice(0, 4))).filter(Boolean))].sort(); if (!state.calendarYear || !years.includes(state.calendarYear)) { state.calendarYear = years.includes(defaultYear) ? defaultYear : years[0]; state.calendarMonth = 0; } const planner = create("section", "calendar-planner"); const plannerCopy = create("div", "calendar-planner-copy"); plannerCopy.append( create("p", "calendar-planner-eyebrow", `${data.summary?.calendar_featured || 0} 个重点商机 · ${data.summary?.calendar_events || 0} 个官方日期节点`), create("h3", "", `${state.calendarYear} 市场备货决策日历`), create("p", "", "默认只看与销售、设计、入仓或运营有关的节点;同一节日跨国聚合,并明确区分官方事实与系统经营分析。"), ); const yearSwitch = create("div", "calendar-year-switch"); years.forEach((year) => { const button = create("button", `calendar-year-btn${state.calendarYear === year ? " active" : ""}`, String(year)); button.type = "button"; button.addEventListener("click", () => { state.calendarYear = year; state.calendarMonth = 0; state.renderList?.(data, container); }); yearSwitch.appendChild(button); }); planner.appendChild(plannerCopy); container.appendChild(planner); const marketGroups = [...new Set(all.flatMap((item) => item.affected_regions || []))].sort((a, b) => a.localeCompare(b, "zh-CN")); const marketMatched = all.filter((item) => !state.calendarMarketGroup || (item.affected_regions || []).includes(state.calendarMarketGroup)); const countries = [...new Set(marketMatched.flatMap((item) => item.country_details || []) .map((detail) => detail.country) .filter((value) => value && !/市场|全球/.test(value)))].sort((a, b) => a.localeCompare(b, "zh-CN")); const countryMatched = marketMatched.filter((item) => !state.calendarCountry || (item.affected_countries || []).includes(state.calendarCountry)); const productCategories = [...new Set(countryMatched.flatMap(calendarProductCategories))].sort((a, b) => a.localeCompare(b, "zh-CN")); const platforms = [...new Set(countryMatched.flatMap(calendarPlatformFamilies))].sort((a, b) => a.localeCompare(b, "zh-CN")); if (state.calendarCountry && !countries.includes(state.calendarCountry)) state.calendarCountry = ""; if (state.calendarProduct && !productCategories.includes(state.calendarProduct)) state.calendarProduct = ""; if (state.calendarPlatform && !platforms.includes(state.calendarPlatform)) state.calendarPlatform = ""; const decisionToolbar = create("section", "calendar-decision-toolbar"); const filterRow = create("div", "calendar-filter-row"); const modeRow = create("div", "calendar-mode-row"); const scopeSwitch = create("div", "calendar-scope-switch"); [["opportunity", TEXT.calendar.important], ["all", TEXT.calendar.all]].forEach(([key, label]) => { const button = create("button", `calendar-scope-btn${state.calendarFocus === key ? " active" : ""}`, label); button.type = "button"; button.addEventListener("click", () => { state.calendarFocus = key; state.renderList?.(data, container); }); scopeSwitch.appendChild(button); }); filterRow.append( makeCalendarSelect("市场区域", state.calendarMarketGroup, [["", TEXT.calendar.allMarkets], ...marketGroups.map((value) => [value, value])], (value) => { state.calendarMarketGroup = value; state.calendarCountry = ""; state.renderList?.(data, container); }), makeCalendarSelect("国家/地区", state.calendarCountry, [["", TEXT.calendar.allCountries], ...countries.map((value) => [value, value])], (value) => { state.calendarCountry = value; state.renderList?.(data, container); }), makeCalendarSelect("产品方向", state.calendarProduct, [["", TEXT.calendar.allProducts], ...productCategories.map((value) => [value, value])], (value) => { state.calendarProduct = value; state.renderList?.(data, container); }), makeCalendarSelect("销售平台(经营动作)", state.calendarPlatform, [["", TEXT.calendar.allPlatforms], ...platforms.map((value) => [value, value])], (value) => { state.calendarPlatform = value; state.renderList?.(data, container); }), ); modeRow.append(scopeSwitch, yearSwitch); decisionToolbar.append(filterRow, modeRow); decisionToolbar.appendChild(create("p", "calendar-platform-filter-hint", TEXT.calendar.platformFilterHint)); container.appendChild(decisionToolbar); const selectedMarketItems = all.filter((item) => { if (state.calendarMarketGroup && !(item.affected_regions || []).includes(state.calendarMarketGroup)) return false; if (state.calendarCountry && !(item.affected_countries || []).includes(state.calendarCountry)) return false; if (state.calendarProduct && !calendarProductCategories(item).includes(state.calendarProduct)) return false; if (state.calendarPlatform && !calendarPlatformFamilies(item).includes(state.calendarPlatform)) return false; if (state.calendarFocus === "opportunity" && !isCalendarOpportunity(item, data.as_of_date)) return false; return true; }); const yearItems = selectedMarketItems.filter((item) => Number(String(item.start_date || "").slice(0, 4)) === state.calendarYear); const monthCounts = new Map(); yearItems.forEach((item) => { const month = Number(String(item.start_date || "").slice(5, 7)); monthCounts.set(month, (monthCounts.get(month) || 0) + 1); }); const monthRail = create("div", "calendar-month-rail"); const allButton = create("button", `calendar-month-btn${state.calendarMonth === 0 ? " active" : ""}`); allButton.append(create("strong", "", "全年"), create("span", "calendar-month-count", `${yearItems.length} 条`)); allButton.type = "button"; allButton.addEventListener("click", () => { state.calendarMonth = 0; state.renderList?.(data, container); }); monthRail.appendChild(allButton); for (let month = 1; month <= 12; month += 1) { if (!monthCounts.has(month)) continue; const button = create("button", `calendar-month-btn${state.calendarMonth === month ? " active" : ""}`); button.append(create("strong", "", `${month}月`), create("span", "calendar-month-count", `${monthCounts.get(month)} 条`)); button.type = "button"; button.addEventListener("click", () => { state.calendarMonth = month; state.renderList?.(data, container); }); monthRail.appendChild(button); } container.appendChild(monthRail); const highlightPriority = [ "back-to-school", "singles-day", "black-friday", "cyber-monday", "winter-holiday", "christmas", "lunar-new-year", "eid-fitr", "super-bowl", "la28", ]; const highValueItems = yearItems .filter((item) => item.business_relevance === "high" && isCalendarOpportunity(item, data.as_of_date)) .sort((a, b) => String(a.start_date).localeCompare(String(b.start_date))); const selected = yearItems .filter((item) => !state.calendarMonth || Number(String(item.start_date || "").slice(5, 7)) === state.calendarMonth) .sort((a, b) => String(a.start_date).localeCompare(String(b.start_date)) || Number(b.priority_score || 0) - Number(a.priority_score || 0)); if (state.calendarMonth) container.appendChild(renderCalendarMonthGrid(state.calendarYear, state.calendarMonth, selected)); const highlightPool = state.calendarMonth ? highValueItems.filter((item) => Number(String(item.start_date).slice(5, 7)) === state.calendarMonth) : highValueItems; const highlights = [ ...highlightPriority.map((key) => highlightPool.find((item) => item.festival_key === key)).filter(Boolean), ...highlightPool, ].filter((item, index, rows) => rows.findIndex((row) => row.id === item.id) === index); if (highlights.length) { const highlightRail = create("section", "calendar-highlight-rail"); highlightRail.appendChild(create("strong", "", state.calendarMonth ? `${state.calendarMonth}月重点消费与备货节点` : "全年关键消费与备货节点")); const links = create("div", "calendar-highlight-links"); highlights.forEach((item) => { const button = create("button", "calendar-highlight-button"); button.type = "button"; button.append(create("span", "", String(item.start_date).slice(5).replace("-", "/")), create("b", "", item.title)); button.addEventListener("click", () => { state.calendarMonth = Number(String(item.start_date).slice(5, 7)); state.renderList?.(data, container); }); links.appendChild(button); }); highlightRail.appendChild(links); container.appendChild(highlightRail); } if (!selected.length) { container.appendChild(create("div", "intelligence-empty", "当前市场和月份没有重点备货商机,可切换为“全部节点”查看运营假期。")); return; } const visible = selected; const groups = new Map(); visible.forEach((item) => { const month = Number(String(item.start_date || "").slice(5, 7)); if (!groups.has(month)) groups.set(month, []); groups.get(month).push(item); }); groups.forEach((items, month) => { const section = create("section", "calendar-month-section"); const heading = create("div", "calendar-month-heading"); heading.append(create("h3", "", `${month}月`), create("span", "", `${items.length} 个${state.calendarFocus === "opportunity" ? "精准机会" : "节点"}`)); const grid = create("div", "calendar-event-grid is-insights"); items.forEach((item) => grid.appendChild(makeCalendarCard(item))); section.append(heading, grid); container.appendChild(section); }); } function makePlatformCard(item) { const card = create("article", "platform-card"); const header = create("div", "platform-card-header"); const logoBox = create("div", "platform-logo-box"); const logo = create("img", "platform-logo"); logo.src = `./assets/platform-logos/${item.logo}.svg`; logo.alt = `${item.name} 标志`; logo.width = 112; logo.height = 48; logoBox.appendChild(logo); const title = create("div", "platform-title"); title.append(create("span", "", `${item.region} · ${item.type}`), create("h3", "", item.name)); header.append(logoBox, title, makeBadge(`${item.access_level} ${item.access_label}`, `platform-access is-${String(item.access_level).toLowerCase()}`)); const markets = create("div", "platform-markets"); appendTags(markets, item.markets || []); const fit = create("div", "platform-fit"); fit.append(create("strong", "", "适合谁"), create("p", "", compactList(item.fit_sellers || []))); const category = create("div", "platform-category"); category.append(create("strong", "", "家纺经营重点"), create("p", "", item.textile_category || "打开平台类目核对。")); const access = create("div", "platform-access-note"); access.append(create("strong", "", "入驻判断"), create("p", "", item.cross_border || "以平台当前规则为准。")); const actions = create("div", "platform-actions"); if (item.homepage_url && item.homepage_url !== item.seller_url) actions.appendChild(makePlatformActionGroup("打开平台官网", item.homepage_url, item, "homepage")); actions.appendChild(makePlatformActionGroup("去官方入驻", item.seller_url, item, "seller", "platform-action primary")); if (item.language_url) actions.appendChild(makeExternalLink("官网学习中心(可选中文)", item.language_url, "platform-action language")); const proof = create("p", "platform-proof", `${item.confidence} · ${item.link_status || "待人工复核"} · 核验 ${String(item.checked_at || "").slice(0, 10)}`); card.append(header, markets, category, access, fit, actions, proof); return card; } function renderPlatforms(data, container) { const all = (data.platforms || []).filter(matchesFilters); const toolbar = create("section", "platform-filter-toolbar"); const makeField = (label, control, extraClass = "") => { const field = create("label", `platform-filter-field ${extraClass}`.trim()); field.append(create("span", "", label), control); return field; }; const makeSelect = (placeholder, values, current) => { const select = create("select", "platform-filter-control"); select.appendChild(Object.assign(create("option", "", placeholder), { value: "" })); values.forEach((value) => select.appendChild(Object.assign(create("option", "", value), { value }))); select.value = current; return select; }; const markets = [...new Set(all.flatMap((item) => item.markets || []))].sort((a, b) => a.localeCompare(b, "zh-CN")); const types = [...new Set(all.map((item) => item.type).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN")); const marketSelect = makeSelect("全部市场", markets, state.platformMarket); const typeSelect = makeSelect("全部类型", types, state.platformType); const accessSelect = makeSelect("全部开放等级", ["A", "B", "C"].filter((level) => all.some((item) => item.access_level === level)), state.platformAccess); const search = create("input", "platform-filter-control"); search.type = "search"; search.name = "platform-channel-search"; search.autocomplete = "off"; search.placeholder = "搜索平台名称、国家或家纺类目…"; search.value = state.platformSearch; toolbar.append( makeField("目标市场", marketSelect), makeField("渠道类型", typeSelect), makeField("开放等级", accessSelect), makeField("平台检索", search, "platform-filter-search"), ); const header = create("section", "scene-summary platform-summary"); const grid = create("div", "platform-grid"); const apply = () => { const keyword = normalized(state.platformSearch); const selected = all.filter((item) => { if (state.platformMarket && !(item.markets || []).includes(state.platformMarket)) return false; if (state.platformType && item.type !== state.platformType) return false; if (state.platformAccess && item.access_level !== state.platformAccess) return false; if (!keyword) return true; return normalized([item.name, item.region, item.type, item.textile_category, item.cross_border, ...(item.markets || []), ...(item.fit_sellers || [])].join(" ")).includes(keyword); }).sort((a, b) => (Number(b.home_textile_sales_score || 0) + Number(b.emergence_score || 0)) - (Number(a.home_textile_sales_score || 0) + Number(a.emergence_score || 0)) || String(a.access_level).localeCompare(String(b.access_level)) || a.name.localeCompare(b.name, "zh-CN")); header.innerHTML = ""; header.append(create("strong", "", `${selected.length}/${all.length} 个家纺销售渠道`), create("span", "", "准入条件以平台卖家中心为准")); grid.innerHTML = ""; selected.forEach((item) => grid.appendChild(makePlatformCard(item))); if (!selected.length) grid.appendChild(create("div", "intelligence-empty", "当前筛选条件下没有已核验平台,请调整市场或开放等级。")); }; marketSelect.addEventListener("change", () => { state.platformMarket = marketSelect.value; apply(); }); typeSelect.addEventListener("change", () => { state.platformType = typeSelect.value; apply(); }); accessSelect.addEventListener("change", () => { state.platformAccess = accessSelect.value; apply(); }); search.addEventListener("input", () => { state.platformSearch = search.value; apply(); }); container.append(toolbar, header, grid); apply(); } export { TEXT, create, openLightbox, sectionItems, trendItems, normalized, matchesFilters, formatDate, formatVerified, sectionOf, makeVisual, makeBadge, makeExternalLink, sourceLinkLabel, currentTranslationProvider, preferredTranslation, detectViewerCountry, cultureReferenceUrl, appendTranslationLinks, appendPrefixedTranslationLink, makePlatformActionGroup, itemShareUrl, makeShareButton, makeCard, appendTags, formatMonthDay, compactList, calendarProductCategories, escapeIcs, nextDateValue, makeCalendarDownloadButton, calendarScopeText, makeCalendarFact, makeLocalHolidayNames, makeCalendarDateVariants, countryFlag, isCalendarOpportunity, makeCalendarCard, makeCalendarSelect, renderCalendarMonthGrid, renderCalendar, makePlatformCard, renderPlatforms };