Compare commits
2
Commits
7e74bd989e
...
7af253f081
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af253f081 | ||
|
|
b1b7c13c97 |
@@ -1,5 +1,34 @@
|
||||
# 更新日志
|
||||
|
||||
## 2026-08-17
|
||||
|
||||
### 日历数据规则与旧快照修复
|
||||
|
||||
- 按当前 UTC 日期重建公开日历快照,过滤已结束事件并保留未来事件的可用排序。
|
||||
- 将元旦/新年假期、开斋节、宰牲节和农历新年按年度合并,卡内保留不同国家的日期差异。
|
||||
- 修正日本 `Coming of Age Day`、`Greenery Day`、印度尼西亚独立日和菲律宾国家英雄日的中文标题;阻止“国家纪念与公共假日”等泛称跨国误合并。
|
||||
- 中国市场节日不再生成无意义的机器翻译入口;百度百科元旦链接固定指向“元旦”;Bing 图片查询收敛为标准节日名、国家和 `holiday`。
|
||||
- 同步更新 Nager 日历翻译字段、公开 JSON 快照和回归测试,代码与数据应作为同一提交回滚。
|
||||
|
||||
### 验证与恢复
|
||||
|
||||
- Python 合同测试 11 项、Node 前端测试 14 项通过。
|
||||
- `docs/CODEX_TASK_STATE.md` 固化任务目标、验收标准和回滚方式,避免服务中断后丢失上下文。
|
||||
|
||||
## 2026-08-15
|
||||
|
||||
### 日历实用性与筛选交互
|
||||
|
||||
- 日历排序统一使用当前 UTC 日期:未来与当天项目按日期升序展示,已过项目降权到当月未来项目之后,并在已过项目之间按日期倒序排列。
|
||||
- 当前年份的默认日历视图不再把今天之前的节日放入首屏月份统计;用户主动选择某个月时仍可查看该月完整历史记录。
|
||||
- “目标区域”在日历页直接隐藏,避免与下方市场区域、国家/地区、产品方向和销售平台筛选重复;离开日历页后自动恢复。
|
||||
- 删除主站校验时间行里的旧式箭头分享控件对应逻辑;独立站保留正常的顶部分享按钮。同步更新独立站静态资源版本号,避免部署命中旧缓存。
|
||||
|
||||
### 验证
|
||||
|
||||
- 独立站 Node 前端测试 14 项、Python 合同测试 11 项通过。
|
||||
- 独立站静态构建成功;390×844 移动端检查通过,无控制台错误,日历首条为 2026-08-15。
|
||||
|
||||
本文件按日期倒序记录独立版的代码、数据、交互和部署变化。每次提交必须先更新本文件,确保公司研发人员可以判断影响范围并使用 `git revert <commit>` 回滚。
|
||||
|
||||
## 2026-08-14 - 平台渠道入口与数据快照对齐
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
## 当前基线
|
||||
|
||||
当前提交数据以 `data/*.json` 为准,业务基准日为 **2026-08-14**:
|
||||
当前提交数据以 `data/*.json` 为准,业务基准日为 **2026-08-17**:
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| 严格权威源 | 59/59 |
|
||||
| 日历原始事件 / 聚合机会 | 1270 / 673 |
|
||||
| 日历原始事件 / 聚合机会 | 1261 / 640 |
|
||||
| 今日必看 | 24 |
|
||||
| 展会 | 79 |
|
||||
| 政策与召回 | 48 |
|
||||
|
||||
+15273
-20809
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
# 任务状态:日历数据规则与旧快照修复
|
||||
|
||||
更新时间:2026-08-17(Asia/Shanghai)
|
||||
|
||||
## 目标
|
||||
|
||||
- 日历按当前 UTC 日期过滤和排序,已过日期降权,不让过期节日占据默认首屏。
|
||||
- 合并同一节日的跨天、跨国家记录;保留各国日期差异,不把连续假期拆成重复卡片。
|
||||
- 清理国家纪念类泛称、错误中文译名和失效百度百科链接。
|
||||
- 中国市场记录不生成无意义的机器翻译入口;图片查询只使用节日标准名、国家和 `holiday` 短查询词。
|
||||
- 独立站与主站分别维护、分别测试、分别提交和推送。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 独立站 Python 11 项、Node 前端 14 项测试通过。
|
||||
- 2027 年元旦/新年假期合并为一张卡,至少覆盖 1 月 1 日至 1 月 8 日,并包含中国与俄罗斯语境。
|
||||
- 2027 年宰牲节合并为一张连续假期卡;日本 `Coming of Age Day`、`Greenery Day` 显示中文规范名。
|
||||
- 不输出带英文副标题的“国家纪念与公共假日”等泛称卡片。
|
||||
|
||||
## 回滚
|
||||
|
||||
本次代码、测试和公开 JSON 必须作为同一提交回滚:`git revert <commit>`,随后重新构建静态目录。不要把父目录工作树的改动提交到本仓库。
|
||||
@@ -67,21 +67,6 @@
|
||||
.intelligence-overview-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
|
||||
#intelligenceUpdatedAt { color: var(--muted); font-size: 12px; }
|
||||
|
||||
.intelligence-share-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.intelligence-share-btn:hover { border-color: var(--intel-brand-blue); color: var(--intel-brand-blue); }
|
||||
|
||||
.intelligence-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
@@ -608,9 +593,8 @@
|
||||
.intelligence-overview p:last-child { color: var(--ink-secondary); font-size: 16px; line-height: 1.65; }
|
||||
.intelligence-overview-actions { align-items: center; }
|
||||
.intelligence-overview-actions > span { color: var(--ink-secondary); font-size: 13px; }
|
||||
.intelligence-share-btn { display: inline-grid; width: 44px; min-width: 44px; height: 44px; padding: 0; place-items: center; border: 1px solid var(--line); background: transparent; color: var(--muted); font-size: 20px; }
|
||||
.intelligence-share-btn:hover, .intelligence-share-btn:focus-visible { border-color: var(--ink-secondary); background: var(--trans-bg); color: var(--ink); }
|
||||
.intelligence-search-field { display: grid; gap: 6px; min-width: 0; color: var(--ink-secondary); font-size: 14px; font-weight: 700; }
|
||||
.intelligence-search-field[hidden] { display: none !important; }
|
||||
.intelligence-toolbar input, .intelligence-toolbar select { min-height: 48px; font-size: 16px; }
|
||||
.intelligence-tab { min-height: 48px; font-size: 16px; }
|
||||
.intelligence-tab-count, .intelligence-badge { font-size: 13px; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getIntelligenceSection, renderIntelligence, setIntelligenceSection, shareIntelligence } from "./render-intelligence.js?v=20260814o";
|
||||
import { getIntelligenceSection, renderIntelligence, setIntelligenceSection, shareIntelligence } from "./render-intelligence.js?v=20260815a";
|
||||
|
||||
const DATA_ROOT = "./data";
|
||||
|
||||
|
||||
@@ -543,6 +543,28 @@ function formatMonthDay(value) {
|
||||
};
|
||||
}
|
||||
|
||||
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("、") || "暂无专项信息";
|
||||
@@ -902,23 +924,25 @@ function renderCalendar(data, container) {
|
||||
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, data.as_of_date)) 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();
|
||||
yearItems.forEach((item) => {
|
||||
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", `${yearItems.length} 条`));
|
||||
allButton.append(create("strong", "", "全年"), create("span", "calendar-month-count", `${displayYearItems.length} 条`));
|
||||
allButton.type = "button";
|
||||
allButton.addEventListener("click", () => {
|
||||
state.calendarMonth = 0;
|
||||
@@ -942,12 +966,11 @@ function renderCalendar(data, container) {
|
||||
"back-to-school", "singles-day", "black-friday", "cyber-monday", "winter-holiday",
|
||||
"christmas", "lunar-new-year", "eid-fitr", "super-bowl", "la28",
|
||||
];
|
||||
const highValueItems = yearItems
|
||||
.filter((item) => item.business_relevance === "high" && isCalendarOpportunity(item, data.as_of_date))
|
||||
.sort((a, b) => String(a.start_date).localeCompare(String(b.start_date)));
|
||||
const selected = yearItems
|
||||
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)
|
||||
.sort((a, b) => String(a.start_date).localeCompare(String(b.start_date)) || Number(b.priority_score || 0) - Number(a.priority_score || 0));
|
||||
, 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)
|
||||
@@ -996,91 +1019,4 @@ function renderCalendar(data, container) {
|
||||
});
|
||||
}
|
||||
|
||||
function makePlatformCard(item) {
|
||||
const card = create("article", "platform-card");
|
||||
const header = create("div", "platform-card-header");
|
||||
const logoBox = create("div", "platform-logo-box");
|
||||
const logo = create("img", "platform-logo");
|
||||
logo.src = `./assets/platform-logos/${item.logo}.svg`;
|
||||
logo.alt = `${item.name} 标志`;
|
||||
logo.width = 112;
|
||||
logo.height = 48;
|
||||
logoBox.appendChild(logo);
|
||||
const title = create("div", "platform-title");
|
||||
title.append(create("span", "", `${item.region} · ${item.type}`), create("h3", "", item.name));
|
||||
header.append(logoBox, title, makeBadge(`${item.access_level} ${item.access_label}`, `platform-access is-${String(item.access_level).toLowerCase()}`));
|
||||
const markets = create("div", "platform-markets");
|
||||
appendTags(markets, item.markets || []);
|
||||
const fit = create("div", "platform-fit");
|
||||
fit.append(create("strong", "", "适合谁"), create("p", "", compactList(item.fit_sellers || [])));
|
||||
const category = create("div", "platform-category");
|
||||
category.append(create("strong", "", "家纺经营重点"), create("p", "", item.textile_category || "打开平台类目核对。"));
|
||||
const access = create("div", "platform-access-note");
|
||||
access.append(create("strong", "", "入驻判断"), create("p", "", item.cross_border || "以平台当前规则为准。"));
|
||||
const actions = create("div", "platform-actions");
|
||||
if (item.homepage_url && item.homepage_url !== item.seller_url) actions.appendChild(makePlatformActionGroup("打开平台官网", item.homepage_url, item, "homepage"));
|
||||
actions.appendChild(makePlatformActionGroup("去官方入驻", item.seller_url, item, "seller", "platform-action primary"));
|
||||
if (item.language_url) actions.appendChild(makeExternalLink("官网学习中心(可选中文)", item.language_url, "platform-action language"));
|
||||
const proof = create("p", "platform-proof", `${item.confidence} · ${item.link_status || "待人工复核"} · 核验 ${String(item.checked_at || "").slice(0, 10)}`);
|
||||
card.append(header, markets, category, access, fit, actions, proof);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderPlatforms(data, container) {
|
||||
const all = (data.platforms || []).filter(matchesFilters);
|
||||
const toolbar = create("section", "platform-filter-toolbar");
|
||||
const makeField = (label, control, extraClass = "") => {
|
||||
const field = create("label", `platform-filter-field ${extraClass}`.trim());
|
||||
field.append(create("span", "", label), control);
|
||||
return field;
|
||||
};
|
||||
const makeSelect = (placeholder, values, current) => {
|
||||
const select = create("select", "platform-filter-control");
|
||||
select.appendChild(Object.assign(create("option", "", placeholder), { value: "" }));
|
||||
values.forEach((value) => select.appendChild(Object.assign(create("option", "", value), { value })));
|
||||
select.value = current;
|
||||
return select;
|
||||
};
|
||||
const markets = [...new Set(all.flatMap((item) => item.markets || []))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||||
const types = [...new Set(all.map((item) => item.type).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||||
const marketSelect = makeSelect("全部市场", markets, state.platformMarket);
|
||||
const typeSelect = makeSelect("全部类型", types, state.platformType);
|
||||
const accessSelect = makeSelect("全部开放等级", ["A", "B", "C"].filter((level) => all.some((item) => item.access_level === level)), state.platformAccess);
|
||||
const search = create("input", "platform-filter-control");
|
||||
search.type = "search";
|
||||
search.name = "platform-channel-search";
|
||||
search.autocomplete = "off";
|
||||
search.placeholder = "搜索平台名称、国家或家纺类目…";
|
||||
search.value = state.platformSearch;
|
||||
toolbar.append(
|
||||
makeField("目标市场", marketSelect),
|
||||
makeField("渠道类型", typeSelect),
|
||||
makeField("开放等级", accessSelect),
|
||||
makeField("平台检索", search, "platform-filter-search"),
|
||||
);
|
||||
const header = create("section", "scene-summary platform-summary");
|
||||
const grid = create("div", "platform-grid");
|
||||
const apply = () => {
|
||||
const keyword = normalized(state.platformSearch);
|
||||
const selected = all.filter((item) => {
|
||||
if (state.platformMarket && !(item.markets || []).includes(state.platformMarket)) return false;
|
||||
if (state.platformType && item.type !== state.platformType) return false;
|
||||
if (state.platformAccess && item.access_level !== state.platformAccess) return false;
|
||||
if (!keyword) return true;
|
||||
return normalized([item.name, item.region, item.type, item.textile_category, item.cross_border, ...(item.markets || []), ...(item.fit_sellers || [])].join(" ")).includes(keyword);
|
||||
}).sort((a, b) => (Number(b.home_textile_sales_score || 0) + Number(b.emergence_score || 0)) - (Number(a.home_textile_sales_score || 0) + Number(a.emergence_score || 0)) || String(a.access_level).localeCompare(String(b.access_level)) || a.name.localeCompare(b.name, "zh-CN"));
|
||||
header.innerHTML = "";
|
||||
header.append(create("strong", "", `${selected.length}/${all.length} 个家纺销售渠道`), create("span", "", "准入条件以平台卖家中心为准"));
|
||||
grid.innerHTML = "";
|
||||
selected.forEach((item) => grid.appendChild(makePlatformCard(item)));
|
||||
if (!selected.length) grid.appendChild(create("div", "intelligence-empty", "当前筛选条件下没有已核验平台,请调整市场或开放等级。"));
|
||||
};
|
||||
marketSelect.addEventListener("change", () => { state.platformMarket = marketSelect.value; apply(); });
|
||||
typeSelect.addEventListener("change", () => { state.platformType = typeSelect.value; apply(); });
|
||||
accessSelect.addEventListener("change", () => { state.platformAccess = accessSelect.value; apply(); });
|
||||
search.addEventListener("input", () => { state.platformSearch = search.value; apply(); });
|
||||
container.append(toolbar, header, grid);
|
||||
apply();
|
||||
}
|
||||
|
||||
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, makePlatformCard, renderPlatforms };
|
||||
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 };
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { state } from "./render-intelligence-state.js?v=20260814e";
|
||||
import { create, normalized, matchesFilters, makeBadge, makeExternalLink, makePlatformActionGroup, appendTags, compactList } from "./render-intelligence-core.js?v=20260815a";
|
||||
|
||||
function makePlatformCard(item) {
|
||||
const card = create("article", "platform-card");
|
||||
const header = create("div", "platform-card-header");
|
||||
const logoBox = create("div", "platform-logo-box");
|
||||
const logo = create("img", "platform-logo");
|
||||
logo.src = `./assets/platform-logos/${item.logo}.svg`;
|
||||
logo.alt = `${item.name} 标志`;
|
||||
logo.width = 112;
|
||||
logo.height = 48;
|
||||
logoBox.appendChild(logo);
|
||||
const title = create("div", "platform-title");
|
||||
title.append(create("span", "", `${item.region} · ${item.type}`), create("h3", "", item.name));
|
||||
header.append(logoBox, title, makeBadge(`${item.access_level} ${item.access_label}`, `platform-access is-${String(item.access_level).toLowerCase()}`));
|
||||
const markets = create("div", "platform-markets");
|
||||
appendTags(markets, item.markets || []);
|
||||
const fit = create("div", "platform-fit");
|
||||
fit.append(create("strong", "", "适合谁"), create("p", "", compactList(item.fit_sellers || [])));
|
||||
const category = create("div", "platform-category");
|
||||
category.append(create("strong", "", "家纺经营重点"), create("p", "", item.textile_category || "打开平台类目核对。"));
|
||||
const access = create("div", "platform-access-note");
|
||||
access.append(create("strong", "", "入驻判断"), create("p", "", item.cross_border || "以平台当前规则为准。"));
|
||||
const actions = create("div", "platform-actions");
|
||||
if (item.homepage_url && item.homepage_url !== item.seller_url) actions.appendChild(makePlatformActionGroup("打开平台官网", item.homepage_url, item, "homepage"));
|
||||
actions.appendChild(makePlatformActionGroup("去官方入驻", item.seller_url, item, "seller", "platform-action primary"));
|
||||
if (item.language_url) actions.appendChild(makeExternalLink("官网学习中心(可选中文)", item.language_url, "platform-action language"));
|
||||
const proof = create("p", "platform-proof", `${item.confidence} · ${item.link_status || "待人工复核"} · 核验 ${String(item.checked_at || "").slice(0, 10)}`);
|
||||
card.append(header, markets, category, access, fit, actions, proof);
|
||||
return card;
|
||||
}
|
||||
|
||||
export function renderPlatforms(data, container) {
|
||||
const all = (data.platforms || []).filter(matchesFilters);
|
||||
const toolbar = create("section", "platform-filter-toolbar");
|
||||
const makeField = (label, control, extraClass = "") => {
|
||||
const field = create("label", `platform-filter-field ${extraClass}`.trim());
|
||||
field.append(create("span", "", label), control);
|
||||
return field;
|
||||
};
|
||||
const makeSelect = (placeholder, values, current) => {
|
||||
const select = create("select", "platform-filter-control");
|
||||
select.appendChild(Object.assign(create("option", "", placeholder), { value: "" }));
|
||||
values.forEach((value) => select.appendChild(Object.assign(create("option", "", value), { value })));
|
||||
select.value = current;
|
||||
return select;
|
||||
};
|
||||
const markets = [...new Set(all.flatMap((item) => item.markets || []))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||||
const types = [...new Set(all.map((item) => item.type).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN"));
|
||||
const marketSelect = makeSelect("全部市场", markets, state.platformMarket);
|
||||
const typeSelect = makeSelect("全部类型", types, state.platformType);
|
||||
const accessSelect = makeSelect("全部开放等级", ["A", "B", "C"].filter((level) => all.some((item) => item.access_level === level)), state.platformAccess);
|
||||
const search = create("input", "platform-filter-control");
|
||||
search.type = "search";
|
||||
search.name = "platform-channel-search";
|
||||
search.autocomplete = "off";
|
||||
search.placeholder = "搜索平台名称、国家或家纺类目…";
|
||||
search.value = state.platformSearch;
|
||||
toolbar.append(makeField("目标市场", marketSelect), makeField("渠道类型", typeSelect), makeField("开放等级", accessSelect), makeField("平台检索", search, "platform-filter-search"));
|
||||
const header = create("section", "scene-summary platform-summary");
|
||||
const grid = create("div", "platform-grid");
|
||||
const apply = () => {
|
||||
const keyword = normalized(state.platformSearch);
|
||||
const selected = all.filter((item) => {
|
||||
if (state.platformMarket && !(item.markets || []).includes(state.platformMarket)) return false;
|
||||
if (state.platformType && item.type !== state.platformType) return false;
|
||||
if (state.platformAccess && item.access_level !== state.platformAccess) return false;
|
||||
if (!keyword) return true;
|
||||
return normalized([item.name, item.region, item.type, item.textile_category, item.cross_border, ...(item.markets || []), ...(item.fit_sellers || [])].join(" ")).includes(keyword);
|
||||
}).sort((a, b) => (Number(b.home_textile_sales_score || 0) + Number(b.emergence_score || 0)) - (Number(a.home_textile_sales_score || 0) + Number(a.emergence_score || 0)) || String(a.access_level).localeCompare(String(b.access_level)) || a.name.localeCompare(b.name, "zh-CN"));
|
||||
header.innerHTML = "";
|
||||
header.append(create("strong", "", `${selected.length}/${all.length} 个家纺销售渠道`), create("span", "", "准入条件以平台卖家中心为准"));
|
||||
grid.innerHTML = "";
|
||||
selected.forEach((item) => grid.appendChild(makePlatformCard(item)));
|
||||
if (!selected.length) grid.appendChild(create("div", "intelligence-empty", "当前筛选条件下没有已核验平台,请调整市场或开放等级。"));
|
||||
};
|
||||
marketSelect.addEventListener("change", () => { state.platformMarket = marketSelect.value; apply(); });
|
||||
typeSelect.addEventListener("change", () => { state.platformType = typeSelect.value; apply(); });
|
||||
accessSelect.addEventListener("change", () => { state.platformAccess = accessSelect.value; apply(); });
|
||||
search.addEventListener("input", () => { state.platformSearch = search.value; apply(); });
|
||||
container.append(toolbar, header, grid);
|
||||
apply();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { state } from "./render-intelligence-state.js?v=20260814e";
|
||||
import { TEXT, create, openLightbox, normalized, matchesFilters, makeVisual, makeBadge, makeExternalLink, sourceLinkLabel, currentTranslationProvider, appendTranslationLinks, appendTags, formatDate, formatVerified, makeShareButton } from "./render-intelligence-core.js?v=20260814i";
|
||||
import { TEXT, create, openLightbox, normalized, matchesFilters, makeVisual, makeBadge, makeExternalLink, sourceLinkLabel, currentTranslationProvider, appendTranslationLinks, appendTags, formatDate, formatVerified, makeShareButton } from "./render-intelligence-core.js?v=20260815a";
|
||||
|
||||
function makeExhibitionCard(item) {
|
||||
const article = create("article", "exhibition-card");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { state } from "./render-intelligence-state.js?v=20260814e";
|
||||
import { TEXT, create, detectViewerCountry, currentTranslationProvider, formatVerified, sectionItems, matchesFilters, makeCard, renderCalendar, renderPlatforms } from "./render-intelligence-core.js?v=20260814l";
|
||||
import { TEXT, create, detectViewerCountry, currentTranslationProvider, formatVerified, sectionItems, matchesFilters, makeCard, renderCalendar } from "./render-intelligence-core.js?v=20260815a";
|
||||
import { renderExhibitions, renderPolicies, renderStandards, renderTrendNews, renderTrendSearchHub, trendItems } from "./render-intelligence-scenes.js?v=20260814i";
|
||||
import { renderActivities } from "./render-intelligence-activities.js?v=20260814h";
|
||||
import { renderPlatforms } from "./render-intelligence-platforms.js?v=20260815a";
|
||||
|
||||
function renderStats(data, container) {
|
||||
if (!container) return;
|
||||
@@ -48,6 +49,8 @@ function renderTabs(data, container, render) {
|
||||
|
||||
function renderRegionOptions(data, select) {
|
||||
if (!select) return;
|
||||
const regionField = typeof select.closest === "function" ? select.closest("label") : null;
|
||||
if (regionField) regionField.hidden = state.activeSection === "calendar";
|
||||
const previous = state.region;
|
||||
select.innerHTML = "";
|
||||
if (state.activeSection === "calendar") {
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet" />
|
||||
<link href="./assets/fonts/dongguan-bold/result.css?v=20260814a" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="./assets/css/base.css?v=20260814h" />
|
||||
<link rel="stylesheet" href="./assets/css/intelligence.css?v=20260814s" />
|
||||
<link rel="stylesheet" href="./assets/css/intelligence.css?v=20260815a" />
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@graph":[{"@type":"WebSite","name":"三笙AI情报局","inLanguage":"zh-CN","description":"面向家纺跨境经营团队的备货、展会、政策、平台、活动与趋势情报站"},{"@type":"Organization","name":"三笙AI情报局","alternateName":["三笙AI","家纺AI"],"url":"./"}]}</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -78,6 +78,6 @@
|
||||
</main>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<script type="module" src="./assets/js/app.js?v=20260814q"></script>
|
||||
<script type="module" src="./assets/js/app.js?v=20260815a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -109,6 +109,7 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
|
||||
"AU": ("Boxing Day (observed)", "英语"), "GB": ("Boxing Day (observed)", "英语"),
|
||||
},
|
||||
"lunar-new-year": {"CN": ("春节", "简体中文"), "KR": ("설날", "韩语"), "VN": ("Tết Nguyên Đán", "越南语"), "SG": ("农历新年 / Chinese New Year", "简体中文 / 英语")},
|
||||
"new-year-eve": {"DK": ("Nytårsaften", "丹麦语"), "SE": ("Nyårsafton", "瑞典语"), "PH": ("一年中的最后一天", "英语 / 菲律宾语")},
|
||||
"eid-fitr": {"AE": ("عيد الفطر", "阿拉伯语"), "TR": ("Ramazan Bayramı", "土耳其语"), "ID": ("Idul Fitri", "印度尼西亚语")},
|
||||
"eid-adha": {"AE": ("عيد الأضحى", "阿拉伯语"), "TR": ("Kurban Bayramı", "土耳其语"), "ID": ("Idul Adha", "印度尼西亚语")},
|
||||
"assumption-day": {
|
||||
@@ -118,6 +119,11 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
|
||||
"PT": ("Assunção de Nossa Senhora", "葡萄牙语"), "ES": ("Asunción", "西班牙语"),
|
||||
},
|
||||
"indonesia-independence": {"ID": ("Hari Kemerdekaan Republik Indonesia", "印度尼西亚语")},
|
||||
"new-year": {
|
||||
"CN": ("元旦", "简体中文"), "JP": ("元日", "日语"), "RU": ("Новогодние каникулы", "俄语"),
|
||||
},
|
||||
"public-holiday:JP:coming of age day": {"JP": ("成人の日", "日语")},
|
||||
"public-holiday:JP:greenery day": {"JP": ("みどりの日", "日语")},
|
||||
}
|
||||
|
||||
FESTIVAL_TARGET_COUNTRIES = {
|
||||
@@ -139,6 +145,10 @@ HOLIDAY_TRANSLATIONS = {
|
||||
"whit monday": "圣灵降临节星期一", "pentecost monday": "圣灵降临节星期一",
|
||||
"king's birthday": "国王诞辰", "mountain day": "山之日", "marine day": "海之日",
|
||||
"respect for the aged day": "敬老日", "culture day": "文化日", "children's day": "儿童节",
|
||||
"new year s day": "元旦", "new year holiday": "新年假期",
|
||||
"new year s eve": "跨年夜", "last day of the year": "跨年夜",
|
||||
"coming of age day": "成人节", "greenery day": "绿之日",
|
||||
"labour thanksgiving day": "勤劳感谢之日", "labor thanksgiving day": "勤劳感谢之日",
|
||||
}
|
||||
|
||||
SUBDIVISION_NAMES = {
|
||||
@@ -282,6 +292,12 @@ FESTIVAL_PROFILES = {
|
||||
"design": ["使用新年焕新而非单一宗教表达", "按南北半球季节调整材质"],
|
||||
"avoid": ["南半球正值夏季,不要统一推保暖产品"],
|
||||
},
|
||||
"new-year-eve": {
|
||||
"title": "跨年夜", "relevance": "low", "decision": "仅用于运营排班",
|
||||
"what": "公历新年前夜或年末公共休假节点,主要影响跨年营业、物流和客服排班。",
|
||||
"scenes": ["跨年营业", "物流排班"], "products": [], "design": [],
|
||||
"avoid": ["不要把跨年夜与元旦或连续新年假期拆成多个重复消费节"],
|
||||
},
|
||||
"super-bowl": {
|
||||
"title": "超级碗观赛消费节点", "relevance": "high", "decision": "值得场景备货",
|
||||
"what": "美国职业橄榄球年度决赛,带动家庭观赛、聚会、酒店和城市活动消费。",
|
||||
@@ -371,9 +387,11 @@ FESTIVAL_PROFILES = {
|
||||
|
||||
GLOBAL_AGGREGATE_KEYS = {
|
||||
"black-friday", "cyber-monday", "winter-holiday", "christmas", "boxing-day",
|
||||
"lunar-new-year", "eid-fitr", "eid-adha", "easter", "assumption-day", "new-year", "st-stephens-day",
|
||||
"lunar-new-year", "eid-fitr", "eid-adha", "easter", "assumption-day", "new-year", "new-year-eve", "st-stephens-day",
|
||||
}
|
||||
|
||||
RANGE_AGGREGATE_KEYS = {"new-year", "eid-fitr", "eid-adha", "lunar-new-year"}
|
||||
|
||||
FESTIVAL_ENGLISH_TITLES = {
|
||||
"back-to-school": "Back to School",
|
||||
"black-friday": "Black Friday",
|
||||
@@ -385,6 +403,7 @@ FESTIVAL_ENGLISH_TITLES = {
|
||||
"eid-fitr": "Eid al-Fitr",
|
||||
"lunar-new-year": "Lunar New Year",
|
||||
"new-year": "New Year's Day",
|
||||
"new-year-eve": "New Year's Eve",
|
||||
"singles-day": "Singles' Day",
|
||||
"st-stephens-day": "St. Stephen's Day",
|
||||
"super-bowl": "Super Bowl",
|
||||
@@ -393,6 +412,7 @@ FESTIVAL_ENGLISH_TITLES = {
|
||||
}
|
||||
|
||||
GENERIC_HOLIDAY_TITLES = {"国家纪念与公共假日", "地区公共假日"}
|
||||
UNSAFE_CROSS_COUNTRY_TITLES = {"独立日", "国庆日", "共和国日", "宪法日", "解放日", "胜利日", "纪念日"}
|
||||
HOLIDAY_TITLE_ALIASES = {"五一劳动节": "劳动节"}
|
||||
|
||||
|
||||
@@ -446,7 +466,9 @@ def festival_key(name: str, source_id: str = "", country_code: str = "") -> str:
|
||||
return "eid-adha"
|
||||
if any(token in lower for token in ("easter", "good friday", "pentecost", "whit monday", "复活节", "耶稣受难")):
|
||||
return "easter"
|
||||
if any(token in lower for token in ("new year's day", "new year holiday", "元旦", "新年假期")):
|
||||
if any(token in lower for token in ("new year s eve", "last day of the year", "跨年夜")):
|
||||
return "new-year-eve"
|
||||
if any(token in lower for token in ("new year s day", "new year holiday", "new year s holiday", "元旦", "新年假期")):
|
||||
return "new-year"
|
||||
if any(token in lower for token in ("memorial", "remembrance", "all souls", "追悼", "纪念")):
|
||||
return "memorial"
|
||||
@@ -477,7 +499,12 @@ def calendar_event_type(name: str, source_id: str = "") -> str:
|
||||
|
||||
|
||||
def holiday_title(local_name: str, english_name: str, country_code: str) -> str:
|
||||
translated = HOLIDAY_TRANSLATIONS.get(english_name.lower())
|
||||
normalized = _normalized_name(english_name)
|
||||
if country_code == "ID" and normalized == "independence day":
|
||||
return "印度尼西亚独立日"
|
||||
if country_code == "PH" and normalized == "national heroes day":
|
||||
return "菲律宾国家英雄日"
|
||||
translated = HOLIDAY_TRANSLATIONS.get(english_name.lower()) or HOLIDAY_TRANSLATIONS.get(_normalized_name(english_name))
|
||||
if translated:
|
||||
return translated
|
||||
if country_code in ("CN", "JP", "KR") and local_name:
|
||||
@@ -650,17 +677,20 @@ def culture_reference_url(title: str) -> str:
|
||||
return f"https://zh.wikipedia.org/wiki/{quote(title.replace(' ', '_'), safe='')}"
|
||||
|
||||
|
||||
def culture_reference_cn_url(title: str) -> str:
|
||||
def culture_reference_cn_url(title: str, festival_key: str = "") -> str:
|
||||
"""中国大陆访客使用国内网络更易访问的百度百科。"""
|
||||
return f"https://baike.baidu.com/item/{quote(title, safe='')}"
|
||||
aliases = {"new-year": "元旦"}
|
||||
return f"https://baike.baidu.com/item/{quote(aliases.get(festival_key, title), safe='')}"
|
||||
|
||||
|
||||
def visual_reference_url(title: str, country_details: list[dict[str, Any]]) -> str:
|
||||
def visual_reference_url(title: str, country_details: list[dict[str, Any]], title_english: str = "") -> str:
|
||||
"""图片入口只搜索节日本身,不复用来源页标题。"""
|
||||
terms = [title]
|
||||
for detail in country_details[:2]:
|
||||
terms.extend((str(detail.get("local_name") or ""), str(detail.get("country") or "")))
|
||||
query = " ".join(_dedupe(terms))
|
||||
detail = next((row for row in country_details if row.get("country")), {})
|
||||
query_title = title_english.strip() or str(detail.get("local_name") or title).strip()
|
||||
if query_title in GENERIC_HOLIDAY_TITLES or not query_title:
|
||||
query_title = str(detail.get("local_name") or title).strip()
|
||||
country = str(detail.get("country_english") or detail.get("country") or "").strip()
|
||||
query = " ".join(_dedupe([query_title, country, "holiday"]))[:120]
|
||||
return f"https://www.bing.com/images/search?q={quote(query, safe='')}&setlang=zh-hans"
|
||||
|
||||
|
||||
@@ -668,7 +698,11 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
|
||||
"""同日同名的明确节日跨国家聚合;国家纪念类泛称仍按国家拆分。"""
|
||||
key = str(item.get("festival_key") or "public-holiday")
|
||||
title = HOLIDAY_TITLE_ALIASES.get(str(item.get("title") or "").strip(), str(item.get("title") or "").strip())
|
||||
if title and title not in GENERIC_HOLIDAY_TITLES and (key.startswith("public-holiday:") or key == "memorial"):
|
||||
if title and title not in GENERIC_HOLIDAY_TITLES and (
|
||||
key.startswith("public-holiday:")
|
||||
or key == "memorial"
|
||||
or (key.startswith("national:") and title not in UNSAFE_CROSS_COUNTRY_TITLES)
|
||||
):
|
||||
return f"named-holiday:{_normalized_name(title)}", ""
|
||||
country_partition = "" if key in GLOBAL_AGGREGATE_KEYS else str(item.get("country_code") or item.get("market_area") or item.get("region") or "")
|
||||
return key, country_partition
|
||||
@@ -677,6 +711,8 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
|
||||
def calendar_group_date(item: dict[str, Any]) -> str:
|
||||
"""圣诞季共用一张决策卡,其他节日仍按实际日期聚合。"""
|
||||
start_date = str(item["start_date"])
|
||||
if item.get("festival_key") in RANGE_AGGREGATE_KEYS:
|
||||
return f"@{start_date[:4]}"
|
||||
if item.get("festival_key") != "christmas":
|
||||
return start_date
|
||||
value = date.fromisoformat(start_date)
|
||||
@@ -704,8 +740,15 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
grouped.setdefault((calendar_group_date(item), group_key, country_partition), []).append(item)
|
||||
|
||||
insights: list[dict[str, Any]] = []
|
||||
for (start_date, group_key, _), rows in grouped.items():
|
||||
rows = sorted(rows, key=lambda row: (str(row.get("start_date")) != start_date, str(row.get("start_date"))))
|
||||
for (group_date, group_key, _), rows in grouped.items():
|
||||
range_group = group_date.startswith("@")
|
||||
start_date = min(str(row.get("start_date") or group_date) for row in rows) if range_group else group_date
|
||||
rows = sorted(rows, key=lambda row: (
|
||||
str(row.get("start_date")) != start_date,
|
||||
0 if str(row.get("country_code") or "") == "CN" else 1,
|
||||
str(row.get("country_code") or ""),
|
||||
str(row.get("start_date") or ""),
|
||||
))
|
||||
row_keys = _dedupe([str(row.get("festival_key") or "public-holiday") for row in rows])
|
||||
key = row_keys[0] if len(row_keys) == 1 else f"public-holiday:{group_key.removeprefix('named-holiday:')}"
|
||||
profile = profile_for_market(key, str(rows[0].get("country_code") or ""))
|
||||
@@ -715,7 +758,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
platforms = _dedupe([value for row in rows for value in row.get("platforms") or []])
|
||||
titles = [str(row.get("title") or "") for row in rows if row.get("title")]
|
||||
title = str(profile.get("title") or Counter(titles).most_common(1)[0][0])
|
||||
if key.startswith("public-holiday:") or key in ("national", "memorial"):
|
||||
if key.startswith(("public-holiday:", "national:")) or key in ("national", "memorial"):
|
||||
title = Counter(titles).most_common(1)[0][0]
|
||||
english_titles = [str(row.get("title_english") or "").strip() for row in rows if row.get("title_english")]
|
||||
title_english = FESTIVAL_ENGLISH_TITLES.get(key, "")
|
||||
@@ -822,6 +865,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
"label": christmas_date_label(value, start_date) if key == "christmas" else "地区日期",
|
||||
"countries": variant_countries,
|
||||
})
|
||||
end_date = start_date if key == "christmas" else max(str(row.get("end_date") or start_date) for row in rows)
|
||||
if key == "new-year" and "RU" in country_codes:
|
||||
end_date = max(end_date, f"{start_date[:4]}-01-08")
|
||||
insight = {
|
||||
"id": f"calendar-insight-{digest}",
|
||||
"section": "calendar",
|
||||
@@ -842,7 +888,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
"change_status": "官方日期已核验",
|
||||
"confidence": "高" if official_sources else "中高",
|
||||
"start_date": start_date,
|
||||
"end_date": start_date if key == "christmas" else max(str(row.get("end_date") or start_date) for row in rows),
|
||||
"end_date": end_date,
|
||||
"related_dates": related_dates,
|
||||
"date_variants": date_variants,
|
||||
"date_variation_note": (
|
||||
@@ -876,9 +922,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
"days_until": (date.fromisoformat(start_date) - now).days,
|
||||
"priority_score": max(int(row.get("priority_score") or 0) for row in rows) + min(12, len(countries)),
|
||||
"analysis_basis": "事实层:官方日期与适用国家;分析层:家纺经营场景推断",
|
||||
"visual_reference_url": visual_reference_url(title, country_details),
|
||||
"visual_reference_url": visual_reference_url(title, country_details, title_english),
|
||||
"culture_reference_url": culture_reference_url(title),
|
||||
"culture_reference_cn_url": culture_reference_cn_url(title),
|
||||
"culture_reference_cn_url": culture_reference_cn_url(title, key),
|
||||
}
|
||||
translation = {
|
||||
key: first[key]
|
||||
@@ -889,6 +935,17 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
|
||||
)
|
||||
if key in first
|
||||
}
|
||||
if "CN" in country_codes:
|
||||
translation.update({
|
||||
"source_language": "zh-CN",
|
||||
"needs_translation": False,
|
||||
"google_translation_supported": False,
|
||||
"translation_url": "",
|
||||
"translation_provider": "",
|
||||
"translation_fallback_url": "",
|
||||
"translation_fallback_provider": "",
|
||||
"translation_notice": "该节日包含中国市场,中文访客直接查看中文节日资料。",
|
||||
})
|
||||
insight.update(translation)
|
||||
insight["poster"] = poster_for(insight["id"], "calendar", insight["culture_key"])
|
||||
insights.append(insight)
|
||||
|
||||
@@ -157,6 +157,8 @@ def build_nager_events(now: date, horizon: date, source: SourceSpec, result: Fet
|
||||
continue
|
||||
local_title = normalize_text(str(row.get("localName") or ""))
|
||||
english_title = normalize_text(str(row.get("name") or local_title or "公共假日"))
|
||||
if english_title.lower().replace("’", "'") == "new year's day" and day.month == 12 and day.day == 31:
|
||||
continue
|
||||
title = market_holiday_title(local_title, english_title, code)
|
||||
subdivision_codes = [normalize_text(str(value)) for value in row.get("counties") or [] if value]
|
||||
applicable_areas, country_scope = calendar_scope(bool(row.get("global", True)), subdivision_codes)
|
||||
@@ -183,6 +185,11 @@ def build_nager_events(now: date, horizon: date, source: SourceSpec, result: Fet
|
||||
"source_url": event_source_url,
|
||||
"authority": "结构化公共假日数据",
|
||||
})
|
||||
item.update(translation_fields(
|
||||
event_source_url,
|
||||
"zh-CN" if code == "CN" else "en",
|
||||
local_title or english_title,
|
||||
))
|
||||
items.append(enrich_market_calendar_item(item, now, code, poster_for))
|
||||
return items
|
||||
def build_govuk_events(now: date, horizon: date, source: SourceSpec, result: FetchResult) -> list[dict[str, Any]]:
|
||||
|
||||
+23
-5
@@ -10,11 +10,12 @@ test("独立首页只展示家纺情报并加载独立入口", async () => {
|
||||
const dongguanCss = await text("public/assets/fonts/dongguan-bold/result.css");
|
||||
assert.match(html, /三笙AI情报局/);
|
||||
assert.match(html, /assets\/js\/app\.js/);
|
||||
assert.match(html, /assets\/js\/app\.js\?v=20260814q/);
|
||||
assert.match(html, /assets\/js\/app\.js\?v=20260815a/);
|
||||
assert.match(html, /assets\/css\/base\.css\?v=20260814h/);
|
||||
assert.match(html, /assets\/css\/intelligence\.css\?v=20260814s/);
|
||||
assert.match(html, /assets\/css\/intelligence\.css\?v=20260815a/);
|
||||
assert.match(html, /<details[^>]+class="intelligence-metrics"/);
|
||||
assert.match(html, /<summary[^>]+class="intelligence-utility-row"/);
|
||||
assert.doesNotMatch(html, /id="intelligenceShareBtn"/);
|
||||
assert.match(html, /class="tool-icon"/);
|
||||
assert.match(html, /assets\/fonts\/dongguan-bold\/result\.css/);
|
||||
assert.doesNotMatch(html, /cn-font\.claude-code-best\.win/);
|
||||
@@ -33,7 +34,7 @@ test("数据快照不复用浏览器旧缓存", async () => {
|
||||
|
||||
test("七个业务入口与趋势四子类均保留", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js");
|
||||
for (const label of ["今日必看", "备货日历", "展会通", "政策预警", "趋势雷达", "平台渠道", "跨境活动"]) {
|
||||
assert.match(modules, new RegExp(label));
|
||||
}
|
||||
@@ -59,6 +60,7 @@ test("所有外部业务链接使用新窗口和安全rel", async () => {
|
||||
await text("public/assets/js/render-intelligence.js"),
|
||||
await text("public/assets/js/render-intelligence-core.js"),
|
||||
await text("public/assets/js/render-intelligence-scenes.js"),
|
||||
await text("public/assets/js/render-intelligence-platforms.js"),
|
||||
await text("public/assets/js/render-intelligence-activities.js"),
|
||||
].join("\n");
|
||||
assert.match(files, /target\s*=\s*["']_blank["']/);
|
||||
@@ -67,7 +69,7 @@ test("所有外部业务链接使用新窗口和安全rel", async () => {
|
||||
|
||||
test("趋势分类读取真实 items 数组而不是分类对象", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js");
|
||||
assert.match(modules, /category\.items/);
|
||||
assert.match(modules, /item\.image\s*\|\|\s*item\.thumbnail/);
|
||||
assert.match(modules, /function renderCalendarMonthGrid/);
|
||||
@@ -153,9 +155,23 @@ test("跨境活动按访问当天重新计算相对日期并排除已结束活
|
||||
assert.equal(activityDayOffset("2026-08-15", now), 1);
|
||||
});
|
||||
|
||||
test("日历按当前 UTC 日期优先展示未过期节点并把已过节点压到后面", async () => {
|
||||
const { calendarTodayIso, sortCalendarItems, calendarDefaultItems } = await import(new URL(`../public/assets/js/render-intelligence-core.js?calendar-date=${Date.now()}`, import.meta.url));
|
||||
const now = new Date("2026-08-15T00:30:00.000Z");
|
||||
const items = [
|
||||
{ id: "july", start_date: "2026-07-15", priority_score: 90 },
|
||||
{ id: "past-august", start_date: "2026-08-10", priority_score: 90 },
|
||||
{ id: "today", start_date: "2026-08-15", priority_score: 1 },
|
||||
{ id: "future", start_date: "2026-08-17", priority_score: 1 },
|
||||
];
|
||||
assert.equal(calendarTodayIso(now), "2026-08-15");
|
||||
assert.deepEqual(sortCalendarItems(items, now).map((item) => item.id), ["today", "future", "past-august", "july"]);
|
||||
assert.deepEqual(calendarDefaultItems(items, 2026, now).map((item) => item.id), ["today", "future"]);
|
||||
});
|
||||
|
||||
test("最新家纺交互与移动端契约已同步", async () => {
|
||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js");
|
||||
const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js");
|
||||
const baseCss = await text("public/assets/css/base.css");
|
||||
const css = await text("public/assets/css/intelligence.css");
|
||||
assert.doesNotMatch(renderer, /displayLimit/);
|
||||
@@ -171,6 +187,8 @@ test("最新家纺交互与移动端契约已同步", async () => {
|
||||
assert.doesNotMatch(modules, /家纺成熟度|新兴参考/);
|
||||
assert.doesNotMatch(modules, /scope_type === "state\.region"|\["state\.region", "地区政策"\]/);
|
||||
assert.match(modules, /scope_type === "region"/);
|
||||
assert.match(renderer, /closest\("label"\)/);
|
||||
assert.match(renderer, /regionField\.hidden\s*=\s*state\.activeSection === "calendar"/);
|
||||
assert.match(css, /@media \(max-width: 680px\)/);
|
||||
assert.match(css, /\.intelligence-tabs\s*\{[^}]*grid-template-columns: repeat\(3/);
|
||||
assert.match(css, /min-height: 48px/);
|
||||
|
||||
@@ -146,6 +146,32 @@ class StandaloneProjectContractTests(unittest.TestCase):
|
||||
if item.get("title") == title and item.get("start_date", "").startswith("2026-")
|
||||
]
|
||||
self.assertEqual(expected, len(matches), f"{title} 应跨国家聚合为单一日历卡片")
|
||||
new_year = [
|
||||
item for item in intelligence["calendar_insights"]
|
||||
if item.get("festival_key") == "new-year" and item.get("start_date") == "2027-01-01"
|
||||
]
|
||||
self.assertEqual(len(new_year), 1)
|
||||
self.assertGreaterEqual(new_year[0].get("end_date", ""), "2027-01-08")
|
||||
self.assertLessEqual(new_year[0].get("end_date", ""), "2027-01-08")
|
||||
self.assertIn("中国", new_year[0].get("affected_countries", []))
|
||||
self.assertIn("俄罗斯", new_year[0].get("affected_countries", []))
|
||||
self.assertFalse(new_year[0].get("needs_translation"))
|
||||
new_year_eve = [
|
||||
item for item in intelligence["calendar_insights"]
|
||||
if item.get("festival_key") == "new-year-eve" and item.get("start_date") == "2026-12-31"
|
||||
]
|
||||
self.assertEqual(len(new_year_eve), 1)
|
||||
self.assertEqual(new_year_eve[0].get("title"), "跨年夜")
|
||||
self.assertFalse(any(item.get("title_english") == "New Year's Day" and item.get("start_date", "")[5:] == "12-31" for item in intelligence["calendar"]))
|
||||
eid_adha = [
|
||||
item for item in intelligence["calendar_insights"]
|
||||
if item.get("festival_key") == "eid-adha" and item.get("start_date") == "2027-05-16"
|
||||
]
|
||||
self.assertEqual(len(eid_adha), 1)
|
||||
self.assertGreaterEqual(eid_adha[0].get("end_date", ""), "2027-05-19")
|
||||
self.assertEqual(next(item["title"] for item in intelligence["calendar_insights"] if item["id"] == "calendar-insight-4f8974ea2f346b"), "成人节")
|
||||
self.assertEqual(next(item["title"] for item in intelligence["calendar_insights"] if item["id"] == "calendar-insight-87f06bf4075a17"), "绿之日")
|
||||
self.assertFalse(any(item.get("title") == "国家纪念与公共假日" and item.get("title_english") for item in intelligence["calendar_insights"]))
|
||||
self.assertTrue(all(item.get("title_english") for item in intelligence["calendar_insights"] if item.get("country_details")))
|
||||
boxing_day = next(item for item in intelligence["calendar_insights"] if item.get("festival_key") == "boxing-day")
|
||||
self.assertEqual(boxing_day["title_english"], "Boxing Day")
|
||||
|
||||
Reference in New Issue
Block a user