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:
+34
-1
@@ -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
@@ -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]] });
|
||||
|
||||
Reference in New Issue
Block a user