Files
Sansheng-Intelligence/public/assets/js/render-intelligence-core.js
T

1023 lines
46 KiB
JavaScript
Raw Normal View History

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 };