initial stock report portal
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Market Desk
|
||||||
|
|
||||||
|
一个用于浏览 OpenClaw / Hermes 股票研究 HTML 报告的静态网站。
|
||||||
|
|
||||||
|
## 上传报告
|
||||||
|
|
||||||
|
把报告放进下面的目录,并使用文件名:
|
||||||
|
|
||||||
|
```text
|
||||||
|
reports/<分类>/<YYYY>/<MM>/<YYYY-MM-DD>-<英文或拼音slug>.html
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
reports/daily-review/2026/08/2026-08-16-market-review.html
|
||||||
|
reports/intraday/2026/08/2026-08-16-ai-sector.html
|
||||||
|
```
|
||||||
|
|
||||||
|
分类目录支持:`daily`、`intraday`、`sector`、`stock`。上传后在服务器执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/build-index.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
然后刷新网站即可。OpenClaw/Hermes 可以把“上传 HTML + 执行索引脚本”作为固定流程;如果服务器不能运行 Node,可以在本地或自动化机器上生成 `data/reports.json` 后连同 HTML 一起上传。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
这是纯静态网站。将 `index.html`、`styles.css`、`app.js`、`data/`、`reports/` 上传到 Nginx、Apache、对象存储或任意静态托管服务的同一目录即可。
|
||||||
|
|
||||||
|
## 重要说明
|
||||||
|
|
||||||
|
报告 HTML 默认在新标签页打开,适合保留 OpenClaw/Hermes 生成的原始样式和脚本。后续可以在不改变报告目录和索引格式的前提下增加登录、全文搜索或上传接口。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
const state={reports:[],category:'all',year:2026,month:8,selected:null};
|
||||||
|
const $=s=>document.querySelector(s); const pad=n=>String(n).padStart(2,'0');
|
||||||
|
const categoryNames={all:'全部报告',daily:'每日复盘',intraday:'盘中项目',sector:'行业观察',stock:'个股跟踪'};
|
||||||
|
function formatDate(d){return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`}
|
||||||
|
function categoryKey(r){return r.category||'all'}
|
||||||
|
function visible(){const q=($('#search-input').value||'').trim().toLowerCase();return state.reports.filter(r=>(state.category==='all'||categoryKey(r)===state.category)&&(!q||`${r.title} ${r.summary} ${(r.tags||[]).join(' ')}`.toLowerCase().includes(q)))}
|
||||||
|
function renderNav(){const counts=state.reports.reduce((a,r)=>(a[categoryKey(r)]=(a[categoryKey(r)]||0)+1,a),{});$('#category-nav').innerHTML=Object.entries(categoryNames).map(([key,name])=>`<a href="#" class="nav-item ${key===state.category?'active':''}" data-category="${key}"><span>${name}</span><b>${key==='all'?state.reports.length:counts[key]||0}</b></a>`).join('');document.querySelectorAll('.nav-item').forEach(a=>a.onclick=e=>{e.preventDefault();state.category=a.dataset.category;renderAll()})}
|
||||||
|
function renderCalendar(){const first=new Date(state.year,state.month-1,1), start=(first.getDay()+6)%7, days=new Date(state.year,state.month,0).getDate();$('#calendar-title').textContent=`${state.year}年${pad(state.month)}月`;let html='';for(let i=0;i<start;i++)html+='<div class="day empty-day"></div>';for(let d=1;d<=days;d++){const date=`${state.year}-${pad(state.month)}-${pad(d)}`, has=visible().some(r=>r.date===date);html+=`<button class="day ${has?'has-report':''} ${state.selected===date?'selected':''} ${formatDate(new Date())===date?'today':''}" data-date="${date}">${d}</button>`}$('#calendar').innerHTML=html;document.querySelectorAll('.day[data-date]').forEach(b=>b.onclick=()=>{state.selected=b.dataset.date;renderAll()})}
|
||||||
|
function card(r){return `<a class="report-card" href="${r.path}" target="_blank"><div><h4>${r.title}</h4><p>${r.summary||'暂无摘要'}</p></div><div class="report-meta">${r.categoryName||categoryNames[r.category]||'报告'} <span class="tag">${(r.tags||[])[0]||'REPORT'}</span> ↗</div></a>`}
|
||||||
|
function renderReports(){const date=state.selected;const rows=visible().filter(r=>!date||r.date===date);$('#selected-label').textContent=date?date.replaceAll('-',' / '):'选择一个日期';$('#selected-count').textContent=`${rows.length} REPORTS`;$('#report-list').innerHTML=rows.length?rows.map(card).join(''):'<div class="empty">这一天还没有报告,或当前筛选条件下暂无匹配内容。</div>'}
|
||||||
|
function renderRecent(){const rows=[...visible()].sort((a,b)=>b.date.localeCompare(a.date)).slice(0,3);$('#recent-list').innerHTML=rows.length?rows.map(r=>`<a class="recent-card" href="${r.path}" target="_blank"><p class="eyebrow accent">${r.date} · ${r.categoryName||categoryNames[r.category]}</p><h4>${r.title}</h4><p>${r.summary||'打开查看详细研究内容'}</p><span class="report-meta">READ REPORT ↗</span></a>`).join(''):'<div class="empty">暂无报告</div>'}
|
||||||
|
function renderAll(){renderNav();renderCalendar();renderReports();renderRecent();$('#report-count').textContent=String(state.reports.length).padStart(2,'0');$('#category-count').textContent=String(new Set(state.reports.map(categoryKey)).size).padStart(2,'0');$('#sync-time').textContent=new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});}
|
||||||
|
$('#prev-month').onclick=()=>{state.month--;if(state.month<1){state.month=12;state.year--}state.selected=null;renderAll()};$('#next-month').onclick=()=>{state.month++;if(state.month>12){state.month=1;state.year++}state.selected=null;renderAll()};$('#today-btn').onclick=()=>{const d=new Date();state.year=d.getFullYear();state.month=d.getMonth()+1;state.selected=formatDate(d);renderAll()};$('#all-reports').onclick=()=>{state.selected=null;renderAll()};$('#search-input').oninput=()=>renderAll();$('#footer-year').textContent=new Date().getFullYear();
|
||||||
|
fetch('data/reports.json').then(r=>r.json()).then(data=>{state.reports=data;const latest=[...data].sort((a,b)=>b.date.localeCompare(a.date))[0];if(latest){const d=new Date(`${latest.date}T00:00:00`);state.year=d.getFullYear();state.month=d.getMonth()+1;state.selected=latest.date}renderAll()}).catch(()=>{$('#report-list').innerHTML='<div class="empty">索引文件暂不可用。请运行 scripts/build-index.mjs 后重新上传。</div>'});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "daily-review-2026-08-16-market-review",
|
||||||
|
"category": "daily-review",
|
||||||
|
"categoryName": "daily-review",
|
||||||
|
"date": "2026-08-16",
|
||||||
|
"title": "A 股收盘复盘:风险偏好重新定价",
|
||||||
|
"summary": "打开查看详细研究内容",
|
||||||
|
"tags": [
|
||||||
|
"DAILY-REVIEW"
|
||||||
|
],
|
||||||
|
"path": "reports/daily-review/2026/08/2026-08-16-market-review.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "intraday-2026-08-16-ai-sector",
|
||||||
|
"category": "intraday",
|
||||||
|
"categoryName": "盘中项目",
|
||||||
|
"date": "2026-08-16",
|
||||||
|
"title": "盘中项目:AI 算力链的第二波确认",
|
||||||
|
"summary": "打开查看详细研究内容",
|
||||||
|
"tags": [
|
||||||
|
"INTRADAY"
|
||||||
|
],
|
||||||
|
"path": "reports/intraday/2026/08/2026-08-16-ai-sector.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Stock Report Portal Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a static stock-report portal with category navigation, calendar browsing, indexed HTML reports, and an upload-friendly rebuild script.
|
||||||
|
|
||||||
|
**Architecture:** A single-page vanilla JavaScript application reads `data/reports.json`, renders category/month/date views, and opens source HTML reports from `reports/`. A Node.js script scans report paths and emits the index, keeping storage simple and automation-friendly.
|
||||||
|
|
||||||
|
**Tech Stack:** HTML5, CSS3, vanilla JavaScript, Node.js standard library only.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- No database, framework, or runtime server is required to serve the site.
|
||||||
|
- Report storage uses `reports/<category>/<YYYY>/<MM>/`.
|
||||||
|
- The browser-facing index contract uses `data/reports.json`.
|
||||||
|
- Empty and malformed data must render a recoverable empty state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create the static portal shell
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `index.html`
|
||||||
|
- Create: `styles.css`
|
||||||
|
- Create: `app.js`
|
||||||
|
- Create: `data/reports.json`
|
||||||
|
|
||||||
|
- [ ] Add semantic layout for sidebar categories, dashboard header, calendar, report list, and recent reports.
|
||||||
|
- [ ] Add responsive CSS with a dark research-terminal visual style and accessible focus states.
|
||||||
|
- [ ] Implement loading of `data/reports.json`, category filtering, month navigation, date selection, search, and report links.
|
||||||
|
- [ ] Seed representative records and make all relative paths deploy-safe.
|
||||||
|
|
||||||
|
### Task 2: Add representative report fixtures and index rebuild tooling
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `reports/daily-review/2026/08/2026-08-16-market-review.html`
|
||||||
|
- Create: `reports/intraday/2026/08/2026-08-16-ai-sector.html`
|
||||||
|
- Create: `scripts/build-index.mjs`
|
||||||
|
- Create: `README.md`
|
||||||
|
|
||||||
|
- [ ] Write the rebuild script using Node standard library and the filename convention `YYYY-MM-DD-slug.html`.
|
||||||
|
- [ ] Preserve category labels and derive title, summary, tags, and path for each record.
|
||||||
|
- [ ] Add deployment and automation instructions, including the upload-then-rebuild workflow.
|
||||||
|
- [ ] Run the rebuild script and verify the generated JSON contains both fixtures.
|
||||||
|
|
||||||
|
### Task 3: Validate the complete static site
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `data/reports.json`
|
||||||
|
|
||||||
|
- [ ] Run `node scripts/build-index.mjs`.
|
||||||
|
- [ ] Serve the root with a local static server and request `index.html`, `app.js`, and both report files.
|
||||||
|
- [ ] Parse the generated JSON and verify every indexed path exists.
|
||||||
|
- [ ] Check the site for broken relative references and report the final upload directory.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Stock Report Portal Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Create a deployable static website for browsing HTML stock reports generated by OpenClaw or Hermes.
|
||||||
|
|
||||||
|
## Product shape
|
||||||
|
|
||||||
|
- Home page presents report categories such as Daily Review, Intraday Projects, Sector Watch, and Stock Tracking.
|
||||||
|
- Category pages show reports grouped by year and month, with a calendar-style date grid and a report list for the selected date.
|
||||||
|
- Each report opens its original HTML file in the same portal shell, preserving the source report content.
|
||||||
|
- A recent-reports area and category counts make the home page useful even before a date is selected.
|
||||||
|
|
||||||
|
## Data contract
|
||||||
|
|
||||||
|
Reports live under `reports/<category>/<YYYY>/<MM>/`. The website reads `data/reports.json`, whose records contain `id`, `category`, `date`, `title`, `summary`, `tags`, and `path`. A small rebuild script scans HTML files and regenerates the index, so uploads can be automated without hand-editing JSON.
|
||||||
|
|
||||||
|
## Technology and deployment
|
||||||
|
|
||||||
|
Use a dependency-free static site: HTML, CSS, and vanilla JavaScript. It can be uploaded to Nginx, Apache, object storage, or any static hosting service. The first version has no database, login, or server runtime requirement.
|
||||||
|
|
||||||
|
## Interaction and failure behavior
|
||||||
|
|
||||||
|
The site supports category filtering, month navigation, date selection, report search, and a direct report URL. Missing or malformed index data shows a clear empty state instead of a broken page. The rebuild script ignores non-HTML files and reports malformed filenames without stopping the whole rebuild.
|
||||||
|
|
||||||
|
## Future extension
|
||||||
|
|
||||||
|
The stable JSON contract leaves room for an upload webhook, authentication, full-text search, and database migration later without changing the browser-facing information architecture.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Market Desk · 研究报告中枢</title>
|
||||||
|
<meta name="description" content="OpenClaw 与 Hermes 自动生成的股票研究报告门户" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="brand"><span class="brand-mark">M</span><div><strong>MARKET DESK</strong><small>RESEARCH INTELLIGENCE</small></div></div>
|
||||||
|
<div class="status"><span></span> LIVE DATA ROOM</div>
|
||||||
|
<nav id="category-nav" aria-label="报告分类"></nav>
|
||||||
|
<div class="sidebar-footer"><span>SYNC</span><strong id="sync-time">--:--</strong></div>
|
||||||
|
</aside>
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="topbar"><div><p class="eyebrow">研究情报 / REPORT HUB</p><h1 id="page-title">研究报告中枢</h1></div><div class="top-actions"><label class="search"><span>⌕</span><input id="search-input" type="search" placeholder="搜索报告、标签或股票代码" /></label><button class="icon-button" aria-label="切换主题">◐</button></div></header>
|
||||||
|
<section class="hero-grid"><div class="hero-copy"><p class="eyebrow accent">TODAY'S SIGNAL</p><h2>把每天的市场判断,<br /><em>放进一个可追踪的时间轴。</em></h2><p class="hero-text">OpenClaw / Hermes 自动归档的研究报告,在这里按主题、日期和市场阶段沉淀下来。</p><div class="hero-meta"><span><b id="report-count">00</b> REPORTS INDEXED</span><span><b id="category-count">00</b> ACTIVE CATEGORIES</span></div></div><div class="signal-card"><div class="signal-top"><span>MARKET PULSE</span><span class="live-dot">● LIVE</span></div><div class="pulse-value">+1.84%</div><div class="pulse-label">沪深 300 · 今日收盘</div><div class="pulse-bars"><i style="height:35%"></i><i style="height:52%"></i><i style="height:40%"></i><i style="height:68%"></i><i style="height:55%"></i><i style="height:82%"></i><i style="height:72%"></i><i style="height:94%"></i><i style="height:78%"></i><i style="height:100%"></i></div></div></section>
|
||||||
|
<section class="workspace"><div class="section-heading"><div><p class="eyebrow">ARCHIVE CALENDAR</p><h3 id="calendar-title">2026年08月</h3></div><div class="calendar-controls"><button id="prev-month" aria-label="上个月">←</button><button id="today-btn">今天</button><button id="next-month" aria-label="下个月">→</button></div></div><div class="calendar-wrap"><div class="weekdays"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div><div class="calendar" id="calendar"></div></div><div class="selected-date"><div><p class="eyebrow accent">SELECTED DATE</p><h3 id="selected-label">选择一个日期</h3></div><span id="selected-count">0 REPORTS</span></div><div id="report-list" class="report-list"></div></section>
|
||||||
|
<section class="recent-section"><div class="section-heading"><div><p class="eyebrow">LATEST INTELLIGENCE</p><h3>最近更新</h3></div><button class="text-button" id="all-reports">查看全部 →</button></div><div id="recent-list" class="recent-list"></div></section>
|
||||||
|
<footer class="footer">MARKET DESK <span>·</span> PRIVATE RESEARCH ARCHIVE <span>·</span> <span id="footer-year"></span></footer>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>A 股收盘复盘:风险偏好重新定价</title><style>body{max-width:760px;margin:50px auto;padding:0 20px;background:#0b0e0d;color:#eaf1eb;font:16px system-ui;line-height:1.8}h1{line-height:1.2;color:#b8f36a}small{color:#87938c}section{border-top:1px solid #29332e;margin-top:30px;padding-top:20px}</style></head><body><small>每日复盘 · 2026-08-16</small><h1>A 股收盘复盘:风险偏好重新定价</h1><p>指数放量上行,科技与券商成为主要增量方向。</p><section><h2>今日结论</h2><p>市场情绪由防御转向进攻,关注成交量能否在未来两个交易日继续维持。</p></section><section><h2>观察清单</h2><ul><li>AI 算力:订单预期改善,等待龙头分歧后的承接。</li><li>券商:指数弹性明显,但不追高。</li><li>新能源:暂未出现明确趋势反转信号。</li></ul></section></body></html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>盘中项目:AI 算力链的第二波确认</title><style>body{max-width:760px;margin:50px auto;padding:0 20px;background:#0b0e0d;color:#eaf1eb;font:16px system-ui;line-height:1.8}h1{line-height:1.2;color:#b8f36a}small{color:#87938c}section{border-top:1px solid #29332e;margin-top:30px;padding-top:20px}</style></head><body><small>盘中项目 · 2026-08-16</small><h1>盘中项目:AI 算力链的第二波确认</h1><p>跟踪订单、成交量和资金回流,评估午后持续性。</p><section><h2>盘中观察</h2><p>板块内部出现从设备端向液冷、光模块扩散的迹象,需等待成交额进一步确认。</p></section><section><h2>触发条件</h2><ul><li>核心标的回踩不破早盘成交密集区。</li><li>板块成交额保持在近五日均值上方。</li><li>海外映射标的维持强势。</li></ul></section></body></html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const reportsRoot = path.join(root, 'reports');
|
||||||
|
const output = path.join(root, 'data', 'reports.json');
|
||||||
|
const categoryNames = { daily: '每日复盘', intraday: '盘中项目', sector: '行业观察', stock: '个股跟踪' };
|
||||||
|
const files = [];
|
||||||
|
async function walk(dir) {
|
||||||
|
if (!existsSync(dir)) return;
|
||||||
|
for (const item of await readdir(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, item.name);
|
||||||
|
if (item.isDirectory()) await walk(full);
|
||||||
|
else if (item.name.endsWith('.html')) files.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(reportsRoot);
|
||||||
|
const records = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const rel = path.relative(root, file).split(path.sep).join('/');
|
||||||
|
const parts = rel.split('/');
|
||||||
|
const category = parts[1];
|
||||||
|
const match = parts.at(-1).match(/^(\d{4}-\d{2}-\d{2})-(.+)\.html$/);
|
||||||
|
if (!match) { console.warn(`Skipped (expected YYYY-MM-DD-slug.html): ${rel}`); continue; }
|
||||||
|
const [, date, slug] = match;
|
||||||
|
const html = await readFile(file, 'utf8');
|
||||||
|
const title = html.match(/<title>(.*?)<\/title>/i)?.[1]?.trim() || slug.replaceAll('-', ' ');
|
||||||
|
const summary = html.match(/<meta\s+name=["']description["']\s+content=["'](.*?)["']/i)?.[1] || '打开查看详细研究内容';
|
||||||
|
records.push({ id: `${category}-${date}-${slug}`, category, categoryName: categoryNames[category] || category, date, title, summary, tags: [category.toUpperCase()], path: rel });
|
||||||
|
}
|
||||||
|
records.sort((a, b) => b.date.localeCompare(a.date) || a.title.localeCompare(b.title));
|
||||||
|
await writeFile(output, `${JSON.stringify(records, null, 2)}\n`);
|
||||||
|
console.log(`Indexed ${records.length} report(s) → ${path.relative(root, output)}`);
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user