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