feat: 重要事件聚合视图 + stats 英文键格式加固

- normalizeKV 显式识别 intl 英文键格式(importance/count、event_type/count、source/count),不再依赖键顺序
- 新增"重要事件"视图 Tab:跨日报聚合(/api/news/events/),支持 天数/重要度/类型 筛选,复用事件行渲染
- 兼容文档 v1.1:intl sentiment 对象格式、finance news.event_types 对象格式
This commit is contained in:
2026-08-06 09:25:16 +08:00
parent f8bca284a8
commit 4da1fc9301
2 changed files with 110 additions and 3 deletions
+34 -1
View File
@@ -35,7 +35,7 @@
.badge-neg { @apply bg-red-100 text-red-700; }
.badge-neu { @apply bg-gray-200 text-gray-600; }
.type-tab {
.type-tab, .view-tab {
@apply px-4 py-2 rounded-lg text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/5 transition-colors cursor-pointer;
}
.tab-active { @apply bg-primary text-white hover:text-white hover:bg-primary shadow-sm; }
@@ -148,6 +148,10 @@
<!-- 筛选栏 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-6">
<div class="flex flex-wrap items-center gap-3">
<div class="flex bg-gray-100 rounded-lg p-1">
<span class="view-tab tab-active" data-view="list">日报列表</span>
<span class="view-tab" data-view="events">重要事件</span>
</div>
<div class="flex bg-gray-100 rounded-lg p-1">
<span class="type-tab tab-active" data-type="">全部</span>
<span class="type-tab" data-type="finance">国内</span>
@@ -169,6 +173,35 @@
<div id="reportList" class="space-y-4"></div>
</div>
<!-- 重要事件聚合视图 -->
<div id="eventsView" class="hidden">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-4">
<div class="flex flex-wrap items-center gap-3">
<span class="text-sm text-gray-500"></span>
<select id="evDays" class="filter-input">
<option value="7" selected>7</option>
<option value="14">14</option>
<option value="30">30</option>
</select>
<span class="text-sm text-gray-500"> · 重要度 </span>
<select id="evImportance" class="filter-input">
<option value="4" selected>4</option>
<option value="5">5</option>
<option value="3">3</option>
</select>
<span class="text-sm text-gray-500">类型</span>
<select id="evType" class="filter-input">
<option value="">全部</option>
<option value="finance">国内</option>
<option value="intl">国际</option>
</select>
<button id="btnEvQuery" class="btn-primary"><i class="fa-solid fa-magnifying-glass mr-1"></i>查询</button>
</div>
<div class="mt-3 text-sm text-gray-400" id="eventsInfo"></div>
</div>
<div id="eventsList"></div>
</div>
<!-- 详情视图 -->
<div id="detailView" class="hidden">
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
+76 -2
View File
@@ -14,7 +14,7 @@ var NewsReports = {
SECTION_ICONS: { xwlb: 'fa-tower-broadcast', news: 'fa-fire', cninfo: 'fa-clipboard-list', intl: 'fa-earth-asia' },
SENTIMENT: { positive: { icon: '🟢', cls: 'badge-pos', txt: '利好' }, negative: { icon: '🔴', cls: 'badge-neg', txt: '利空' }, neutral: { icon: '⚪', cls: 'badge-neu', txt: '中性' } },
state: { reportType: '', startDate: '', endDate: '', currentId: null },
state: { reportType: '', startDate: '', endDate: '', currentId: null, eventsLoaded: false },
init: function () {
var now = new Date();
@@ -39,6 +39,12 @@ var NewsReports = {
bindEvents: function () {
var self = this;
// 视图切换 (日报列表 / 重要事件)
$('.view-tab').on('click', function () {
$('.view-tab').removeClass('tab-active');
$(this).addClass('tab-active');
self.switchView($(this).data('view'));
});
// 类型 Tabs
$('.type-tab').on('click', function () {
$('.type-tab').removeClass('tab-active');
@@ -52,12 +58,73 @@ var NewsReports = {
self.state.endDate = $('#endDate').val() || self.state.endDate;
self.loadReports();
});
// 重要事件查询
$('#btnEvQuery').on('click', function () {
self.loadEvents();
});
// 返回列表
$('#btnBack').on('click', function () {
self.showList();
});
},
// ==================== 重要事件聚合 ====================
switchView: function (view) {
if (view === 'events') {
$('#listView').addClass('hidden');
$('#detailView').addClass('hidden');
$('#eventsView').removeClass('hidden');
if (!this.state.eventsLoaded) this.loadEvents();
} else {
$('#eventsView').addClass('hidden');
$('#listView').removeClass('hidden');
}
},
loadEvents: function () {
var self = this;
this.showLoading('正在加载重要事件...');
var days = $('#evDays').val() || 7;
var imp = $('#evImportance').val() || 4;
var type = $('#evType').val() || '';
var url = this.API_EVENTS + '?days=' + days + '&importance=' + imp + '&limit=300';
if (type) url += '&report_type=' + type;
$.getJSON(url)
.done(function (data) {
self.renderEvents(data);
self.state.eventsLoaded = true;
self.hideLoading();
})
.fail(function (xhr) {
self.renderEventsError(xhr);
self.hideLoading();
});
},
renderEvents: function (list) {
var self = this;
if (!Array.isArray(list) || list.length === 0) {
$('#eventsList').html('<div class="empty-box"><i class="fa-solid fa-inbox"></i><p>该条件下暂无重要事件</p></div>');
$('#eventsInfo').text('');
return;
}
$('#eventsInfo').text('共 ' + list.length + ' 条重要事件(按重要度、日期降序)');
var rows = list.map(function (e, i) {
var typeName = self.TYPE_NAMES[e.report_type] || e.report_type;
var secName = self.SECTION_NAMES[e.section] || e.section;
var extra = ['<span class="meta-item"><i class="fa-regular fa-calendar mr-1"></i>' + selfEscape(e.report_date) + ' · ' + selfEscape(typeName) + ' · ' + selfEscape(secName) + '</span>'];
return self.renderEventRow(e, i + 1, extra);
}).join('');
$('#eventsList').html('<div class="module-card"><div class="module-title"><i class="fa-solid fa-fire text-indigo-600"></i>重要事件聚合<span class="module-sub">跨日报检索 · 重要度 ≥ ' + $('#evImportance').val() + '</span></div><div class="divide-y divide-gray-100">' + rows + '</div></div>');
},
renderEventsError: function (xhr) {
var detail = '';
if (xhr && xhr.responseJSON && xhr.responseJSON.error) detail = xhr.responseJSON.error;
$('#eventsList').html('<div class="empty-box"><i class="fa-solid fa-triangle-exclamation text-red-500"></i><p>重要事件加载失败</p>' + (detail ? '<p class="text-sm text-gray-400">' + selfEscape(detail) + '</p>' : '') + '</div>');
$('#eventsInfo').text('');
},
// ==================== 列表 ====================
loadReports: function () {
var self = this;
@@ -230,13 +297,14 @@ var NewsReports = {
'</div>';
},
renderEventRow: function (e, idx) {
renderEventRow: function (e, idx, extraMeta) {
var sen = this.SENTIMENT[e.sentiment] || this.SENTIMENT.neutral;
var impCls = e.importance >= 5 ? 'imp-5' : (e.importance >= 4 ? 'imp-4' : '');
var title = selfEscape(e.title || '');
if (e.url) title = '<a href="' + selfEscape(e.url) + '" target="_blank" rel="noopener" class="event-link">' + title + '<i class="fa-solid fa-up-right-from-square ml-1 text-xs"></i></a>';
var summary = e.summary ? '<p class="event-summary">' + selfEscape(e.summary) + '</p>' : '';
var meta = [];
if (extraMeta) meta = meta.concat(extraMeta);
if (e.event_type) meta.push('<span class="meta-item"><i class="fa-regular fa-tag mr-1"></i>' + selfEscape(e.event_type) + '</span>');
if (e.source) meta.push('<span class="meta-item"><i class="fa-regular fa-newspaper mr-1"></i>' + selfEscape(e.source) + '</span>');
return '' +
@@ -291,6 +359,12 @@ var NewsReports = {
}
return;
}
if ('count' in item) {
// intl 英文键格式: {"importance":2,"count":2} / {"event_type":"宏观","count":15} / {"source":"X","count":30}
var lk2 = Object.keys(item).filter(function (k) { return k !== 'count'; })[0];
out.push({ label: lk2 ? item[lk2] : '—', value: item['count'] });
return;
}
var keys = Object.keys(item);
if (keys.length >= 2) out.push({ label: item[keys[0]], value: item[keys[1]] });
else if (keys.length === 1) out.push({ label: keys[0], value: item[keys[0]] });