fix: 按UTC日期优化独立站日历筛选
2026-08-15:日历默认视图按当前 UTC 日期过滤与排序,未来和当天节点优先,已过节点降权;显式月份仍保留完整历史。日历页隐藏重复目标区域筛选,平台渲染模块独立拆分,更新静态资源版本号与测试契约,避免 Gitea/IIS/Nginx 部署命中旧缓存。
This commit is contained in:
@@ -1,5 +1,19 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## 2026-08-15
|
||||||
|
|
||||||
|
### 日历实用性与筛选交互
|
||||||
|
|
||||||
|
- 日历排序统一使用当前 UTC 日期:未来与当天项目按日期升序展示,已过项目降权到当月未来项目之后,并在已过项目之间按日期倒序排列。
|
||||||
|
- 当前年份的默认日历视图不再把今天之前的节日放入首屏月份统计;用户主动选择某个月时仍可查看该月完整历史记录。
|
||||||
|
- “目标区域”在日历页直接隐藏,避免与下方市场区域、国家/地区、产品方向和销售平台筛选重复;离开日历页后自动恢复。
|
||||||
|
- 删除主站校验时间行里的旧式箭头分享控件对应逻辑;独立站保留正常的顶部分享按钮。同步更新独立站静态资源版本号,避免部署命中旧缓存。
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- 独立站 Node 前端测试 14 项、Python 合同测试 11 项通过。
|
||||||
|
- 独立站静态构建成功;390×844 移动端检查通过,无控制台错误,日历首条为 2026-08-15。
|
||||||
|
|
||||||
本文件按日期倒序记录独立版的代码、数据、交互和部署变化。每次提交必须先更新本文件,确保公司研发人员可以判断影响范围并使用 `git revert <commit>` 回滚。
|
本文件按日期倒序记录独立版的代码、数据、交互和部署变化。每次提交必须先更新本文件,确保公司研发人员可以判断影响范围并使用 `git revert <commit>` 回滚。
|
||||||
|
|
||||||
## 2026-08-14 - 平台渠道入口与数据快照对齐
|
## 2026-08-14 - 平台渠道入口与数据快照对齐
|
||||||
|
|||||||
@@ -67,21 +67,6 @@
|
|||||||
.intelligence-overview-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
|
.intelligence-overview-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
|
||||||
#intelligenceUpdatedAt { color: var(--muted); font-size: 12px; }
|
#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 {
|
.intelligence-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
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 p:last-child { color: var(--ink-secondary); font-size: 16px; line-height: 1.65; }
|
||||||
.intelligence-overview-actions { align-items: center; }
|
.intelligence-overview-actions { align-items: center; }
|
||||||
.intelligence-overview-actions > span { color: var(--ink-secondary); font-size: 13px; }
|
.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 { 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-toolbar input, .intelligence-toolbar select { min-height: 48px; font-size: 16px; }
|
||||||
.intelligence-tab { min-height: 48px; font-size: 16px; }
|
.intelligence-tab { min-height: 48px; font-size: 16px; }
|
||||||
.intelligence-tab-count, .intelligence-badge { font-size: 13px; }
|
.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";
|
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) {
|
function compactList(values, limit = 5) {
|
||||||
const unique = [...new Set((values || []).filter(Boolean))];
|
const unique = [...new Set((values || []).filter(Boolean))];
|
||||||
if (unique.length <= limit) return unique.join("、") || "暂无专项信息";
|
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));
|
decisionToolbar.appendChild(create("p", "calendar-platform-filter-hint", TEXT.calendar.platformFilterHint));
|
||||||
container.appendChild(decisionToolbar);
|
container.appendChild(decisionToolbar);
|
||||||
|
|
||||||
|
const today = calendarTodayIso();
|
||||||
const selectedMarketItems = all.filter((item) => {
|
const selectedMarketItems = all.filter((item) => {
|
||||||
if (state.calendarMarketGroup && !(item.affected_regions || []).includes(state.calendarMarketGroup)) return false;
|
if (state.calendarMarketGroup && !(item.affected_regions || []).includes(state.calendarMarketGroup)) return false;
|
||||||
if (state.calendarCountry && !(item.affected_countries || []).includes(state.calendarCountry)) 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.calendarProduct && !calendarProductCategories(item).includes(state.calendarProduct)) return false;
|
||||||
if (state.calendarPlatform && !calendarPlatformFamilies(item).includes(state.calendarPlatform)) 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;
|
return true;
|
||||||
});
|
});
|
||||||
const yearItems = selectedMarketItems.filter((item) => Number(String(item.start_date || "").slice(0, 4)) === state.calendarYear);
|
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();
|
const monthCounts = new Map();
|
||||||
yearItems.forEach((item) => {
|
displayYearItems.forEach((item) => {
|
||||||
const month = Number(String(item.start_date || "").slice(5, 7));
|
const month = Number(String(item.start_date || "").slice(5, 7));
|
||||||
monthCounts.set(month, (monthCounts.get(month) || 0) + 1);
|
monthCounts.set(month, (monthCounts.get(month) || 0) + 1);
|
||||||
});
|
});
|
||||||
const monthRail = create("div", "calendar-month-rail");
|
const monthRail = create("div", "calendar-month-rail");
|
||||||
const allButton = create("button", `calendar-month-btn${state.calendarMonth === 0 ? " active" : ""}`);
|
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.type = "button";
|
||||||
allButton.addEventListener("click", () => {
|
allButton.addEventListener("click", () => {
|
||||||
state.calendarMonth = 0;
|
state.calendarMonth = 0;
|
||||||
@@ -942,12 +966,11 @@ function renderCalendar(data, container) {
|
|||||||
"back-to-school", "singles-day", "black-friday", "cyber-monday", "winter-holiday",
|
"back-to-school", "singles-day", "black-friday", "cyber-monday", "winter-holiday",
|
||||||
"christmas", "lunar-new-year", "eid-fitr", "super-bowl", "la28",
|
"christmas", "lunar-new-year", "eid-fitr", "super-bowl", "la28",
|
||||||
];
|
];
|
||||||
const highValueItems = yearItems
|
const highValueItems = displayYearItems
|
||||||
.filter((item) => item.business_relevance === "high" && isCalendarOpportunity(item, data.as_of_date))
|
.filter((item) => item.business_relevance === "high" && isCalendarOpportunity(item, today));
|
||||||
.sort((a, b) => String(a.start_date).localeCompare(String(b.start_date)));
|
const selected = sortCalendarItems(displayYearItems
|
||||||
const selected = yearItems
|
|
||||||
.filter((item) => !state.calendarMonth || Number(String(item.start_date || "").slice(5, 7)) === state.calendarMonth)
|
.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));
|
if (state.calendarMonth) container.appendChild(renderCalendarMonthGrid(state.calendarYear, state.calendarMonth, selected));
|
||||||
const highlightPool = state.calendarMonth
|
const highlightPool = state.calendarMonth
|
||||||
? highValueItems.filter((item) => Number(String(item.start_date).slice(5, 7)) === 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) {
|
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 };
|
||||||
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 };
|
|
||||||
|
|||||||
@@ -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 { 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) {
|
function makeExhibitionCard(item) {
|
||||||
const article = create("article", "exhibition-card");
|
const article = create("article", "exhibition-card");
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { state } from "./render-intelligence-state.js?v=20260814e";
|
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 { renderExhibitions, renderPolicies, renderStandards, renderTrendNews, renderTrendSearchHub, trendItems } from "./render-intelligence-scenes.js?v=20260814i";
|
||||||
import { renderActivities } from "./render-intelligence-activities.js?v=20260814h";
|
import { renderActivities } from "./render-intelligence-activities.js?v=20260814h";
|
||||||
|
import { renderPlatforms } from "./render-intelligence-platforms.js?v=20260815a";
|
||||||
|
|
||||||
function renderStats(data, container) {
|
function renderStats(data, container) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -48,6 +49,8 @@ function renderTabs(data, container, render) {
|
|||||||
|
|
||||||
function renderRegionOptions(data, select) {
|
function renderRegionOptions(data, select) {
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
const regionField = typeof select.closest === "function" ? select.closest("label") : null;
|
||||||
|
if (regionField) regionField.hidden = state.activeSection === "calendar";
|
||||||
const previous = state.region;
|
const previous = state.region;
|
||||||
select.innerHTML = "";
|
select.innerHTML = "";
|
||||||
if (state.activeSection === "calendar") {
|
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="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 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/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>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -78,6 +78,6 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+23
-5
@@ -10,11 +10,12 @@ test("独立首页只展示家纺情报并加载独立入口", async () => {
|
|||||||
const dongguanCss = await text("public/assets/fonts/dongguan-bold/result.css");
|
const dongguanCss = await text("public/assets/fonts/dongguan-bold/result.css");
|
||||||
assert.match(html, /三笙AI情报局/);
|
assert.match(html, /三笙AI情报局/);
|
||||||
assert.match(html, /assets\/js\/app\.js/);
|
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\/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, /<details[^>]+class="intelligence-metrics"/);
|
||||||
assert.match(html, /<summary[^>]+class="intelligence-utility-row"/);
|
assert.match(html, /<summary[^>]+class="intelligence-utility-row"/);
|
||||||
|
assert.doesNotMatch(html, /id="intelligenceShareBtn"/);
|
||||||
assert.match(html, /class="tool-icon"/);
|
assert.match(html, /class="tool-icon"/);
|
||||||
assert.match(html, /assets\/fonts\/dongguan-bold\/result\.css/);
|
assert.match(html, /assets\/fonts\/dongguan-bold\/result\.css/);
|
||||||
assert.doesNotMatch(html, /cn-font\.claude-code-best\.win/);
|
assert.doesNotMatch(html, /cn-font\.claude-code-best\.win/);
|
||||||
@@ -33,7 +34,7 @@ test("数据快照不复用浏览器旧缓存", async () => {
|
|||||||
|
|
||||||
test("七个业务入口与趋势四子类均保留", async () => {
|
test("七个业务入口与趋势四子类均保留", async () => {
|
||||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
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 ["今日必看", "备货日历", "展会通", "政策预警", "趋势雷达", "平台渠道", "跨境活动"]) {
|
for (const label of ["今日必看", "备货日历", "展会通", "政策预警", "趋势雷达", "平台渠道", "跨境活动"]) {
|
||||||
assert.match(modules, new RegExp(label));
|
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.js"),
|
||||||
await text("public/assets/js/render-intelligence-core.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-scenes.js"),
|
||||||
|
await text("public/assets/js/render-intelligence-platforms.js"),
|
||||||
await text("public/assets/js/render-intelligence-activities.js"),
|
await text("public/assets/js/render-intelligence-activities.js"),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
assert.match(files, /target\s*=\s*["']_blank["']/);
|
assert.match(files, /target\s*=\s*["']_blank["']/);
|
||||||
@@ -67,7 +69,7 @@ test("所有外部业务链接使用新窗口和安全rel", async () => {
|
|||||||
|
|
||||||
test("趋势分类读取真实 items 数组而不是分类对象", async () => {
|
test("趋势分类读取真实 items 数组而不是分类对象", async () => {
|
||||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
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, /category\.items/);
|
||||||
assert.match(modules, /item\.image\s*\|\|\s*item\.thumbnail/);
|
assert.match(modules, /item\.image\s*\|\|\s*item\.thumbnail/);
|
||||||
assert.match(modules, /function renderCalendarMonthGrid/);
|
assert.match(modules, /function renderCalendarMonthGrid/);
|
||||||
@@ -153,9 +155,23 @@ test("跨境活动按访问当天重新计算相对日期并排除已结束活
|
|||||||
assert.equal(activityDayOffset("2026-08-15", now), 1);
|
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 () => {
|
test("最新家纺交互与移动端契约已同步", async () => {
|
||||||
const renderer = await text("public/assets/js/render-intelligence.js");
|
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 baseCss = await text("public/assets/css/base.css");
|
||||||
const css = await text("public/assets/css/intelligence.css");
|
const css = await text("public/assets/css/intelligence.css");
|
||||||
assert.doesNotMatch(renderer, /displayLimit/);
|
assert.doesNotMatch(renderer, /displayLimit/);
|
||||||
@@ -171,6 +187,8 @@ test("最新家纺交互与移动端契约已同步", async () => {
|
|||||||
assert.doesNotMatch(modules, /家纺成熟度|新兴参考/);
|
assert.doesNotMatch(modules, /家纺成熟度|新兴参考/);
|
||||||
assert.doesNotMatch(modules, /scope_type === "state\.region"|\["state\.region", "地区政策"\]/);
|
assert.doesNotMatch(modules, /scope_type === "state\.region"|\["state\.region", "地区政策"\]/);
|
||||||
assert.match(modules, /scope_type === "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, /@media \(max-width: 680px\)/);
|
||||||
assert.match(css, /\.intelligence-tabs\s*\{[^}]*grid-template-columns: repeat\(3/);
|
assert.match(css, /\.intelligence-tabs\s*\{[^}]*grid-template-columns: repeat\(3/);
|
||||||
assert.match(css, /min-height: 48px/);
|
assert.match(css, /min-height: 48px/);
|
||||||
|
|||||||
Reference in New Issue
Block a user