Files
Sansheng-Intelligence/public/assets/js/render-intelligence-activities.js
Jack f1ce583fd1 feat: 发布三笙AI情报局独立版公司研发基线
同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。

补充面向 C# 研发的 README,明确当前无 LLM、多模态与生图模型,记录翻译请求参数、缓存与失败策略、数据处理边界、前端职责和无 GitHub Pages 部署流程。

新增按日期倒序维护的 CHANGELOG,记录影响、验证与代码/数据/dist 回滚方法。

验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
2026-08-14 19:26:39 +08:00

170 lines
8.4 KiB
JavaScript
Raw Permalink 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.
/** 跨境活动独立渲染器,避免继续膨胀情报主渲染文件。 */
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();
}