/**
* 投资资讯日报 前端逻辑
* 数据源: https://api.doorcome.cn/api/news/reports/ (日报列表/详情)
* https://api.doorcome.cn/api/news/events/ (重要事件聚合, 预留)
* 模式 B: 页面加载后由 JS fetch 获取数据并渲染, 无服务端注入
*/
var NewsReports = {
API_REPORTS: 'https://api.doorcome.cn/api/news/reports/',
API_EVENTS: 'https://api.doorcome.cn/api/news/events/',
// 类型/section 中文映射
TYPE_NAMES: { finance: '国内投资资讯', intl: '国际投资资讯' },
SECTION_NAMES: { xwlb: '新闻联播要闻', news: '重要事件 · 财经新闻', cninfo: '重要事件 · 公告/调研/互动', intl: '国际要闻' },
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 },
init: function () {
var now = new Date();
var start = new Date(now.getTime() - 30 * 86400000);
this.state.startDate = this.fmtDate(start);
this.state.endDate = this.fmtDate(now);
this.bindEvents();
this.loadReports();
},
fmtDate: function (d) {
return d.getFullYear() + '-' + this.pad2(d.getMonth() + 1) + '-' + this.pad2(d.getDate());
},
pad2: function (n) { return n < 10 ? '0' + n : '' + n; },
// 时间显示: 2026-08-03T07:00:00 -> 2026-08-03 07:00
fmtTime: function (iso) {
if (!iso) return '';
return iso.replace('T', ' ').substring(0, 16);
},
bindEvents: function () {
var self = this;
// 类型 Tabs
$('.type-tab').on('click', function () {
$('.type-tab').removeClass('tab-active');
$(this).addClass('tab-active');
self.state.reportType = $(this).data('type');
self.loadReports();
});
// 查询按钮
$('#btnQuery').on('click', function () {
self.state.startDate = $('#startDate').val() || self.state.startDate;
self.state.endDate = $('#endDate').val() || self.state.endDate;
self.loadReports();
});
// 返回列表
$('#btnBack').on('click', function () {
self.showList();
});
},
// ==================== 列表 ====================
loadReports: function () {
var self = this;
this.showLoading('正在加载日报列表...');
var url = this.API_REPORTS + '?start_date=' + this.state.startDate + '&end_date=' + this.state.endDate;
if (this.state.reportType) url += '&report_type=' + this.state.reportType;
$.getJSON(url)
.done(function (data) {
if (!Array.isArray(data) || data.length === 0) {
self.renderEmpty();
return;
}
self.state.reports = data;
self.renderList(data);
})
.fail(function (xhr) {
self.renderError('列表加载失败', xhr);
});
},
renderList: function (reports) {
var self = this;
var html = '';
reports.forEach(function (r) {
var typeName = self.TYPE_NAMES[r.report_type] || r.report_type;
var typeCls = r.report_type === 'finance' ? 'badge-finance' : 'badge-intl';
var summary = self.truncate(r.ai_summary || '', 140);
var statBits = self.listStatBits(r);
html += '' +
'
' +
'
' +
'
' +
'' + typeName + ' ' +
'' + r.report_date + ' ' +
' ' + self.fmtTime(r.generated_at) + ' 生成 ' +
'
' +
'
查看' +
'
' +
'
' + summary + '
' +
(statBits ? '
' + statBits + '
' : '') +
'
';
});
$('#reportList').html(html);
this.hideLoading();
this.updateRangeInfo(reports);
},
// 列表卡片上的统计数据小徽章 (防御性读取)
listStatBits: function (r) {
var bits = [];
var s = r.stats || {};
if (s.xwlb && s.xwlb.total) bits.push(' 新闻联播 ' + s.xwlb.total + ' ');
if (s.news && s.news.total) bits.push(' 财经新闻 ' + s.news.total + ' ');
if (s.cninfo && s.cninfo.total) bits.push(' 公告调研 ' + s.cninfo.total + ' ');
if (s.source_dist && s.source_dist.length) bits.push(' 来源 ' + s.source_dist.length + ' ');
if (s.sources) bits.push(' 来源 ' + Object.keys(s.sources).length + ' ');
if (s.pipeline) {
var p = s.pipeline;
var pKeys = Object.keys(p);
for (var i = pKeys.length - 1; i >= 0; i--) {
var pv = p[pKeys[i]];
if (pv == null || typeof pv === 'object') continue;
bits.push(' ' + pKeys[i] + ' ' + pv + ' ');
break;
}
}
return bits.join('');
},
truncate: function (txt, n) {
txt = (txt || '').replace(/\s+/g, ' ').trim();
if (!txt) return '(无摘要)';
return txt.length > n ? txt.substring(0, n) + '…' : txt;
},
updateRangeInfo: function (reports) {
var dates = reports.map(function (r) { return r.report_date; });
$('#rangeInfo').text('共 ' + reports.length + ' 份日报' + (dates.length ? ' · ' + dates[0] + ' ~ ' + dates[dates.length - 1] : ''));
},
renderEmpty: function () {
$('#reportList').html('');
$('#rangeInfo').text('');
this.hideLoading();
},
renderError: function (msg, xhr) {
var detail = '';
if (xhr && xhr.responseJSON && xhr.responseJSON.error) detail = xhr.responseJSON.error;
$('#reportList').html('' + msg + '
' + (detail ? '
' + detail + '
' : '') + '
');
this.hideLoading();
},
// ==================== 详情 ====================
openDetail: function (id) {
var self = this;
this.state.currentId = id;
this.showDetail();
this.showLoading('正在加载日报详情...');
$('#detailContent').html('');
$.getJSON(this.API_REPORTS + '?id=' + id)
.done(function (r) {
self.renderDetail(r);
self.hideLoading();
window.scrollTo({ top: 0, behavior: 'smooth' });
})
.fail(function (xhr) {
self.renderError('日报详情加载失败', xhr);
self.hideLoading();
});
},
renderDetail: function (r) {
var self = this;
var typeName = this.TYPE_NAMES[r.report_type] || r.report_type;
var typeCls = r.report_type === 'finance' ? 'badge-finance' : 'badge-intl';
// 头部
$('#detailHeader').html(
'' + typeName + ' ' +
'' + r.report_date + ' 日报 ' +
' ' + this.fmtTime(r.generated_at) + ' 生成 '
);
var html = '';
// ① AI 摘要
html += this.renderSummary(r.ai_summary);
// ② 事件模块 (固定顺序: 国内 xwlb→news→cninfo, 国际 intl)
var sections = r.report_type === 'finance' ? ['xwlb', 'news', 'cninfo'] : ['intl'];
var events = r.events || [];
sections.forEach(function (sec) {
var secEvents = events.filter(function (e) { return e.section === sec; });
if (secEvents.length === 0) return;
html += self.renderEventModule(sec, secEvents);
});
// ③ 数据总览
html += this.renderStats(r.stats);
$('#detailContent').html(html);
this.bindEventHover();
},
// AI 摘要: 按条目分行渲染
renderSummary: function (summary) {
if (!summary) return '';
var items = summary.split('\n').map(function (s) { return s.replace(/^[-*\s]+/, '').trim(); }).filter(Boolean);
if (items.length === 0) return '';
var lis = items.map(function (s) { return '' + selfEscape(s) + ' '; }).join('');
return '' +
'';
},
// 事件模块
renderEventModule: function (sec, events) {
var self = this;
var name = this.SECTION_NAMES[sec] || sec;
var icon = this.SECTION_ICONS[sec] || 'fa-list';
var imp = events.filter(function (e) { return e.importance >= 4; }).length;
var rows = events.map(function (e, i) { return self.renderEventRow(e, i + 1); }).join('');
return '' +
'' +
'
' + name +
'共 ' + events.length + ' 条' + (imp ? ' · 重要(≥4) ' + imp + ' 条' : '') + ' ' +
'
' +
'
' + rows + '
' +
'
';
},
renderEventRow: function (e, idx) {
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 = '' + title + ' ';
var summary = e.summary ? '' + selfEscape(e.summary) + '
' : '';
var meta = [];
if (e.event_type) meta.push(' ' + selfEscape(e.event_type) + ' ');
if (e.source) meta.push(' ' + selfEscape(e.source) + ' ');
return '' +
'' +
'
' +
'' + idx + ' ' +
'' + sen.icon + ' ' +
'
' +
'
' +
'
' +
'
' + (e.importance != null ? e.importance : '-') + ' ' +
'
' + title + '
' +
'
' +
summary +
(meta.length ? '
' + meta.join('') + '
' : '') +
'
' +
'
';
},
// ==================== 数据总览 ====================
// stats 格式随生成日期变化(2026-08 前后两套结构), 统一归一化后渲染:
// A. 字符串数组 ["🟢 利好 5 (11%)", ...] (intl / finance旧版 sentiment)
// B. 对象 {"1": 81, "2": 265} / {"东方财富": 16} (importances / sources)
// C. 对象数组 [{"重要度":"等级 2","数量":"4"}] 或合并格式 [{"重要度":"数量","等级 1":"46"}]
normalizeKV: function (data) {
var out = [];
if (!data) return out;
if (Array.isArray(data)) {
data.forEach(function (item) {
if (item == null) return;
if (typeof item === 'string' || typeof item === 'number') {
out.push({ label: String(item), value: '' });
} else if (typeof item === 'object') {
if (item['重要度'] === '数量') {
// 合并格式: {"重要度":"数量","等级 1":"46",...} → 展开各等级键
Object.keys(item).forEach(function (k) {
if (k === '重要度') return;
out.push({ label: k, value: item[k] });
});
return;
}
if ('数量' in item) {
if (item['数量'] === '数量') {
// 合并格式: {"重要度":"数量","等级 1":"46",...} → 展开各等级键
Object.keys(item).forEach(function (k) {
if (k === '重要度') return;
out.push({ label: k, value: item[k] });
});
} else {
var lk = Object.keys(item).filter(function (k) { return k !== '数量'; })[0];
out.push({ label: lk ? item[lk] : '—', value: item['数量'] });
}
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]] });
}
});
} else if (typeof data === 'object') {
Object.keys(data).forEach(function (k) {
var v = data[k];
if (v == null || typeof v === 'object') return; // 嵌套对象跳过, 单独处理
out.push({ label: k, value: v });
});
}
return out;
},
renderStats: function (s) {
if (!s) return '';
var html = '' +
'
数据总览
';
// pipeline 管道 (M1→M6 / raw_total 两种键名, 嵌套对象为来源明细)
if (s.pipeline) {
var keys = Object.keys(s.pipeline);
var nums = keys.filter(function (k) { return typeof s.pipeline[k] !== 'object'; });
if (nums.length) {
html += '
';
nums.forEach(function (k) {
var v = s.pipeline[k];
if (v == null) return;
html += '
' + v + '
' + selfEscape(String(k).replace(/^M\d+\s*/, 'M')) + '
';
});
html += '
';
}
keys.forEach(function (k) {
var v = s.pipeline[k];
if (v == null || typeof v !== 'object') return;
var label = String(k).replace(/^M\d+\s*/, '');
html += '
' + selfEscape(label) + '
';
Object.keys(v).forEach(function (sk) {
html += '
' + v[sk] + '
' + selfEscape(sk) + '
';
});
html += '
';
});
}
// finance: news / cninfo / xwlb 统计
if (s.news) html += this.renderFinanceNewsStats(s.news);
if (s.cninfo) html += this.renderCninfoStats(s.cninfo);
if (s.xwlb) html += this.renderXwlbStats(s.xwlb);
// 顶层: sentiment / importance / event_types / source_dist / sources (intl & finance旧版)
if (s.sentiment && s.sentiment.length) html += this.renderSentiment(s.sentiment);
if (s.importance && s.importance.length) html += this.renderKVTable('重要度分布', s.importance);
if (s.event_types && s.event_types.length) html += this.renderKVTable('事件类型分布', s.event_types);
if (s.source_dist && s.source_dist.length) html += this.renderKVTable('来源分布', s.source_dist);
if (s.sources) html += this.renderSources(s.sources);
html += '
';
return html;
},
renderFinanceNewsStats: function (news) {
var html = '';
if (news.total != null) html += '
';
if (news.hi_threshold != null) html += '
' + news.hi_threshold + '
重要(≥阈值)
';
html += '
';
if (news.sentiments) html += this.renderSentimentBar(news.sentiments);
if (news.importances) html += this.renderKVTable('新闻重要度', news.importances);
if (news.event_types && news.event_types.length) html += this.renderKVTable('新闻事件类型', news.event_types);
return html;
},
// sentiment: 字符串数组(已含emoji/百分比, intl & finance旧版) 或 对象(finance新版 news.sentiments)
renderSentiment: function (data) {
if (data.length && typeof data[0] === 'string') {
var items = data.map(function (s) { return '' + selfEscape(s) + ' '; }).join('');
return '情感分布
' + items + '
';
}
return this.renderSentimentBar(data);
},
renderCninfoStats: function (cn) {
var html = '';
if (cn.total != null) html += '
';
if (cn.hi_threshold != null) html += '
' + cn.hi_threshold + '
重要(≥阈值)
';
if (cn.announcement != null) html += '
' + cn.announcement + '
公司公告
';
if (cn.research != null) html += '
';
if (cn.irm != null) html += '
';
html += '
';
if (cn.by_day) {
var days = Object.keys(cn.by_day).sort();
if (days.length) {
html += '日期 数量 ';
days.forEach(function (d) { html += '' + selfEscape(d) + ' ' + cn.by_day[d] + ' '; });
html += '
';
}
}
return html;
},
renderXwlbStats: function (xw) {
var html = '';
if (xw.total != null) html += '
';
if (xw.date) html += '
' + selfEscape(xw.date) + '
联播日期
';
html += '
';
return html;
},
renderKVTable: function (title, data) {
var items = this.normalizeKV(data);
if (!items.length) return '';
var rows = items.map(function (it) {
return '' + selfEscape(it.label) + ' ' + (it.value === '' ? '' : selfEscape(it.value)) + ' ';
}).join('');
return '' + selfEscape(title) + '
' +
'
';
},
renderSentimentBar: function (sent) {
var pos = sent.positive || sent.pos || 0;
var neg = sent.negative || sent.neg || 0;
var neu = sent.neutral || sent.neu || 0;
var total = pos + neg + neu;
if (!total) return '';
var pct = function (v) { return (v / total * 100).toFixed(1) + '%'; };
return '' +
'' +
'
' +
'
' +
' 利好 ' + pos + ' (' + pct(pos) + ') ' +
' 利空 ' + neg + ' (' + pct(neg) + ') ' +
' 中性 ' + neu + ' (' + pct(neu) + ') ' +
'
' +
'
';
},
renderSources: function (sources) {
var self = this;
var keys = Object.keys(sources);
if (!keys.length) return '';
var items = keys.map(function (k) {
return '' + sources[k] + '
' + selfEscape(k) + '
';
}).join('');
return '各源数据
' + items + '
';
},
// ==================== UI 控制 ====================
showList: function () {
$('#listView').removeClass('hidden');
$('#detailView').addClass('hidden');
$('#detailHeader').html('');
$('#detailContent').html('');
},
// 打开详情: 隐藏列表, 独占视口
showDetail: function () {
$('#listView').addClass('hidden');
$('#detailView').removeClass('hidden');
},
showLoading: function (msg) {
$('#loadingOverlay').removeClass('hidden');
$('#loadingMsg').text(msg || '加载中...');
},
hideLoading: function () {
$('#loadingOverlay').addClass('hidden');
},
bindEventHover: function () {
// 事件行悬停样式已由 CSS 处理, 无需额外逻辑 (保留钩子)
}
};
// HTML 转义 (防 XSS, 数据来自外部 API)
function selfEscape(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
$(function () {
NewsReports.init();
});