/** * 投资资讯日报 前端逻辑 * 数据源: 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 '' + '
    ' + '
    AI 摘要
    ' + '' + '
    '; }, // 事件模块 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 += '
    ' + news.total + '
    新闻总量
    '; 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 += '
    ' + cn.total + '
    公告调研总量
    '; if (cn.hi_threshold != null) html += '
    ' + cn.hi_threshold + '
    重要(≥阈值)
    '; if (cn.announcement != null) html += '
    ' + cn.announcement + '
    公司公告
    '; if (cn.research != null) html += '
    ' + cn.research + '
    调研报告
    '; if (cn.irm != null) html += '
    ' + cn.irm + '
    互动易
    '; html += '
    '; if (cn.by_day) { var days = Object.keys(cn.by_day).sort(); if (days.length) { html += '
    '; days.forEach(function (d) { html += ''; }); html += '
    日期数量
    ' + selfEscape(d) + '' + cn.by_day[d] + '
    '; } } return html; }, renderXwlbStats: function (xw) { var html = '
    '; if (xw.total != null) html += '
    ' + xw.total + '
    新闻联播条数
    '; 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) + '
    ' + '' + rows + '
    类别数量
    '; }, 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(); });