feat: 发布三笙AI情报局独立版公司研发基线

同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。

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

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

验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
This commit is contained in:
Jack
2026-08-14 19:26:39 +08:00
commit f1ce583fd1
231 changed files with 245559 additions and 0 deletions
@@ -0,0 +1,169 @@
/** 跨境活动独立渲染器,避免继续膨胀情报主渲染文件。 */
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();
}