Files
Sansheng-Intelligence/public/assets/js/render-intelligence-core.js
T
Jack b1b7c13c97 fix: 按UTC日期优化独立站日历筛选
2026-08-15:日历默认视图按当前 UTC 日期过滤与排序,未来和当天节点优先,已过节点降权;显式月份仍保留完整历史。日历页隐藏重复目标区域筛选,平台渲染模块独立拆分,更新静态资源版本号与测试契约,避免 Gitea/IIS/Nginx 部署命中旧缓存。
2026-08-15 19:17:16 +08:00

1023 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '<span aria-hidden="true">↓</span><span>下载原图</span>';
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 = '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3v11m0-11 4 4m-4-4L8 7M5 11v8h14v-8"/></svg>';
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),
};
}
export function calendarTodayIso(now = new Date()) {
return now.toISOString().slice(0, 10);
}
export function sortCalendarItems(items, now = new Date()) {
const today = calendarTodayIso(now);
return [...items].sort((a, b) => {
const dateA = String(a.start_date || "").slice(0, 10);
const dateB = String(b.start_date || "").slice(0, 10);
const pastOrder = Number(dateA < today) - Number(dateB < today);
if (pastOrder) return pastOrder;
const dateOrder = dateA < today && dateB < today ? dateB.localeCompare(dateA) : dateA.localeCompare(dateB);
return dateOrder || Number(b.priority_score || 0) - Number(a.priority_score || 0);
});
}
export function calendarDefaultItems(items, year, now = new Date()) {
const today = calendarTodayIso(now);
if (Number(year) !== Number(today.slice(0, 4))) return [...items];
return items.filter((item) => String(item.start_date || "").slice(0, 10) >= today);
}
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 today = calendarTodayIso();
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, today)) return false;
return true;
});
const yearItems = selectedMarketItems.filter((item) => Number(String(item.start_date || "").slice(0, 4)) === state.calendarYear);
const displayYearItems = state.calendarMonth ? yearItems : calendarDefaultItems(yearItems, state.calendarYear);
const monthCounts = new Map();
displayYearItems.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", `${displayYearItems.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 = displayYearItems
.filter((item) => item.business_relevance === "high" && isCalendarOpportunity(item, today));
const selected = sortCalendarItems(displayYearItems
.filter((item) => !state.calendarMonth || Number(String(item.start_date || "").slice(5, 7)) === state.calendarMonth)
, new Date());
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);
});
}
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 };