diff --git a/.gitignore b/.gitignore
index 5e85fe6..ac9a88d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,9 +4,6 @@
*.pyc
.DS_Store
-# V2 运行时数据(SQLite + 本地媒体)
-data/
-
# 反编译参考资料(约 40 个 bundle),设计结论已写入 docs/extension/plan.md §2
reference/
diff --git a/README.md b/README.md
index 0fe36d4..d6f1441 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,9 @@ php -S localhost:8899
### 2. 打开浏览器
-访问 **http://localhost:8899**,会看到 5 张示例图表和 11 张泡泡玛特财报图表:
+访问 **http://localhost:8899**。页面左侧选择公司,中间用 Tab 切换分析维度,右侧可跳转到当前分类的图表。
+
+当前 manifest 内登记了 5 张示例图表和 14 张泡泡玛特财报图表:
**示例图表:**
1. **存货、营收与存货周转天数** — 双柱 + 折线,双 Y 轴
@@ -50,7 +52,7 @@ php -S localhost:8899
### 3. 修改数据
-编辑 `data/*.csv`,保存后点击页面右上角的 **「重新加载数据」** 按钮。
+编辑对应公司的 CSV,保存后点击页面右上角的 **「重新加载数据」** 按钮。数据目录按公司划分,例如 `data/ppmt/ppmt_profit.csv`、`data/demo/inventory.csv`。
CSV 格式:首行是表头,逗号分隔,数字列会自动转换。示例:
@@ -69,22 +71,20 @@ period,inventory,revenue,turnover_days
## 项目结构
```
-echart-demo/
+smart-fin-chart/
├── index.html # 主页面
-├── data/ # 数据文件
-│ ├── inventory.csv # 图 1-5:示例数据
-│ ├── margin.csv
-│ ├── growth.csv
-│ ├── fx.csv
-│ ├── ppmt.xlsx # 泡泡玛特原始财报
-│ └── ppmt_*.csv # 图 6-16:从 xlsx 提取的各口径数据
+├── manifest.json # 公司、Tab、图表、CSV 的唯一配置来源
+├── data/ # 数据文件(按公司划分)
+│ ├── demo/
+│ └── ppmt/
├── js/
-│ ├── main.js # 入口:CHARTS 清单、渲染、导出 PNG
+│ ├── main.js # 入口:读取 manifest、渲染、导出 PNG
+│ ├── ui.js # 三栏 UI 构建
│ ├── csv.js # CSV 解析工具
│ ├── theme.js # 主题:配色、字体、水印、共享系列构造器
-│ └── charts/ # 一图一文件,共 16 张
+│ └── charts/ # 图表构建函数与 registry
├── test/
-│ └── regress.mjs # 回归测试:SSR 渲染 16 张图 + 样式规范断言
+│ └── regress.mjs # 回归测试:遍历 manifest + 样式规范断言
└── css/
└── style.css # 页面样式
```
@@ -95,13 +95,13 @@ echart-demo/
npm test
```
-用 ECharts 的 SSR 模式把 16 张图渲染成 SVG,断言:
+用 ECharts 的 SSR 模式遍历 manifest 中的图表并渲染成 SVG,断言:
- `legend.top` = 85、`grid.top` = 145(统一间距)
- 折线 `symbolSize` = 9、`borderWidth` = 2.5(空心圆点规范)
- SVG 属性未被字体名里的引号提前闭合
-加新图后往 `regress.mjs` 顶部的 `all` 数组加一行,否则不会被测到。
+回归测试会遍历 `manifest.json`,不用再手工维护图表清单。
---
@@ -138,9 +138,9 @@ A:点击页面右上角的 **「重新加载数据」** 按钮。普通刷新
A:
1. 在 `data/` 下创建新 CSV
2. 在 `js/charts/` 下创建新 `.js`,从 `theme.js` 取共享配置(`lineSeries()`、`barSeries()`、`standardLegend()`、`periodAxis`),不要手写等价值
-3. 在 `js/main.js` 的 `CHARTS` 数组里加一行
-4. 在 `index.html` 加一个 `
` 容器
-5. 在 `test/regress.mjs` 的 `all` 数组里加一行,然后跑 `npm test`
+3. 在 `js/charts/index.js` 注册 chart key
+4. 在 `manifest.json` 对应公司与 Tab 中添加图表配置
+5. 跑 `node test/regress.mjs
`;浏览器检查标签、颜色和脚注
**Q:能改水印文字吗?**
A:编辑 `js/theme.js` 的 `WATERMARK_TEXT` 常量。
diff --git a/css/style.css b/css/style.css
index b2785ca..c4b64df 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1,101 +1,219 @@
:root {
--ink: #333;
- --grey: #8c8c8c;
+ --muted: #8c8c8c;
--line: #e8e8e8;
--bg: #f5f4f1;
+ --surface: #fff;
--accent: #6b4a2b;
+ --accent-soft: #f0e9e1;
}
* { box-sizing: border-box; }
body {
margin: 0;
- padding: 32px 24px 64px;
background: var(--bg);
color: var(--ink);
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
-header {
- max-width: 1180px;
- margin: 0 auto 28px;
- display: flex;
- align-items: baseline;
- gap: 16px;
- flex-wrap: wrap;
+button {
+ font: inherit;
+ font-size: 13px;
+ color: var(--ink);
+ cursor: pointer;
}
-header h1 {
- margin: 0;
- font-size: 22px;
+.app-header {
+ min-height: 76px;
+ padding: 16px 28px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ background: var(--surface);
+ border-bottom: 1px solid var(--line);
+}
+
+.app-brand h1,
+.app-brand p,
+.card-title,
+.sidebar-title { margin: 0; }
+
+.app-brand h1 { font-size: 21px; font-weight: 600; }
+.app-brand p { margin-top: 4px; color: var(--muted); font-size: 13px; }
+
+.reload-button,
+.export-button {
+ padding: 7px 13px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: var(--surface);
+ transition: border-color .15s, color .15s, background .15s;
+}
+
+.reload-button {
+ color: #fff;
+ background: var(--accent);
+ border-color: var(--accent);
+}
+
+.reload-button:hover { opacity: .88; }
+.export-button:hover { color: var(--accent); border-color: var(--accent); }
+
+.app-layout {
+ display: grid;
+ grid-template-columns: 184px minmax(0, 1fr) 184px;
+ gap: 24px;
+ max-width: 1600px;
+ margin: 0 auto;
+ padding: 28px 24px 64px;
+}
+
+.sidebar {
+ align-self: start;
+ position: sticky;
+ top: 20px;
+ padding: 14px 10px;
+ background: rgba(255, 255, 255, .58);
+ border: 1px solid rgba(232, 232, 232, .9);
+ border-radius: 10px;
+}
+
+.sidebar-title {
+ padding: 0 8px 10px;
+ color: var(--muted);
+ font-size: 12px;
font-weight: 600;
}
-header p {
- margin: 0;
- color: var(--grey);
- font-size: 14px;
-}
+.company-list,
+.anchor-list { display: grid; gap: 3px; }
-button {
- font-family: inherit;
- font-size: 13px;
- padding: 6px 14px;
- border: 1px solid var(--line);
+.company-button,
+.anchor-button,
+.tab-button,
+.view-button {
+ border: 0;
border-radius: 6px;
- background: #fff;
- color: var(--ink);
- cursor: pointer;
- transition: border-color .15s, color .15s;
+ background: transparent;
+ text-align: left;
+ transition: background .15s, color .15s;
}
-button:hover {
- border-color: var(--accent);
+.company-button {
+ width: 100%;
+ padding: 8px;
+}
+
+.company-button:hover,
+.anchor-button:hover,
+.tab-button:hover { color: var(--accent); background: var(--accent-soft); }
+
+.company-button[data-active="true"],
+.tab-button[data-active="true"] {
color: var(--accent);
+ font-weight: 600;
+ background: var(--accent-soft);
}
-#btn-reload {
- margin-left: auto;
- background: var(--accent);
- color: #fff;
- border-color: var(--accent);
+.anchor-button {
+ padding: 6px 8px;
+ color: #5f5f5f;
+ font-size: 12px;
+ line-height: 1.45;
}
-#btn-reload:hover {
- opacity: .88;
- color: #fff;
+.content { min-width: 0; }
+
+.tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 18px;
+ padding: 5px;
+ background: rgba(255, 255, 255, .6);
+ border: 1px solid var(--line);
+ border-radius: 9px;
}
+.tab-button { padding: 7px 11px; }
+
+.cards { display: grid; gap: 28px; }
+
.card {
- max-width: 1180px;
- margin: 0 auto 28px;
- background: #fff;
+ overflow: hidden;
+ background: var(--surface);
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .06);
- overflow: hidden;
+ scroll-margin-top: 20px;
}
.card-bar {
display: flex;
align-items: center;
gap: 12px;
- padding: 10px 16px;
+ min-height: 52px;
+ padding: 9px 16px;
border-bottom: 1px solid var(--line);
}
-.card-bar .src {
- font-size: 12px;
- color: var(--grey);
+.card-heading { min-width: 0; margin-right: auto; }
+.card-title { font-size: 14px; font-weight: 600; }
+
+.src {
+ display: block;
+ overflow: hidden;
+ margin-top: 2px;
+ color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.card-bar button { margin-left: auto; }
+.view-switcher {
+ display: flex;
+ padding: 2px;
+ background: #f4f2ee;
+ border-radius: 6px;
+}
+
+.view-button { padding: 4px 7px; color: var(--muted); font-size: 12px; }
+.view-button[data-active="true"] { color: var(--accent); background: var(--surface); box-shadow: 0 1px 2px rgba(0, 0, 0, .08); }
.chart { width: 100%; }
-.chart-error {
- padding: 40px;
- color: #c0392b;
+.chart-error,
+.empty-state,
+.sidebar-empty {
+ margin: 0;
+ padding: 36px 20px;
+ color: #a63f32;
font-size: 14px;
text-align: center;
}
+
+.sidebar-empty { padding: 8px; color: var(--muted); text-align: left; font-size: 12px; }
+
+@media (max-width: 1040px) {
+ .app-layout { grid-template-columns: 160px minmax(0, 1fr); }
+ .chart-sidebar { display: none; }
+}
+
+@media (max-width: 720px) {
+ .app-header { padding: 14px 16px; }
+ .app-brand p { display: none; }
+ .app-layout { display: block; padding: 16px 12px 40px; }
+ .sidebar { position: static; margin-bottom: 12px; }
+ .company-sidebar { padding: 7px; }
+ .company-sidebar .sidebar-title { display: none; }
+ .company-list { display: flex; overflow-x: auto; }
+ .company-button { flex: 0 0 auto; width: auto; }
+ .tabs { overflow-x: auto; flex-wrap: nowrap; }
+ .tab-button { flex: 0 0 auto; }
+ .card-bar { align-items: flex-start; flex-wrap: wrap; }
+ .export-button { margin-left: auto; }
+ .chart { min-width: 560px; }
+ .card { overflow-x: auto; }
+}
diff --git a/index.html b/index.html
index 30717e6..e6b78d6 100644
--- a/index.html
+++ b/index.html
@@ -8,172 +8,7 @@
-
-
-
-
-
- data/inventory.csv
-
-
-
-
-
-
-
- data/margin.csv
-
-
-
-
-
-
-
- data/margin.csv(毛利率 + 净利率两列)
-
-
-
-
-
-
-
- data/growth.csv
-
-
-
-
-
-
-
- data/fx.csv
-
-
-
-
-
-
-
-
- 📊 泡泡玛特财报分析
- 基于 data/ppmt.xlsx 自动生成
-
-
-
-
- data/ppmt_assets.csv
-
-
-
-
-
-
-
- data/ppmt_profit.csv
-
-
-
-
-
-
-
- data/ppmt_ip.csv
-
-
-
-
-
-
-
- data/ppmt_category.csv
-
-
-
-
-
-
-
- data/ppmt_sankey_2026h1.csv
-
-
-
-
-
-
-
- data/ppmt_region_channel_yoy.csv
-
-
-
-
-
-
-
- data/ppmt_growth.csv
-
-
-
-
-
-
-
- data/ppmt_margins.csv
-
-
-
-
-
-
-
- data/ppmt_margin_bridge.csv
-
-
-
-
-
-
-
- data/ppmt_core_profit.csv
-
-
-
-
-
-
-
- data/ppmt_core_margin.csv
-
-
-
-
-
-
-
- data/ppmt_ip_share_2026h1.csv
-
-
-
-
-
-
-
- data/ppmt_ip_share_history.csv
-
-
-
-
-
-
-
- data/ppmt_ip_ex_monsters.csv
-
-
-
-
-
-
+
+