Publish dataease via gitea-publish skill
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# DataEase V2 全能技能
|
||||
|
||||
一站式 DataEase 自动化解决方案,融合图表部署与资源管理能力。
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
| 功能 | 命令 | 说明 |
|
||||
|------|------|------|
|
||||
| 📊 数据探索 | `inspect_data.py` | 查询数据集、字段信息 |
|
||||
| 📈 图表部署 | `deploy.py` | 创建图表并自动截图 |
|
||||
| 📋 多图看板 | `multi_deploy.py` | 创建多图表仪表板 |
|
||||
| 🏢 组织管理 | `capture_dashboard.py` | 查询/切换组织 |
|
||||
| 📑 资源列表 | `capture_dashboard.py` | 列出仪表板/大屏 |
|
||||
| 📸 截图导出 | `capture_dashboard.py` | 导出截图或PDF |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### 2. 配置环境
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件:
|
||||
|
||||
```bash
|
||||
DATAEASE_BASE_URL=https://your-dataease.example.com
|
||||
DATAEASE_API_PREFIX=/de2api
|
||||
DATAEASE_ACCESS_KEY=your_access_key
|
||||
DATAEASE_SECRET_KEY=your_secret_key
|
||||
DATAEASE_USERNAME=admin
|
||||
DATAEASE_PASSWORD=your_password
|
||||
DATAEASE_LOGIN_ORIGIN=0
|
||||
```
|
||||
|
||||
**认证方式(二选一):**
|
||||
- AK/SK:配置 `ACCESS_KEY` + `SECRET_KEY`
|
||||
- 密码登录:配置 `USERNAME` + `PASSWORD`
|
||||
|
||||
### 3. 使用示例
|
||||
|
||||
```bash
|
||||
# 查询数据集
|
||||
python3 scripts/inspect_data.py --list-datasets
|
||||
|
||||
# 查询字段
|
||||
python3 scripts/inspect_data.py --dataset "销售数据"
|
||||
|
||||
# 创建图表(自动截图)
|
||||
python3 scripts/deploy.py bar '各产品销售额' '销售数据' '产品' '实际销售'
|
||||
|
||||
# 创建多图表看板
|
||||
python3 scripts/multi_deploy.py '销售分析' '[{"type":"bar","title":"销售","dataset_name":"销售数据","x_axis":["产品"],"y_axis":["销售额"]}]'
|
||||
|
||||
# 查询组织
|
||||
python3 scripts/capture_dashboard.py list-orgs
|
||||
|
||||
# 导出截图
|
||||
python3 scripts/capture_dashboard.py capture --resource-id <ID> --busi-type dashboard --output-dir ./output
|
||||
|
||||
# 导出 PDF
|
||||
python3 scripts/capture_dashboard.py capture --resource-id <ID> --busi-type dashboard --result-format 1 --output-dir ./output
|
||||
```
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
dataease-v2-chart-skill/
|
||||
├── SKILL.md # 技能文档(Agent 读取)
|
||||
├── README.md # 本文档
|
||||
├── .env.example # 环境变量模板
|
||||
├── package.json # Node 依赖
|
||||
├── scripts/
|
||||
│ ├── inspect_data.py # 数据探索
|
||||
│ ├── deploy.py # 图表部署
|
||||
│ ├── multi_deploy.py # 多图表部署
|
||||
│ ├── engine.py # 图表引擎
|
||||
│ ├── client.py # API 客户端
|
||||
│ ├── capture_dashboard.py # 截图/资源管理
|
||||
│ └── browser_capture.mjs # 浏览器截图
|
||||
├── templates/ # 图表模板
|
||||
├── references/ # API 参考
|
||||
└── agents/ # Agent 配置
|
||||
```
|
||||
|
||||
## 📊 支持的图表类型
|
||||
|
||||
- `bar` - 柱状图
|
||||
- `line` - 折线图
|
||||
- `pie` - 饼图
|
||||
- `table_info` - 明细表
|
||||
|
||||
## 📸 截图参数
|
||||
|
||||
- `--pixel`: 分辨率,默认 `1920*1080`,可设 `2560*1440`
|
||||
- `--ext-wait-time`: 额外等待秒数
|
||||
- `--result-format`: `0`=JPEG, `1`=PDF
|
||||
|
||||
## 📝 许可证
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,363 @@
|
||||
---
|
||||
name: dataease
|
||||
description: DataEase V2 全能技能 - 创建图表看板、查询数据集、管理组织、导出截图/PDF。支持柱状图、折线图、饼图、明细表,部署后自动截图展示。
|
||||
environment:
|
||||
required:
|
||||
- DATAEASE_ACCESS_KEY
|
||||
- DATAEASE_SECRET_KEY
|
||||
- DATAEASE_BASE_URL
|
||||
security:
|
||||
requiresSecrets: true
|
||||
sensitiveEnvironment: true
|
||||
externalNetworkAccess: true
|
||||
---
|
||||
|
||||
# DataEase V2 全能技能
|
||||
|
||||
一站式 DataEase 自动化解决方案,融合图表部署与资源管理能力。
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
| 功能模块 | 命令 | 说明 |
|
||||
|---------|------|------|
|
||||
| 📊 数据探索 | `inspect_data.py` | 查询数据集列表、字段信息 |
|
||||
| 📈 图表部署 | `deploy.py` | 创建单图表并自动截图 |
|
||||
| 📋 多图看板 | `multi_deploy.py` | 创建多图表仪表板并自动截图 |
|
||||
| 🏢 组织管理 | `capture_dashboard.py list-orgs/switch-org` | 查询/切换组织 |
|
||||
| 📑 资源列表 | `capture_dashboard.py list-resources` | 列出仪表板/大屏 |
|
||||
| 📸 截图导出 | `capture_dashboard.py capture` | 导出仪表板截图或PDF |
|
||||
|
||||
## 🎭 沟通风格 (Persona)
|
||||
|
||||
作为 DataEase 自动化专家:
|
||||
1. **执行优先**: 直接运行工具,不预先展示代码
|
||||
2. **结果导向**: 优先展示截图,再说明逻辑
|
||||
3. **单次往复**: 一次完成从探索到部署的全过程
|
||||
|
||||
## 🚀 环境配置
|
||||
|
||||
### 1. 配置凭据
|
||||
|
||||
复制 `.env.example` 为 `.env`,填写:
|
||||
|
||||
```bash
|
||||
DATAEASE_BASE_URL=https://your-dataease.example.com
|
||||
DATAEASE_ACCESS_KEY=your_access_key
|
||||
DATAEASE_SECRET_KEY=your_secret_key
|
||||
DATAEASE_USERNAME=admin # 可选,用于密码登录
|
||||
DATAEASE_PASSWORD=your_password # 可选
|
||||
DATAEASE_LOGIN_ORIGIN=0 # 登录源
|
||||
|
||||
# 截图输出目录(必须在 OpenClaw workspace 内才能用 MEDIA: 展示,不输入默认直接在 OpenClaw workspace 内创建)
|
||||
DATAEASE_OUTPUT_DIR=/Users/username/.openclaw/workspace/dataease-output
|
||||
```
|
||||
|
||||
### 2. 安装截图依赖
|
||||
|
||||
首次使用需安装 Playwright 浏览器:
|
||||
|
||||
```bash
|
||||
cd {baseDir}
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### 3. 验证连接
|
||||
|
||||
```bash
|
||||
python3 scripts/inspect_data.py --list-datasets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 数据探索
|
||||
|
||||
### 查询所有数据集
|
||||
```bash
|
||||
python3 scripts/inspect_data.py --list-datasets
|
||||
```
|
||||
|
||||
### 查询特定数据集字段
|
||||
```bash
|
||||
python3 scripts/inspect_data.py --dataset "<数据集名称或ID>"
|
||||
```
|
||||
|
||||
**返回示例**:
|
||||
```json
|
||||
[
|
||||
{"name": "产品", "type": 0, "id": "..."},
|
||||
{"name": "销售额", "type": 2, "id": "..."}
|
||||
]
|
||||
```
|
||||
|
||||
字段类型:`0`=文本, `1`=日期, `2`=指标, `3`=数值
|
||||
|
||||
---
|
||||
|
||||
## 📈 图表部署
|
||||
|
||||
### 单图表部署(自动截图)
|
||||
```bash
|
||||
python3 scripts/deploy.py <type> <title> <dataset> <x_fields> <y_fields>
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `type`: `bar`(柱状图), `line`(折线图), `pie`(饼图), `table_info`(明细表)
|
||||
- `title`: 图表标题
|
||||
- `dataset`: 数据集名称或ID
|
||||
- `x_fields`: 维度字段(逗号分隔)
|
||||
- `y_fields`: 指标字段(逗号分隔)
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
python3 scripts/deploy.py bar '各产品销售额' '销售数据' '产品' '实际销售'
|
||||
python3 scripts/deploy.py bar '销售对比' '销售数据' '公司' '期望销售,实际销售'
|
||||
python3 scripts/deploy.py pie '产品占比' '销售数据' '产品类别' '实际销售'
|
||||
```
|
||||
|
||||
**可选参数**:
|
||||
- `--no-screenshot`: 跳过截图,仅返回链接
|
||||
|
||||
**输出**:
|
||||
- 自动截图并返回 JSON 结果(包含 `screenshot` 路径)
|
||||
- Agent 应使用 `MEDIA:<screenshot_path>` 展示截图
|
||||
|
||||
### 多图表看板部署
|
||||
```bash
|
||||
python3 scripts/multi_deploy.py <dashboard_title> '<charts_json>'
|
||||
```
|
||||
|
||||
**JSON 结构**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "bar",
|
||||
"title": "图表标题",
|
||||
"dataset_name": "数据集名",
|
||||
"x_axis": ["维度字段"],
|
||||
"y_axis": ["指标1", "指标2"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
python3 scripts/multi_deploy.py '销售分析看板' '[
|
||||
{"type":"bar","title":"各产品销售","dataset_name":"销售数据","x_axis":["产品"],"y_axis":["实际销售"]},
|
||||
{"type":"pie","title":"类别占比","dataset_name":"销售数据","x_axis":["产品类别"],"y_axis":["实际销售"]}
|
||||
]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏢 组织管理
|
||||
|
||||
### 查询组织列表
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py list-orgs
|
||||
```
|
||||
|
||||
**返回**:所有可用组织及其 ID
|
||||
|
||||
### 切换组织
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py switch-org --org-id <组织ID>
|
||||
```
|
||||
|
||||
切换后,后续操作将在新组织上下文中执行。
|
||||
|
||||
---
|
||||
|
||||
## 📑 资源列表
|
||||
|
||||
### 列出仪表板
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py list-resources --busi-type dashboard
|
||||
```
|
||||
|
||||
### 列出数据大屏
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py list-resources --busi-type dataV
|
||||
```
|
||||
|
||||
### 指定组织查询
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py list-resources --org-id <组织ID> --busi-type dashboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📸 截图导出
|
||||
|
||||
### 导出仪表板截图
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py capture \
|
||||
--resource-id <仪表板ID> \
|
||||
--busi-type dashboard \
|
||||
--output-dir <输出目录>
|
||||
```
|
||||
|
||||
### 导出数据大屏
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py capture \
|
||||
--resource-id <大屏ID> \
|
||||
--busi-type dataV \
|
||||
--output-dir <输出目录>
|
||||
```
|
||||
|
||||
### 导出 PDF
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py capture \
|
||||
--resource-id <ID> \
|
||||
--busi-type dashboard \
|
||||
--result-format 1 \
|
||||
--output-dir <输出目录>
|
||||
```
|
||||
|
||||
### 按名称匹配导出
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py capture \
|
||||
--resource-name "销售总览" \
|
||||
--busi-type dashboard
|
||||
```
|
||||
|
||||
### 高级参数
|
||||
- `--pixel`: 分辨率,默认 `1920*1080`,可用 `2560*1440`
|
||||
- `--ext-wait-time`: 额外等待时间(秒),用于复杂图表
|
||||
- `--org-id`: 指定组织 ID
|
||||
|
||||
### ⚠️ 导出结果处理
|
||||
|
||||
**重要:导出的图片和 PDF 必须直接发送到对话中展示!**
|
||||
|
||||
截图或 PDF 导出成功后,返回的 JSON 中包含 `saved_file` 字段:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"saved_file": "/path/to/file.jpg" // 或 .pdf
|
||||
}
|
||||
```
|
||||
|
||||
**Agent 必须执行:**
|
||||
1. 读取 `saved_file` 路径
|
||||
2. 使用 `MEDIA:<saved_file>` 语法直接发送到对话中
|
||||
|
||||
**示例:**
|
||||
```
|
||||
MEDIA:/path/to/output/仪表板名_123456.jpg
|
||||
MEDIA:/path/to/output/仪表板名_123456.pdf
|
||||
```
|
||||
|
||||
**不要只返回文件路径,必须使用 MEDIA: 发送文件!**
|
||||
|
||||
---
|
||||
|
||||
## 🛠 参数规范
|
||||
|
||||
### 图表类型
|
||||
| 类型 | 说明 | 适用场景 |
|
||||
|-----|------|---------|
|
||||
| `bar` | 柱状图 | 分类对比、排名 |
|
||||
| `line` | 折线图 | 趋势分析、时序数据 |
|
||||
| `pie` | 饼图 | 占比分析、构成分布 |
|
||||
| `table_info` | 明细表 | 数据明细展示 |
|
||||
|
||||
### 维度与指标
|
||||
- **维度 (x_axis)**: 类别、日期等分组字段
|
||||
- **指标 (y_axis)**: 数值、金额等聚合字段
|
||||
- 明细表的 `y_axis` 传空,`x_axis` 列出所有展示字段
|
||||
|
||||
### 业务类型
|
||||
- `dashboard`: 仪表板
|
||||
- `dataV`: 数据大屏
|
||||
|
||||
---
|
||||
|
||||
## 📸 结果处理
|
||||
|
||||
### 图表部署结果
|
||||
|
||||
部署成功后输出 JSON:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"dashboard_id": "1243929230048366592",
|
||||
"url": "https://...",
|
||||
"screenshot": "/path/to/screenshot.jpg",
|
||||
"title": "图表标题"
|
||||
}
|
||||
```
|
||||
|
||||
**Agent 处理流程**:
|
||||
1. 读取 `screenshot` 路径图片
|
||||
2. 使用 `MEDIA:<screenshot_path>` 展示
|
||||
3. 同时提供访问链接
|
||||
|
||||
### 截图导出结果
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"resource_id": "...",
|
||||
"resource_name": "...",
|
||||
"saved_file": "/path/to/file.jpg",
|
||||
"pixel": "1920*1080",
|
||||
"capture_engine": "local_playwright"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见问题
|
||||
|
||||
### SSL 证书错误
|
||||
macOS 上 Python 可能缺少系统证书,运行:
|
||||
```bash
|
||||
/Applications/Python\ 3.13/Install\ Certificates.command
|
||||
```
|
||||
|
||||
### Playwright 浏览器未安装
|
||||
```bash
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### 截图超时
|
||||
使用 `--ext-wait-time` 增加等待时间:
|
||||
```bash
|
||||
python3 scripts/capture_dashboard.py capture --resource-id <ID> --ext-wait-time 5 --busi-type dashboard
|
||||
```
|
||||
|
||||
### 资源名称匹配失败
|
||||
使用 `--resource-id` 精确指定,或查看 `candidates` 字段获取候选列表。
|
||||
|
||||
---
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
skills/dataease/
|
||||
├── SKILL.md # 本文档
|
||||
├── .env # 环境配置
|
||||
├── .env.example # 配置模板
|
||||
├── package.json # Node 依赖
|
||||
├── scripts/
|
||||
│ ├── inspect_data.py # 数据探索
|
||||
│ ├── deploy.py # 单图表部署
|
||||
│ ├── multi_deploy.py # 多图表部署
|
||||
│ ├── engine.py # 图表引擎
|
||||
│ ├── multi_engine.py # 多图表引擎
|
||||
│ ├── client.py # API 客户端
|
||||
│ ├── capture_dashboard.py # 截图/资源管理
|
||||
│ └── browser_capture.mjs # 浏览器截图
|
||||
├── templates/ # 图表模板
|
||||
│ ├── chart_bar/
|
||||
│ ├── chart_line/
|
||||
│ ├── chart_pie/
|
||||
│ └── chart_table_info/
|
||||
├── references/ # 参考文档
|
||||
│ ├── api.md
|
||||
│ └── resource_aliases.json
|
||||
├── agents/
|
||||
│ └── openai.yaml
|
||||
└── output/ # 截图输出目录
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "DataEase 全能技能"
|
||||
short_description: "创建图表看板、查询数据、管理组织、导出截图/PDF"
|
||||
icon: "chart-bar"
|
||||
color: "blue"
|
||||
default_prompt: "Use dataease skill to explore datasets, create charts and dashboards, manage organizations, list resources, and export screenshots or PDFs. Charts are automatically captured and displayed."
|
||||
Generated
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"name": "dataease-skills",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dataease-skills",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"pdf-lib": "^1.17.1",
|
||||
"playwright": "^1.59.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.59.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "dataease-skills",
|
||||
"version": "1.0.0",
|
||||
"description": "DataEase v2 Chart Skill with auto-screenshot",
|
||||
"dependencies": {
|
||||
"pdf-lib": "^1.17.1",
|
||||
"playwright": "^1.59.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# DataEase 接口说明
|
||||
|
||||
## 服务地址
|
||||
|
||||
- 技能执行时建议从环境变量 `DATAEASE_BASE_URL` 读取,避免把环境地址写死到脚本参数中。
|
||||
|
||||
## 1. 查询组织树
|
||||
|
||||
### 接口地址
|
||||
|
||||
`POST /de2api/org/page/tree`
|
||||
|
||||
### 请求参数
|
||||
|
||||
- `keyword`
|
||||
- 组织名称关键字
|
||||
- `desc`
|
||||
- 是否倒序
|
||||
|
||||
### 本技能的用途
|
||||
|
||||
用于查看当前凭证可访问的组织树,并辅助确认目标资源是否位于其他组织。
|
||||
|
||||
## 2. 切换组织
|
||||
|
||||
### 接口地址
|
||||
|
||||
`POST /de2api/user/switch/{orgId}`
|
||||
|
||||
### 本技能的用途
|
||||
|
||||
在用户显式提供 `orgId` 时切换组织上下文,并使用响应中的 `data.token` 作为后续资源树查询和本地预览截图的 `x-de-token`。
|
||||
|
||||
## 3. 查询可视化资源树
|
||||
|
||||
### 接口地址
|
||||
|
||||
`POST /de2api/dataVisualization/tree`
|
||||
|
||||
### 请求参数
|
||||
|
||||
- `busiFlag`
|
||||
- `dashboard`:仪表板
|
||||
- `dataV`:数据大屏
|
||||
- `resourceTable`
|
||||
- 默认值:`core`
|
||||
|
||||
### 响应字段
|
||||
|
||||
- `code`
|
||||
- `msg`
|
||||
- `data`
|
||||
- `id`
|
||||
- `name`
|
||||
- `leaf`
|
||||
- `type`
|
||||
- `children`
|
||||
|
||||
### 本技能的用途
|
||||
|
||||
通过该接口获取 dashboard 或 dataV 的资源树,并展开为资源列表,供查询和导出使用。
|
||||
|
||||
## 4. 本地预览截图
|
||||
|
||||
### 预览页地址
|
||||
|
||||
- `/#/preview?dvId={resourceId}`
|
||||
- 当 `busiType=dashboard` 时追加 `&report=true`
|
||||
|
||||
### 本技能的用途
|
||||
|
||||
`capture` 命令不再调用 `/de2api/report/export`,而是:
|
||||
|
||||
1. 通过 `/de2api/dataVisualization/tree` 定位目标资源
|
||||
2. 获取可用于前端预览页的 `x-de-token`
|
||||
3. 打开预览页并将 token 注入 `localStorage.user.token`
|
||||
4. 等待 `.canvas-container` 渲染完成后本地导出
|
||||
|
||||
### 导出参数
|
||||
|
||||
- `pixel`
|
||||
- 浏览器视口大小,格式为 `宽*高`
|
||||
- `extWaitTime`
|
||||
- 预览画布可见后额外等待的秒数
|
||||
- `resultFormat`
|
||||
- `0`:JPEG
|
||||
- `1`:PDF
|
||||
|
||||
### 默认值
|
||||
|
||||
- `busiType=dashboard`
|
||||
- `pixel=1920*1080`
|
||||
- `extWaitTime=0`
|
||||
- `resultFormat=0`
|
||||
|
||||
## 5. 鉴权说明
|
||||
|
||||
当前按以下配置项接入:
|
||||
|
||||
- `DATAEASE_BASE_URL`
|
||||
- `DATAEASE_ACCESS_KEY`
|
||||
- `DATAEASE_SECRET_KEY`
|
||||
- `DATAEASE_USERNAME`
|
||||
- `DATAEASE_PASSWORD`
|
||||
- `DATAEASE_LOGIN_ORIGIN`
|
||||
|
||||
### 用户名密码登录方式
|
||||
|
||||
- 先通过 `GET /de2api/dekey` 获取前端加密所需的 dekey
|
||||
- 使用 dekey 解出 RSA 公钥后,对用户名和密码做 RSA 加密
|
||||
- 再调用 `POST /de2api/login/localLogin`
|
||||
- 响应中的 `data.token` 可直接作为业务接口和预览页使用的 `x-de-token`
|
||||
- `origin=0` 表示本地账号,`origin=1` 表示 LDAP
|
||||
|
||||
### ask-token 生成方式
|
||||
|
||||
- 原始签名串格式:`<accessKey>|<uuid>|<timestamp_ms>`
|
||||
- 使用 `secretKey` 作为 AES key,`accessKey` 作为 IV
|
||||
- 加密算法:`AES/CBC/PKCS5Padding`
|
||||
- 将加密结果做 Base64,得到 `signature`
|
||||
- 使用 `secretKey` 对 JWT 进行 `HS256` 签名
|
||||
- JWT claim 包含:
|
||||
- `accessKey`
|
||||
- `signature`
|
||||
|
||||
### 请求头
|
||||
|
||||
默认请求头:
|
||||
|
||||
- `accessKey`
|
||||
- `signature`
|
||||
- `x-de-ask-token`
|
||||
|
||||
切换组织后查询资源或导出时:
|
||||
|
||||
- `x-de-token`
|
||||
|
||||
## 6. 当前脚本对应动作
|
||||
|
||||
- `list-orgs`
|
||||
- `switch-org`
|
||||
- `list-resources`
|
||||
- `capture`
|
||||
|
||||
如果实际网关存在额外签名规则,请按部署环境调整 `scripts/capture_dashboard.py`。
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"销售总览": "销售经营总览看板",
|
||||
"华东分析": "华东区域经营分析",
|
||||
"门店大屏": "门店运营监控"
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const MAX_DEBUG_LOGS = 20;
|
||||
const RENDER_SETTLE_POLL_MS = 500;
|
||||
const RENDER_SETTLE_IDLE_MS = 2000;
|
||||
const MAX_CAPTURE_VIEWPORT_HEIGHT = 12000;
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/browser_capture.mjs --url <url> --token <x-de-token> --width 1920 --height 1080 --wait-seconds 0 --result-format 0 --output /abs/path/file.jpg
|
||||
|
||||
Options:
|
||||
--url DataEase preview URL
|
||||
--token X-DE-TOKEN to inject into localStorage as user.token
|
||||
--width Browser viewport width in pixels
|
||||
--height Browser viewport height in pixels
|
||||
--wait-seconds Extra wait time after canvas is visible
|
||||
--result-format 0=jpeg, 1=pdf
|
||||
--output Absolute or relative output path
|
||||
-h, --help Show this help message
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const current = argv[index];
|
||||
if (current === '-h' || current === '--help') {
|
||||
args.help = true;
|
||||
continue;
|
||||
}
|
||||
if (!current.startsWith('--')) {
|
||||
throw new Error(`Unexpected argument: ${current}`);
|
||||
}
|
||||
const key = current.slice(2);
|
||||
const value = argv[index + 1];
|
||||
if (value == null || value.startsWith('--')) {
|
||||
throw new Error(`Missing value for --${key}`);
|
||||
}
|
||||
args[key] = value;
|
||||
index += 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value, name) {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(number) || number <= 0) {
|
||||
throw new Error(`${name} must be a positive integer`);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function parseWaitSeconds(value) {
|
||||
const number = Number.parseInt(value ?? '0', 10);
|
||||
if (!Number.isFinite(number) || number < 0) {
|
||||
throw new Error('wait-seconds must be a non-negative integer');
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
async function waitForCanvasReady(page, selector, timeout) {
|
||||
const locator = page.locator(selector).first();
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
await page.waitForFunction(
|
||||
targetSelector => {
|
||||
const element = document.querySelector(targetSelector);
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
},
|
||||
selector,
|
||||
{ timeout }
|
||||
);
|
||||
await page.waitForTimeout(500);
|
||||
return locator;
|
||||
}
|
||||
|
||||
function buildWsCacheItem(value, now = Date.now(), expiresAt = 253402300799000) {
|
||||
return JSON.stringify({
|
||||
c: now,
|
||||
e: expiresAt,
|
||||
v: JSON.stringify(value)
|
||||
});
|
||||
}
|
||||
|
||||
async function collectDiagnostics(page, debugLogs) {
|
||||
const state = await page.evaluate(() => {
|
||||
const app = document.querySelector('#app');
|
||||
const visible = element => {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0 && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
return {
|
||||
href: location.href,
|
||||
title: document.title,
|
||||
hasCanvas: !!document.querySelector('.canvas-container'),
|
||||
hasContent: !!document.querySelector('.content'),
|
||||
hasEmpty: !!document.querySelector('.empty-background, .el-empty'),
|
||||
visibleLoadingMasks: Array.from(document.querySelectorAll('.el-loading-mask,.ed-loading-mask,.v-loading-mask')).filter(visible).length,
|
||||
unfinishedReportLoads: document.querySelectorAll('.report-load:not(.report-load-finish)').length,
|
||||
bodyText: (document.body?.innerText || '').slice(0, 400),
|
||||
appHtml: (app?.innerHTML || '').slice(0, 1200)
|
||||
};
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
logs: debugLogs.slice(-MAX_DEBUG_LOGS)
|
||||
};
|
||||
}
|
||||
|
||||
async function getRenderState(page, selector) {
|
||||
return page.evaluate(targetSelector => {
|
||||
const visible = element => {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0 && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const canvas = document.querySelector(targetSelector);
|
||||
const loadingSelectors = '.el-loading-mask,.ed-loading-mask,.v-loading-mask,[class*="loading-mask"]';
|
||||
const visibleLoadingMasks = Array.from(document.querySelectorAll(loadingSelectors)).filter(visible).length;
|
||||
const unfinishedReportLoads = document.querySelectorAll('.report-load:not(.report-load-finish)').length;
|
||||
const box = canvas?.getBoundingClientRect();
|
||||
return {
|
||||
hasCanvas: !!canvas,
|
||||
visibleLoadingMasks,
|
||||
unfinishedReportLoads,
|
||||
width: Math.round(box?.width || 0),
|
||||
height: Math.round(box?.height || 0)
|
||||
};
|
||||
}, selector);
|
||||
}
|
||||
|
||||
async function waitForRenderSettled(page, selector, timeout, debugLogs, getPendingRequests) {
|
||||
const startedAt = Date.now();
|
||||
let stableSince = 0;
|
||||
let lastSnapshot = '';
|
||||
|
||||
while (Date.now() - startedAt < timeout) {
|
||||
const state = await getRenderState(page, selector);
|
||||
const pendingRequests = getPendingRequests();
|
||||
const snapshot = JSON.stringify({ ...state, pendingRequests });
|
||||
if (snapshot !== lastSnapshot) {
|
||||
debugLogs.push(`render-state ${snapshot}`);
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
|
||||
const settled =
|
||||
state.hasCanvas &&
|
||||
state.width > 0 &&
|
||||
state.height > 0 &&
|
||||
state.visibleLoadingMasks === 0 &&
|
||||
state.unfinishedReportLoads === 0 &&
|
||||
pendingRequests === 0;
|
||||
|
||||
if (settled) {
|
||||
if (!stableSince) {
|
||||
stableSince = Date.now();
|
||||
}
|
||||
if (Date.now() - stableSince >= RENDER_SETTLE_IDLE_MS) {
|
||||
return state;
|
||||
}
|
||||
} else {
|
||||
stableSince = 0;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(RENDER_SETTLE_POLL_MS);
|
||||
}
|
||||
|
||||
const state = await getRenderState(page, selector);
|
||||
throw new Error(
|
||||
`render settle timeout: ${JSON.stringify({
|
||||
...state,
|
||||
pendingRequests: getPendingRequests()
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
async function expandScrollableCapture(page, selector, viewportWidth, viewportHeight) {
|
||||
return page.evaluate(
|
||||
({ targetSelector, maxViewportHeight, minViewportHeight, viewportWidth }) => {
|
||||
const canvas = document.querySelector(targetSelector);
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isScrollable = element => {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(element);
|
||||
const overflowY = style.overflowY;
|
||||
return ['auto', 'scroll', 'overlay'].includes(overflowY) && element.scrollHeight - element.clientHeight > 2;
|
||||
};
|
||||
|
||||
const applyStyle = (element, key, value) => {
|
||||
const styleKey = key;
|
||||
element.style[styleKey] = value;
|
||||
};
|
||||
|
||||
let scrollableAncestor = null;
|
||||
let current = canvas.parentElement;
|
||||
while (current && current !== document.body) {
|
||||
if (isScrollable(current)) {
|
||||
scrollableAncestor = current;
|
||||
break;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
const scrollingElement = document.scrollingElement || document.documentElement;
|
||||
const initialRect = canvas.getBoundingClientRect();
|
||||
|
||||
if (scrollableAncestor) {
|
||||
scrollableAncestor.scrollTop = 0;
|
||||
applyStyle(scrollableAncestor, 'overflowY', 'visible');
|
||||
applyStyle(scrollableAncestor, 'height', `${scrollableAncestor.scrollHeight}px`);
|
||||
applyStyle(scrollableAncestor, 'maxHeight', 'none');
|
||||
}
|
||||
|
||||
if (scrollingElement) {
|
||||
scrollingElement.scrollTop = 0;
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const fullHeight = Math.max(
|
||||
Math.ceil(rect.height),
|
||||
Math.ceil(canvas.scrollHeight || 0),
|
||||
Math.ceil(scrollableAncestor?.scrollHeight || 0),
|
||||
Math.ceil(document.documentElement.scrollHeight || 0)
|
||||
);
|
||||
|
||||
return {
|
||||
hadScrollableAncestor: Boolean(scrollableAncestor),
|
||||
initialHeight: Math.ceil(initialRect.height),
|
||||
finalHeight: Math.ceil(rect.height),
|
||||
captureHeight: fullHeight,
|
||||
viewportHeight: Math.min(Math.max(fullHeight, minViewportHeight), maxViewportHeight),
|
||||
viewportWidth: viewportWidth
|
||||
};
|
||||
},
|
||||
{
|
||||
targetSelector: selector,
|
||||
maxViewportHeight: MAX_CAPTURE_VIEWPORT_HEIGHT,
|
||||
minViewportHeight: viewportHeight,
|
||||
viewportWidth: viewportWidth
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function toPdfBuffer(pngBytes, PDFDocument) {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const image = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([image.width, image.height]);
|
||||
page.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height
|
||||
});
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfBytes);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let browser;
|
||||
let page;
|
||||
const debugLogs = [];
|
||||
try {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = args.url;
|
||||
const token = args.token;
|
||||
const output = args.output;
|
||||
if (!url || !token || !output) {
|
||||
throw new Error('url, token and output are required');
|
||||
}
|
||||
|
||||
const width = parsePositiveInteger(args.width ?? '1920', 'width');
|
||||
const height = parsePositiveInteger(args.height ?? '1080', 'height');
|
||||
const waitSeconds = parseWaitSeconds(args['wait-seconds']);
|
||||
const resultFormat = Number.parseInt(args['result-format'] ?? '0', 10);
|
||||
if (![0, 1].includes(resultFormat)) {
|
||||
throw new Error('result-format must be 0 or 1');
|
||||
}
|
||||
|
||||
const [{ chromium }, { PDFDocument }] = await Promise.all([
|
||||
import('playwright'),
|
||||
import('pdf-lib')
|
||||
]);
|
||||
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width, height },
|
||||
deviceScaleFactor: 1,
|
||||
ignoreHTTPSErrors: true,
|
||||
extraHTTPHeaders: {
|
||||
'X-DE-TOKEN': token
|
||||
}
|
||||
});
|
||||
await context.route('**/de2api/**', async route => {
|
||||
const request = route.request();
|
||||
const headers = {
|
||||
...request.headers(),
|
||||
'X-DE-TOKEN': token
|
||||
};
|
||||
const url = request.url();
|
||||
if (url.includes('/de2api/outerParams/getOuterParamsInfo/')) {
|
||||
try {
|
||||
const response = await route.fetch({ headers });
|
||||
if (response.status() < 500) {
|
||||
await route.fulfill({ response });
|
||||
return;
|
||||
}
|
||||
debugLogs.push(`outerParams fallback: ${response.status()} ${url}`);
|
||||
} catch (error) {
|
||||
debugLogs.push(`outerParams fallback error: ${error?.message || String(error)}`);
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: {
|
||||
outerParamsInfoMap: {},
|
||||
outerParamsInfoBaseMap: {}
|
||||
}
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.continue({ headers });
|
||||
});
|
||||
page = await context.newPage();
|
||||
const selector = '.canvas-container';
|
||||
const timeout = 120000;
|
||||
const pendingRequests = new Set();
|
||||
const trackRequest = request => {
|
||||
const resourceType = request.resourceType();
|
||||
if (!['fetch', 'xhr'].includes(resourceType)) {
|
||||
return false;
|
||||
}
|
||||
return request.url().includes('/de2api/');
|
||||
};
|
||||
page.on('request', request => {
|
||||
if (trackRequest(request)) {
|
||||
pendingRequests.add(request);
|
||||
}
|
||||
});
|
||||
page.on('requestfinished', request => {
|
||||
pendingRequests.delete(request);
|
||||
});
|
||||
page.on('requestfailed', request => {
|
||||
pendingRequests.delete(request);
|
||||
});
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400 && response.url().includes('/de2api/')) {
|
||||
debugLogs.push(`response ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
page.on('pageerror', error => {
|
||||
debugLogs.push(`pageerror ${error.message}`);
|
||||
});
|
||||
page.on('console', message => {
|
||||
if (message.type() === 'error') {
|
||||
debugLogs.push(`console ${message.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
await page.addInitScript(
|
||||
({ injectedToken, cacheToken, cacheExp, cacheTime }) => {
|
||||
localStorage.setItem('user.token', cacheToken);
|
||||
localStorage.setItem('user.exp', cacheExp);
|
||||
localStorage.setItem('user.time', cacheTime);
|
||||
localStorage.setItem('__de_raw_token__', injectedToken);
|
||||
},
|
||||
{
|
||||
injectedToken: token,
|
||||
cacheToken: buildWsCacheItem(token, now),
|
||||
cacheExp: buildWsCacheItem(now + 3600 * 1000, now),
|
||||
cacheTime: buildWsCacheItem(now, now)
|
||||
}
|
||||
);
|
||||
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
|
||||
try {
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
} catch {
|
||||
// Preview pages may keep network connections alive; selector checks below are stricter.
|
||||
}
|
||||
|
||||
const locator = await waitForCanvasReady(page, selector, timeout);
|
||||
const renderState = await waitForRenderSettled(
|
||||
page,
|
||||
selector,
|
||||
timeout,
|
||||
debugLogs,
|
||||
() => pendingRequests.size
|
||||
);
|
||||
const expandedCapture = await expandScrollableCapture(page, selector, width, height);
|
||||
if (expandedCapture?.captureHeight > height) {
|
||||
await page.setViewportSize({
|
||||
width,
|
||||
height: expandedCapture.viewportHeight
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await waitForRenderSettled(
|
||||
page,
|
||||
selector,
|
||||
timeout,
|
||||
debugLogs,
|
||||
() => pendingRequests.size
|
||||
);
|
||||
}
|
||||
if (waitSeconds > 0) {
|
||||
await page.waitForTimeout(waitSeconds * 1000);
|
||||
}
|
||||
const box = await locator.boundingBox();
|
||||
if (!box || box.width < 1 || box.height < 1) {
|
||||
throw new Error('canvas container is empty');
|
||||
}
|
||||
|
||||
if (resultFormat === 1) {
|
||||
const pngBytes = await locator.screenshot({
|
||||
type: 'png',
|
||||
animations: 'disabled'
|
||||
});
|
||||
const pdfBytes = await toPdfBuffer(pngBytes, PDFDocument);
|
||||
await fs.writeFile(output, pdfBytes);
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
format: 'pdf',
|
||||
width: Math.round(box.width),
|
||||
height: Math.round(box.height),
|
||||
renderState,
|
||||
expandedCapture,
|
||||
output
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
await locator.screenshot({
|
||||
path: output,
|
||||
type: 'jpeg',
|
||||
quality: 90,
|
||||
animations: 'disabled'
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
format: 'jpeg',
|
||||
width: Math.round(box.width),
|
||||
height: Math.round(box.height),
|
||||
renderState,
|
||||
expandedCapture,
|
||||
output
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
console.error('Missing runtime dependency. Please run `npm install` and `npx playwright install chromium` first.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const diagnostics = page
|
||||
? await collectDiagnostics(page, debugLogs)
|
||||
: null;
|
||||
if (diagnostics) {
|
||||
console.error(`${error && error.stack ? error.stack : String(error)}\nDiagnostics: ${JSON.stringify(diagnostics)}`);
|
||||
} else {
|
||||
console.error(error && error.stack ? error.stack : String(error));
|
||||
}
|
||||
} catch {
|
||||
console.error(error && error.stack ? error.stack : String(error));
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import base64
|
||||
import os
|
||||
import requests
|
||||
import jwt
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
class DataEaseClient:
|
||||
def __init__(self, base_url, access_key, secret_key):
|
||||
# Separate base URL and API prefix
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_prefix = os.environ.get('DATAEASE_API_PREFIX', '/de2api')
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
|
||||
def _get_signature(self, uid, timestamp):
|
||||
src_str = f"{self.access_key}|{uid}|{timestamp}"
|
||||
padder = padding.PKCS7(128).padder()
|
||||
padded_data = padder.update(src_str.encode('utf-8')) + padder.finalize()
|
||||
cipher = Cipher(
|
||||
algorithms.AES(self.secret_key.encode('utf-8')),
|
||||
modes.CBC(self.access_key.encode('utf-8')),
|
||||
backend=default_backend()
|
||||
)
|
||||
encryptor = cipher.encryptor()
|
||||
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
||||
return base64.b64encode(ciphertext).decode('utf-8')
|
||||
|
||||
def _get_headers(self):
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
uid = str(uuid.uuid4())
|
||||
signature = self._get_signature(uid, timestamp)
|
||||
claims = {
|
||||
"accessKey": self.access_key,
|
||||
"signature": signature
|
||||
}
|
||||
token = jwt.encode(claims, self.secret_key, algorithm="HS256")
|
||||
if isinstance(token, bytes):
|
||||
token = token.decode('utf-8')
|
||||
|
||||
return {
|
||||
"accessKey": self.access_key,
|
||||
"signature": signature,
|
||||
"x-de-ask-token": token,
|
||||
"timestamp": timestamp,
|
||||
"nonce": uid,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def get(self, path, params=None):
|
||||
url = f"{self.base_url}{self.api_prefix}{path}"
|
||||
response = requests.get(url, headers=self._get_headers(), params=params, verify=False)
|
||||
return response
|
||||
|
||||
def post(self, path, payload=None):
|
||||
url = f"{self.base_url}{self.api_prefix}{path}"
|
||||
response = requests.post(url, headers=self._get_headers(), json=payload, verify=False)
|
||||
return response
|
||||
|
||||
def get_dataset_fields(self, dataset_id):
|
||||
return self.post(f"/datasetField/listByDatasetGroup/{dataset_id}").json()
|
||||
|
||||
def update_publish_status(self, dashboard_id, name, status=1, type='dashboard', mobile_layout=False, active_view_ids=None):
|
||||
url = f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus"
|
||||
payload = {
|
||||
"id": dashboard_id,
|
||||
"name": name,
|
||||
"mobileLayout": mobile_layout,
|
||||
"activeViewIds": active_view_ids or [],
|
||||
"status": status,
|
||||
"type": type
|
||||
}
|
||||
response = requests.post(url, headers=self._get_headers(), json=payload, verify=False)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -0,0 +1,158 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
# Add local path for engine import
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from engine import DataEaseChartEngine
|
||||
|
||||
# --- Configuration from Environment ---
|
||||
def load_dotenv():
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
os.environ[key.strip()] = value.strip().strip('"').strip("'")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ACCESS_KEY = os.environ.get("DATAEASE_ACCESS_KEY")
|
||||
SECRET_KEY = os.environ.get("DATAEASE_SECRET_KEY")
|
||||
BASE_URL = os.environ.get("DATAEASE_BASE_URL")
|
||||
USERNAME = os.environ.get("DATAEASE_USERNAME")
|
||||
PASSWORD = os.environ.get("DATAEASE_PASSWORD")
|
||||
LOGIN_ORIGIN = os.environ.get("DATAEASE_LOGIN_ORIGIN", "0")
|
||||
|
||||
# Screenshot configuration - auto-detect OpenClaw workspace for MEDIA: display
|
||||
def _default_output_dir():
|
||||
"""优先使用 OpenClaw workspace 目录,否则用 skill 本地 output 目录"""
|
||||
# 1. 环境变量显式指定
|
||||
env_dir = os.environ.get("DATAEASE_OUTPUT_DIR")
|
||||
if env_dir:
|
||||
return env_dir
|
||||
# 2. 自动检测 OpenClaw workspace
|
||||
home = os.path.expanduser("~")
|
||||
workspace_dir = os.path.join(home, ".openclaw", "workspace", "dataease-output")
|
||||
if os.path.isdir(os.path.join(home, ".openclaw", "workspace")):
|
||||
os.makedirs(workspace_dir, exist_ok=True)
|
||||
return workspace_dir
|
||||
# 3. 回退到 skill 本地 output
|
||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output")
|
||||
|
||||
OUTPUT_DIR = _default_output_dir()
|
||||
DEFAULT_PIXEL = "2560*1440" # 更高分辨率,图片更清晰
|
||||
|
||||
def capture_dashboard(dashboard_id, output_format="jpeg"):
|
||||
"""调用 capture_dashboard.py 进行截图"""
|
||||
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
capture_script = os.path.join(scripts_dir, "capture_dashboard.py")
|
||||
|
||||
if not os.path.exists(capture_script):
|
||||
return None, "Screenshot script not found"
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# capture_dashboard.py 使用相同的 BASE_URL(不含 /de2api 后缀)
|
||||
capture_base_url = BASE_URL.rstrip("/")
|
||||
|
||||
# 调用截图脚本
|
||||
result_format = "0" if output_format == "jpeg" else "1" # 0=jpeg, 1=pdf
|
||||
cmd = [
|
||||
"python3", capture_script,
|
||||
"capture",
|
||||
"--resource-id", str(dashboard_id),
|
||||
"--busi-type", "dashboard",
|
||||
"--output-dir", OUTPUT_DIR,
|
||||
"--result-format", result_format,
|
||||
"--base-url", capture_base_url,
|
||||
"--pixel", DEFAULT_PIXEL
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode == 0:
|
||||
output = json.loads(result.stdout)
|
||||
if output.get("ok"):
|
||||
return output.get("saved_file"), None
|
||||
else:
|
||||
return None, output.get("error", "Unknown capture error")
|
||||
else:
|
||||
return None, result.stderr or "Capture failed"
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "Capture timeout"
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
def main():
|
||||
if not BASE_URL:
|
||||
print("Error: Missing required environment variable DATAEASE_BASE_URL.")
|
||||
sys.exit(1)
|
||||
if not ACCESS_KEY and not USERNAME:
|
||||
print("Error: Missing authentication. Please set either:")
|
||||
print(" DATAEASE_ACCESS_KEY + DATAEASE_SECRET_KEY")
|
||||
print(" or DATAEASE_USERNAME + DATAEASE_PASSWORD")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) < 5:
|
||||
print("Usage: python3 deploy.py <type> <title> <dataset_name_or_id> <x_fields> <y_fields> [--no-screenshot]")
|
||||
print("Example: python3 deploy.py line 'Skill Test' '电商用户购买行为' '访问平台' '访问次数'")
|
||||
sys.exit(1)
|
||||
|
||||
chart_type = sys.argv[1]
|
||||
title = sys.argv[2]
|
||||
dataset_id = sys.argv[3]
|
||||
x_axis = [f.strip() for f in sys.argv[4].split(',') if f.strip()]
|
||||
y_axis = [f.strip() for f in sys.argv[5].split(',') if f.strip()]
|
||||
|
||||
no_screenshot = "--no-screenshot" in sys.argv
|
||||
|
||||
if ACCESS_KEY and SECRET_KEY:
|
||||
engine = DataEaseChartEngine(BASE_URL, ACCESS_KEY, SECRET_KEY)
|
||||
else:
|
||||
engine = DataEaseChartEngine(BASE_URL, USERNAME, PASSWORD, auth_mode="password")
|
||||
|
||||
try:
|
||||
did, url = engine.deploy(chart_type, title, dataset_id, x_axis, y_axis)
|
||||
print(f"\n✅ Successfully deployed!")
|
||||
print(f"Dashboard ID: {did}")
|
||||
print(f"URL: {url}")
|
||||
|
||||
# 自动截图
|
||||
if not no_screenshot:
|
||||
print("\n📸 Capturing screenshot...")
|
||||
screenshot_path, error = capture_dashboard(did)
|
||||
if screenshot_path:
|
||||
print(f"Screenshot saved: {screenshot_path}")
|
||||
# 输出 JSON 结果供 Agent 解析
|
||||
result = {
|
||||
"ok": True,
|
||||
"dashboard_id": str(did),
|
||||
"url": url,
|
||||
"screenshot": screenshot_path,
|
||||
"title": title
|
||||
}
|
||||
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
|
||||
else:
|
||||
print(f"⚠️ Screenshot failed: {error}")
|
||||
result = {
|
||||
"ok": True,
|
||||
"dashboard_id": str(did),
|
||||
"url": url,
|
||||
"screenshot": None,
|
||||
"screenshot_error": error,
|
||||
"title": title
|
||||
}
|
||||
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Deployment failed: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,326 @@
|
||||
import json, re, time, random, os, sys, requests
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
|
||||
# Local SDK client
|
||||
from client import DataEaseClient
|
||||
|
||||
class DataEaseChartEngine:
|
||||
def __init__(self, base_url, ak_or_user, sk_or_password, auth_mode=None):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_prefix = os.environ.get('DATAEASE_API_PREFIX', '/de2api')
|
||||
self.auth_mode = auth_mode or 'aksk'
|
||||
|
||||
if self.auth_mode == 'password':
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("inspect_data", os.path.join(os.path.dirname(__file__), "inspect_data.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
x_de_token = mod.login_with_password(base_url, self.api_prefix, ak_or_user, sk_or_password)
|
||||
self.headers = mod.get_headers_token(x_de_token)
|
||||
self.client = None
|
||||
else:
|
||||
self.client = DataEaseClient(base_url, ak_or_user, sk_or_password)
|
||||
self.headers = None
|
||||
|
||||
def _get_headers(self):
|
||||
if self.headers:
|
||||
return self.headers
|
||||
return self._get_headers()
|
||||
|
||||
def _post(self, path, payload=None):
|
||||
url = f"{self.base_url}{self.api_prefix}{path}"
|
||||
return requests.post(url, headers=self._get_headers(), json=payload, verify=False, timeout=30)
|
||||
|
||||
def _get(self, path, params=None):
|
||||
url = f"{self.base_url}{self.api_prefix}{path}"
|
||||
return requests.get(url, headers=self._get_headers(), params=params, verify=False, timeout=30)
|
||||
|
||||
def resolve_dataset_id(self, name_or_id):
|
||||
"""Resolve dataset name to ID using /datasetTree/tree if needed"""
|
||||
if str(name_or_id).isdigit() and len(str(name_or_id)) > 10:
|
||||
return name_or_id
|
||||
|
||||
url = f"{self.base_url}{self.api_prefix}/datasetTree/tree"
|
||||
headers = self._get_headers()
|
||||
resp = requests.post(url, headers=headers, json={"busiFlag": "dataset"}, verify=False, timeout=30)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise Exception(f"Failed to fetch dataset tree: {resp.text}")
|
||||
|
||||
nodes = resp.json().get('data', [])
|
||||
|
||||
def find_in_tree(items, target_name):
|
||||
for item in items:
|
||||
if item.get('name') == target_name:
|
||||
return item.get('id')
|
||||
children = item.get('children', [])
|
||||
if children:
|
||||
found = find_in_tree(children, target_name)
|
||||
if found: return found
|
||||
return None
|
||||
|
||||
dataset_id = find_in_tree(nodes, name_or_id)
|
||||
if not dataset_id:
|
||||
raise ValueError(f"Dataset '{name_or_id}' not found in DataEase")
|
||||
|
||||
return dataset_id
|
||||
|
||||
def get_dataset_ctx(self, dataset_name_or_id, x_names, y_names):
|
||||
"""Fetch dataset metadata and build rendering context"""
|
||||
dataset_id = self.resolve_dataset_id(dataset_name_or_id)
|
||||
|
||||
# DataEase v2 often uses datasetGroup for the tree structure
|
||||
url = f"{self.base_url}{self.api_prefix}/datasetTree/details/{dataset_id}"
|
||||
headers = self._get_headers()
|
||||
resp = requests.get(url, headers=headers, verify=False, timeout=30)
|
||||
|
||||
if resp.status_code != 200:
|
||||
# Fallback to the original method if the specific tree detail endpoint fails
|
||||
resp = self._post(f"/datasetField/listByDatasetGroup/{dataset_id}")
|
||||
fields = resp.json().get('data', [])
|
||||
else:
|
||||
data = resp.json().get('data', {})
|
||||
fields = data.get('allFields', [])
|
||||
if not fields:
|
||||
# Try to get from datasetField/listByDatasetGroup if tree details is empty
|
||||
resp = self._post(f"/datasetField/listByDatasetGroup/{dataset_id}")
|
||||
fields = resp.json().get('data', [])
|
||||
|
||||
if not fields:
|
||||
raise ValueError(f"No fields found for dataset {dataset_id}")
|
||||
|
||||
f_map = {f['name']: f for f in fields}
|
||||
|
||||
# Get table/datasource IDs from the first field (they should be common)
|
||||
datasource_id = fields[0].get('datasourceId', "")
|
||||
table_id = fields[0].get('datasetTableId', "")
|
||||
|
||||
ctx = {
|
||||
"DATASET_GROUP_ID": dataset_id,
|
||||
"DATASOURCE_ID": datasource_id,
|
||||
"DATASET_TABLE_ID": table_id,
|
||||
}
|
||||
|
||||
# Map axis fields (supports multi-measure if needed)
|
||||
for i, name in enumerate(x_names):
|
||||
suffix = "" if i == 0 else str(i+1)
|
||||
f = f_map.get(name)
|
||||
if not f: raise ValueError(f"X-Axis Field '{name}' not found")
|
||||
ctx[f"XAXIS{suffix}_FIELD_ID"] = f['id']
|
||||
ctx[f"XAXIS{suffix}_DE_NAME"] = f['dataeaseName']
|
||||
|
||||
for i, name in enumerate(y_names):
|
||||
suffix = "" if i == 0 else str(i+1)
|
||||
f = f_map.get(name)
|
||||
if not f: raise ValueError(f"Y-Axis Field '{name}' not found")
|
||||
ctx[f"YAXIS{suffix}_FIELD_ID"] = f['id']
|
||||
ctx[f"YAXIS{suffix}_DE_NAME"] = f['dataeaseName']
|
||||
ctx[f"YAXIS{suffix}_SERIES_ID"] = f"{f['id']}-yAxis"
|
||||
|
||||
# Additional keys for deep parameterization
|
||||
if i == 0:
|
||||
ctx["YAXIS_FIELD_ID"] = f['id']
|
||||
ctx["YAXIS_DE_NAME"] = f['dataeaseName']
|
||||
ctx["YAXIS_SERIES_ID"] = f"{f['id']}-yAxis"
|
||||
elif i == 1:
|
||||
ctx["YAXIS2_FIELD_ID"] = f['id']
|
||||
ctx["YAXIS2_DE_NAME"] = f['dataeaseName']
|
||||
ctx["YAXIS2_SERIES_ID"] = f"{f['id']}-yAxis"
|
||||
|
||||
return ctx
|
||||
|
||||
def deploy(self, chart_type, title, dataset_id, x_names, y_names, layout=None):
|
||||
# 1. Directory standardization
|
||||
# Use relative path from this script (scripts/engine.py) to templates/
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
tpl_dir = os.path.join(base_dir, "..", "templates", f"chart_{chart_type}")
|
||||
|
||||
if not os.path.exists(tpl_dir):
|
||||
raise FileNotFoundError(f"Template directory {tpl_dir} not found")
|
||||
|
||||
with open(os.path.join(tpl_dir, "template.j2")) as f: template_str = f.read()
|
||||
with open(os.path.join(tpl_dir, "params.json")) as f:
|
||||
raw_params = json.load(f)
|
||||
|
||||
# 2. Build Context (Flatten params + dynamic IDs)
|
||||
ctx = {}
|
||||
# Flatten nested params.json
|
||||
def flatten_dict(d):
|
||||
items = {}
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
items.update(flatten_dict(v))
|
||||
elif not k.startswith("_"):
|
||||
items[k] = v
|
||||
return items
|
||||
|
||||
ctx.update(flatten_dict(raw_params))
|
||||
ctx.update(self.get_dataset_ctx(dataset_id, x_names, y_names))
|
||||
|
||||
# 3. Runtime Randomization
|
||||
view_id, content_id = self.rand_id(), self.rand_id()
|
||||
ctx.update({"VIEW_ID": view_id, "CONTENT_ID": content_id, "SCENE_ID": "0"})
|
||||
|
||||
# 4. Thorough Parameterization (Global substitution)
|
||||
def rep(m):
|
||||
k = m.group(1).strip()
|
||||
if k in ctx:
|
||||
return str(ctx[k])
|
||||
return m.group(0) # Keep placeholder if not found
|
||||
|
||||
rendered = re.sub(r"\{\{\s*(\w+)\s*\}\}", rep, template_str)
|
||||
payload = json.loads(rendered)
|
||||
|
||||
# 5. Type Closure & Payload Finalization
|
||||
board_name = f"{title}_{int(time.time())}"
|
||||
|
||||
# Override title in canvasViewInfo
|
||||
if "canvasViewInfo" in payload and view_id in payload["canvasViewInfo"]:
|
||||
view_info = payload["canvasViewInfo"][view_id]
|
||||
view_info["title"] = title
|
||||
|
||||
# Deep update all field names (xAxis, yAxis, labels, tooltips, etc.)
|
||||
def deep_update_field_names(obj, target_id, new_name):
|
||||
if isinstance(obj, dict):
|
||||
if str(obj.get("id")) == str(target_id):
|
||||
obj["name"] = new_name
|
||||
if "description" in obj: obj["description"] = new_name
|
||||
if "originName" in obj: obj["originName"] = new_name
|
||||
# 核心修复:DataEase V2 在某些场景下会回退到 dbFieldName,强制设为 null 或同步
|
||||
if "dbFieldName" in obj: obj["dbFieldName"] = None
|
||||
|
||||
# 特殊修复:处理 DataEase 特有的标签和提示框拼接字段 (如 "访问平台(求和)")
|
||||
# 我们无法预知原始模板里的名字,所以这里尝试匹配常见的拼接模式
|
||||
if "optionLabel" in obj:
|
||||
obj["optionLabel"] = new_name + (obj["optionLabel"].partition("(")[1] + obj["optionLabel"].partition("(")[2] if "(" in obj["optionLabel"] else "")
|
||||
if "optionShowName" in obj:
|
||||
obj["optionShowName"] = new_name + (obj["optionShowName"].partition("(")[1] + obj["optionShowName"].partition("(")[2] if "(" in obj["optionShowName"] else "")
|
||||
|
||||
for v in obj.values():
|
||||
deep_update_field_names(v, target_id, new_name)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
deep_update_field_names(item, target_id, new_name)
|
||||
|
||||
# Apply updates for X and Y axis fields
|
||||
# We use the field IDs obtained from dataset metadata to find and replace
|
||||
dataset_ctx = self.get_dataset_ctx(dataset_id, x_names, y_names)
|
||||
|
||||
# Pre-scan templates/params.json for the old names to ensure replacement works
|
||||
# or use common default names from templates
|
||||
old_names_to_clear = ["访问次数", "访问平台", "浏览量"]
|
||||
|
||||
for i, x_name in enumerate(x_names):
|
||||
f_id = dataset_ctx.get(f"XAXIS{'' if i==0 else i+1}_FIELD_ID")
|
||||
if f_id:
|
||||
deep_update_field_names(view_info, f_id, x_name)
|
||||
|
||||
for i, y_name in enumerate(y_names):
|
||||
f_id = dataset_ctx.get(f"YAXIS{'' if i==0 else i+1}_FIELD_ID")
|
||||
if f_id:
|
||||
deep_update_field_names(view_info, f_id, y_name)
|
||||
|
||||
# 最终保底:如果还有残留的“访问次数”或“访问平台”,强行全局替换
|
||||
def brute_force_replace(obj, search_list, replace_list):
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str):
|
||||
for s, r in zip(search_list, replace_list):
|
||||
if s in v:
|
||||
obj[k] = v.replace(s, r)
|
||||
else:
|
||||
brute_force_replace(v, search_list, replace_list)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
brute_force_replace(item, search_list, replace_list)
|
||||
|
||||
# Use primary X/Y names for brute force fallback
|
||||
if x_names and y_names:
|
||||
brute_force_replace(view_info, ["访问平台", "访问次数", "浏览量"], [x_names[0], y_names[0], y_names[0]])
|
||||
|
||||
# If layout is provided, override it in componentData
|
||||
if "componentData" in payload:
|
||||
components = json.loads(payload["componentData"])
|
||||
if components:
|
||||
target = components[0]
|
||||
# Update component name to title
|
||||
target["name"] = title
|
||||
target["label"] = title
|
||||
|
||||
# 单图表模式:充满整个画布
|
||||
if layout is None and len(components) == 1:
|
||||
# DataEase 单图表网格 72x36,让图表充满整个画布
|
||||
target["x"] = 1
|
||||
target["y"] = 1
|
||||
target["sizeX"] = 72
|
||||
target["sizeY"] = 36
|
||||
if "style" not in target:
|
||||
target["style"] = {}
|
||||
target["style"]["width"] = 1920
|
||||
target["style"]["height"] = 1080
|
||||
target["style"]["left"] = 0
|
||||
target["style"]["top"] = 0
|
||||
elif layout:
|
||||
if "x" in layout: target["x"] = layout["x"]
|
||||
if "y" in layout: target["y"] = layout["y"]
|
||||
if "sizeX" in layout: target["sizeX"] = layout["sizeX"]
|
||||
if "sizeY" in layout: target["sizeY"] = layout["sizeY"]
|
||||
if "style" not in target: target["style"] = {}
|
||||
if "width" in layout: target["style"]["width"] = layout["width"]
|
||||
if "height" in layout: target["style"]["height"] = layout["height"]
|
||||
if "left" in layout: target["style"]["left"] = layout["left"]
|
||||
if "top" in layout: target["style"]["top"] = layout["top"]
|
||||
payload["componentData"] = json.dumps(components, separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
# Ensure canvasStyleData is also compact
|
||||
if "canvasStyleData" in payload:
|
||||
style = json.loads(payload["canvasStyleData"])
|
||||
payload["canvasStyleData"] = json.dumps(style, separators=(',', ':'), ensure_ascii=False)
|
||||
|
||||
payload.update({
|
||||
"id": None,
|
||||
"name": board_name,
|
||||
"type": "dashboard",
|
||||
"status": 0,
|
||||
"dataState": "ready",
|
||||
"selfWatermarkStatus": True,
|
||||
"checkVersion": "2.10.20",
|
||||
"pid": "0",
|
||||
"mobileLayout": False
|
||||
})
|
||||
|
||||
headers = self._get_headers()
|
||||
print(f"Deploying chart '{title}' (Type: {chart_type})...")
|
||||
|
||||
save_r = requests.post(
|
||||
f"{self.base_url}{self.api_prefix}/dataVisualization/saveCanvas",
|
||||
headers=headers, json=payload, verify=False, timeout=30
|
||||
)
|
||||
|
||||
if save_r.status_code != 200:
|
||||
raise Exception(f"saveCanvas failed: {save_r.text}")
|
||||
|
||||
dashboard_id = str(save_r.json()["data"])
|
||||
|
||||
# 6. Publish
|
||||
requests.post(
|
||||
f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus",
|
||||
headers=headers, verify=False, timeout=30,
|
||||
json={
|
||||
"id": dashboard_id,
|
||||
"name": board_name,
|
||||
"activeViewIds": [view_id],
|
||||
"status": 1,
|
||||
"type": "dashboard",
|
||||
"mobileLayout": False
|
||||
}
|
||||
)
|
||||
|
||||
# Generate preview URL
|
||||
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
|
||||
|
||||
@staticmethod
|
||||
def rand_id():
|
||||
return str(int(time.time() * 1000) + random.randint(1000, 9999))
|
||||
@@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DataEase 数据集探索脚本
|
||||
支持 AK/SK 和密码登录两种认证方式
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import uuid
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
# Add scripts to path for engine import
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def load_dotenv():
|
||||
"""加载 .env 文件"""
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, encoding="utf8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
os.environ[key.strip()] = value.strip().strip('"').strip("'")
|
||||
|
||||
|
||||
def base64url(raw):
|
||||
"""Base64 URL-safe 编码"""
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def sign_jwt(payload, secret_key):
|
||||
"""使用 HMAC-SHA256 签名 JWT"""
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
header_part = base64url(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
||||
payload_part = base64url(json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
|
||||
signing_input = f"{header_part}.{payload_part}".encode("ascii")
|
||||
signature = hmac.new(secret_key.encode("utf-8"), signing_input, hashlib.sha256).digest()
|
||||
return f"{header_part}.{payload_part}.{base64url(signature)}"
|
||||
|
||||
|
||||
def aes_cipher_name(secret_key):
|
||||
"""根据密钥长度返回 AES 加密算法名称"""
|
||||
length = len(secret_key.encode("utf-8"))
|
||||
if length == 16:
|
||||
return "aes-128-cbc"
|
||||
if length == 24:
|
||||
return "aes-192-cbc"
|
||||
if length == 32:
|
||||
return "aes-256-cbc"
|
||||
raise ValueError("Secret Key 长度必须是 16、24 或 32 字节")
|
||||
|
||||
|
||||
def aes_encrypt(plain_text, secret_key, iv):
|
||||
"""AES 加密"""
|
||||
if shutil.which("openssl") is None:
|
||||
raise RuntimeError("当前环境缺少 openssl 命令,无法生成鉴权签名")
|
||||
if len(iv.encode("utf-8")) != 16:
|
||||
raise ValueError("Access Key 长度必须是 16 字节,才能作为 AES IV")
|
||||
|
||||
cmd = [
|
||||
"openssl", "enc", f"-{aes_cipher_name(secret_key)}",
|
||||
"-base64", "-A", "-nosalt",
|
||||
"-K", secret_key.encode("utf-8").hex(),
|
||||
"-iv", iv.encode("utf-8").hex(),
|
||||
]
|
||||
proc = subprocess.run(cmd, input=plain_text.encode("utf-8"), capture_output=True, check=False)
|
||||
if proc.returncode != 0:
|
||||
stderr = proc.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise RuntimeError(stderr or "openssl 加密失败")
|
||||
return proc.stdout.decode("utf-8").strip()
|
||||
|
||||
|
||||
def build_ask_auth(access_key, secret_key):
|
||||
"""构建 ASK 认证信息"""
|
||||
source = f"{access_key}|{uuid.uuid4()}|{int(time.time() * 1000)}"
|
||||
signature = aes_encrypt(source, secret_key, access_key)
|
||||
token = sign_jwt({"accessKey": access_key, "signature": signature}, secret_key)
|
||||
return {
|
||||
"access_key": access_key,
|
||||
"signature": signature,
|
||||
"x_de_ask_token": token,
|
||||
}
|
||||
|
||||
|
||||
def get_headers_ask(ask_auth):
|
||||
"""构建 ASK 认证请求头"""
|
||||
return {
|
||||
"Accept": "application/json;charset=UTF-8",
|
||||
"Content-Type": "application/json",
|
||||
"accessKey": ask_auth["access_key"],
|
||||
"signature": ask_auth["signature"],
|
||||
"X-DE-ASK-TOKEN": ask_auth["x_de_ask_token"],
|
||||
}
|
||||
|
||||
|
||||
def fetch_dekey(base_url, api_prefix):
|
||||
"""获取 dekey 用于密码登录"""
|
||||
url = f"{base_url.rstrip('/')}{api_prefix}/dekey"
|
||||
request = Request(url, headers={"Accept": "application/json;charset=UTF-8"}, method="GET")
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, str) or not data:
|
||||
raise ValueError("dekey 接口未返回有效字符串")
|
||||
return data
|
||||
except HTTPError as err:
|
||||
raise RuntimeError(f"获取 dekey 失败: {err.code}")
|
||||
|
||||
|
||||
def split_dekey(dekey):
|
||||
"""拆分 dekey"""
|
||||
separator = base64.urlsafe_b64encode(b"-pk_separator-").decode("ascii")
|
||||
if separator and separator in dekey:
|
||||
parts = dekey.split(separator, 1)
|
||||
if len(parts) == 2 and parts[0] and parts[1]:
|
||||
return parts[0], parts[1]
|
||||
raise ValueError("dekey 格式不符合预期")
|
||||
|
||||
|
||||
def format_public_key(public_key):
|
||||
"""格式化公钥"""
|
||||
body = "\n".join(public_key[index:index + 64] for index in range(0, len(public_key), 64))
|
||||
return f"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----\n"
|
||||
|
||||
# fixed by xuhuanqing
|
||||
'''
|
||||
def rsa_encrypt(plain_text, public_key):
|
||||
"""RSA 加密"""
|
||||
import tempfile
|
||||
if shutil.which("openssl") is None:
|
||||
raise RuntimeError("当前环境缺少 openssl 命令")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dataease-pubkey-") as tmpdir:
|
||||
key_path = Path(tmpdir) / "public.pem"
|
||||
key_path.write_text(format_public_key(public_key), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", str(key_path)],
|
||||
input=plain_text.encode("utf-8"),
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.decode("utf-8", errors="replace").strip() or "RSA 加密失败")
|
||||
return base64.b64encode(proc.stdout).decode("ascii")
|
||||
'''
|
||||
|
||||
def rsa_encrypt(plain_text, public_key):
|
||||
"""RSA加密,兼容OpenSSL pkeyutl参数规范,修复no private key报错"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
import base64
|
||||
from shutil import which
|
||||
from pathlib import Path
|
||||
|
||||
if which("openssl") is None:
|
||||
raise RuntimeError("当前环境缺少 openssl 命令")
|
||||
|
||||
# 格式化标准PEM公钥
|
||||
def format_public_key(pub_raw):
|
||||
body = "\n".join(pub_raw[index:index + 64] for index in range(0, len(pub_raw), 64))
|
||||
return f"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----\n"
|
||||
|
||||
pub_pem = format_public_key(public_key)
|
||||
# 创建临时公钥文件
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False, encoding="utf-8") as tmp_key:
|
||||
tmp_key.write(pub_pem)
|
||||
tmp_key_path = tmp_key.name
|
||||
try:
|
||||
# 标准规范参数:-inkey 指定公钥文件、-pubin 标识文件类型为公钥
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"openssl", "pkeyutl",
|
||||
"-encrypt",
|
||||
"-pubin",
|
||||
"-inkey", tmp_key_path
|
||||
],
|
||||
input=plain_text.encode("utf-8"),
|
||||
capture_output=True,
|
||||
check=False
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err_msg = proc.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise RuntimeError(f"RSA加密失败: {err_msg}")
|
||||
return base64.b64encode(proc.stdout).decode("ascii")
|
||||
finally:
|
||||
# 执行完毕强制删除临时密钥文件
|
||||
Path(tmp_key_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def login_with_password(base_url, api_prefix, username, password):
|
||||
"""使用密码登录获取 x-de-token"""
|
||||
dekey = fetch_dekey(base_url, api_prefix)
|
||||
encrypted_pk, aes_key_str = split_dekey(dekey)
|
||||
|
||||
# 解密获取公钥
|
||||
cmd = [
|
||||
"openssl", "enc", f"-{aes_cipher_name(aes_key_str)}", "-d",
|
||||
"-base64", "-A", "-nosalt",
|
||||
"-K", aes_key_str.encode("utf-8").hex(),
|
||||
"-iv", b"0000000000000000".hex(),
|
||||
]
|
||||
proc = subprocess.run(cmd, input=encrypted_pk.encode("utf-8"), capture_output=True, check=False)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("解密 dekey 失败")
|
||||
public_key = proc.stdout.decode("utf-8").strip()
|
||||
|
||||
# RSA 加密用户名和密码
|
||||
# fixed by xuhuanqing
|
||||
# encrypted_name = rsa_encrypt(username, public_key)
|
||||
# encrypted_pwd = rsa_encrypt(password, public_key)
|
||||
encrypted_name = rsa_encrypt(username, public_key)
|
||||
encrypted_pwd = rsa_encrypt(password, public_key)
|
||||
|
||||
login_origin = int(os.environ.get("DATAEASE_LOGIN_ORIGIN", "0"))
|
||||
|
||||
payload = {
|
||||
"name": encrypted_name,
|
||||
"pwd": encrypted_pwd,
|
||||
"origin": login_origin,
|
||||
}
|
||||
|
||||
url = f"{base_url.rstrip('/')}{api_prefix}/login/localLogin"
|
||||
request = Request(
|
||||
url,
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
if result.get("code") not in (None, 0):
|
||||
raise RuntimeError(f"登录失败: {result.get('msg', '未知错误')}")
|
||||
# fixed by xuhuanqing
|
||||
# return result.get("data")
|
||||
return result.get("data")["token"]
|
||||
except HTTPError as err:
|
||||
body = err.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"登录请求失败: {err.code} - {body}")
|
||||
|
||||
|
||||
def get_headers_token(x_de_token):
|
||||
"""构建 Token 认证请求头"""
|
||||
return {
|
||||
"Accept": "application/json;charset=UTF-8",
|
||||
"Content-Type": "application/json",
|
||||
"X-DE-TOKEN": x_de_token,
|
||||
}
|
||||
|
||||
|
||||
def make_request(url, headers, payload=None, method="POST"):
|
||||
"""发送 HTTP 请求"""
|
||||
data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
request = Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as err:
|
||||
body = err.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"请求失败: {err.code} - {body}")
|
||||
|
||||
|
||||
def list_datasets(base_url, api_prefix, headers):
|
||||
"""列出所有数据集"""
|
||||
url = f"{base_url.rstrip('/')}{api_prefix}/datasetTree/tree"
|
||||
payload = {"busiFlag": "dataset"}
|
||||
result = make_request(url, headers, payload)
|
||||
|
||||
nodes = result.get('data', [])
|
||||
datasets = []
|
||||
|
||||
def collect_leaf(items):
|
||||
for item in items:
|
||||
if item.get('leaf'):
|
||||
datasets.append({"name": item.get('name'), "id": item.get('id')})
|
||||
children = item.get('children', [])
|
||||
if children:
|
||||
collect_leaf(children)
|
||||
|
||||
collect_leaf(nodes)
|
||||
return datasets
|
||||
|
||||
|
||||
def get_dataset_fields(base_url, api_prefix, headers, dataset_id):
|
||||
"""获取数据集字段"""
|
||||
# 先尝试详情接口
|
||||
url = f"{base_url.rstrip('/')}{api_prefix}/datasetTree/details/{dataset_id}"
|
||||
request = Request(url, headers=headers, method="GET")
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
fields = result.get('data', {}).get('allFields', [])
|
||||
if fields:
|
||||
return fields
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退到字段列表接口
|
||||
url = f"{base_url.rstrip('/')}{api_prefix}/datasetField/listByDatasetGroup/{dataset_id}"
|
||||
result = make_request(url, headers, method="POST")
|
||||
return result.get('data', [])
|
||||
|
||||
|
||||
def resolve_dataset_id(base_url, api_prefix, headers, name_or_id):
|
||||
"""解析数据集名称为 ID"""
|
||||
if str(name_or_id).isdigit() and len(str(name_or_id)) > 10:
|
||||
return name_or_id
|
||||
|
||||
datasets = list_datasets(base_url, api_prefix, headers)
|
||||
for ds in datasets:
|
||||
if ds['name'] == name_or_id:
|
||||
return ds['id']
|
||||
|
||||
raise ValueError(f"未找到数据集: {name_or_id}")
|
||||
|
||||
|
||||
def main():
|
||||
load_dotenv()
|
||||
|
||||
base_url = os.environ.get("DATAEASE_BASE_URL", "")
|
||||
api_prefix = os.environ.get("DATAEASE_API_PREFIX", "/de2api")
|
||||
access_key = os.environ.get("DATAEASE_ACCESS_KEY", "")
|
||||
secret_key = os.environ.get("DATAEASE_SECRET_KEY", "")
|
||||
username = os.environ.get("DATAEASE_USERNAME", "")
|
||||
password = os.environ.get("DATAEASE_PASSWORD", "")
|
||||
|
||||
if not base_url:
|
||||
print("Error: 请设置 DATAEASE_BASE_URL")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(description="探索 DataEase 数据集和字段")
|
||||
parser.add_argument("--list-datasets", action="store_true", help="列出所有数据集")
|
||||
parser.add_argument("--dataset", type=str, help="查看指定数据集的字段")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 认证:优先 AK/SK,否则用密码登录
|
||||
try:
|
||||
if access_key and secret_key:
|
||||
ask_auth = build_ask_auth(access_key, secret_key)
|
||||
headers = get_headers_ask(ask_auth)
|
||||
auth_mode = "AK/SK"
|
||||
elif username and password:
|
||||
x_de_token = login_with_password(base_url, api_prefix, username, password)
|
||||
headers = get_headers_token(x_de_token)
|
||||
auth_mode = "密码登录"
|
||||
else:
|
||||
print("Error: 请配置 AK/SK (DATAEASE_ACCESS_KEY + DATAEASE_SECRET_KEY) 或用户名密码 (DATAEASE_USERNAME + DATAEASE_PASSWORD)")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"认证失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.list_datasets:
|
||||
try:
|
||||
datasets = list_datasets(base_url, api_prefix, headers)
|
||||
print(json.dumps(datasets, ensure_ascii=False, indent=2))
|
||||
except Exception as e:
|
||||
print(f"获取数据集列表失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.dataset:
|
||||
try:
|
||||
dataset_id = resolve_dataset_id(base_url, api_prefix, headers, args.dataset)
|
||||
fields = get_dataset_fields(base_url, api_prefix, headers, dataset_id)
|
||||
result = [
|
||||
{"name": f['name'], "id": f['id'], "type": f.get('deType'), "dataeaseName": f.get('dataeaseName')}
|
||||
for f in fields
|
||||
]
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
except Exception as e:
|
||||
print(f"获取数据集字段失败: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
# Add local path for engine import
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from multi_engine import MultiDataEaseChartEngine
|
||||
|
||||
# --- Configuration from Environment ---
|
||||
def load_dotenv():
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
os.environ[key.strip()] = value.strip().strip('"').strip("'")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ACCESS_KEY = os.environ.get("DATAEASE_ACCESS_KEY")
|
||||
SECRET_KEY = os.environ.get("DATAEASE_SECRET_KEY")
|
||||
BASE_URL = os.environ.get("DATAEASE_BASE_URL")
|
||||
|
||||
# Screenshot configuration - auto-detect OpenClaw workspace for MEDIA: display
|
||||
def _default_output_dir():
|
||||
"""优先使用 OpenClaw workspace 目录,否则用 skill 本地 output 目录"""
|
||||
env_dir = os.environ.get("DATAEASE_OUTPUT_DIR")
|
||||
if env_dir:
|
||||
return env_dir
|
||||
home = os.path.expanduser("~")
|
||||
workspace_dir = os.path.join(home, ".openclaw", "workspace", "dataease-output")
|
||||
if os.path.isdir(os.path.join(home, ".openclaw", "workspace")):
|
||||
os.makedirs(workspace_dir, exist_ok=True)
|
||||
return workspace_dir
|
||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output")
|
||||
|
||||
OUTPUT_DIR = _default_output_dir()
|
||||
DEFAULT_PIXEL = "2560*1440" # 更高分辨率,图片更清晰
|
||||
|
||||
def capture_dashboard(dashboard_id, output_format="jpeg"):
|
||||
"""调用 capture_dashboard.py 进行截图"""
|
||||
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
capture_script = os.path.join(scripts_dir, "capture_dashboard.py")
|
||||
|
||||
if not os.path.exists(capture_script):
|
||||
return None, "Screenshot script not found"
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# capture_dashboard.py 使用相同的 BASE_URL(不含 /de2api 后缀)
|
||||
capture_base_url = BASE_URL.rstrip("/")
|
||||
|
||||
# 调用截图脚本
|
||||
result_format = "0" if output_format == "jpeg" else "1" # 0=jpeg, 1=pdf
|
||||
cmd = [
|
||||
"python3", capture_script,
|
||||
"capture",
|
||||
"--resource-id", str(dashboard_id),
|
||||
"--busi-type", "dashboard",
|
||||
"--output-dir", OUTPUT_DIR,
|
||||
"--result-format", result_format,
|
||||
"--base-url", capture_base_url,
|
||||
"--pixel", DEFAULT_PIXEL
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if result.returncode == 0:
|
||||
output = json.loads(result.stdout)
|
||||
if output.get("ok"):
|
||||
return output.get("saved_file"), None
|
||||
else:
|
||||
return None, output.get("error", "Unknown capture error")
|
||||
else:
|
||||
return None, result.stderr or "Capture failed"
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "Capture timeout"
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
def main():
|
||||
if not all([ACCESS_KEY, SECRET_KEY, BASE_URL]):
|
||||
print("Error: Missing required environment variables.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python3 multi_deploy.py <dashboard_title> <charts_json_config> [--no-screenshot]")
|
||||
print("Example: python3 multi_deploy.py '综合分析' '[{\"type\":\"bar\",\"dataset_name\":\"ds1\",\"x_axis\":[\"f1\"],\"y_axis\":[\"f2\"]}]'")
|
||||
sys.exit(1)
|
||||
|
||||
title = sys.argv[1]
|
||||
try:
|
||||
charts_config = json.loads(sys.argv[2])
|
||||
except Exception as e:
|
||||
print(f"Error parsing JSON config: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查是否禁用截图
|
||||
no_screenshot = "--no-screenshot" in sys.argv
|
||||
|
||||
engine = MultiDataEaseChartEngine(BASE_URL, ACCESS_KEY, SECRET_KEY)
|
||||
|
||||
try:
|
||||
did, url = engine.deploy_multi(title, charts_config)
|
||||
print(f"\n✅ Successfully deployed multi-chart dashboard!")
|
||||
print(f"Dashboard ID: {did}")
|
||||
print(f"URL: {url}")
|
||||
|
||||
# 自动截图
|
||||
if not no_screenshot:
|
||||
print("\n📸 Capturing screenshot...")
|
||||
screenshot_path, error = capture_dashboard(did)
|
||||
if screenshot_path:
|
||||
print(f"Screenshot saved: {screenshot_path}")
|
||||
# 输出 JSON 结果供 Agent 解析
|
||||
result = {
|
||||
"ok": True,
|
||||
"dashboard_id": str(did),
|
||||
"url": url,
|
||||
"screenshot": screenshot_path,
|
||||
"title": title,
|
||||
"charts_count": len(charts_config)
|
||||
}
|
||||
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
|
||||
else:
|
||||
print(f"⚠️ Screenshot failed: {error}")
|
||||
result = {
|
||||
"ok": True,
|
||||
"dashboard_id": str(did),
|
||||
"url": url,
|
||||
"screenshot": None,
|
||||
"screenshot_error": error,
|
||||
"title": title,
|
||||
"charts_count": len(charts_config)
|
||||
}
|
||||
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Deployment failed: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,247 @@
|
||||
import json, re, time, random, os, sys, requests
|
||||
from typing import List, Dict, Any
|
||||
|
||||
# Local SDK client
|
||||
from client import DataEaseClient
|
||||
from engine import DataEaseChartEngine
|
||||
|
||||
class MultiDataEaseChartEngine(DataEaseChartEngine):
|
||||
def __init__(self, base_url, ak, sk):
|
||||
super().__init__(base_url, ak, sk)
|
||||
|
||||
def extract_chart_payload(self, chart_type: str, dataset_id: str, x_names: list, y_names: list, view_id: str, layout: dict, title: str = None):
|
||||
"""
|
||||
Reads the single chart template, applies parameters, and extracts the componentData & canvasViewInfo.
|
||||
"""
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
tpl_dir = os.path.join(base_dir, "..", "templates", f"chart_{chart_type}")
|
||||
|
||||
if not os.path.exists(tpl_dir):
|
||||
raise FileNotFoundError(f"Template directory {tpl_dir} not found")
|
||||
|
||||
with open(os.path.join(tpl_dir, "template.j2")) as f:
|
||||
template_str = f.read()
|
||||
|
||||
with open(os.path.join(tpl_dir, "params.json")) as f:
|
||||
raw_params = json.load(f)
|
||||
|
||||
# Build Context (Flatten params + dynamic IDs)
|
||||
ctx = {}
|
||||
def flatten_dict(d):
|
||||
items = {}
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
items.update(flatten_dict(v))
|
||||
elif not k.startswith("_"):
|
||||
items[k] = v
|
||||
return items
|
||||
|
||||
ctx.update(flatten_dict(raw_params))
|
||||
ctx.update(self.get_dataset_ctx(dataset_id, x_names, y_names))
|
||||
|
||||
# Runtime Randomization
|
||||
# SCENE_ID is usually "0" for base
|
||||
ctx.update({"VIEW_ID": view_id, "SCENE_ID": "0"})
|
||||
|
||||
# Sub placeholder
|
||||
def rep(m):
|
||||
k = m.group(1).strip()
|
||||
if k in ctx:
|
||||
return str(ctx[k])
|
||||
return m.group(0)
|
||||
|
||||
rendered = re.sub(r"\{\{\s*(\w+)\s*\}\}", rep, template_str)
|
||||
payload = json.loads(rendered)
|
||||
|
||||
# Extract canvasViewInfo
|
||||
canvas_view_info = payload.get("canvasViewInfo", {})
|
||||
target_view_info = canvas_view_info.get(view_id)
|
||||
if not target_view_info:
|
||||
raise ValueError(f"View ID {view_id} not found in the rendered template!")
|
||||
|
||||
# Override Chart Title in view_info if provided
|
||||
if title:
|
||||
target_view_info["title"] = title
|
||||
|
||||
# --- Fix: Robust field name replacement ---
|
||||
def deep_update_field_names(obj, target_id, new_name):
|
||||
if isinstance(obj, dict):
|
||||
if str(obj.get("id")) == str(target_id):
|
||||
obj["name"] = new_name
|
||||
if "description" in obj: obj["description"] = new_name
|
||||
if "originName" in obj: obj["originName"] = new_name
|
||||
if "dbFieldName" in obj: obj["dbFieldName"] = None
|
||||
if "optionLabel" in obj:
|
||||
obj["optionLabel"] = new_name + (obj["optionLabel"].partition("(")[1] + obj["optionLabel"].partition("(")[2] if "(" in obj["optionLabel"] else "")
|
||||
if "optionShowName" in obj:
|
||||
obj["optionShowName"] = new_name + (obj["optionShowName"].partition("(")[1] + obj["optionShowName"].partition("(")[2] if "(" in obj["optionShowName"] else "")
|
||||
for v in obj.values():
|
||||
deep_update_field_names(v, target_id, new_name)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj:
|
||||
deep_update_field_names(item, target_id, new_name)
|
||||
|
||||
dataset_ctx = self.get_dataset_ctx(dataset_id, x_names, y_names)
|
||||
for i, x_name in enumerate(x_names):
|
||||
f_id = dataset_ctx.get(f"XAXIS{'' if i==0 else i+1}_FIELD_ID")
|
||||
if f_id: deep_update_field_names(target_view_info, f_id, x_name)
|
||||
|
||||
for i, y_name in enumerate(y_names):
|
||||
f_id = dataset_ctx.get(f"YAXIS{'' if i==0 else i+1}_FIELD_ID")
|
||||
if f_id: deep_update_field_names(target_view_info, f_id, y_name)
|
||||
|
||||
def brute_force_replace(obj, search_list, replace_list):
|
||||
if isinstance(obj, dict):
|
||||
for k, v in obj.items():
|
||||
if isinstance(v, str):
|
||||
for s, r in zip(search_list, replace_list):
|
||||
if s in v: obj[k] = v.replace(s, r)
|
||||
else: brute_force_replace(v, search_list, replace_list)
|
||||
elif isinstance(obj, list):
|
||||
for item in obj: brute_force_replace(item, search_list, replace_list)
|
||||
|
||||
if x_names and y_names:
|
||||
brute_force_replace(target_view_info, ["访问平台", "访问次数", "浏览量"], [x_names[0], y_names[0], y_names[0]])
|
||||
# --- End Fix ---
|
||||
|
||||
# Extract componentData
|
||||
component_data_str = payload.get("componentData", "[]")
|
||||
components = json.loads(component_data_str)
|
||||
if not components:
|
||||
raise ValueError(f"No component data found in the rendered template")
|
||||
|
||||
# We only take the first component as it was a single chart template
|
||||
target_component = components[0]
|
||||
|
||||
# Override component name/label if title provided
|
||||
if title:
|
||||
target_component["name"] = title
|
||||
target_component["label"] = title
|
||||
|
||||
# Override layout
|
||||
if "layout" in layout:
|
||||
layout = layout["layout"] # in case it's nested
|
||||
|
||||
if "style" not in target_component:
|
||||
target_component["style"] = {}
|
||||
|
||||
if "x" in layout: target_component["x"] = layout["x"]
|
||||
if "y" in layout: target_component["y"] = layout["y"]
|
||||
if "sizeX" in layout: target_component["sizeX"] = layout["sizeX"]
|
||||
if "sizeY" in layout: target_component["sizeY"] = layout["sizeY"]
|
||||
if "width" in layout: target_component["style"]["width"] = layout["width"]
|
||||
if "height" in layout: target_component["style"]["height"] = layout["height"]
|
||||
if "left" in layout: target_component["style"]["left"] = layout["left"]
|
||||
if "top" in layout: target_component["style"]["top"] = layout["top"]
|
||||
|
||||
return target_component, target_view_info
|
||||
|
||||
def deploy_multi(self, title: str, charts_config: List[Dict[str, Any]]):
|
||||
"""
|
||||
Deploy multiple charts into a single dashboard.
|
||||
charts_config supports optional 'layout'. If missing, it will auto-layout.
|
||||
"""
|
||||
content_id = self.rand_id()
|
||||
board_name = f"{title}_{int(time.time())}"
|
||||
|
||||
# Load base dashboard style from template
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
dashboard_tpl_path = os.path.join(base_dir, "..", "templates", "dashboard", "base.json")
|
||||
if os.path.exists(dashboard_tpl_path):
|
||||
with open(dashboard_tpl_path) as f:
|
||||
base_tpl = json.load(f)
|
||||
canvas_style_data = json.loads(base_tpl.get("canvasStyleData", "{}"))
|
||||
else:
|
||||
canvas_style_data = {
|
||||
"width": 1920, "height": 1080, "screenAdaptor": "widthFirst",
|
||||
"dashboard": {"gap": "yes", "gapSize": 5, "matrixBase": 4}
|
||||
}
|
||||
|
||||
final_component_data = []
|
||||
final_canvas_view_info = {}
|
||||
active_view_ids = []
|
||||
|
||||
# Auto-layout calculation constants
|
||||
GRID_COLS = 72
|
||||
CHART_WIDTH = 36
|
||||
CHART_HEIGHT = 14
|
||||
|
||||
# Process each chart sequentially
|
||||
for idx, c_conf in enumerate(charts_config):
|
||||
view_id = self.rand_id()
|
||||
active_view_ids.append(view_id)
|
||||
|
||||
# Calculate auto-layout if not provided
|
||||
layout = c_conf.get("layout", {})
|
||||
if not layout:
|
||||
row = idx // 2
|
||||
col = idx % 2
|
||||
layout = {
|
||||
"x": col * CHART_WIDTH + 1,
|
||||
"y": row * CHART_HEIGHT + 1,
|
||||
"sizeX": CHART_WIDTH,
|
||||
"sizeY": CHART_HEIGHT,
|
||||
"width": 519, "height": 65, # Standard relative sizes
|
||||
"left": col * 519, "top": row * 65
|
||||
}
|
||||
|
||||
comp_data, view_info = self.extract_chart_payload(
|
||||
chart_type=c_conf["type"],
|
||||
dataset_id=c_conf["dataset_name"],
|
||||
x_names=c_conf.get("x_axis", []),
|
||||
y_names=c_conf.get("y_axis", []),
|
||||
view_id=view_id,
|
||||
layout=layout,
|
||||
title=c_conf.get("title")
|
||||
)
|
||||
|
||||
comp_data["_dragId"] = idx
|
||||
final_component_data.append(comp_data)
|
||||
final_canvas_view_info[view_id] = view_info
|
||||
|
||||
dashboard_payload = {
|
||||
"id": None,
|
||||
"name": board_name,
|
||||
"type": "dashboard",
|
||||
"status": 0,
|
||||
"dataState": "ready",
|
||||
"selfWatermarkStatus": True,
|
||||
"checkVersion": "2.10.20",
|
||||
"pid": "0",
|
||||
"mobileLayout": False,
|
||||
"canvasStyleData": json.dumps(canvas_style_data, separators=(',', ':'), ensure_ascii=False),
|
||||
"componentData": json.dumps(final_component_data, separators=(',', ':'), ensure_ascii=False),
|
||||
"canvasViewInfo": final_canvas_view_info,
|
||||
"contentId": content_id
|
||||
}
|
||||
|
||||
headers = self.client._get_headers()
|
||||
print(f"Deploying multi-chart dashboard '{title}' with {len(charts_config)} charts...")
|
||||
|
||||
save_r = requests.post(
|
||||
f"{self.base_url}{self.api_prefix}/dataVisualization/saveCanvas",
|
||||
headers=headers, json=dashboard_payload, verify=False, timeout=30
|
||||
)
|
||||
|
||||
if save_r.status_code != 200:
|
||||
raise Exception(f"saveCanvas failed: {save_r.text}")
|
||||
|
||||
dashboard_id = str(save_r.json()["data"])
|
||||
|
||||
# Publish
|
||||
requests.post(
|
||||
f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus",
|
||||
headers=headers, verify=False, timeout=30,
|
||||
json={
|
||||
"id": dashboard_id,
|
||||
"name": board_name,
|
||||
"activeViewIds": active_view_ids,
|
||||
"status": 1,
|
||||
"type": "dashboard",
|
||||
"mobileLayout": False
|
||||
}
|
||||
)
|
||||
|
||||
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
|
||||
|
||||
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"_comment": "chart_bar 模板参数(来源:DataEase 抓包 + 参数化)",
|
||||
"view": {
|
||||
"VIEW_ID": "7447105738385657856",
|
||||
"CONTENT_ID": "7447106138476122112",
|
||||
"SCENE_ID": "0"
|
||||
},
|
||||
"dataset": {
|
||||
"DATASOURCE_ID": "1166370337189924864",
|
||||
"DATASET_TABLE_ID": "7387069976563159040",
|
||||
"DATASET_GROUP_ID": "1178801986976485376"
|
||||
},
|
||||
"fields": {
|
||||
"xAxis": {
|
||||
"XAXIS_FIELD_ID": "1761214724097",
|
||||
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
|
||||
},
|
||||
"yAxis": {
|
||||
"YAXIS_FIELD_ID": "1761214724096",
|
||||
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
|
||||
"YAXIS_SERIES_ID": "1761214724096-yAxis"
|
||||
},
|
||||
"yAxis2": {
|
||||
"YAXIS2_FIELD_ID": "1761214724100",
|
||||
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"_comment": "chart_line 模板参数(来源:DataEase 抓包 + 参数化)",
|
||||
"view": {
|
||||
"VIEW_ID": "7447105738385657856",
|
||||
"CONTENT_ID": "7447106138476122112",
|
||||
"SCENE_ID": "0"
|
||||
},
|
||||
"dataset": {
|
||||
"DATASOURCE_ID": "1166370337189924864",
|
||||
"DATASET_TABLE_ID": "7387069976563159040",
|
||||
"DATASET_GROUP_ID": "1178801986976485376"
|
||||
},
|
||||
"fields": {
|
||||
"xAxis": {
|
||||
"XAXIS_FIELD_ID": "1761214724097",
|
||||
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
|
||||
},
|
||||
"yAxis": {
|
||||
"YAXIS_FIELD_ID": "1761214724096",
|
||||
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
|
||||
"YAXIS_SERIES_ID": "1761214724096-yAxis"
|
||||
},
|
||||
"yAxis2": {
|
||||
"YAXIS2_FIELD_ID": "1761214724100",
|
||||
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"_comment": "chart_pie 模板参数(来源:DataEase 抓包 + 参数化)",
|
||||
"view": {
|
||||
"VIEW_ID": "7446557568647827456",
|
||||
"CONTENT_ID": "7446557957866655744",
|
||||
"SCENE_ID": "0"
|
||||
},
|
||||
"dataset": {
|
||||
"DATASOURCE_ID": "1166370337189924864",
|
||||
"DATASET_TABLE_ID": "7387069976563159040",
|
||||
"DATASET_GROUP_ID": "1178801986976485376"
|
||||
},
|
||||
"fields": {
|
||||
"xAxis": {
|
||||
"XAXIS_FIELD_ID": "1761214724097",
|
||||
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
|
||||
},
|
||||
"yAxis": {
|
||||
"YAXIS_FIELD_ID": "1761214724096",
|
||||
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
|
||||
"YAXIS_SERIES_ID": "1761214724096-yAxis"
|
||||
},
|
||||
"yAxis2": {
|
||||
"YAXIS2_FIELD_ID": "1761214724100",
|
||||
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"view": {
|
||||
"VIEW_ID": "7448293416766541824",
|
||||
"CONTENT_ID": "7449036212012060672",
|
||||
"SCENE_ID": "1240025405293989888"
|
||||
},
|
||||
"dataset": {
|
||||
"DATASET_GROUP_ID": "1178801986976485376",
|
||||
"DATASOURCE_ID": "1166370337189924864",
|
||||
"DATASET_TABLE_ID": "7387069976563159040"
|
||||
},
|
||||
"fields": {
|
||||
"xAxis": {
|
||||
"XAXIS_FIELD_ID": "1761214724097",
|
||||
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
|
||||
},
|
||||
"xAxis2": {
|
||||
"XAXIS2_FIELD_ID": "1761214724096",
|
||||
"XAXIS2_DE_NAME": "f_97666027e11d7dc5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
{
|
||||
"canvasStyleData": "{\"width\":1920,\"height\":1080,\"refreshBrowserEnable\":false,\"refreshBrowserUnit\":\"minute\",\"refreshBrowserTime\":5,\"refreshViewEnable\":false,\"refreshViewLoading\":true,\"refreshUnit\":\"minute\",\"refreshTime\":5,\"popupAvailable\":true,\"popupButtonAvailable\":true,\"suspensionButtonAvailable\":false,\"screenAdaptor\":\"widthFirst\",\"dashboardAdaptor\":\"keepHeightAndWidth\",\"scale\":33,\"scaleWidth\":24,\"scaleHeight\":24,\"backgroundColorSelect\":true,\"backgroundImageEnable\":false,\"backgroundType\":\"backgroundColor\",\"background\":\"\",\"openCommonStyle\":true,\"opacity\":1,\"fontSize\":14,\"fontFamily\":\"PingFang\",\"themeId\":\"10001\",\"color\":\"#000000\",\"backgroundColor\":\"#f5f6f7\",\"dashboard\":{\"gap\":\"yes\",\"gapSize\":5,\"gapMode\":\"middle\",\"showGrid\":false,\"matrixBase\":4,\"resultMode\":\"all\",\"resultCount\":1000,\"themeColor\":\"light\",\"mobileSetting\":{\"customSetting\":false,\"imageUrl\":null,\"backgroundType\":\"image\",\"color\":\"#000\"}},\"component\":{\"chartTitle\":{\"show\":true,\"fontSize\":16,\"hPosition\":\"left\",\"vPosition\":\"top\",\"isItalic\":false,\"isBolder\":true,\"remarkShow\":false,\"remark\":\"\",\"fontFamily\":\"\",\"letterSpace\":\"0\",\"fontShadow\":false,\"color\":\"#000000\",\"remarkBackgroundColor\":\"#ffffff\"},\"chartColor\":{\"basicStyle\":{\"colorScheme\":\"default\",\"colors\":[\"#1E90FF\",\"#90EE90\",\"#00CED1\",\"#E2BD84\",\"#7A90E0\",\"#3BA272\",\"#2BE7FF\",\"#0A8ADA\",\"#FFD700\"],\"alpha\":100,\"gradient\":false,\"mapStyle\":\"normal\",\"areaBaseColor\":\"#FFFFFF\",\"areaBorderColor\":\"#303133\",\"gaugeStyle\":\"default\",\"tableBorderColor\":\"#E6E7E4\",\"tableScrollBarColor\":\"rgba(0, 0, 0, 0.15)\",\"zoomButtonColor\":\"#aaa\",\"zoomBackground\":\"#fff\"},\"misc\":{\"flowMapConfig\":{\"lineConfig\":{\"mapLineAnimate\":true,\"mapLineGradient\":false,\"mapLineSourceColor\":\"#146C94\",\"mapLineTargetColor\":\"#576CBC\"}},\"nameFontColor\":\"#000000\",\"valueFontColor\":\"#5470c6\"},\"tableHeader\":{\"tableHeaderBgColor\":\"#1E90FF\",\"tableHeaderCornerBgColor\":\"#1E90FF\",\"tableHeaderColBgColor\":\"#1E90FF\",\"tableHeaderFontColor\":\"#000000\",\"tableHeaderCornerFontColor\":\"#000000\",\"tableHeaderColFontColor\":\"#000000\"},\"tableCell\":{\"tableItemBgColor\":\"#FFFFFF\",\"tableFontColor\":\"#000000\",\"tableItemSubBgColor\":\"#1E90FF\"}},\"chartCommonStyle\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"filterStyle\":{\"layout\":\"horizontal\",\"titleLayout\":\"left\",\"labelColor\":\"#1f2329\",\"titleColor\":\"#1f2329\",\"color\":\"#1f2329\",\"borderColor\":\"#bbbfc4\",\"text\":\"#1f2329\",\"bgColor\":\"#FFFFFF\"},\"tabStyle\":{\"headPosition\":\"left\",\"headFontColor\":\"#000000\",\"headFontActiveColor\":\"#000000\",\"headBorderColor\":\"#ffffff\",\"headBorderActiveColor\":\"#ffffff\"},\"seniorStyleSetting\":{\"linkageIconColor\":\"#a6a6a6\",\"drillLayerColor\":\"#a6a6a6\",\"pagerColor\":\"#a6a6a6\"},\"formatterItem\":{\"type\":\"auto\",\"unitLanguage\":\"ch\",\"unit\":1,\"suffix\":\"\",\"decimalCount\":2,\"thousandSeparator\":true}},\"dialogBackgroundColor\":\"rgba(255, 255, 255, 1)\",\"dialogButton\":\"#020408\"}",
|
||||
"componentData": "[{\"animations\":[],\"canvasId\":\"canvas-main\",\"events\":{\"checked\":false,\"showTips\":false,\"type\":\"jump\",\"typeList\":[{\"key\":\"jump\",\"label\":\"jump\"},{\"key\":\"download\",\"label\":\"download\"},{\"key\":\"share\",\"label\":\"share\"},{\"key\":\"fullScreen\",\"label\":\"fullScreen\"},{\"key\":\"showHidden\",\"label\":\"showHidden\"},{\"key\":\"refreshDataV\",\"label\":\"refreshDataV\"},{\"key\":\"refreshView\",\"label\":\"refreshView\"}],\"jump\":{\"value\":\"https://\",\"type\":\"_blank\"},\"download\":{\"value\":true},\"share\":{\"value\":true,\"target\":\"all\"}},\"carousel\":{\"enable\":false,\"time\":10},\"multiDimensional\":{\"enable\":false,\"x\":0,\"y\":0,\"z\":0},\"groupStyle\":{},\"isLock\":false,\"maintainRadio\":false,\"aspectRatio\":1,\"isShow\":true,\"dashboardHidden\":false,\"category\":\"base\",\"dragging\":false,\"resizing\":false,\"collapseName\":[\"position\",\"background\",\"style\",\"picture\",\"frameLinks\",\"videoLinks\",\"streamLinks\",\"carouselInfo\",\"events\",\"decoration_style\"],\"linkage\":{\"duration\":0,\"data\":[{\"id\":\"\",\"label\":\"\",\"event\":\"\",\"style\":[{\"key\":\"\",\"value\":\"\"}]}]},\"component\":\"UserView\",\"name\":\"明细表\",\"label\":\"明细表\",\"propValue\":{\"textValue\":\"\",\"urlList\":[]},\"icon\":\"bar\",\"innerType\":\"table-info\",\"editing\":false,\"canvasActive\":false,\"actionSelection\":{\"linkageActive\":\"custom\"},\"x\":1,\"y\":1,\"sizeX\":36,\"sizeY\":14,\"style\":{\"rotate\":0,\"opacity\":1,\"borderActive\":false,\"borderWidth\":1,\"borderRadius\":5,\"borderStyle\":\"solid\",\"borderColor\":\"rgba(204, 204, 204, 1)\",\"adaptation\":\"adaptation\",\"width\":744,\"height\":139.22222222222223,\"left\":0,\"top\":0},\"matrixStyle\":{},\"commonBackground\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"state\":\"ready\",\"render\":\"antv\",\"isPlugin\":false,\"id\":\"{{ VIEW_ID }}\",\"_dragId\":0,\"linkageFilters\":[],\"expand\":false,\"resizeInnerKeep\":false}]",
|
||||
"canvasViewInfo": {
|
||||
"{{ VIEW_ID }}": {
|
||||
"id": "{{ VIEW_ID }}",
|
||||
"title": "明细表",
|
||||
"sceneId": "{{ SCENE_ID }}",
|
||||
"tableId": "{{ DATASET_GROUP_ID }}",
|
||||
"type": "table-info",
|
||||
"render": "antv",
|
||||
"resultCount": 1000,
|
||||
"resultMode": "custom",
|
||||
"extStack": [],
|
||||
"extBubble": [],
|
||||
"extLabel": [],
|
||||
"extTooltip": [],
|
||||
"customAttr": {
|
||||
"basicStyle": {
|
||||
"alpha": 100,
|
||||
"tableBorderColor": "#E6E7E4",
|
||||
"tableScrollBarColor": "rgba(0, 0, 0, 0.15)",
|
||||
"tableColumnMode": "adapt",
|
||||
"tableColumnWidth": 100,
|
||||
"tableFieldWidth": [],
|
||||
"tablePageMode": "page",
|
||||
"tablePageStyle": "simple",
|
||||
"tablePageSize": 20,
|
||||
"gaugeStyle": "default",
|
||||
"colorScheme": "default",
|
||||
"colors": [
|
||||
"#1E90FF",
|
||||
"#90EE90",
|
||||
"#00CED1",
|
||||
"#E2BD84",
|
||||
"#7A90E0",
|
||||
"#3BA272",
|
||||
"#2BE7FF",
|
||||
"#0A8ADA",
|
||||
"#FFD700"
|
||||
],
|
||||
"mapVendor": "amap",
|
||||
"gradient": false,
|
||||
"lineWidth": 2,
|
||||
"lineSymbol": "circle",
|
||||
"lineSymbolSize": 4,
|
||||
"lineSmooth": true,
|
||||
"barDefault": true,
|
||||
"radiusColumnBar": "rightAngle",
|
||||
"columnBarRightAngleRadius": 20,
|
||||
"columnWidthRatio": 60,
|
||||
"barWidth": 40,
|
||||
"barGap": 0.4,
|
||||
"lineType": "solid",
|
||||
"scatterSymbol": "circle",
|
||||
"scatterSymbolSize": 8,
|
||||
"radarShape": "polygon",
|
||||
"mapStyle": "normal",
|
||||
"heatMapType": "heatmap",
|
||||
"heatMapIntensity": 2,
|
||||
"heatMapRadius": 20,
|
||||
"areaBorderColor": "#303133",
|
||||
"areaBaseColor": "#FFFFFF",
|
||||
"mapSymbolOpacity": 0.7,
|
||||
"mapSymbolStrokeWidth": 2,
|
||||
"mapSymbol": "circle",
|
||||
"mapSymbolSize": 6,
|
||||
"radius": 80,
|
||||
"innerRadius": 60,
|
||||
"showZoom": true,
|
||||
"zoomButtonColor": "#aaa",
|
||||
"zoomBackground": "#fff",
|
||||
"tableLayoutMode": "grid",
|
||||
"defaultExpandLevel": 1,
|
||||
"calcTopN": false,
|
||||
"topN": 5,
|
||||
"topNLabel": "其他",
|
||||
"gaugeAxisLine": true,
|
||||
"gaugePercentLabel": true,
|
||||
"showSummary": false,
|
||||
"summaryLabel": "总计",
|
||||
"seriesColor": [],
|
||||
"layout": "horizontal",
|
||||
"mapSymbolSizeMin": 4,
|
||||
"mapSymbolSizeMax": 30,
|
||||
"showLabel": true,
|
||||
"mapStyleUrl": "",
|
||||
"autoFit": true,
|
||||
"mapCenter": {
|
||||
"longitude": 117.232,
|
||||
"latitude": 39.354
|
||||
},
|
||||
"zoomLevel": 7,
|
||||
"customIcon": "",
|
||||
"showHoverStyle": true,
|
||||
"autoWrap": false,
|
||||
"maxLines": 3,
|
||||
"radarShowPoint": true,
|
||||
"radarPointSize": 4,
|
||||
"radarAreaColor": true,
|
||||
"circleBorderColor": "#fff",
|
||||
"circleBorderWidth": 0,
|
||||
"circlePadding": 0,
|
||||
"quotaPosition": "col",
|
||||
"quotaColLabel": "数值",
|
||||
"tableRowHeaderMode": "adapt",
|
||||
"tableRowHeaderWidth": 120,
|
||||
"tableRowHeaderWidthPercent": 20
|
||||
},
|
||||
"misc": {
|
||||
"pieInnerRadius": 0,
|
||||
"pieOuterRadius": 80,
|
||||
"radarShape": "polygon",
|
||||
"radarSize": 80,
|
||||
"gaugeMinType": "fix",
|
||||
"gaugeMinField": {
|
||||
"id": "",
|
||||
"summary": ""
|
||||
},
|
||||
"gaugeMin": 0,
|
||||
"gaugeMaxType": "dynamic",
|
||||
"gaugeMaxField": {
|
||||
"id": "",
|
||||
"summary": ""
|
||||
},
|
||||
"gaugeStartAngle": 225,
|
||||
"gaugeEndAngle": -45,
|
||||
"nameFontSize": 18,
|
||||
"valueFontSize": 18,
|
||||
"nameValueSpace": 10,
|
||||
"valueFontColor": "#5470c6",
|
||||
"valueFontFamily": "Microsoft YaHei",
|
||||
"valueFontIsBolder": false,
|
||||
"valueFontIsItalic": false,
|
||||
"valueLetterSpace": 0,
|
||||
"valueFontShadow": false,
|
||||
"showName": true,
|
||||
"nameFontColor": "#000000",
|
||||
"nameFontFamily": "Microsoft YaHei",
|
||||
"nameFontIsBolder": false,
|
||||
"nameFontIsItalic": false,
|
||||
"nameLetterSpace": "0",
|
||||
"nameFontShadow": false,
|
||||
"treemapWidth": 80,
|
||||
"treemapHeight": 80,
|
||||
"liquidMaxType": "dynamic",
|
||||
"liquidMaxField": {
|
||||
"id": "",
|
||||
"summary": ""
|
||||
},
|
||||
"liquidSize": 80,
|
||||
"liquidShape": "circle",
|
||||
"hPosition": "center",
|
||||
"vPosition": "center",
|
||||
"mapPitch": 0,
|
||||
"wordSizeRange": [
|
||||
8,
|
||||
32
|
||||
],
|
||||
"wordSpacing": 6,
|
||||
"mapAutoLegend": true,
|
||||
"mapLegendMax": 0,
|
||||
"mapLegendMin": 0,
|
||||
"mapLegendNumber": 9,
|
||||
"mapLegendRangeType": "quantize",
|
||||
"mapLegendCustomRange": [],
|
||||
"flowMapConfig": {
|
||||
"lineConfig": {
|
||||
"mapLineAnimate": true,
|
||||
"mapLineType": "arc",
|
||||
"mapLineWidth": 1,
|
||||
"mapLineAnimateDuration": 3,
|
||||
"mapLineGradient": false,
|
||||
"mapLineSourceColor": "#146C94",
|
||||
"mapLineTargetColor": "#576CBC",
|
||||
"alpha": 100
|
||||
},
|
||||
"pointConfig": {
|
||||
"text": {
|
||||
"color": "#146C94",
|
||||
"fontSize": 10
|
||||
},
|
||||
"point": {
|
||||
"color": "#146C94",
|
||||
"size": 4,
|
||||
"animate": false,
|
||||
"speed": 0.01
|
||||
}
|
||||
}
|
||||
},
|
||||
"wordCloudAxisValueRange": {
|
||||
"auto": true,
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"bullet": {
|
||||
"bar": {
|
||||
"ranges": {
|
||||
"fill": [
|
||||
"rgba(0,128,255,0.3)"
|
||||
],
|
||||
"size": 20,
|
||||
"showType": "dynamic",
|
||||
"fixedRangeNumber": 3,
|
||||
"symbol": "circle",
|
||||
"symbolSize": 4
|
||||
},
|
||||
"measures": {
|
||||
"fill": [
|
||||
"rgba(0,128,255,1)"
|
||||
],
|
||||
"size": 15,
|
||||
"symbol": "circle",
|
||||
"symbolSize": 4
|
||||
},
|
||||
"target": {
|
||||
"fill": "#000000",
|
||||
"size": 20,
|
||||
"showType": "dynamic",
|
||||
"value": 0,
|
||||
"symbol": "line",
|
||||
"symbolSize": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"liquidShowBorder": false,
|
||||
"liquidBorderWidth": 4,
|
||||
"liquidBorderDistance": 8
|
||||
},
|
||||
"label": {
|
||||
"show": false,
|
||||
"childrenShow": true,
|
||||
"position": "top",
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"formatter": "",
|
||||
"labelLine": {
|
||||
"show": true
|
||||
},
|
||||
"labelFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"reserveDecimalCount": 2,
|
||||
"labelShadow": false,
|
||||
"labelBgColor": "",
|
||||
"labelShadowColor": "",
|
||||
"quotaLabelFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"showDimension": true,
|
||||
"showQuota": false,
|
||||
"showProportion": true,
|
||||
"seriesLabelFormatter": [],
|
||||
"conversionTag": {
|
||||
"show": false,
|
||||
"precision": 2,
|
||||
"text": "转化率"
|
||||
},
|
||||
"showTotal": false,
|
||||
"totalFontSize": 12,
|
||||
"totalColor": "#FFF",
|
||||
"totalFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"showStackQuota": false,
|
||||
"fullDisplay": false,
|
||||
"proportionSeriesFormatter": {
|
||||
"show": false,
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"formatterCfg": {
|
||||
"decimalCount": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"show": true,
|
||||
"trigger": "item",
|
||||
"confine": true,
|
||||
"fontSize": 12,
|
||||
"color": "#000000",
|
||||
"tooltipFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"seriesTooltipFormatter": [],
|
||||
"carousel": {
|
||||
"enable": false,
|
||||
"stayTime": 3,
|
||||
"intervalTime": 1
|
||||
}
|
||||
},
|
||||
"tableTotal": {
|
||||
"row": {
|
||||
"showGrandTotals": true,
|
||||
"showSubTotals": true,
|
||||
"reverseLayout": false,
|
||||
"reverseSubLayout": false,
|
||||
"label": "总计",
|
||||
"subLabel": "小计",
|
||||
"subTotalsDimensions": [],
|
||||
"subTotalsDimensionsNew": true,
|
||||
"calcTotals": {
|
||||
"aggregation": "SUM",
|
||||
"cfg": []
|
||||
},
|
||||
"calcSubTotals": {
|
||||
"aggregation": "SUM",
|
||||
"cfg": []
|
||||
},
|
||||
"totalSort": "none",
|
||||
"totalSortField": ""
|
||||
},
|
||||
"col": {
|
||||
"showGrandTotals": true,
|
||||
"showSubTotals": true,
|
||||
"reverseLayout": false,
|
||||
"reverseSubLayout": false,
|
||||
"label": "总计",
|
||||
"subLabel": "小计",
|
||||
"subTotalsDimensions": [],
|
||||
"calcTotals": {
|
||||
"aggregation": "SUM",
|
||||
"cfg": []
|
||||
},
|
||||
"calcSubTotals": {
|
||||
"aggregation": "SUM",
|
||||
"cfg": []
|
||||
},
|
||||
"totalSort": "none",
|
||||
"totalSortField": ""
|
||||
}
|
||||
},
|
||||
"tableHeader": {
|
||||
"indexLabel": "序号",
|
||||
"showIndex": false,
|
||||
"tableHeaderAlign": "left",
|
||||
"tableHeaderCornerAlign": "left",
|
||||
"tableHeaderColAlign": "left",
|
||||
"tableHeaderBgColor": "#1E90FF",
|
||||
"tableHeaderCornerBgColor": "#1E90FF",
|
||||
"tableHeaderColBgColor": "#1E90FF",
|
||||
"tableHeaderFontColor": "#000000",
|
||||
"tableHeaderCornerFontColor": "#000000",
|
||||
"tableHeaderColFontColor": "#000000",
|
||||
"tableTitleFontSize": 12,
|
||||
"tableTitleCornerFontSize": 12,
|
||||
"tableTitleColFontSize": 12,
|
||||
"tableTitleHeight": 36,
|
||||
"tableHeaderSort": false,
|
||||
"showColTooltip": false,
|
||||
"showRowTooltip": false,
|
||||
"showTableHeader": true,
|
||||
"showHorizonBorder": true,
|
||||
"showVerticalBorder": true,
|
||||
"isItalic": false,
|
||||
"isCornerItalic": false,
|
||||
"isColItalic": false,
|
||||
"isBolder": true,
|
||||
"isCornerBolder": true,
|
||||
"isColBolder": true,
|
||||
"headerGroup": false,
|
||||
"headerGroupConfig": {
|
||||
"columns": [],
|
||||
"meta": []
|
||||
},
|
||||
"rowHeaderFreeze": true,
|
||||
"alignConfig": []
|
||||
},
|
||||
"tableCell": {
|
||||
"tableFontColor": "#000000",
|
||||
"tableItemAlign": "right",
|
||||
"tableItemBgColor": "#FFFFFF",
|
||||
"tableItemFontSize": 12,
|
||||
"tableItemHeight": 36,
|
||||
"enableTableCrossBG": false,
|
||||
"tableItemSubBgColor": "#1E90FF",
|
||||
"showTooltip": false,
|
||||
"showHorizonBorder": true,
|
||||
"showVerticalBorder": true,
|
||||
"isItalic": false,
|
||||
"isBolder": false,
|
||||
"tableFreeze": false,
|
||||
"tableColumnFreezeHead": 0,
|
||||
"tableRowFreezeHead": 0,
|
||||
"mergeCells": true,
|
||||
"alignConfig": []
|
||||
},
|
||||
"indicator": {
|
||||
"show": true,
|
||||
"fontSize": 20,
|
||||
"color": "#5470C6ff",
|
||||
"hPosition": "center",
|
||||
"vPosition": "center",
|
||||
"isItalic": false,
|
||||
"isBolder": true,
|
||||
"fontFamily": "Microsoft YaHei",
|
||||
"letterSpace": 0,
|
||||
"fontShadow": false,
|
||||
"backgroundColor": "",
|
||||
"suffixEnable": true,
|
||||
"suffix": "",
|
||||
"suffixFontSize": 14,
|
||||
"suffixColor": "#5470C6ff",
|
||||
"suffixIsItalic": false,
|
||||
"suffixIsBolder": true,
|
||||
"suffixFontFamily": "Microsoft YaHei",
|
||||
"suffixLetterSpace": 0,
|
||||
"suffixFontShadow": false
|
||||
},
|
||||
"indicatorName": {
|
||||
"show": true,
|
||||
"fontSize": 18,
|
||||
"color": "#ffffffff",
|
||||
"isItalic": false,
|
||||
"isBolder": true,
|
||||
"fontFamily": "Microsoft YaHei",
|
||||
"letterSpace": 0,
|
||||
"fontShadow": false,
|
||||
"nameValueSpacing": 0,
|
||||
"namePosition": "bottom"
|
||||
},
|
||||
"map": {
|
||||
"id": "",
|
||||
"level": "world"
|
||||
}
|
||||
},
|
||||
"customAttrMobile": null,
|
||||
"customStyle": {
|
||||
"text": {
|
||||
"show": true,
|
||||
"fontSize": 16,
|
||||
"hPosition": "left",
|
||||
"vPosition": "top",
|
||||
"isItalic": false,
|
||||
"isBolder": true,
|
||||
"remarkShow": false,
|
||||
"remark": "",
|
||||
"fontFamily": "PingFang",
|
||||
"letterSpace": "0",
|
||||
"fontShadow": false,
|
||||
"color": "#000000",
|
||||
"remarkBackgroundColor": "#ffffff"
|
||||
},
|
||||
"legend": {
|
||||
"show": true,
|
||||
"hPosition": "center",
|
||||
"vPosition": "bottom",
|
||||
"orient": "horizontal",
|
||||
"icon": "circle",
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"size": 4,
|
||||
"showRange": true,
|
||||
"sort": "none",
|
||||
"customSort": []
|
||||
},
|
||||
"xAxis": {
|
||||
"show": true,
|
||||
"position": "bottom",
|
||||
"nameShow": false,
|
||||
"name": "",
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"rotate": 0,
|
||||
"formatter": "{value}",
|
||||
"lengthLimit": 10
|
||||
},
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#CCCCCC",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"axisValue": {
|
||||
"auto": true,
|
||||
"min": 10,
|
||||
"max": 100,
|
||||
"split": 10,
|
||||
"splitCount": 10
|
||||
},
|
||||
"axisLabelFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
}
|
||||
},
|
||||
"yAxis": {
|
||||
"show": true,
|
||||
"position": "left",
|
||||
"nameShow": false,
|
||||
"name": "",
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"rotate": 0,
|
||||
"formatter": "{value}",
|
||||
"lengthLimit": 10
|
||||
},
|
||||
"axisLine": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#CCCCCC",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"axisValue": {
|
||||
"auto": true,
|
||||
"min": 10,
|
||||
"max": 100,
|
||||
"split": 10,
|
||||
"splitCount": 10
|
||||
},
|
||||
"axisLabelFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
}
|
||||
},
|
||||
"yAxisExt": {
|
||||
"show": true,
|
||||
"position": "right",
|
||||
"name": "",
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"rotate": 0,
|
||||
"formatter": "{value}"
|
||||
},
|
||||
"axisLine": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#CCCCCC",
|
||||
"width": 1,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
"axisValue": {
|
||||
"auto": true,
|
||||
"min": 10,
|
||||
"max": 100,
|
||||
"split": 10,
|
||||
"splitCount": 10
|
||||
},
|
||||
"axisLabelFormatter": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"showName": false,
|
||||
"color": "#000000",
|
||||
"fontSize": 12,
|
||||
"axisColor": "#999",
|
||||
"splitNumber": 5,
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#CCCCCC",
|
||||
"width": 1,
|
||||
"type": "solid"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"length": 5,
|
||||
"lineStyle": {
|
||||
"color": "#000000",
|
||||
"width": 1,
|
||||
"type": "solid"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": false,
|
||||
"rotate": 0,
|
||||
"margin": 8,
|
||||
"color": "#000000",
|
||||
"fontSize": "12",
|
||||
"formatter": "{value}"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#CCCCCC",
|
||||
"width": 1,
|
||||
"type": "solid"
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": true
|
||||
},
|
||||
"axisValue": {
|
||||
"auto": true,
|
||||
"min": 10,
|
||||
"max": 100,
|
||||
"split": 10,
|
||||
"splitCount": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"customStyleMobile": null,
|
||||
"customFilter": {
|
||||
"logic": null,
|
||||
"items": null
|
||||
},
|
||||
"drillFields": [],
|
||||
"senior": {
|
||||
"functionCfg": {
|
||||
"sliderShow": false,
|
||||
"sliderRange": [
|
||||
0,
|
||||
10
|
||||
],
|
||||
"sliderBg": "#FFFFFF",
|
||||
"sliderFillBg": "#BCD6F1",
|
||||
"sliderTextColor": "#999999",
|
||||
"emptyDataStrategy": "breakLine",
|
||||
"emptyDataCustomValue": "",
|
||||
"emptyDataFieldCtrl": []
|
||||
},
|
||||
"assistLineCfg": {
|
||||
"enable": false,
|
||||
"assistLine": []
|
||||
},
|
||||
"threshold": {
|
||||
"enable": false,
|
||||
"gaugeThreshold": "",
|
||||
"liquidThreshold": "",
|
||||
"labelThreshold": [],
|
||||
"tableThreshold": [],
|
||||
"textLabelThreshold": [],
|
||||
"lineLabelThreshold": []
|
||||
},
|
||||
"scrollCfg": {
|
||||
"open": false,
|
||||
"row": 1,
|
||||
"interval": 2000,
|
||||
"step": 50
|
||||
},
|
||||
"areaMapping": {},
|
||||
"bubbleCfg": {
|
||||
"enable": false,
|
||||
"speed": 1,
|
||||
"rings": 1,
|
||||
"type": "wave"
|
||||
}
|
||||
},
|
||||
"createBy": null,
|
||||
"createTime": null,
|
||||
"updateTime": null,
|
||||
"snapshot": null,
|
||||
"stylePriority": "panel",
|
||||
"chartType": "private",
|
||||
"isPlugin": false,
|
||||
"dataFrom": "calc",
|
||||
"viewFields": [],
|
||||
"refreshViewEnable": false,
|
||||
"refreshUnit": "minute",
|
||||
"refreshTime": 5,
|
||||
"linkageActive": false,
|
||||
"jumpActive": false,
|
||||
"aggregate": null,
|
||||
"flowMapStartName": [],
|
||||
"flowMapEndName": [],
|
||||
"calParams": [],
|
||||
"extColor": null,
|
||||
"sortPriority": [],
|
||||
"data": null,
|
||||
"privileges": null,
|
||||
"isLeaf": null,
|
||||
"pid": null,
|
||||
"sql": null,
|
||||
"drill": false,
|
||||
"drillFilters": null,
|
||||
"position": null,
|
||||
"totalPage": 0,
|
||||
"totalItems": 0,
|
||||
"datasetMode": 0,
|
||||
"datasourceType": null,
|
||||
"chartExtRequest": null,
|
||||
"isExcelExport": false,
|
||||
"exportDatasetOriginData": false,
|
||||
"cache": false,
|
||||
"sourceTableId": null,
|
||||
"downloadType": null,
|
||||
"xAxis": [
|
||||
{
|
||||
"id": "{{ XAXIS_FIELD_ID }}",
|
||||
"datasourceId": "{{ DATASOURCE_ID }}",
|
||||
"datasetTableId": "{{ DATASET_TABLE_ID }}",
|
||||
"datasetGroupId": "{{ DATASET_GROUP_ID }}",
|
||||
"chartId": null,
|
||||
"originName": "access_platform",
|
||||
"name": "访问平台",
|
||||
"dbFieldName": null,
|
||||
"description": "访问平台",
|
||||
"dataeaseName": "{{ XAXIS_DE_NAME }}",
|
||||
"groupType": "d",
|
||||
"type": "VARCHAR",
|
||||
"precision": null,
|
||||
"scale": null,
|
||||
"deType": 0,
|
||||
"deExtractType": 0,
|
||||
"extField": 0,
|
||||
"checked": true,
|
||||
"columnIndex": null,
|
||||
"lastSyncTime": null,
|
||||
"dateFormat": null,
|
||||
"dateFormatType": null,
|
||||
"fieldShortName": "{{ XAXIS_DE_NAME }}",
|
||||
"groupList": [],
|
||||
"otherGroup": null,
|
||||
"desensitized": false,
|
||||
"orderChecked": false,
|
||||
"params": [],
|
||||
"summary": "count",
|
||||
"sort": "none",
|
||||
"dateStyle": "y_M_d",
|
||||
"datePattern": "date_sub",
|
||||
"dateShowFormat": "y_M_d",
|
||||
"chartType": "bar",
|
||||
"compareCalc": {
|
||||
"type": "none",
|
||||
"resultData": "percent",
|
||||
"field": null,
|
||||
"custom": null
|
||||
},
|
||||
"logic": null,
|
||||
"filterType": null,
|
||||
"index": null,
|
||||
"formatterCfg": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"chartShowName": null,
|
||||
"filter": [],
|
||||
"customSort": null,
|
||||
"busiType": null,
|
||||
"hide": false,
|
||||
"field": null,
|
||||
"agg": false
|
||||
},
|
||||
{
|
||||
"id": "{{ XAXIS2_FIELD_ID }}",
|
||||
"datasourceId": "{{ DATASOURCE_ID }}",
|
||||
"datasetTableId": "{{ DATASET_TABLE_ID }}",
|
||||
"datasetGroupId": "{{ DATASET_GROUP_ID }}",
|
||||
"chartId": null,
|
||||
"originName": "access_count",
|
||||
"name": "访问次数",
|
||||
"dbFieldName": null,
|
||||
"description": "访问次数",
|
||||
"dataeaseName": "{{ XAXIS2_DE_NAME }}",
|
||||
"groupType": "q",
|
||||
"type": "INT",
|
||||
"precision": null,
|
||||
"scale": null,
|
||||
"deType": 2,
|
||||
"deExtractType": 2,
|
||||
"extField": 0,
|
||||
"checked": true,
|
||||
"columnIndex": null,
|
||||
"lastSyncTime": null,
|
||||
"dateFormat": null,
|
||||
"dateFormatType": null,
|
||||
"fieldShortName": "{{ XAXIS2_DE_NAME }}",
|
||||
"groupList": [],
|
||||
"otherGroup": null,
|
||||
"desensitized": false,
|
||||
"orderChecked": false,
|
||||
"params": [],
|
||||
"summary": "sum",
|
||||
"sort": "none",
|
||||
"dateStyle": "y_M_d",
|
||||
"datePattern": "date_sub",
|
||||
"dateShowFormat": "y_M_d",
|
||||
"chartType": "bar",
|
||||
"compareCalc": {
|
||||
"type": "none",
|
||||
"resultData": "percent",
|
||||
"field": null,
|
||||
"custom": null
|
||||
},
|
||||
"logic": null,
|
||||
"filterType": null,
|
||||
"index": null,
|
||||
"formatterCfg": {
|
||||
"type": "auto",
|
||||
"unitLanguage": "ch",
|
||||
"unit": 1,
|
||||
"suffix": "",
|
||||
"decimalCount": 2,
|
||||
"thousandSeparator": true
|
||||
},
|
||||
"chartShowName": null,
|
||||
"filter": [],
|
||||
"customSort": null,
|
||||
"busiType": null,
|
||||
"hide": false,
|
||||
"field": null,
|
||||
"agg": false
|
||||
}
|
||||
],
|
||||
"xAxisExt": [],
|
||||
"yAxis": [],
|
||||
"yAxisExt": [],
|
||||
"pageInfo": {
|
||||
"total": 1000,
|
||||
"pageSize": 20,
|
||||
"currentPage": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"appData": null,
|
||||
"id": "{{ SCENE_ID }}",
|
||||
"name": "单明细表",
|
||||
"pid": "0",
|
||||
"status": 1,
|
||||
"selfWatermarkStatus": true,
|
||||
"type": "dashboard",
|
||||
"creatorName": "系统管理员",
|
||||
"updateName": "系统管理员",
|
||||
"createTime": 1775811560811,
|
||||
"updateTime": 1775812302437,
|
||||
"watermarkInfo": null,
|
||||
"weight": 9,
|
||||
"ext": 0,
|
||||
"contentId": "{{ CONTENT_ID }}",
|
||||
"mobileLayout": false,
|
||||
"checkVersion": "2.10.20"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"canvasStyleData": "{\"width\":1920,\"height\":1080,\"refreshBrowserEnable\":false,\"refreshBrowserUnit\":\"minute\",\"refreshBrowserTime\":5,\"refreshViewEnable\":false,\"refreshViewLoading\":true,\"refreshUnit\":\"minute\",\"refreshTime\":5,\"popupAvailable\":true,\"popupButtonAvailable\":true,\"suspensionButtonAvailable\":false,\"screenAdaptor\":\"widthFirst\",\"dashboardAdaptor\":\"keepHeightAndWidth\",\"scale\":17,\"scaleWidth\":60,\"scaleHeight\":60,\"backgroundColorSelect\":true,\"backgroundImageEnable\":false,\"backgroundType\":\"backgroundColor\",\"background\":\"\",\"openCommonStyle\":true,\"opacity\":1,\"fontSize\":14,\"fontFamily\":\"PingFang\",\"themeId\":\"10001\",\"color\":\"#000000\",\"backgroundColor\":\"rgba(245, 246, 247, 1)\",\"dashboard\":{\"gap\":\"yes\",\"gapSize\":5,\"gapMode\":\"middle\",\"showGrid\":false,\"matrixBase\":4,\"resultMode\":\"all\",\"resultCount\":1000,\"themeColor\":\"light\",\"mobileSetting\":{\"customSetting\":false,\"imageUrl\":null,\"backgroundType\":\"image\",\"color\":\"#000\"}},\"component\":{\"chartTitle\":{\"show\":true,\"fontSize\":16,\"hPosition\":\"left\",\"vPosition\":\"top\",\"isItalic\":false,\"isBolder\":true,\"remarkShow\":false,\"remark\":\"\",\"fontFamily\":\"\",\"letterSpace\":\"0\",\"fontShadow\":false,\"color\":\"#000000\",\"remarkBackgroundColor\":\"#ffffff\"},\"chartColor\":{\"basicStyle\":{\"colorScheme\":\"default\",\"colors\":[\"#1E90FF\",\"#90EE90\",\"#00CED1\",\"#E2BD84\",\"#7A90E0\",\"#3BA272\",\"#2BE7FF\",\"#0A8ADA\",\"#FFD700\"],\"alpha\":100,\"gradient\":false,\"mapStyle\":\"normal\",\"areaBaseColor\":\"#FFFFFF\",\"areaBorderColor\":\"#303133\",\"gaugeStyle\":\"default\",\"tableBorderColor\":\"rgba(230, 231, 228, 1)\",\"tableScrollBarColor\":\"rgba(0, 0, 0, 0.15)\",\"zoomButtonColor\":\"#aaa\",\"zoomBackground\":\"#fff\"},\"misc\":{\"flowMapConfig\":{\"lineConfig\":{\"mapLineAnimate\":true,\"mapLineGradient\":false,\"mapLineSourceColor\":\"#146C94\",\"mapLineTargetColor\":\"#576CBC\"}},\"nameFontColor\":\"#000000\",\"valueFontColor\":\"#5470c6\"},\"tableHeader\":{\"tableHeaderBgColor\":\"#1E90FF\",\"tableHeaderCornerBgColor\":\"#1E90FF\",\"tableHeaderColBgColor\":\"#1E90FF\",\"tableHeaderFontColor\":\"#000000\",\"tableHeaderCornerFontColor\":\"#000000\",\"tableHeaderColFontColor\":\"#000000\"},\"tableCell\":{\"tableItemBgColor\":\"rgba(255, 255, 255, 1)\",\"tableFontColor\":\"#000000\",\"tableItemSubBgColor\":\"#1E90FF\"}},\"chartCommonStyle\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"filterStyle\":{\"layout\":\"horizontal\",\"titleLayout\":\"left\",\"labelColor\":\"#1F2329\",\"titleColor\":\"#1F2329\",\"color\":\"#1f2329\",\"borderColor\":\"#BBBFC4\",\"text\":\"#1F2329\",\"bgColor\":\"#FFFFFF\"},\"tabStyle\":{\"headPosition\":\"left\",\"headFontColor\":\"#000000\",\"headFontActiveColor\":\"#000000\",\"headBorderColor\":\"#ffffff\",\"headBorderActiveColor\":\"#ffffff\"},\"seniorStyleSetting\":{\"linkageIconColor\":\"#A6A6A6\",\"drillLayerColor\":\"#A6A6A6\",\"pagerColor\":\"rgba(166, 166, 166, 1)\"},\"formatterItem\":{\"type\":\"auto\",\"unitLanguage\":\"ch\",\"unit\":1,\"suffix\":\"\",\"decimalCount\":2,\"thousandSeparator\":true}},\"dialogBackgroundColor\":\"rgba(255, 255, 255, 1)\",\"dialogButton\":\"#020408\"}"
|
||||
}
|
||||
Reference in New Issue
Block a user