feat: 投资资讯日报查询页 + 文档更新 + 拉取服务器最新内容
- 新增 charts/news_reports.php 报告查询导航页(列表+详情一体,fetch API 渲染) - 新增 js/newsReports.js:AI 摘要/新闻联播/财经新闻/公告调研/数据总览固定模块,stats 归一化兼容多种格式 - index-2.php 研究报告卡片改为导航链接,移除 PHP 目录下拉框 - 更新 AGENTS.md/CLAUDE.md(路径、页面清单、数据模式等核实修正) - 同步服务器拉取的文档:quant 使用手册、podcast-docs - 生成 continuation.md 会话检查点
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* 投资资讯日报 前端逻辑
|
||||
* 数据源: 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 += '' +
|
||||
'<div class="report-card aos-init" data-id="' + r.id + '" onclick="NewsReports.openDetail(' + r.id + ')">' +
|
||||
'<div class="flex items-start justify-between gap-3">' +
|
||||
'<div class="flex items-center gap-2 flex-wrap">' +
|
||||
'<span class="badge ' + typeCls + '">' + typeName + '</span>' +
|
||||
'<span class="text-lg font-bold text-dark">' + r.report_date + '</span>' +
|
||||
'<span class="text-sm text-gray-400"><i class="fa-regular fa-clock mr-1"></i>' + self.fmtTime(r.generated_at) + ' 生成</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn-view"><i class="fa-solid fa-book-open mr-1"></i>查看</button>' +
|
||||
'</div>' +
|
||||
'<p class="mt-2.5 text-base text-gray-600 leading-relaxed line-clamp-3">' + summary + '</p>' +
|
||||
(statBits ? '<div class="mt-3 flex flex-wrap gap-2">' + statBits + '</div>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
$('#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('<span class="stat-bit"><i class="fa-solid fa-tower-broadcast text-sky-600"></i>新闻联播 ' + s.xwlb.total + '</span>');
|
||||
if (s.news && s.news.total) bits.push('<span class="stat-bit"><i class="fa-solid fa-newspaper text-indigo-600"></i>财经新闻 ' + s.news.total + '</span>');
|
||||
if (s.cninfo && s.cninfo.total) bits.push('<span class="stat-bit"><i class="fa-solid fa-clipboard-list text-emerald-600"></i>公告调研 ' + s.cninfo.total + '</span>');
|
||||
if (s.source_dist && s.source_dist.length) bits.push('<span class="stat-bit"><i class="fa-solid fa-globe text-teal-600"></i>来源 ' + s.source_dist.length + '</span>');
|
||||
if (s.sources) bits.push('<span class="stat-bit"><i class="fa-solid fa-globe text-teal-600"></i>来源 ' + Object.keys(s.sources).length + '</span>');
|
||||
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('<span class="stat-bit"><i class="fa-solid fa-database text-slate-600"></i>' + pKeys[i] + ' ' + pv + '</span>');
|
||||
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('<div class="empty-box"><i class="fa-solid fa-inbox"></i><p>该时间段暂无日报</p></div>');
|
||||
$('#rangeInfo').text('');
|
||||
this.hideLoading();
|
||||
},
|
||||
|
||||
renderError: function (msg, xhr) {
|
||||
var detail = '';
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.error) detail = xhr.responseJSON.error;
|
||||
$('#reportList').html('<div class="empty-box"><i class="fa-solid fa-triangle-exclamation text-red-500"></i><p>' + msg + '</p>' + (detail ? '<p class="text-sm text-gray-400">' + detail + '</p>' : '') + '</div>');
|
||||
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(
|
||||
'<span class="badge ' + typeCls + '">' + typeName + '</span>' +
|
||||
'<span class="text-xl font-bold text-dark">' + r.report_date + ' 日报</span>' +
|
||||
'<span class="text-sm text-gray-400"><i class="fa-regular fa-clock mr-1"></i>' + this.fmtTime(r.generated_at) + ' 生成</span>'
|
||||
);
|
||||
|
||||
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 '<li>' + selfEscape(s) + '</li>'; }).join('');
|
||||
return '' +
|
||||
'<div class="module-card">' +
|
||||
'<div class="module-title"><i class="fa-solid fa-wand-magic-sparkles text-indigo-600"></i>AI 摘要</div>' +
|
||||
'<ul class="ai-summary-list">' + lis + '</ul>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
// 事件模块
|
||||
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 '' +
|
||||
'<div class="module-card">' +
|
||||
'<div class="module-title"><i class="fa-solid ' + icon + ' text-indigo-600"></i>' + name +
|
||||
'<span class="module-sub">共 ' + events.length + ' 条' + (imp ? ' · 重要(≥4) ' + imp + ' 条' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="divide-y divide-gray-100">' + rows + '</div>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
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 = '<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 (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 '' +
|
||||
'<div class="event-row">' +
|
||||
'<div class="event-left">' +
|
||||
'<span class="event-rank">' + idx + '</span>' +
|
||||
'<span class="badge ' + sen.cls + '" title="' + sen.txt + '">' + sen.icon + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="event-body">' +
|
||||
'<div class="event-title-line">' +
|
||||
'<span class="imp-badge ' + impCls + '">' + (e.importance != null ? e.importance : '-') + '</span>' +
|
||||
'<div class="event-title">' + title + '</div>' +
|
||||
'</div>' +
|
||||
summary +
|
||||
(meta.length ? '<div class="event-meta">' + meta.join('') + '</div>' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
// ==================== 数据总览 ====================
|
||||
// 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 = '<div class="module-card">' +
|
||||
'<div class="module-title"><i class="fa-solid fa-chart-simple text-indigo-600"></i>数据总览</div>';
|
||||
|
||||
// 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 += '<div class="stats-grid">';
|
||||
nums.forEach(function (k) {
|
||||
var v = s.pipeline[k];
|
||||
if (v == null) return;
|
||||
html += '<div class="stat-card"><div class="stat-num">' + v + '</div><div class="stat-label">' + selfEscape(String(k).replace(/^M\d+\s*/, 'M')) + '</div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
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 += '<div class="stats-sub-title">' + selfEscape(label) + '</div><div class="source-grid">';
|
||||
Object.keys(v).forEach(function (sk) {
|
||||
html += '<div class="source-item"><div class="s-count">' + v[sk] + '</div><div class="s-name">' + selfEscape(sk) + '</div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// 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 += '</div>';
|
||||
return html;
|
||||
},
|
||||
|
||||
renderFinanceNewsStats: function (news) {
|
||||
var html = '<div class="stats-sub">';
|
||||
if (news.total != null) html += '<div class="stat-card"><div class="stat-num">' + news.total + '</div><div class="stat-label">新闻总量</div></div>';
|
||||
if (news.hi_threshold != null) html += '<div class="stat-card"><div class="stat-num">' + news.hi_threshold + '</div><div class="stat-label">重要(≥阈值)</div></div>';
|
||||
html += '</div>';
|
||||
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 '<span class="badge badge-neu">' + selfEscape(s) + '</span>'; }).join('');
|
||||
return '<div class="stats-sub-title">情感分布</div><div class="flex flex-wrap gap-2 mb-3">' + items + '</div>';
|
||||
}
|
||||
return this.renderSentimentBar(data);
|
||||
},
|
||||
|
||||
renderCninfoStats: function (cn) {
|
||||
var html = '<div class="stats-sub">';
|
||||
if (cn.total != null) html += '<div class="stat-card"><div class="stat-num">' + cn.total + '</div><div class="stat-label">公告调研总量</div></div>';
|
||||
if (cn.hi_threshold != null) html += '<div class="stat-card"><div class="stat-num">' + cn.hi_threshold + '</div><div class="stat-label">重要(≥阈值)</div></div>';
|
||||
if (cn.announcement != null) html += '<div class="stat-card"><div class="stat-num">' + cn.announcement + '</div><div class="stat-label">公司公告</div></div>';
|
||||
if (cn.research != null) html += '<div class="stat-card"><div class="stat-num">' + cn.research + '</div><div class="stat-label">调研报告</div></div>';
|
||||
if (cn.irm != null) html += '<div class="stat-card"><div class="stat-num">' + cn.irm + '</div><div class="stat-label">互动易</div></div>';
|
||||
html += '</div>';
|
||||
if (cn.by_day) {
|
||||
var days = Object.keys(cn.by_day).sort();
|
||||
if (days.length) {
|
||||
html += '<div class="kv-table-wrap"><table class="kv-table"><thead><tr><th>日期</th><th>数量</th></tr></thead><tbody>';
|
||||
days.forEach(function (d) { html += '<tr><td>' + selfEscape(d) + '</td><td>' + cn.by_day[d] + '</td></tr>'; });
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
},
|
||||
|
||||
renderXwlbStats: function (xw) {
|
||||
var html = '<div class="stats-sub">';
|
||||
if (xw.total != null) html += '<div class="stat-card"><div class="stat-num">' + xw.total + '</div><div class="stat-label">新闻联播条数</div></div>';
|
||||
if (xw.date) html += '<div class="stat-card"><div class="stat-num text-sm">' + selfEscape(xw.date) + '</div><div class="stat-label">联播日期</div></div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
},
|
||||
|
||||
renderKVTable: function (title, data) {
|
||||
var items = this.normalizeKV(data);
|
||||
if (!items.length) return '';
|
||||
var rows = items.map(function (it) {
|
||||
return '<tr><td>' + selfEscape(it.label) + '</td><td>' + (it.value === '' ? '' : selfEscape(it.value)) + '</td></tr>';
|
||||
}).join('');
|
||||
return '<div class="kv-table-wrap"><div class="kv-title">' + selfEscape(title) + '</div>' +
|
||||
'<table class="kv-table"><thead><tr><th>类别</th><th>数量</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
|
||||
},
|
||||
|
||||
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 '' +
|
||||
'<div class="sentiment-wrap">' +
|
||||
'<div class="sentiment-bar"><div class="s-pos" style="width:' + pct(pos) + '"></div><div class="s-neg" style="width:' + pct(neg) + '"></div><div class="s-neu" style="width:' + pct(neu) + '"></div></div>' +
|
||||
'<div class="sentiment-legend">' +
|
||||
'<span><i class="s-dot s-pos"></i>利好 ' + pos + ' (' + pct(pos) + ')</span>' +
|
||||
'<span><i class="s-dot s-neg"></i>利空 ' + neg + ' (' + pct(neg) + ')</span>' +
|
||||
'<span><i class="s-dot s-neu"></i>中性 ' + neu + ' (' + pct(neu) + ')</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
renderSources: function (sources) {
|
||||
var self = this;
|
||||
var keys = Object.keys(sources);
|
||||
if (!keys.length) return '';
|
||||
var items = keys.map(function (k) {
|
||||
return '<div class="source-item"><div class="s-count">' + sources[k] + '</div><div class="s-name">' + selfEscape(k) + '</div></div>';
|
||||
}).join('');
|
||||
return '<div class="stats-sub-title">各源数据</div><div class="source-grid">' + items + '</div>';
|
||||
},
|
||||
|
||||
// ==================== 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, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
$(function () {
|
||||
NewsReports.init();
|
||||
});
|
||||
Reference in New Issue
Block a user