同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。 补充面向 C# 研发的 README,明确当前无 LLM、多模态与生图模型,记录翻译请求参数、缓存与失败策略、数据处理边界、前端职责和无 GitHub Pages 部署流程。 新增按日期倒序维护的 CHANGELOG,记录影响、验证与代码/数据/dist 回滚方法。 验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
170 lines
8.4 KiB
JavaScript
170 lines
8.4 KiB
JavaScript
/** 跨境活动独立渲染器,避免继续膨胀情报主渲染文件。 */
|
||
|
||
let activityFocus = "priority";
|
||
let activityRegion = "";
|
||
let activityPlatform = "";
|
||
let activityFormat = "";
|
||
|
||
export function activityDayOffset(startDate, now = new Date()) {
|
||
const [year, month, day] = String(startDate || "").slice(0, 10).split("-").map(Number);
|
||
const eventDay = Date.UTC(year, month - 1, day);
|
||
const currentDay = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
|
||
return Math.round((eventDay - currentDay) / 86400000);
|
||
}
|
||
|
||
function create(tag, className = "", text = "") {
|
||
const node = document.createElement(tag);
|
||
if (className) node.className = className;
|
||
if (text) node.textContent = text;
|
||
return node;
|
||
}
|
||
|
||
function link(label, href, className = "activity-action") {
|
||
const node = create("a", className, label);
|
||
node.href = href;
|
||
node.target = "_blank";
|
||
node.rel = "noopener noreferrer nofollow";
|
||
return node;
|
||
}
|
||
|
||
function selectControl(label, placeholder, values, value) {
|
||
const field = create("label", "activity-filter-field");
|
||
field.appendChild(create("span", "", label));
|
||
const select = create("select", "activity-filter-control");
|
||
const all = create("option", "", placeholder);
|
||
all.value = "";
|
||
select.appendChild(all);
|
||
values.forEach((item) => {
|
||
const option = create("option", "", item);
|
||
option.value = item;
|
||
select.appendChild(option);
|
||
});
|
||
select.value = value;
|
||
field.appendChild(select);
|
||
return { field, select };
|
||
}
|
||
|
||
function activityCard(item, translationProvider) {
|
||
const card = create("article", "activity-card");
|
||
const visual = create("div", "activity-card-visual");
|
||
const poster = create("div", "activity-generated-poster");
|
||
poster.append(
|
||
create("span", "activity-poster-source", item.source || item.authority || "官方活动"),
|
||
create("strong", "", item.title),
|
||
create("small", "", `${item.region || "全球"} · ${item.activity_type || "跨境活动"}`),
|
||
);
|
||
visual.appendChild(poster);
|
||
if (item.image) {
|
||
const image = create("img", "activity-card-image");
|
||
image.src = item.image;
|
||
image.alt = `${item.title} 活动海报`;
|
||
image.width = 640;
|
||
image.height = 360;
|
||
image.loading = "lazy";
|
||
image.referrerPolicy = "no-referrer";
|
||
image.addEventListener("error", () => image.remove());
|
||
visual.appendChild(image);
|
||
}
|
||
const dateBlock = create("div", "activity-date-block");
|
||
const daysUntil = activityDayOffset(item.start_date);
|
||
const eventDate = new Date(`${item.start_date}T00:00:00`);
|
||
const monthLabel = new Intl.DateTimeFormat("zh-CN", { month: "long" }).format(eventDate);
|
||
const dayLabel = new Intl.DateTimeFormat("zh-CN", { day: "numeric" }).format(eventDate).replace(/日$/, "");
|
||
const countdownLabel = daysUntil === 0 ? "今天" : `${daysUntil} 天后`;
|
||
dateBlock.setAttribute("aria-label", `${item.start_date},${countdownLabel}`);
|
||
dateBlock.append(
|
||
create("span", "activity-date-month", monthLabel),
|
||
create("strong", "activity-date-day", dayLabel),
|
||
create("span", "activity-date-countdown", countdownLabel),
|
||
);
|
||
visual.appendChild(dateBlock);
|
||
|
||
const body = create("div", "activity-card-body");
|
||
const meta = create("div", "activity-card-meta");
|
||
meta.append(
|
||
create("span", `activity-source-level${item.official_activity ? " is-official" : ""}`, item.source_level),
|
||
create("span", "", item.activity_type),
|
||
create("span", "", item.format),
|
||
);
|
||
body.append(meta, create("h3", "", item.title));
|
||
if (item.title_original) body.appendChild(create("p", "activity-title-original", item.title_original));
|
||
body.appendChild(create("p", "activity-location", `${item.start_date} · ${item.location || item.region}`));
|
||
const tags = create("div", "activity-platforms");
|
||
(item.platforms || []).forEach((platform) => tags.appendChild(create("span", "", platform)));
|
||
if (tags.children.length) body.appendChild(tags);
|
||
body.append(
|
||
create("p", "activity-summary", item.summary),
|
||
create("p", "activity-next-action", item.next_action),
|
||
);
|
||
const actions = create("div", "activity-actions");
|
||
const registrationUrl = item.registration_url || item.url;
|
||
if (registrationUrl) actions.appendChild(link(item.format === "线上" ? "查看并报名" : "查看地点并报名", registrationUrl, "activity-action primary"));
|
||
const sourceUrl = item.source_url || "";
|
||
if (sourceUrl && sourceUrl !== registrationUrl && sourceUrl !== item.url) actions.appendChild(link("查看来源页", sourceUrl));
|
||
const googleUrl = item.translation_url;
|
||
const yandexUrl = item.translation_fallback_url;
|
||
const translationUrl = translationProvider === "yandex" ? (yandexUrl || googleUrl) : translationProvider === "google" ? (googleUrl || yandexUrl) : "";
|
||
if (translationUrl) actions.appendChild(link("查看译文版", translationUrl, `activity-action translate${translationUrl === yandexUrl ? " is-fallback" : ""}`));
|
||
body.append(actions, create("p", "activity-proof", `${item.source} · ${item.authority} · 核验 ${String(item.verified_at || "").slice(0, 10)}`));
|
||
card.append(visual, body);
|
||
return card;
|
||
}
|
||
|
||
export function renderActivities(items, container, translationProvider = "") {
|
||
const sourceItems = Array.isArray(items) ? items : [];
|
||
const currentItems = sourceItems.filter((item) => activityDayOffset(item.start_date) >= 0);
|
||
const toolbar = create("section", "activity-filter-toolbar");
|
||
const focus = create("div", "activity-focus-switch");
|
||
[
|
||
["priority", "优先参加"],
|
||
["direct", "家纺直接相关"],
|
||
["must", "跨境必看"],
|
||
["all", "全部活动"],
|
||
].forEach(([value, label]) => {
|
||
const button = create("button", `activity-focus-btn${activityFocus === value ? " active" : ""}`, label);
|
||
button.type = "button";
|
||
button.addEventListener("click", () => {
|
||
activityFocus = value;
|
||
render();
|
||
});
|
||
focus.appendChild(button);
|
||
});
|
||
const regions = [...new Set(currentItems.map((item) => item.region).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||
const platforms = [...new Set(currentItems.flatMap((item) => item.platforms || []))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||
const regionField = selectControl("目标地区", "全部地区", regions, activityRegion);
|
||
const platformField = selectControl("相关平台", "全部平台", platforms, activityPlatform);
|
||
const formatField = selectControl("参加方式", "线上和线下", ["线上", "线下"], activityFormat);
|
||
const fields = create("div", "activity-filter-fields");
|
||
fields.append(regionField.field, platformField.field, formatField.field);
|
||
toolbar.append(focus, fields);
|
||
const summary = create("section", "scene-summary activity-summary-bar");
|
||
const grid = create("div", "activity-grid");
|
||
|
||
const render = () => {
|
||
focus.querySelectorAll?.("button").forEach?.((button, index) => {
|
||
button.classList.toggle("active", ["priority", "direct", "must", "all"][index] === activityFocus);
|
||
});
|
||
const selected = currentItems.filter((item) => {
|
||
if (activityFocus === "priority" && !["direct", "must"].includes(item.relevance)) return false;
|
||
if (["direct", "must"].includes(activityFocus) && item.relevance !== activityFocus) return false;
|
||
if (activityRegion && item.region !== activityRegion) return false;
|
||
if (activityPlatform && !(item.platforms || []).includes(activityPlatform)) return false;
|
||
if (activityFormat && item.format !== activityFormat) return false;
|
||
return true;
|
||
}).sort((a, b) => String(a.start_date).localeCompare(String(b.start_date)) || Number(b.official_activity) - Number(a.official_activity));
|
||
summary.innerHTML = "";
|
||
summary.append(
|
||
create("strong", "", `${selected.length}/${currentItems.length} 场可参加活动`),
|
||
create("span", "", "官方活动优先;行业媒体用于发现,报名与议程以主办方页面为准"),
|
||
);
|
||
grid.innerHTML = "";
|
||
selected.forEach((item) => grid.appendChild(activityCard(item, translationProvider)));
|
||
if (!selected.length) grid.appendChild(create("div", "intelligence-empty", "当前筛选下没有可参加活动,请调整地区、平台或活动类型。"));
|
||
};
|
||
regionField.select.addEventListener("change", () => { activityRegion = regionField.select.value; render(); });
|
||
platformField.select.addEventListener("change", () => { activityPlatform = platformField.select.value; render(); });
|
||
formatField.select.addEventListener("change", () => { activityFormat = formatField.select.value; render(); });
|
||
container.append(toolbar, summary, grid);
|
||
render();
|
||
}
|