init
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
data/
|
||||
*.csv
|
||||
.DS_Store
|
||||
@@ -0,0 +1,331 @@
|
||||
# KLineChart CSV 可视化 Demo — 项目说明书
|
||||
|
||||
## 1. 项目概览
|
||||
|
||||
基于 KLineChart v10 的独立 K 线图表可视化 Demo,支持从本地 CSV 文件加载 OHLCV 历史数据,提供周期切换、指标叠加、深浅主题、缩放拖拽、截图导出等完整交互能力。
|
||||
|
||||
**技术栈:**
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| KLineChart | 10.0.2 | 核心 K 线渲染引擎 |
|
||||
| Vite | 5.x | 本地开发服务器 |
|
||||
| Canvas 2D | 原生 | 图表绘制 |
|
||||
|
||||
## 2. 文件结构
|
||||
|
||||
```
|
||||
debug/
|
||||
├── index.html # 页面结构:工具栏 + 图表容器
|
||||
├── main.js # 全部业务逻辑(约 600 行)
|
||||
├── style.css # 界面样式,含深色主题
|
||||
├── package.json # 依赖声明(klinecharts + vite)
|
||||
├── package-lock.json # 版本锁定
|
||||
└── data/
|
||||
└── eurgbp_m1.csv # 示例数据(EUR/GBP 1分钟,2021-2026,约 186 万行)
|
||||
```
|
||||
|
||||
## 3. 功能清单
|
||||
|
||||
| 功能 | 操作方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| **CSV 加载** | 📂 按钮选择 / 拖拽 | 自动识别 5 种 CSV 格式 |
|
||||
| **周期切换** | 1m / 5m / 15m / 1h / 1d | 实时聚合原始 1m 数据 |
|
||||
| **缩放** | +/−/⟲ 按钮、鼠标滚轮、键盘 +/− | barSpace 范围 2–50 |
|
||||
| **翻页** | ←/→ 键盘、拖拽、滚动条 | 分页加载(每次 500 根) |
|
||||
| **指标** | 工具栏按钮 或 右键菜单 | MA / BOLL / MACD / RSI / VOL |
|
||||
| **深色主题** | ☾/☀ 按钮 或 D 键 | `chart.setStyles('dark')` |
|
||||
| **截图** | ▣ 按钮 或 S 键 | `getConvertPictureUrl()` 下载 PNG |
|
||||
| **十字光标面板** | 鼠标移动 | 顶部实时显示 OHLCV + 涨跌幅 |
|
||||
| **柱体详情** | 左键点击柱体 | 弹出详细 OHLCV 浮窗 |
|
||||
| **右键菜单** | 右键图表 | 切换指标 / 主题 / 截图 |
|
||||
| **自定义指标** | `registerIndicator()` | 扩展任意指标 |
|
||||
|
||||
## 4. CSV 数据格式
|
||||
|
||||
自动识别以下格式(列顺序可自定义,支持带表头/无表头):
|
||||
|
||||
### 格式 A:Binance 格式(时间戳 + OHLCV)
|
||||
|
||||
```
|
||||
1630000000000,0.00145000,0.00145000,0.00144000,0.00144000,5259702
|
||||
```
|
||||
|
||||
### 格式 B:带表头格式
|
||||
|
||||
```
|
||||
timestamp,open,high,low,close,volume
|
||||
1630000000000,0.00145000,0.00145000,0.00144000,0.00144000,5259702
|
||||
```
|
||||
|
||||
### 格式 C:点号日期格式(当前示例)
|
||||
|
||||
```
|
||||
2021.07.06,00:10,0.85627,0.85629,0.85625,0.85628,7000000.0
|
||||
```
|
||||
|
||||
列顺序:`日期, 时间, O, H, L, C, V`
|
||||
|
||||
### 格式 D:横杠日期格式
|
||||
|
||||
```
|
||||
2021-07-06,00:10,0.85627,0.85629,0.85625,0.85628,7000000.0
|
||||
```
|
||||
|
||||
### 格式 E:YYYYMMDD 格式
|
||||
|
||||
```
|
||||
20210706,0.85627,0.85629,0.85625,0.85628,7000000.0
|
||||
```
|
||||
|
||||
**时间戳自动处理:** 13 位数字 → 毫秒;10 位数字 → 秒(自动 ×1000)
|
||||
|
||||
## 5. 快速开始
|
||||
|
||||
```bash
|
||||
cd debug
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
浏览器打开 `http://localhost:5173`,拖入或点击 📂 选择 CSV 文件即可。
|
||||
|
||||
## 6. 核心代码结构(main.js)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ loadRawCSV(source) │ 1. 读取文件(File 或 URL)
|
||||
│ └─ parseCSV(text) │ 2. 自动识别格式,解析到 TypedArray
|
||||
├─────────────────────────────────────┤
|
||||
│ aggregateBars(data, range, span) │ 3. 1m → 5m/15m/1h/1d 聚合
|
||||
├─────────────────────────────────────┤
|
||||
│ loadDataLoader() │ 4. 返回 setDataLoader 参数
|
||||
│ └─ getBars({type, timestamp…}) │ 5. init/forward/backward 分页
|
||||
├─────────────────────────────────────┤
|
||||
│ mountChart(source) │ 6. init → setSymbol → setPeriod
|
||||
│ ├─ chart.setDataLoader(...) │ 7. setDataLoader → 图表加载数据
|
||||
│ ├─ chart.createIndicator(...) │ 8. 创建指标
|
||||
│ └─ subscribeAction(...) │ 9. 绑定 crosshair/click 事件
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 7. 自定义指标开发
|
||||
|
||||
### 7.1 注册指标
|
||||
|
||||
在 `init()` 调用之前注册:
|
||||
|
||||
```js
|
||||
import { registerIndicator, init } from 'klinecharts'
|
||||
|
||||
registerIndicator({
|
||||
name: 'MYAVG',
|
||||
shortName: 'MYAVG',
|
||||
series: 'price',
|
||||
calcParams: [5, 10, 20],
|
||||
precision: 5,
|
||||
shouldOhlc: true,
|
||||
figures: [
|
||||
{ key: 'a1', title: 'A5: ', type: 'line' },
|
||||
{ key: 'a2', title: 'A10: ', type: 'line' },
|
||||
{ key: 'a3', title: 'A20: ', type: 'line' }
|
||||
],
|
||||
regenerateFigures: (params) =>
|
||||
params.map((p, i) => ({ key: `a${i+1}`, title: `A${p}: `, type: 'line' })),
|
||||
calc: (dataList, indicator) => {
|
||||
const sums = indicator.calcParams.map(() => 0)
|
||||
return dataList.map((bar, i) => {
|
||||
const out = {}
|
||||
indicator.calcParams.forEach((p, idx) => {
|
||||
sums[idx] += bar.close
|
||||
if (i >= p - 1) out[`a${idx+1}`] = sums[idx] / (i + 1)
|
||||
})
|
||||
return out
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 注册后再 init
|
||||
const chart = init('chart', { /* options */ })
|
||||
chart.createIndicator({ name: 'MYAVG', paneId: 'new' })
|
||||
```
|
||||
|
||||
### 7.2 字段说明
|
||||
|
||||
| 字段 | 必需 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `name` | 是 | string | 唯一标识,用于 `createIndicator({ name })` 匹配 |
|
||||
| `shortName` | 是 | string | 工具提示中显示的简称 |
|
||||
| `series` | 是 | `'price' \| 'volume' \| 'normal'` | 系列类型,决定 Y 轴行为 |
|
||||
| `calcParams` | 否 | `number[]` | 计算参数,可被 `overrideIndicator` 修改 |
|
||||
| `precision` | 否 | number | 结果数值的小数位数 |
|
||||
| `shouldOhlc` | 否 | boolean | 计算是否需要 OHLC 字段 |
|
||||
| `figures` | 是 | `Figure[]` | 图形配置,定义画几条线/柱 |
|
||||
| `calc` | 是 | `() => D[]` | 核心计算函数 |
|
||||
| `minValue` / `maxValue` | 否 | number | 固定 Y 轴最小/最大值 |
|
||||
| `styles` | 否 | `Partial<IndicatorStyle>` | 覆盖默认配色 |
|
||||
|
||||
### 7.3 Figure 类型
|
||||
|
||||
| `type` | 说明 |
|
||||
|--------|------|
|
||||
| `line` | 折线 |
|
||||
| `bar` | 柱状图(支持 `baseValue: 0` 画零轴柱) |
|
||||
| `circle` | 圆点 |
|
||||
| `text` | 文字标注 |
|
||||
|
||||
每个 `figure` 至少需要 `key` 和 `type`,`key` 对应 `calc` 返回数据中的字段名。
|
||||
|
||||
### 7.4 计算函数约定
|
||||
|
||||
```js
|
||||
calc: (dataList: KLineData[], indicator) => D[]
|
||||
```
|
||||
|
||||
- `dataList` 是当前可见窗口内的 K 线数据(每根含 `timestamp/open/high/low/close/volume`)
|
||||
- 返回数组长度等于 `dataList` 长度
|
||||
- 每项为对象,键名与 `figures[].key` 对应
|
||||
- 数据不足时某键可省略(不会画该点),例如 EMA 前 N 个点
|
||||
|
||||
## 8. KLineChart v10 API 速查
|
||||
|
||||
### 8.1 图表生命周期
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `init(container, options?)` | 创建图表实例 |
|
||||
| `dispose(container)` | 销毁,释放资源 |
|
||||
| `setSymbol({ ticker, pricePrecision, volumePrecision })` | 设置交易品种信息 |
|
||||
| `setPeriod({ span, type })` | 设置周期,会触发数据重载 |
|
||||
| `setDataLoader(loader)` | 设置数据加载器 |
|
||||
| `resetData()` | 强制重新加载数据 |
|
||||
|
||||
**调用顺序:** `init` → `setSymbol` → `setPeriod` → `setDataLoader`
|
||||
|
||||
### 8.2 指标
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createIndicator(name, isStack?)` | 添加指标,`isStack=true` 叠加主图 |
|
||||
| `createIndicator({ name, paneId: 'new' })` | 新建独立面板 |
|
||||
| `createIndicator({ name, paneId: 'candle_pane' }, true)` | 叠加主图 |
|
||||
| `removeIndicator({ name })` | 移除指标 |
|
||||
| `overrideIndicator({ id, calcParams: [...] })` | 修改指标参数 |
|
||||
| `registerIndicator(template)` | 注册自定义指标(需 `init` 前调用) |
|
||||
| `getSupportedIndicators()` | 获取内置指标名列表 |
|
||||
|
||||
### 8.3 样式与布局
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `setStyles('dark' \| 'light' \| partial)` | 切换主题或局部修改样式 |
|
||||
| `setBarSpace(value)` | 设置柱间距(等同于缩放) |
|
||||
| `getBarSpace()` | 获取当前 barSpace `{bar, gapBar}` |
|
||||
| `setPaneOptions({ paneId, height })` | 设置面板高度 |
|
||||
| `scrollToRealTime()` | 滚动到最新数据 |
|
||||
| `scrollByDistance(dist)` | 按像素距离滚动 |
|
||||
|
||||
### 8.4 事件订阅
|
||||
|
||||
| Action 类型 | 触发时机 | 回调参数 |
|
||||
|-------------|----------|----------|
|
||||
| `onCrosshairChange` | 鼠标移动 | `{ x, y, paneId, kLineData, timestamp, dataIndex }` |
|
||||
| `onCandleBarClick` | 点击柱体 | 同上 |
|
||||
| `onZoom` | 滚轮缩放 | `barSpace` |
|
||||
| `onScroll` | 拖动翻页 | — |
|
||||
| `onVisibleRangeChange` | 可见范围变化 | 起始/结束索引 |
|
||||
| `onPaneDrag` | 面板高度拖拽 | `paneId, height` |
|
||||
|
||||
### 8.5 截图与导出
|
||||
|
||||
```js
|
||||
const url = chart.getConvertPictureUrl(
|
||||
false, // 是否包含叠加层
|
||||
'png', // 'png' | 'jpeg' | 'bmp'
|
||||
'#ffffff' // 背景色
|
||||
)
|
||||
```
|
||||
|
||||
## 9. 内置指标一览
|
||||
|
||||
| 指标 | 叠加/独立 | 说明 |
|
||||
|------|----------|------|
|
||||
| MA | 叠加 | 简单移动平均,参数默认 6/12/20 |
|
||||
| EMA | 独立 | 指数移动平均 |
|
||||
| BOLL | 叠加 | 布林带 |
|
||||
| MACD | 独立 | 异同移动平均 |
|
||||
| RSI | 独立 | 相对强弱指数 |
|
||||
| SAR | 独立 | 抛物线转向 |
|
||||
| BBI | 叠加 | 多空指标 |
|
||||
| SMA | 叠加 | 简易平均线 |
|
||||
| VOL | 独立 | 成交量 |
|
||||
|
||||
## 10. 扩展场景
|
||||
|
||||
### 10.1 接入后端 API
|
||||
|
||||
替换 `loadRawCSV()` 中的 `fetch('/data/eurgbp_m1.csv')` 为后端接口:
|
||||
|
||||
```js
|
||||
function loadRawCSV(source) {
|
||||
return fetch('/api/kline?symbol=BTCUSDT&limit=1000')
|
||||
.then(r => r.json())
|
||||
.then(rows => {
|
||||
// rows 格式: [{t: 1630000000000, o: ..., h: ..., l: ..., c: ..., v: ...}]
|
||||
// 转换为内部格式 {ts, o, h, l, c, v, len}
|
||||
const count = rows.length
|
||||
return {
|
||||
ts: new Float64Array(rows.map(r => r.t)),
|
||||
o: new Float64Array(rows.map(r => r.o)),
|
||||
h: new Float64Array(rows.map(r => r.h)),
|
||||
l: new Float64Array(rows.map(r => r.l)),
|
||||
c: new Float64Array(rows.map(r => r.c)),
|
||||
v: new Float64Array(rows.map(r => r.v || 0)),
|
||||
len: count
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`getBars` 中的 `forward/backward` 分页自动交给后端处理。
|
||||
|
||||
### 10.2 WebSocket 实时推送
|
||||
|
||||
```js
|
||||
chart.setDataLoader({
|
||||
getBars: ({ type, timestamp, period, callback }) => { /* REST 历史 */ },
|
||||
subscribeBar: ({ symbol, period, callback }) => {
|
||||
ws = new WebSocket(`wss://.../ws?symbol=${symbol}`)
|
||||
ws.onmessage = (evt) => {
|
||||
const bar = JSON.parse(evt.data)
|
||||
// 回调单根 K 线,相同 timestamp 更新最后根
|
||||
callback(bar)
|
||||
}
|
||||
},
|
||||
unsubscribeBar: () => { ws.close() }
|
||||
})
|
||||
```
|
||||
|
||||
### 10.3 多图表联动
|
||||
|
||||
```js
|
||||
const chart1 = init('chart1')
|
||||
const chart2 = init('chart2')
|
||||
|
||||
chart1.subscribeAction('onCrosshairChange', (crosshair) => {
|
||||
// 通过 executeAction 同步到另一图表
|
||||
chart2.executeAction('onCrosshairChange', { x: crosshair.x, y: null })
|
||||
})
|
||||
```
|
||||
|
||||
## 11. 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 解决方法 |
|
||||
|------|----------|----------|
|
||||
| 图表空白 | 调用顺序不对 | 确认 `init → setSymbol → setPeriod → setDataLoader` |
|
||||
| 无数据 | getBars 未调用 callback | 确保 callback 一定被调用(即使失败) |
|
||||
| 时间错乱 | 时间戳单位错误 | 确认 `timestamp` 为毫秒 |
|
||||
| 分页失败 | more 标记错误 | 检查 `{ forward, backward }` 返回值 |
|
||||
| 指标不显示 | name 不匹配 | `createIndicator` 的 name 必须等于注册时的 name |
|
||||
| 滚动条消失 | data 太少 | 数据量 < 2 不会显示滚动条 |
|
||||
| 深色/浅色切换失败 | 样式名拼写错误 | 使用 `'dark'` / `'light'` |
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KLineChart debug</title>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<div id="toolbar">
|
||||
<div id="file-picker" title="选择 CSV 文件,或拖拽文件到此处">
|
||||
<button id="btn-file" type="button" title="选择 CSV 文件">📂</button>
|
||||
<span id="file-name">无文件</span>
|
||||
</div>
|
||||
<div id="period-switcher" aria-label="周期切换">
|
||||
<button data-period="1m" type="button">1m</button>
|
||||
<button data-period="5m" type="button">5m</button>
|
||||
<button data-period="15m" type="button">15m</button>
|
||||
<button data-period="1h" type="button">1h</button>
|
||||
<button data-period="1d" type="button">1d</button>
|
||||
</div>
|
||||
<div id="indicator-switcher" aria-label="指标">
|
||||
<button data-indicator="MA" type="button">MA</button>
|
||||
<button data-indicator="BOLL" type="button">BOLL</button>
|
||||
<button data-indicator="MACD" type="button">MACD</button>
|
||||
<button data-indicator="RSI" type="button">RSI</button>
|
||||
<button data-indicator="VOL" type="button">VOL</button>
|
||||
</div>
|
||||
<div id="zoom-controls">
|
||||
<button id="zoom-in" type="button" title="放大">+</button>
|
||||
<button id="zoom-out" type="button" title="缩小">−</button>
|
||||
<button id="zoom-reset" type="button" title="重置缩放">⟲</button>
|
||||
</div>
|
||||
<div id="tool-actions">
|
||||
<button id="btn-theme" type="button" title="切换主题">☾</button>
|
||||
<button id="btn-screenshot" type="button" title="截图">▣</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
<div id="loading" class="loading" style="display:none;">Loading CSV data...</div>
|
||||
<div id="chart"></div>
|
||||
<div id="crosshair-info" class="hidden"></div>
|
||||
<div id="bar-detail" class="popup hidden"></div>
|
||||
<div id="context-menu" class="popup hidden"></div>
|
||||
</div>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,603 @@
|
||||
import { dispose, init } from 'klinecharts'
|
||||
|
||||
const MINUTE = 60 * 1000
|
||||
const LOAD_BAR_COUNT = 500
|
||||
const BAR_SPACE_STEP = 5
|
||||
const BAR_SPACE_MIN = 2
|
||||
const BAR_SPACE_MAX = 50
|
||||
|
||||
const PERIODS = [
|
||||
{ key: '1m', period: { span: 1, type: 'minute' }, candleType: 'candle_solid' },
|
||||
{ key: '5m', period: { span: 5, type: 'minute' }, candleType: 'candle_solid' },
|
||||
{ key: '15m', period: { span: 15, type: 'minute' }, candleType: 'candle_solid' },
|
||||
{ key: '1h', period: { span: 1, type: 'hour' }, candleType: 'candle_solid' },
|
||||
{ key: '1d', period: { span: 1, type: 'day' }, candleType: 'candle_solid' }
|
||||
]
|
||||
|
||||
const INDICATORS = [
|
||||
{ key: 'MA', name: 'MA', stacked: true },
|
||||
{ key: 'BOLL', name: 'BOLL', stacked: true },
|
||||
{ key: 'MACD', name: 'MACD', stacked: false },
|
||||
{ key: 'RSI', name: 'RSI', stacked: false },
|
||||
{ key: 'VOL', name: 'VOL', stacked: false }
|
||||
]
|
||||
|
||||
let chart = null
|
||||
let rawBars = null
|
||||
let rawLoaded = false
|
||||
let activePeriodKey = PERIODS[0].key
|
||||
let isDark = false
|
||||
let activeIndicators = new Set()
|
||||
let closePopup = () => {}
|
||||
let closeCrosshair = () => {}
|
||||
let fileName = null
|
||||
|
||||
function getActivePeriodConfig() {
|
||||
return PERIODS.find(({ key }) => key === activePeriodKey) ?? PERIODS[0]
|
||||
}
|
||||
|
||||
function parseTimestamp(line, parts, fmt) {
|
||||
const tsNum = Number(parts[0])
|
||||
if (!isNaN(tsNum) && tsNum > 1e12 && tsNum < 1e14) return tsNum
|
||||
if (!isNaN(tsNum) && tsNum > 1e9 && tsNum < 1e12) return tsNum * 1000
|
||||
if (fmt === 'binance') {
|
||||
let ts = tsNum
|
||||
if (ts > 0 && ts < 1e13) ts *= 1000
|
||||
return ts
|
||||
}
|
||||
const dateStr = parts[0]
|
||||
const timeStr = parts[1] ?? ''
|
||||
if (dateStr.includes('.')) {
|
||||
const y = Number(dateStr.slice(0, 4))
|
||||
const m = Number(dateStr.slice(5, 7)) - 1
|
||||
const d = Number(dateStr.slice(8, 10))
|
||||
const h = Number(timeStr.slice(0, 2))
|
||||
const mi = Number(timeStr.slice(3, 5))
|
||||
return Date.UTC(y, m, d, h, mi, 0, 0)
|
||||
}
|
||||
if (dateStr.includes('-')) {
|
||||
const y = Number(dateStr.slice(0, 4))
|
||||
const m = Number(dateStr.slice(5, 7)) - 1
|
||||
const d = Number(dateStr.slice(8, 10))
|
||||
const h = Number(timeStr.slice(0, 2) || '0')
|
||||
const mi = Number(timeStr.slice(3, 5) || '0')
|
||||
return Date.UTC(y, m, d, h, mi, 0, 0)
|
||||
}
|
||||
if (dateStr.length === 8 && /^\d+$/.test(dateStr)) {
|
||||
const y = Number(dateStr.slice(0, 4))
|
||||
const m = Number(dateStr.slice(4, 6)) - 1
|
||||
const d = Number(dateStr.slice(6, 8))
|
||||
return Date.UTC(y, m, d, 0, 0, 0, 0)
|
||||
}
|
||||
if (line.includes(' ')) {
|
||||
const dt = line.split(/[,\t]/)[0]
|
||||
const [ds, ts] = dt.split(' ')
|
||||
if (ds && ts) {
|
||||
const y = Number(ds.slice(0, 4))
|
||||
const m = Number(ds.slice(4, 6)) - 1
|
||||
const d = Number(ds.slice(6, 8))
|
||||
const h = Number(ts.slice(0, 2))
|
||||
const mi = Number(ts.slice(3, 5))
|
||||
return Date.UTC(y, m, d, h, mi, 0, 0)
|
||||
}
|
||||
}
|
||||
return NaN
|
||||
}
|
||||
|
||||
function detectFormat(firstLine) {
|
||||
const trimmed = firstLine.trim()
|
||||
const lower = trimmed.toLowerCase()
|
||||
if (lower.startsWith('timestamp') || lower.startsWith('datetime') || lower.startsWith('time') || lower.startsWith('date')) {
|
||||
const cols = trimmed.split(/[,\t]/)
|
||||
const remaining = cols.slice(1).join(',')
|
||||
return { type: 'header', sub: detectFormat(remaining) ?? 'generic', headerCols: cols }
|
||||
}
|
||||
const parts = trimmed.split(/[,\t]/)
|
||||
if (parts.length >= 6 && /^\d{10,13}$/.test(parts[0])) return 'binance'
|
||||
if (parts.length >= 6 && parts[0].includes('.')) return 'dot'
|
||||
if (parts.length >= 5 && parts[0].includes('-')) return 'dash'
|
||||
if (parts.length >= 4 && parts[0].length === 8 && /^\d+$/.test(parts[0])) return 'yyyymmdd'
|
||||
return 'generic'
|
||||
}
|
||||
|
||||
function parseCSV(text) {
|
||||
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0)
|
||||
if (lines.length < 1) return null
|
||||
const fmt = detectFormat(lines[0])
|
||||
const hasHeader = fmt.type === 'header'
|
||||
const startIndex = hasHeader ? 1 : 0
|
||||
if (lines.length <= startIndex) return null
|
||||
const count = lines.length - startIndex
|
||||
const tsArr = new Float64Array(count)
|
||||
const oArr = new Float64Array(count)
|
||||
const hArr = new Float64Array(count)
|
||||
const lArr = new Float64Array(count)
|
||||
const cArr = new Float64Array(count)
|
||||
const vArr = new Float64Array(count)
|
||||
let n = 0
|
||||
for (let i = startIndex; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (!line) continue
|
||||
const parts = line.split(/[,\t]/)
|
||||
if (parts.length < 5) continue
|
||||
let ts, o, h, l, c, v = 0
|
||||
ts = parseTimestamp(line, parts, fmt)
|
||||
if (hasHeader) {
|
||||
o = Number(parts[1])
|
||||
h = Number(parts[2])
|
||||
l = Number(parts[3])
|
||||
c = Number(parts[4])
|
||||
v = Number(parts[5] || 0)
|
||||
} else {
|
||||
o = Number(parts[2])
|
||||
h = Number(parts[3])
|
||||
l = Number(parts[4])
|
||||
c = Number(parts[5])
|
||||
v = Number(parts[6] || 0)
|
||||
}
|
||||
if (Number.isNaN(ts) || !isFinite(o)) continue
|
||||
tsArr[n] = ts
|
||||
oArr[n] = o
|
||||
hArr[n] = h
|
||||
lArr[n] = l
|
||||
cArr[n] = c
|
||||
vArr[n] = v
|
||||
n++
|
||||
}
|
||||
return {
|
||||
ts: tsArr.slice(0, n),
|
||||
o: oArr.slice(0, n),
|
||||
h: hArr.slice(0, n),
|
||||
l: lArr.slice(0, n),
|
||||
c: cArr.slice(0, n),
|
||||
v: vArr.slice(0, n),
|
||||
len: n
|
||||
}
|
||||
}
|
||||
|
||||
function loadRawCSV(source) {
|
||||
const loadText = source instanceof File
|
||||
? () => source.text()
|
||||
: () => fetch(source).then(r => {
|
||||
if (!r.ok) throw new Error('CSV fetch failed: ' + r.status)
|
||||
return r.text()
|
||||
})
|
||||
return loadText().then(text => {
|
||||
const data = parseCSV(text)
|
||||
if (!data || data.len === 0) throw new Error('No data parsed from CSV')
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
function aggregateBars(data, startIndex, endIndex, spanMinutes) {
|
||||
if (spanMinutes === 1) {
|
||||
const result = []
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
result.push({
|
||||
timestamp: data.ts[i],
|
||||
open: data.o[i],
|
||||
high: data.h[i],
|
||||
low: data.l[i],
|
||||
close: data.c[i],
|
||||
volume: data.v[i]
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
const result = []
|
||||
const spanMs = spanMinutes * MINUTE
|
||||
let groupStart = startIndex
|
||||
while (groupStart < endIndex) {
|
||||
let groupEnd = groupStart + 1
|
||||
const groupStartTs = Math.floor(data.ts[groupStart] / spanMs) * spanMs
|
||||
while (groupEnd < endIndex && Math.floor(data.ts[groupEnd] / spanMs) * spanMs === groupStartTs) {
|
||||
groupEnd++
|
||||
}
|
||||
let o = data.o[groupStart]
|
||||
let h = data.h[groupStart]
|
||||
let l = data.l[groupStart]
|
||||
let c = data.c[groupEnd - 1]
|
||||
let v = 0
|
||||
for (let i = groupStart; i < groupEnd; i++) {
|
||||
h = Math.max(h, data.h[i])
|
||||
l = Math.min(l, data.l[i])
|
||||
v += data.v[i]
|
||||
}
|
||||
result.push({
|
||||
timestamp: groupStartTs,
|
||||
open: o,
|
||||
high: h,
|
||||
low: l,
|
||||
close: c,
|
||||
volume: v
|
||||
})
|
||||
groupStart = groupEnd
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function findIndex(data, targetTs) {
|
||||
let lo = 0
|
||||
let hi = data.len - 1
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1
|
||||
if (data.ts[mid] < targetTs) lo = mid + 1
|
||||
else hi = mid - 1
|
||||
}
|
||||
return lo
|
||||
}
|
||||
|
||||
function getSpanMinutes(period) {
|
||||
if (period.type === 'minute') return period.span
|
||||
if (period.type === 'hour') return period.span * 60
|
||||
if (period.type === 'day') return period.span * 1440
|
||||
return 1
|
||||
}
|
||||
|
||||
function loadDataLoader() {
|
||||
return {
|
||||
getBars: ({ type, timestamp, period, callback }) => {
|
||||
if (!rawLoaded) {
|
||||
callback([], { forward: false, backward: false })
|
||||
return
|
||||
}
|
||||
const span = getSpanMinutes(period)
|
||||
const duration = span * MINUTE
|
||||
const bars = []
|
||||
let forwardMore = false
|
||||
let backwardMore = false
|
||||
if (type === 'init') {
|
||||
const latestTs = rawBars.ts[rawBars.len - 1]
|
||||
const startTs = latestTs - LOAD_BAR_COUNT * duration
|
||||
const startIdx = findIndex(rawBars, startTs)
|
||||
const endIdx = findIndex(rawBars, latestTs + 1)
|
||||
bars.push(...aggregateBars(rawBars, startIdx, Math.min(endIdx, rawBars.len), span))
|
||||
forwardMore = startIdx > 0
|
||||
} else if (type === 'forward') {
|
||||
const startIdx = findIndex(rawBars, timestamp)
|
||||
const endIdx = Math.max(0, startIdx - LOAD_BAR_COUNT)
|
||||
bars.push(...aggregateBars(rawBars, endIdx, startIdx, span))
|
||||
forwardMore = endIdx > 0
|
||||
} else if (type === 'backward') {
|
||||
const startIdx = findIndex(rawBars, timestamp)
|
||||
const endIdx = Math.min(rawBars.len, startIdx + LOAD_BAR_COUNT)
|
||||
bars.push(...aggregateBars(rawBars, startIdx, endIdx, span))
|
||||
forwardMore = false
|
||||
backwardMore = endIdx < rawBars.len
|
||||
}
|
||||
callback(bars, { forward: forwardMore, backward: backwardMore })
|
||||
updateStatus()
|
||||
},
|
||||
subscribeBar: () => {},
|
||||
unsubscribeBar: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- UI helpers ----
|
||||
|
||||
function setActivePeriodButton() {
|
||||
document.querySelectorAll('#period-switcher button').forEach((btn) => {
|
||||
btn.classList.toggle('active', btn.dataset.period === activePeriodKey)
|
||||
})
|
||||
}
|
||||
|
||||
function setActiveIndicatorButtons() {
|
||||
document.querySelectorAll('#indicator-switcher button').forEach((btn) => {
|
||||
btn.classList.toggle('active', activeIndicators.has(btn.dataset.indicator))
|
||||
})
|
||||
}
|
||||
|
||||
function updateStatus() {
|
||||
const el = document.getElementById('status')
|
||||
if (!el || chart === null) return
|
||||
const bars = chart.getDataList()
|
||||
if (bars.length > 0) {
|
||||
const from = new Date(bars[0].timestamp).toLocaleDateString()
|
||||
const to = new Date(bars[bars.length - 1].timestamp).toLocaleDateString()
|
||||
el.textContent = `${from} → ${to} | ${bars.length} bars`
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(ts) {
|
||||
const d = new Date(ts)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// ---- Chart actions ----
|
||||
|
||||
function switchPeriod(periodKey) {
|
||||
const config = PERIODS.find(({ key }) => key === periodKey)
|
||||
if (chart === null || config === undefined || activePeriodKey === periodKey) return
|
||||
activePeriodKey = periodKey
|
||||
setActivePeriodButton()
|
||||
chart.setStyles({ candle: { type: config.candleType } })
|
||||
chart.setPeriod(config.period)
|
||||
}
|
||||
|
||||
function toggleIndicator(key) {
|
||||
const spec = INDICATORS.find((i) => i.key === key)
|
||||
if (!spec || chart === null) return
|
||||
if (activeIndicators.has(key)) {
|
||||
activeIndicators.delete(key)
|
||||
chart.removeIndicator({ name: spec.name })
|
||||
} else {
|
||||
activeIndicators.add(key)
|
||||
const opts = spec.stacked
|
||||
? { name: spec.name, paneId: 'candle_pane' }
|
||||
: { name: spec.name, paneId: 'new' }
|
||||
chart.createIndicator(opts, spec.stacked)
|
||||
}
|
||||
setActiveIndicatorButtons()
|
||||
}
|
||||
|
||||
function setTheme(dark) {
|
||||
isDark = dark
|
||||
document.body.classList.toggle('dark', dark)
|
||||
const btn = document.getElementById('btn-theme')
|
||||
btn.textContent = dark ? '☀' : '☾'
|
||||
btn.classList.toggle('active-icon', dark)
|
||||
chart.setStyles(dark ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
function doScreenshot() {
|
||||
if (chart === null) return
|
||||
const url = chart.getConvertPictureUrl(false, 'png', isDark ? '#0d1117' : '#ffffff')
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `kline-${activePeriodKey}-${Date.now()}.png`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
function setBarSpace(value) {
|
||||
if (chart === null) return
|
||||
chart.setBarSpace(value)
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
function doZoomIn() {
|
||||
if (chart === null) return
|
||||
const current = chart.getBarSpace().bar
|
||||
setBarSpace(Math.min(current + BAR_SPACE_STEP, BAR_SPACE_MAX))
|
||||
}
|
||||
|
||||
function doZoomOut() {
|
||||
if (chart === null) return
|
||||
const current = chart.getBarSpace().bar
|
||||
setBarSpace(Math.max(current - BAR_SPACE_STEP, BAR_SPACE_MIN))
|
||||
}
|
||||
|
||||
function doZoomReset() {
|
||||
if (chart === null) return
|
||||
setBarSpace(Math.round((BAR_SPACE_MIN + BAR_SPACE_MAX) / 2))
|
||||
chart.scrollToRealTime()
|
||||
}
|
||||
|
||||
// ---- Crosshair info ----
|
||||
|
||||
function formatPrice(v) { return v != null ? v.toFixed(5) : '-' }
|
||||
function formatVol(v) {
|
||||
if (v == null) return '-'
|
||||
if (v >= 1e9) return (v / 1e9).toFixed(2) + 'B'
|
||||
if (v >= 1e6) return (v / 1e6).toFixed(2) + 'M'
|
||||
if (v >= 1e3) return (v / 1e3).toFixed(2) + 'K'
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function onCrosshairHandler(crosshair) {
|
||||
const el = document.getElementById('crosshair-info')
|
||||
closeCrosshair()
|
||||
closeCrosshair = () => { el.classList.add('hidden') }
|
||||
if (!crosshair || !crosshair.kLineData) {
|
||||
el.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
el.classList.remove('hidden')
|
||||
const k = crosshair.kLineData
|
||||
const d = new Date(k.timestamp)
|
||||
const timeStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
const change = k.close - k.open
|
||||
const changePct = k.open > 0 ? (change / k.open * 100) : 0
|
||||
const color = change >= 0 ? (isDark ? '#ff7b72' : '#cf222e') : (isDark ? '#7ee787' : '#1a7f37')
|
||||
el.innerHTML = `
|
||||
<div style="color:#8b949e;margin-bottom:2px">${timeStr}</div>
|
||||
<div>O <b>${formatPrice(k.open)}</b> H <b>${formatPrice(k.high)}</b></div>
|
||||
<div>L <b>${formatPrice(k.low)}</b> C <b style="color:${color}">${formatPrice(k.close)}</b></div>
|
||||
<div>Δ <b style="color:${color}">${change >= 0 ? '+' : ''}${change.toFixed(5)} (${changePct >= 0 ? '+' : ''}${changePct.toFixed(3)}%)</b></div>
|
||||
<div>Vol <b>${formatVol(k.volume)}</b></div>
|
||||
`
|
||||
}
|
||||
|
||||
// ---- Bar click detail ----
|
||||
|
||||
function onBarClickHandler(crosshair) {
|
||||
const popup = document.getElementById('bar-detail')
|
||||
closePopup()
|
||||
if (!crosshair || !crosshair.kLineData || crosshair.x == null) {
|
||||
popup.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
const k = crosshair.kLineData
|
||||
const bull = k.close >= k.open
|
||||
const cls = bull ? 'bull' : 'bear'
|
||||
popup.classList.remove('hidden')
|
||||
popup.style.left = Math.min(crosshair.x + 20, window.innerWidth - 200) + 'px'
|
||||
popup.style.top = Math.min(crosshair.y + 20, window.innerHeight - 200) + 'px'
|
||||
popup.innerHTML = `
|
||||
<div style="margin-bottom:6px;font-weight:600">${formatDate(k.timestamp)}</div>
|
||||
<div class="row"><span class="label">Open</span><span class="value ${cls}">${formatPrice(k.open)}</span></div>
|
||||
<div class="row"><span class="label">High</span><span class="value">${formatPrice(k.high)}</span></div>
|
||||
<div class="row"><span class="label">Low</span><span class="value">${formatPrice(k.low)}</span></div>
|
||||
<div class="row"><span class="label">Close</span><span class="value ${cls}">${formatPrice(k.close)}</span></div>
|
||||
<div class="row"><span class="label">Volume</span><span class="value">${formatVol(k.volume)}</span></div>
|
||||
`
|
||||
closePopup = () => { popup.classList.add('hidden') }
|
||||
}
|
||||
|
||||
// ---- Right-click context menu ----
|
||||
|
||||
function showContextMenu(x, y) {
|
||||
const menu = document.getElementById('context-menu')
|
||||
const items = INDICATORS.map((ind) => {
|
||||
const on = activeIndicators.has(ind.key)
|
||||
return `<div class="item" data-indicator="${ind.key}"><span class="check">${on ? '✓' : ''}</span>${ind.name}</div>`
|
||||
}).join('')
|
||||
menu.innerHTML = `
|
||||
${items}
|
||||
<div class="sep"></div>
|
||||
<div class="item" data-action="theme"><span class="check"></span>切换主题 <span class="key-hint">D</span></div>
|
||||
<div class="item" data-action="screenshot"><span class="check"></span>截图 <span class="key-hint">S</span></div>
|
||||
`
|
||||
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px'
|
||||
menu.style.top = Math.min(y, window.innerHeight - 260) + 'px'
|
||||
menu.classList.remove('hidden')
|
||||
closePopup()
|
||||
closePopup = () => {
|
||||
menu.classList.add('hidden')
|
||||
menu.querySelectorAll('.item').forEach((el) => el.removeEventListener('click', handleMenuItem))
|
||||
}
|
||||
menu.querySelectorAll('.item').forEach((el) => el.addEventListener('click', handleMenuItem))
|
||||
}
|
||||
|
||||
function handleMenuItem(e) {
|
||||
const el = e.currentTarget
|
||||
const ind = el.dataset.indicator
|
||||
const action = el.dataset.action
|
||||
if (ind) toggleIndicator(ind)
|
||||
if (action === 'theme') setTheme(!isDark)
|
||||
if (action === 'screenshot') doScreenshot()
|
||||
closePopup()
|
||||
}
|
||||
|
||||
// ---- Keyboard ----
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||
if (chart === null) return
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
chart.scrollByDistance(-3 * chart.getBarSpace().bar)
|
||||
e.preventDefault()
|
||||
break
|
||||
case 'ArrowRight':
|
||||
chart.scrollByDistance(3 * chart.getBarSpace().bar)
|
||||
e.preventDefault()
|
||||
break
|
||||
case '+':
|
||||
case '=':
|
||||
doZoomIn()
|
||||
e.preventDefault()
|
||||
break
|
||||
case '-':
|
||||
doZoomOut()
|
||||
e.preventDefault()
|
||||
break
|
||||
case 'Escape':
|
||||
closePopup()
|
||||
closeCrosshair()
|
||||
break
|
||||
case 'd':
|
||||
setTheme(!isDark)
|
||||
break
|
||||
case 's':
|
||||
if (!e.ctrlKey && !e.metaKey) doScreenshot()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Toolbar binding ----
|
||||
|
||||
function bindToolbar() {
|
||||
const picker = document.getElementById('file-picker')
|
||||
const nameEl = document.getElementById('file-name')
|
||||
const btnFile = document.getElementById('btn-file')
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.csv,.txt,.tsv'
|
||||
input.style.display = 'none'
|
||||
document.body.appendChild(input)
|
||||
|
||||
function setFile(file) {
|
||||
fileName = file.name.replace(/\.[^.]+$/, '').replace(/[_\s]+/g, ' ').slice(0, 30)
|
||||
if (nameEl) nameEl.textContent = fileName
|
||||
mountChart(file)
|
||||
}
|
||||
|
||||
btnFile.addEventListener('click', () => input.click())
|
||||
input.addEventListener('change', () => {
|
||||
if (input.files[0]) setFile(input.files[0])
|
||||
})
|
||||
|
||||
picker.addEventListener('dragover', (e) => { e.preventDefault(); picker.classList.add('over') })
|
||||
picker.addEventListener('dragleave', () => picker.classList.remove('over'))
|
||||
picker.addEventListener('drop', (e) => {
|
||||
e.preventDefault()
|
||||
picker.classList.remove('over')
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (file && (file.name.endsWith('.csv') || file.name.endsWith('.txt') || file.name.endsWith('.tsv'))) {
|
||||
setFile(file)
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelectorAll('#period-switcher button').forEach((btn) => {
|
||||
btn.addEventListener('click', () => switchPeriod(btn.dataset.period))
|
||||
})
|
||||
document.querySelectorAll('#indicator-switcher button').forEach((btn) => {
|
||||
btn.addEventListener('click', () => toggleIndicator(btn.dataset.indicator))
|
||||
})
|
||||
document.getElementById('zoom-in').addEventListener('click', doZoomIn)
|
||||
document.getElementById('zoom-out').addEventListener('click', doZoomOut)
|
||||
document.getElementById('zoom-reset').addEventListener('click', doZoomReset)
|
||||
document.getElementById('btn-theme').addEventListener('click', () => setTheme(!isDark))
|
||||
document.getElementById('btn-screenshot').addEventListener('click', doScreenshot)
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
document.addEventListener('click', () => { closePopup(); closeCrosshair() })
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
showContextMenu(e.clientX, e.clientY)
|
||||
})
|
||||
setActivePeriodButton()
|
||||
setActiveIndicatorButtons()
|
||||
}
|
||||
|
||||
function mountChart(source) {
|
||||
const chartDom = document.getElementById('chart')
|
||||
if (chartDom === null) return
|
||||
const loadingEl = document.getElementById('loading')
|
||||
if (loadingEl) loadingEl.style.display = 'block'
|
||||
loadRawCSV(source).then(data => {
|
||||
rawBars = data
|
||||
rawLoaded = true
|
||||
if (loadingEl) loadingEl.style.display = 'none'
|
||||
console.log(`Loaded ${data.len} bars, ${new Date(data.ts[0])} → ${new Date(data.ts[data.len - 1])}`)
|
||||
if (chart) dispose(chartDom)
|
||||
chart = init(chartDom, {
|
||||
styles: {
|
||||
candle: { type: getActivePeriodConfig().candleType },
|
||||
indicator: { lastValueMark: { show: true } }
|
||||
},
|
||||
layout: {
|
||||
barSpaceLimit: { min: BAR_SPACE_MIN, max: BAR_SPACE_MAX },
|
||||
basicParams: {}
|
||||
}
|
||||
})
|
||||
chart.setSymbol({ ticker: fileName || 'CSV', pricePrecision: 5, volumePrecision: 0 })
|
||||
chart.setPeriod(getActivePeriodConfig().period)
|
||||
chart.setDataLoader(loadDataLoader())
|
||||
chart.subscribeAction('onCrosshairChange', onCrosshairHandler)
|
||||
chart.subscribeAction('onCandleBarClick', onBarClickHandler)
|
||||
setTimeout(updateStatus, 200)
|
||||
}).catch(err => {
|
||||
if (loadingEl) loadingEl.textContent = 'Error: ' + err.message
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const chartDom = document.getElementById('chart')
|
||||
if (chartDom !== null) dispose(chartDom)
|
||||
})
|
||||
|
||||
bindToolbar()
|
||||
mountChart('/data/eurgbp_m1.csv')
|
||||
Generated
+997
@@ -0,0 +1,997 @@
|
||||
{
|
||||
"name": "kline-demo",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kline-demo",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"klinecharts": "^10.0.2",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
||||
"integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^22.20 || ^24.12 || >=25"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
|
||||
"integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
|
||||
"integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
|
||||
"integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
|
||||
"integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
|
||||
"integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
|
||||
"integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
|
||||
"integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
|
||||
"integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
|
||||
"integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
|
||||
"integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
|
||||
"integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
|
||||
"integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
|
||||
"integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
|
||||
"integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
|
||||
"integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
|
||||
"integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
|
||||
"integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
|
||||
"integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
|
||||
"integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/klinecharts": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/klinecharts/-/klinecharts-10.0.2.tgz",
|
||||
"integrity": "sha512-OJmaG047vd6RPKXxQAnbGPnfUQ3tKCKBlxk/oMvRJBXO6CyaZuqFGQ8ccoU6YTNew7DV8ynbsfti/JERe+M5IQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
|
||||
"integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.62.4",
|
||||
"@rollup/rollup-android-arm64": "4.62.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.62.4",
|
||||
"@rollup/rollup-darwin-x64": "4.62.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.62.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.62.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.62.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.62.4",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.62.4",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.62.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.62.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.62.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.62.4",
|
||||
"@rollup/rollup-openbsd-x64": "4.62.4",
|
||||
"@rollup/rollup-openharmony-arm64": "4.62.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.62.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.62.4",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.62.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.62.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "kline-demo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"klinecharts": "^10.0.2",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid #d0d7de;
|
||||
background: #f6f8fa;
|
||||
color: #1f2328;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body.dark #toolbar {
|
||||
border-color: #30363d;
|
||||
background: #161b22;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
#period-switcher,
|
||||
#indicator-switcher,
|
||||
#zoom-controls,
|
||||
#tool-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
body.dark #period-switcher,
|
||||
body.dark #indicator-switcher,
|
||||
body.dark #zoom-controls,
|
||||
body.dark #tool-actions {
|
||||
border-color: #30363d;
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #eef2f6;
|
||||
border-color: #c9d0d8;
|
||||
}
|
||||
|
||||
body.dark .toolbar-btn:hover {
|
||||
background: #30363d;
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
border-color: #0969da;
|
||||
background: #0969da;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.toolbar-btn.active:hover {
|
||||
background: #0969da;
|
||||
}
|
||||
|
||||
.toolbar-btn.wide {
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
.toolbar-btn.icon {
|
||||
min-width: 32px;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.toolbar-btn.active-icon {
|
||||
color: #0969da;
|
||||
background: transparent;
|
||||
border-color: #0969da;
|
||||
}
|
||||
|
||||
#file-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 6px;
|
||||
border: 1px dashed #d0d7de;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#file-picker.over {
|
||||
border-color: #0969da;
|
||||
background: #ddf4ff;
|
||||
}
|
||||
|
||||
body.dark #file-picker {
|
||||
border-color: #30363d;
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
body.dark #file-picker.over {
|
||||
border-color: #58a6ff;
|
||||
background: #1f6feb;
|
||||
}
|
||||
|
||||
#btn-file {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#btn-file:hover {
|
||||
background: #eef2f6;
|
||||
}
|
||||
|
||||
body.dark #btn-file:hover {
|
||||
background: #30363d;
|
||||
}
|
||||
|
||||
#file-name {
|
||||
font-size: 12px;
|
||||
color: #57606a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
body.dark #file-name { color: #8b949e; }
|
||||
|
||||
#status {
|
||||
margin-left: auto;
|
||||
color: #57606a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body.dark #status { color: #8b949e; }
|
||||
|
||||
#chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #8b949e;
|
||||
font-size: 14px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#crosshair-info {
|
||||
position: fixed;
|
||||
top: 52px;
|
||||
right: 16px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #d0d7de;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
body.dark #crosshair-info {
|
||||
background: rgba(22, 27, 34, 0.95);
|
||||
border-color: #30363d;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
#bar-detail,
|
||||
#context-menu {
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d0d7de;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
body.dark #bar-detail,
|
||||
body.dark #context-menu {
|
||||
border-color: #30363d;
|
||||
background: #161b22;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#bar-detail {
|
||||
padding: 10px 14px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
#bar-detail .row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
#bar-detail .label { color: #8b949e; }
|
||||
#bar-detail .value { font-weight: 600; font-family: ui-monospace, monospace; }
|
||||
#bar-detail .bull { color: #cf222e; }
|
||||
#bar-detail .bear { color: #1a7f37; }
|
||||
|
||||
body.dark #bar-detail .bull { color: #ff7b72; }
|
||||
body.dark #bar-detail .bear { color: #7ee787; }
|
||||
|
||||
#context-menu {
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
#context-menu .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#context-menu .item:hover {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
body.dark #context-menu .item:hover {
|
||||
background: #30363d;
|
||||
}
|
||||
|
||||
#context-menu .sep {
|
||||
height: 1px;
|
||||
margin: 4px 8px;
|
||||
background: #d0d7de;
|
||||
}
|
||||
|
||||
body.dark #context-menu .sep { background: #30363d; }
|
||||
|
||||
#context-menu .check { width: 14px; color: #0969da; }
|
||||
|
||||
.key-hint {
|
||||
color: #8b949e;
|
||||
font-size: 11px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
Reference in New Issue
Block a user