update at 2026-02-13 22:26:53

This commit is contained in:
douboer@gmail.com
2026-02-13 22:26:53 +08:00
parent 2fe45888ba
commit 43107afff1
54 changed files with 2183 additions and 311 deletions

View File

@@ -1,141 +1,124 @@
# APP_FLOW.md
## 1. Scope
This file defines every user-visible flow for the current product scope.
Primary web route is `/` (single-page app). Miniapp flow is skeleton-only.
本文件定义当前版本所有用户可见流程,覆盖 Web 与小程序两端。
Related docs:
关联文档:
- PRD.md
- FRONTEND_GUIDELINES.md
- BACKEND_STRUCTURE.md
## 2. Screen List and Route Map
- `Web /` : Main Sankey workspace (header + mapping panels + chart preview).
- `Miniapp /pages/index/index` : Layout skeleton only (not full feature parity).
## 2. 页面与路由清单
- `Web /`:主工作区(顶部工具栏 + 数据选择器 + 信息日志 + 桑基图预览)
- `Miniapp /pages/index/index`:小程序可用页(上传、映射、预览、导出)
## 3. Web Flow: Initial Page Load
Trigger:
- User opens `/`.
## 3. Web 流程:首次加载
触发条件:
- 用户打开 `/`
Steps:
1. Render header, mapping panels, and chart container.
2. Initialize ECharts instance.
3. Try loading `/data/example0.xlsx`.
4. Parse file and set default column mapping.
5. Build Sankey data and render preview.
步骤:
1. 渲染页面结构并初始化 ECharts
2. 尝试读取 `/data/example0.xlsx`
3. 解析文件并应用默认列映射
4. 构建 Sankey 数据并渲染预览
Decision points:
- If sample file request fails, show parse error text and keep page interactive.
决策点:
- 若样例文件加载失败,显示错误信息,但页面仍可继续上传文件
Success result:
- User sees a chart without manual upload.
成功结果:
- 用户无需上传即可看到可操作示例图
Error result:
- User sees a clear message and can still upload a file manually.
失败结果:
- 页面给出明确错误提示,保持可恢复
## 4. Web Flow: File Upload (Click)
Trigger:
- User clicks upload icon and selects a local file.
## 4. Web 流程:文件上传(点击/拖拽)
触发条件:
- 点击上传并选择文件,或将文件拖入上传区
Steps:
1. Validate extension (`csv/xls/xlsx`).
2. Parse file to `RawTable`.
3. Reset default mapping by column count.
4. Rebuild Sankey data via reactive effect.
5. Update upload message with filename and row count.
步骤:
1. 校验后缀:`csv/xls/xlsx`
2. 解析为 `RawTable`
3. 应用默认映射或恢复持久化映射
4. 重建聚合并刷新图表
5. 更新上传提示文案(文件名 + 行数)
Decision points:
- Unsupported file type.
- Parse failure (CSV parser/XLSX parser errors).
决策点:
- 不支持的后缀直接报错
- 解析异常显示错误并保留上一次有效状态
Success result:
- Mapping list updates to current headers and preview updates.
## 5. Web 流程:列映射与预览配置
触发条件:
- 用户修改数据列、描述列、方向、gap、padding、主题、汇聚对齐等配置
Error result:
- Error message appears; previous valid state remains usable.
步骤:
1. 更新映射或显示配置
2. 触发重建逻辑
3. 刷新图表与信息日志
4. 持久化配置到 localStorage
## 5. Web Flow: File Upload (Drag and Drop)
Trigger:
- User drops one file into upload area.
决策点:
- `source data` 未选:阻断构建并提示
- `target description` 为空:阻断构建并提示
- 非法数值/空描述:跳过行并写入告警
Steps:
1. Read first file from drop payload.
2. Reuse the same pipeline as click upload.
## 6. Web 流程:导出
触发条件:
- 点击导出 `SVG``PNG`
Decision points:
- No file in payload.
步骤:
1. 检查图表实例是否可用
2. 组装时间戳文件名
3. SVG临时创建 SVG 渲染器导出
4. PNG`pixelRatio=2` 导出
5. 触发浏览器下载
Success and error behaviors:
- Same as click upload.
## 7. 小程序流程:文件上传与默认映射
触发条件:
- 用户点击“文件上传”
## 6. Web Flow: Mapping Configuration
Trigger:
- User changes any mapping item in the left panels.
步骤:
1. 选择 `csv/xls/xlsx` 文件
2. 按后缀分流解析:
- `csv`:文本解析
- `xls/xlsx`SheetJS 解析首个工作表
3. 应用默认映射规则
4. 重建 Sankey 聚合并更新日志
5. 触发 canvas 重绘
Steps:
1. Select one source data column (radio).
2. Toggle source description columns (checkbox list).
3. Toggle target description columns (checkbox list).
4. Optional: toggle target total label display.
5. Reactive builder recomputes links and warnings.
决策点:
- 若 xlsx 模块不可用,提示先在开发者工具执行“构建 npm”
- 解析失败显示错误,不会导致页面失效
Decision points:
- Missing source data column -> block build with message.
- Empty target description selection -> block build with message.
- Invalid numeric source value -> row dropped with warning.
- Empty source/target name after rules -> row dropped with warning.
## 8. 小程序流程:列映射与预览
触发条件:
- 用户点击任意列项调整 source/target 映射
Success result:
- Chart updates immediately.
步骤:
1. 更新映射状态
2. 调用聚合构建
3. 更新统计(节点数、连线数、跳过行)
4. 更新“信息日志”
5. 重绘 canvas 预览
Error result:
- Build error or warning list shown in preview panel.
## 9. 小程序流程:导出
触发条件:
- 点击导出 PNG 或 SVG
## 7. Web Flow: Preview Controls
Trigger:
- User changes direction, gap, padding, or theme.
步骤:
1. PNG
- canvas 导出临时图片
- 保存到系统相册
2. SVG
- 复用布局算法生成 SVG 字符串
- 写入用户目录 `.svg` 文件
- 优先尝试打开文档;若失败则复制文件路径
Steps:
1. Direction toggle swaps link direction (`source<->target`).
2. Gap/padding sliders update chart series layout.
3. Theme picker updates node palette.
Decision points:
- Theme popover closes on outside pointer down.
Success result:
- Chart visual changes apply in-place.
## 8. Web Flow: Export
Trigger:
- User clicks `Export SVG` or `Export PNG`.
Steps:
1. Ensure chart instance exists.
2. Build filename with timestamp.
3. For SVG: prefer serializing DOM `<svg>`; fallback to `getDataURL`.
4. For PNG: use `getDataURL(type='png', pixelRatio=2)`.
5. Trigger browser download.
Decision points:
- No chart instance -> no action.
Success result:
- Downloaded file appears with expected naming.
## 9. Miniapp Skeleton Flow (Current State)
Trigger:
- Open miniapp index page.
Steps:
1. Render static layout blocks.
2. Allow opening/closing theme bottom sheet.
Current limitations:
- No real file parsing, no Sankey rendering, no export pipeline.
决策点:
- 无可用数据时阻断导出并提示
- 相册权限不足时提示用户授权
## 10. Flow Completion Checklist
A flow is considered complete only when:
1. Success path works end-to-end.
2. Error path shows deterministic message.
3. UI state remains recoverable after errors.
流程视为完成需满足:
1. 成功路径可端到端执行
2. 失败路径有明确且可定位的错误信息
3. 错误后页面仍可恢复继续操作

View File

@@ -13,7 +13,10 @@
所有数据处理均在客户端完成:
- 文件解析:`src/core/parser.ts`
- 聚合构建:`src/core/sankey.ts`
- 状态承载:`src/App.vue`(内存态)
- 状态承载:
- Web`src/App.vue`(响应式状态 + localStorage 持久化)
- 小程序:`miniapp/pages/index/index.js`(页面状态)
- 小程序端聚合与解析:`miniapp/utils/sankey.js`
## 3. 当前“数据结构合同”
虽然没有数据库,但有稳定的数据结构合同。
@@ -49,9 +52,12 @@ interface SankeyBuildResult {
```
## 4. 存储规则
- 运行时数据仅驻留内存(浏览器刷新后丢失)。
- 不写入 localStorage / IndexedDB / 远程存储
- 导出结果通过浏览器下载能力交付给用户。
- Web
- 映射配置与上传文件快照持久化到 localStorage刷新可恢复
- 导出结果通过浏览器下载能力交付给用户。
- 小程序:
- 页面状态在当前会话内存中维护(当前未做本地持久化恢复)。
- PNG 导出保存到系统相册SVG 导出写入用户数据目录文件。
## 5. 认证与权限
- 当前不存在用户登录、权限校验、租户隔离。

View File

@@ -103,8 +103,17 @@
- 文本截断区域需保留可读主体(列名优先显示前缀)
## 10. 小程序样式约束(当前)
- 仅用于骨架视觉对齐,不作为完整功能规范
- 维持与 Web 一致的色板、图标命名和信息分区
- 页面结构固定为:
- 顶部工具栏(主题、上传、导出)
- 预览区统计、错误、canvas
- 数据选择区(源/目标列映射)
- 信息日志区
- 小程序使用紧凑密度:
- 工具条图标优先,文本次要
- 上传框允许单行省略,避免整体溢出
- 导出图标必须同时展示 `SVG``PNG` 两个入口
- 日志区与预览区视觉边框与 Web 一致,统一使用 `#fbaca3` 描边
## 11. 变更规则
任何新增组件或样式改动,必须同步更新本文件对应条目。

View File

@@ -15,7 +15,7 @@
- M2Web 交互与图表预览
- M3导出与体验增强
- M4质量门禁与文档闭环
- M5小程序从骨架到可用(未来)
- M5小程序从骨架到可用
## 3. 详细步骤
@@ -72,12 +72,12 @@
- `npm run lint`
- `npm run format`
### M5 小程序从骨架到可用(未来)
- [ ] 5.1 接入文件上传能力
- [ ] 5.2 复用 `src/core` 解析与聚合逻辑
- [ ] 5.3 接入小程序图表渲染容器
- [ ] 5.4 接入导出或保存图片能力
- [ ] 5.5 建立小程序端测试与验收用例
### M5 小程序从骨架到可用
- [x] 5.1 接入文件上传能力csv/xls/xlsx
- [x] 5.2 对齐 Web 聚合规则(默认映射、向下补全、告警)
- [x] 5.3 接入小程序原生 canvas 预览容器
- [x] 5.4 接入导出能力PNG 到相册、SVG 文件导出)
- [ ] 5.5 建立小程序端测试与验收用例(待补)
## 4. 每次迭代执行模板
1.`progress.txt` 读取上下文

20
PRD.md
View File

@@ -1,7 +1,7 @@
# PRD.md
## 1. 文档目的
本文件定义「星程桑基图」的产品需求合同v0.1),用于约束范围、功能验收标准与成功标准。
本文件定义「星程桑基图」的产品需求合同v0.2),用于约束范围、功能验收标准与成功标准。
关联文档:
- APP_FLOW.md
@@ -34,7 +34,8 @@
- 不做云端存储、多人协作、历史版本。
- 不做后端 API 与数据库(见 BACKEND_STRUCTURE.md
- 不做三级/多级桑基图编辑器(当前仅由列拼接形成单级 source->target 关系)。
- 不做 APP 端正式实现(仅保留小程序骨架)
- 不做账号体系下的小程序云同步与跨端协作
- 不追求 Web 与小程序像素级一致,只要求关键流程一致可用。
## 7. 功能范围与验收标准
@@ -73,7 +74,7 @@
- 支持方向切换:`source->target` / `target->source`(仅交换链接方向)。
- 支持节点间距gap与图内边距padding调整。
- 支持主题选择并应用到节点颜色。
- 支持“目标总和”显示开关(只影响 target 标签展示,不影响聚合值)
- 支持标签位置、汇聚对齐等关键可视化配置
### F5 导出
需求:支持导出 `SVG``PNG`
@@ -83,18 +84,21 @@
- PNG 使用 `pixelRatio=2` 导出。
- 若 DOM 可获取 `<svg>`,优先序列化导出 SVG否则使用图表实例导出。
### F6 小程序骨架
需求:提供页面结构与视觉骨架,不承诺完整业务逻辑
### F6 小程序可用版
需求:提供轻量可用的小程序版本,覆盖上传、映射、预览、导出主流程
验收标准:
- 存在页面结构、列选择区、主题底部弹层
- 明确标注为“骨架”,不作为完整功能验收对象
- 支持上传 `csv/xls/xlsx` 并完成解析
- 支持默认映射、列选择、聚合构建与日志展示
- 支持原生 canvas 预览。
- 支持导出 PNG保存相册与 SVG生成文件并可访问路径
## 8. 成功标准Success Metrics
- S1核心单元测试稳定通过`npm run test`)。
- S2类型检查与 lint 稳定通过(`npm run type-check && npm run lint`)。
- S3核心样例 `data/example0.xlsx` 可在默认加载后正常出图。
- S4导出文件在本地可打开且内容与当前视图一致。
- S5小程序端可独立完成“上传 -> 映射 -> 预览 -> 导出”闭环。
## 9. 约束与依赖
- 技术实现必须遵循 TECH_STACK.md 固定版本。
@@ -102,5 +106,5 @@
- 不得在未确认情况下引入新生产依赖。
## 10. 版本状态
- 当前合同版本:`v0.1`
- 当前合同版本:`v0.2`
- 最后更新:`2026-02-13`

View File

@@ -52,19 +52,30 @@
- `src/App.vue`: Web 页面与交互
- `src/styles.css`: 全局样式
- `src/theme-presets.ts`: 主题色预设
- `miniapp/*`: 小程序骨架
- `miniapp/pages/index/*`: 小程序页面与交互
- `miniapp/utils/sankey.js`: 小程序解析与聚合逻辑(含 xlsx 分流)
- `miniapp/README.md`: 小程序端能力与限制说明
## 9. 开发服务器约束
## 9. 小程序运行时能力
- 运行平台:微信小程序原生运行时(非浏览器 DOM 环境)
- 文件读写:`wx.getFileSystemManager()`
- 图表绘制:`canvas` + `wx.createCanvasContext`
- 导出:
- PNG`wx.canvasToTempFilePath` + `wx.saveImageToPhotosAlbum`
- SVG生成 XML 字符串后写本地文件(`writeFile`
- npm 依赖:小程序端 xlsx 解析依赖 `xlsx@0.18.5`,需在开发者工具执行“构建 npm”
## 10. 开发服务器约束
`vite.config.ts` 当前使用本地 HTTPS 证书路径:
- `~/mac.biboer.cn_ecc/fullchain.cer`
- `~/mac.biboer.cn_ecc/mac.biboer.cn.key`
若本机无该证书,`npm run dev` 需要先调整 Vite 配置。
## 10. 依赖治理规则
## 11. 依赖治理规则
- 生产依赖新增/升级必须先确认。
- 版本锁定以 `package-lock.json` + 本文档为准。
- 未在本文档出现的库,默认不可在实现中使用。
## 11. 后端说明
## 12. 后端说明
当前无后端服务、无数据库、无认证(详见 BACKEND_STRUCTURE.md

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.833 0H4.167A4.172 4.172 0 0 0 0 4.166v11.667A4.172 4.172 0 0 0 4.166 20h11.667A4.172 4.172 0 0 0 20 15.834V4.166A4.172 4.172 0 0 0 15.833 0Zm2.5 15.833a2.5 2.5 0 0 1-2.5 2.5H4.167a2.5 2.5 0 0 1-2.5-2.5V4.167a2.5 2.5 0 0 1 2.5-2.5h11.666a2.5 2.5 0 0 1 2.5 2.5v11.666Z"/>
<rect x="1" y="1" width="18" height="18" rx="4" stroke="#8552A1" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 189 B

View File

@@ -1,10 +1,21 @@
# 小程序端骨架
# 小程序端(当前可用范围)
当前目录提供了与 Figma `node-id=584:64` 对齐的页面骨架
当前目录已从骨架升级为可用版,支持以下能力
- 顶部 Logo/主题/上传/导出区域
- 效果预览区域
- 源数据与目标数据列选择区域
- 主题底部选择器(底部弹层)
- 顶部 Logo / 主题 / 上传 / 导出区域
- 文件上传与解析:`csv / xls / xlsx`
- 默认列映射:
- 源数据列优先匹配 `data/value/数据/值`
- 源描述列优先匹配 `source/源`
- 目标描述列优先匹配 `target/目标`
- 若无显式数值表头,则回退到第二行首个数字列
- 源/目标列选择交互与聚合统计
- 信息日志(解析信息 / 告警 / 错误)
- 原生 canvas 桑基图预览
- 导出能力:
- `PNG`:保存到系统相册
- `SVG`:生成 `.svg` 文件写入用户目录,优先尝试打开,失败时复制路径
后续接入时建议直接复用 `src/core` 的解析与聚合逻辑,保持 Web 与小程序一致的数据行为。
注意事项:
- `xlsx` 解析依赖 npm 包,若在微信开发者工具中提示模块缺失,请先执行“工具 -> 构建 npm”。
- SVG 文件导出后是否可直接预览,取决于当前系统与微信版本对 SVG 文档的支持。

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="88" fill="none" viewBox="0 0 34 88">
<rect width="33" height="87" x=".5" y=".5" fill="#2420A8" rx="7.5"/>
<rect width="33" height="87" x=".5" y=".5" stroke="#2420A8" rx="7.5"/>
<path fill="#fff" d="M8.72 35.978h10.8v-.81h1.89v.81h3.852v1.836H21.41v3.33c0 1.188-.648 1.782-1.944 1.782h-2.25l-.432-1.836c.72.072 1.386.126 2.034.126.468 0 .702-.18.702-.54v-2.862H8.72v-1.836Zm13.95-9.198v4.662H12.392v.774c0 .486.252.738.792.738h9.306c.27-.018.45-.108.558-.288.072-.126.144-.54.234-1.242l1.782.594c-.126.828-.252 1.404-.378 1.764-.216.576-.756.864-1.584.9H12.428c-1.296 0-1.926-.576-1.926-1.71V26.78H22.67Zm-10.278 2.97h8.388v-1.278h-8.388v1.278Zm.954 8.154c1.152.9 2.142 1.8 2.934 2.7l-1.404 1.404c-.684-.864-1.638-1.8-2.88-2.808l1.35-1.296Zm8.208 12.384v-4.734h1.89v6.534h-5.472v6.048h4.482v-4.698h1.89v7.308h-1.89v-.792H9.656v-6.516h1.89v4.698h4.482v-6.048h-5.472v-6.534h1.89v4.734h3.582v-6.084h1.944v6.084h3.582Z"/>
</svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<rect x="1" y="1" width="18" height="18" rx="4" stroke="#8552A1" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 189 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.5 0h-11A4.5 4.5 0 0 0 0 4.5v11A4.5 4.5 0 0 0 4.5 20h11a4.5 4.5 0 0 0 4.5-4.5v-11A4.5 4.5 0 0 0 15.5 0Zm1.394 6.902-7.445 7.444-.03.028-.033.028-.037.028-.035.025-.039.024-.036.021-.042.02-.037.018-.044.016-.037.014-.045.01-.038.01-.047.007H8.74l-.048-.006h-.035l-.05-.011-.033-.01-.049-.015-.032-.012-.048-.022-.031-.015-.047-.028-.03-.016c-.017-.011-.033-.024-.05-.036l-.022-.016a.887.887 0 0 1-.067-.06l-3.949-3.95a.883.883 0 0 1 .01-1.26.904.904 0 0 1 1.261.033l3.304 3.303 6.82-6.82a.884.884 0 1 1 1.25 1.25l-.001-.002Z"/>
<path fill="#fff" d="m15.645 5.654-6.82 6.82L5.523 9.17a.904.904 0 0 0-1.263-.03.883.883 0 0 0-.011 1.259l3.95 3.948a.866.866 0 0 0 .068.06l.022.016a.711.711 0 0 0 .05.037l.029.016.047.027.031.015.048.023.033.011.048.017.034.008.049.011h.035l.048.007h.208l.048-.008.037-.008.045-.012.037-.014.044-.016.038-.017.041-.02.036-.022.039-.023.035-.025.037-.028.033-.028.03-.028 7.444-7.444a.886.886 0 0 0 0-1.25.885.885 0 0 0-1.25 0l.001.003Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none" viewBox="0 0 36 36">
<path fill="#FCFCFC" d="M22.774 13.238a6.734 6.734 0 1 1-4.77-1.98 6.702 6.702 0 0 1 4.77 1.98Z"/>
<path fill="#00E8CF" d="M18.005 6.76v4.498a6.703 6.703 0 0 0-4.769 1.98L10.05 10.05a11.238 11.238 0 0 1 7.955-3.292Z"/>
<path fill="#70FFEF" d="M18.005.761V6.76a11.239 11.239 0 0 0-7.955 3.292L5.813 5.815A17.162 17.162 0 0 1 18.005.76Z"/>
<path fill="#0064B5" d="m10.05 10.051 3.186 3.187a6.702 6.702 0 0 0-1.98 4.768H6.759a11.239 11.239 0 0 1 3.292-7.955Z"/>
<path fill="#0091FF" d="M10.05 10.051a11.238 11.238 0 0 0-3.292 7.955H.76A17.162 17.162 0 0 1 5.813 5.815l4.237 4.236Z"/>
<path fill="#31C4FF" d="M11.257 18.006a6.7 6.7 0 0 0 1.98 4.769l-3.187 3.187a11.24 11.24 0 0 1-3.292-7.956h4.499Z"/>
<path fill="#9EEBFF" d="M6.758 18.006a11.24 11.24 0 0 0 3.292 7.956l-4.237 4.236A17.162 17.162 0 0 1 .76 18.006h5.998Z"/>
<path fill="#5F4A9E" d="M18.005 24.754v4.5a11.239 11.239 0 0 1-7.955-3.292l3.186-3.187a6.702 6.702 0 0 0 4.769 1.98Z"/>
<path fill="#9D87E0" d="M10.05 25.962a11.24 11.24 0 0 0 7.955 3.291v5.998a17.16 17.16 0 0 1-12.192-5.053l4.237-4.236Z"/>
<path fill="#FF468C" d="M25.96 25.962a11.241 11.241 0 0 1-7.955 3.291v-4.499a6.7 6.7 0 0 0 4.769-1.98l3.186 3.188Z"/>
<path fill="#FFA1C8" d="m25.96 25.962 4.236 4.236a17.162 17.162 0 0 1-12.191 5.053v-5.998a11.239 11.239 0 0 0 7.955-3.291Z"/>
<path fill="#F03049" d="M24.753 18.006h4.499a11.241 11.241 0 0 1-3.292 7.956l-3.186-3.187a6.7 6.7 0 0 0 1.979-4.769Z"/>
<path fill="#FF636E" d="M29.252 18.006h5.998a17.163 17.163 0 0 1-5.053 12.192l-4.237-4.236a11.241 11.241 0 0 0 3.292-7.956Z"/>
<path fill="#FE8205" d="M25.96 10.051a11.24 11.24 0 0 1 3.292 7.955h-4.499a6.701 6.701 0 0 0-1.98-4.768l3.187-3.187Z"/>
<path fill="#FFA426" d="M35.25 18.006h-5.998a11.24 11.24 0 0 0-3.292-7.955l4.236-4.236a17.163 17.163 0 0 1 5.054 12.191Z"/>
<path fill="#FFC247" d="m25.96 10.051-3.186 3.187a6.702 6.702 0 0 0-4.77-1.98V6.76a11.24 11.24 0 0 1 7.956 3.292Z"/>
<path fill="#FFFD78" d="M30.197 5.815 25.96 10.05a11.24 11.24 0 0 0-7.955-3.292V.761a17.162 17.162 0 0 1 12.192 5.054Z"/>
<path fill="#000" d="m32.863 10.832 1.35-.653a19.376 19.376 0 0 0-.854-1.56L32.08 9.4c.286.467.546.948.783 1.432Z"/>
<path fill="#000" d="m34.88 11.743-1.406.525c.589 1.601.924 3.284.993 4.988H29.97a11.883 11.883 0 0 0-2.967-7.186l3.186-3.185c.317.347.62.707.905 1.08l1.19-.914a17.995 17.995 0 0 0-29.629 20.35l1.277-.786a16.459 16.459 0 0 1-2.405-7.859H6.03a11.882 11.882 0 0 0 2.985 7.18l-3.188 3.187a16.61 16.61 0 0 1-.91-1.075l-1.19.914A18 18 0 0 0 34.88 11.744ZM5.822 6.883l3.185 3.187a11.883 11.883 0 0 0-2.965 7.186H1.543a16.345 16.345 0 0 1 4.28-10.372Zm16.424 15.365a5.998 5.998 0 1 1 1.757-4.242 5.96 5.96 0 0 1-1.757 4.242ZM12.2 13.26a7.424 7.424 0 0 0-1.652 3.995H7.545a10.39 10.39 0 0 1 2.533-6.116l2.121 2.12Zm10.55-1.06a7.423 7.423 0 0 0-3.995-1.651V7.546a10.39 10.39 0 0 1 6.117 2.533L22.75 12.2Zm-5.495-1.655a7.422 7.422 0 0 0-3.993 1.658l-2.124-2.123a10.39 10.39 0 0 1 6.117-2.534v3Zm-6.711 8.211A7.423 7.423 0 0 0 12.2 22.75l-2.123 2.124a10.39 10.39 0 0 1-2.533-6.117h2.999Zm2.715 5.057a7.423 7.423 0 0 0 3.996 1.65v3.004a10.39 10.39 0 0 1-6.117-2.534l2.121-2.12Zm5.496 1.654a7.423 7.423 0 0 0 3.993-1.657l2.123 2.123a10.39 10.39 0 0 1-6.116 2.534v-3Zm7.177-.594-2.121-2.121a7.423 7.423 0 0 0 1.651-3.996h3.003a10.39 10.39 0 0 1-2.533 6.117Zm2.533-7.617h-2.999a7.423 7.423 0 0 0-1.658-3.993l2.124-2.123a10.39 10.39 0 0 1 2.533 6.116Zm-9.71-15.729a16.358 16.358 0 0 1 10.36 4.31l-3.181 3.18a11.89 11.89 0 0 0-7.18-2.985V1.527Zm-1.5.018v4.5a11.89 11.89 0 0 0-7.187 2.964L6.882 5.824a16.345 16.345 0 0 1 10.373-4.279Zm-7.187 25.459a11.89 11.89 0 0 0 7.187 2.965v4.499a16.354 16.354 0 0 1-10.373-4.28l3.186-3.184Zm8.687 7.481V29.98a11.898 11.898 0 0 0 7.186-2.976l3.186 3.186a16.37 16.37 0 0 1-10.372 4.295Zm11.42-5.37-3.18-3.18a11.882 11.882 0 0 0 2.983-7.179h4.506a16.352 16.352 0 0 1-4.31 10.36Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="36" fill="none" viewBox="0 0 23 36">
<path fill="#3EE4C3" d="M3.42 4.593v13.353H0V2.27h9.135V0h3.52v2.271h9.26v13.354a2.15 2.15 0 0 1-.662 1.597c-.44.433-.977.649-1.61.649h-4.168l.749-2.047h1.747a.57.57 0 0 0 .4-.162.537.537 0 0 0 .174-.412l.025-10.657h-5.915v.524l-.624 1.248h2.72l2.87 8.111h-3.519l-2.52-7.188-3.57 7.188H4.493l4.642-9.36v-.523H3.42Z"/>
<path fill="#3EE4C3" d="M3.47 21.224v1.348H0v-3.82h9.01l-.374-.873h3.769l.4.874h6.564a2.716 2.716 0 0 1 1.947.799 2.735 2.735 0 0 1 .798 1.947v1.073h-3.47v-.574c0-.217-.074-.4-.224-.55a.747.747 0 0 0-.549-.224H3.47ZM1.272 29.46h19.494v3.793a2.716 2.716 0 0 1-.798 1.947 2.735 2.735 0 0 1-1.948.8H1.273v-6.539Zm15.05 2.146H5.717v2.221h9.434c.333 0 .612-.112.837-.336a1.14 1.14 0 0 0 .337-.837v-1.048Zm-5.191-5.965-5.167 3.195H.05l8.286-4.967h5.666l-2.645 1.647h5.116l5.491 3.32H16.15l-5.017-3.195Zm1.473-3.47h4.917l4.268 2.621h-4.892l-4.293-2.62ZM5.49 24.793H.6l4.293-2.62h4.892l-4.293 2.62Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" fill="none" viewBox="0 0 19 19">
<path fill="#FF7B15" d="M2.499 15.435a2.105 2.105 0 0 1-2.105-2.104V6.314a2.105 2.105 0 0 1 4.21 0v7.017a2.105 2.105 0 0 1-2.105 2.104Zm6.548 0a2.105 2.105 0 0 1-2.105-2.104V2.104a2.105 2.105 0 0 1 4.21 0V13.33a2.105 2.105 0 0 1-2.105 2.104Zm6.548 0a2.105 2.105 0 0 1-2.105-2.104v-2.807a2.105 2.105 0 1 1 4.21 0v2.807a2.105 2.105 0 0 1-2.105 2.104Zm2.105 2.616H.394a.394.394 0 0 1 0-.788H17.7a.394.394 0 0 1 0 .788Z"/>
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18">
<path fill="#F9D11F" d="M11.327 18.005H2.1a2.043 2.043 0 0 1-1.485-.651A2.291 2.291 0 0 1 0 15.78V2.224C0 1.634.221 1.068.615.65A2.043 2.043 0 0 1 2.1 0h9.227a2 2 0 0 1 .803.17c.255.111.487.275.682.481.195.207.35.452.455.722.106.27.16.559.16.85v13.558c0 .292-.054.581-.16.851-.105.27-.26.515-.455.721a2.1 2.1 0 0 1-.681.482 2 2 0 0 1-.804.17Z"/>
<path fill="#4C86C6" d="m12.187 14.972 5.097-5.396A2.776 2.776 0 0 0 18 7.682a2.772 2.772 0 0 0-.745-1.88 2.473 2.473 0 0 0-1.777-.788 2.469 2.469 0 0 0-1.788.758l-5.096 5.396v3.804h3.593Z"/>
<path fill="#fff" d="M2.506 14.971h6.087v-1.705H2.506v1.705Zm0-10.801h8.415V2.464H2.506V4.17Zm2.328 2.842h3.938V5.306H4.834v1.706Z"/>
<path fill="#1565B2" d="m8.594 11.168-.001 3.803 3.594.001 1.24-1.313V6.051l-4.833 5.117Z"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
<path fill="#000" d="M8.31 6H6.818V4.364a.273.273 0 0 0-.273-.273h-1.09a.273.273 0 0 0-.273.273v1.639l-1.506.007a.123.123 0 0 0-.114.074l-.001.002c-.003.008-.004.017-.006.026-.001.008-.003.016-.003.025v.005c0 .005.002.01.003.014.002.01.005.021.01.032l.008.014c.005.008.008.016.014.023l2.254 2.518c.004.006.01.009.016.013.003.003.004.007.008.01.003.002.007.002.01.005a.139.139 0 0 0 .028.012l.01.004c.038.011.08.008.11-.017.008-.006.012-.014.017-.022.004-.003.009-.005.012-.009l2.346-2.515c.007-.008.01-.017.016-.026l.007-.011a.123.123 0 0 0 .01-.032c0-.004.004-.008.004-.013v-.013A.121.121 0 0 0 8.31 6ZM5.455 3.818h1.09a.272.272 0 0 0 .273-.272v-.273A.273.273 0 0 0 6.545 3h-1.09a.273.273 0 0 0-.273.273v.273c0 .15.122.272.273.272ZM6 0a6 6 0 1 0 0 12A6 6 0 0 0 6 0Zm0 10.91a4.909 4.909 0 1 1 0-9.82 4.909 4.909 0 0 1 0 9.818Z"/>
</svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="none" viewBox="0 0 15 15">
<path fill="#000" d="M7.5 0a7.5 7.5 0 0 0 0 15 7.5 7.5 0 0 0 0-15Zm4.242 6.567L8.03 10.28a.751.751 0 0 1-1.062 0l-3.71-3.713A.751.751 0 0 1 4.32 5.505L7.5 8.688l3.183-3.18a.75.75 0 1 1 1.06 1.06Z"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12">
<rect width="25" height="11.908" fill="#FFE4BA" rx="4"/>
<path fill="#1D2129" d="M3 9.908V2h2.762c.535 0 .94.033 1.216.1.275.067.539.199.79.396.685.535 1.027 1.357 1.027 2.466 0 .842-.252 1.523-.755 2.042a2.27 2.27 0 0 1-.868.567c-.326.118-.726.177-1.198.177H4.582v2.16H3Zm2.656-6.68H4.582V6.52h.967c.512 0 .886-.11 1.122-.33.33-.291.495-.74.495-1.346 0-.519-.13-.918-.39-1.198-.259-.279-.633-.419-1.12-.419Zm4.296 4.52V2h3.045c.456 0 .805.033 1.044.1.24.067.459.187.655.36.402.37.602.952.602 1.747v3.54h-1.581V4.03c0-.283-.063-.488-.189-.614s-.334-.189-.626-.189h-1.369v4.52H9.952ZM19.335 2h2.95v5.713c0 .322-.037.61-.111.861a1.802 1.802 0 0 1-.325.638c-.181.22-.4.371-.655.454-.256.083-.628.124-1.116.124h-3.15V8.562h2.938c.346 0 .573-.053.679-.159.106-.106.16-.325.16-.655h-1.453c-.464 0-.848-.05-1.15-.148a2.173 2.173 0 0 1-.82-.49c-.575-.542-.862-1.29-.862-2.242 0-1.086.35-1.877 1.05-2.372a2.44 2.44 0 0 1 .797-.384c.28-.075.635-.112 1.068-.112Zm1.37 4.52V3.229h-1.311c-.913 0-1.37.55-1.37 1.652 0 .535.125.942.373 1.221.248.28.608.42 1.08.42h1.227Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="24" fill="#2420A8" opacity=".2"/>
<circle cx="24" cy="24" r="18" fill="#2420A8"/>
<path fill="#F3EDF7" d="M9 31.301v-12.3h4.296c.833 0 1.463.051 1.891.155.429.104.839.31 1.23.615 1.065.832 1.598 2.112 1.598 3.837 0 1.31-.392 2.369-1.175 3.177a3.537 3.537 0 0 1-1.35.88c-.508.184-1.129.276-1.863.276H11.46v3.36H9Zm4.131-10.391h-1.67v5.122h1.505c.795 0 1.377-.172 1.744-.514.514-.453.771-1.15.771-2.093 0-.808-.202-1.43-.606-1.864-.404-.434-.985-.651-1.744-.651Zm6.683 7.031v-8.94h4.737c.71 0 1.251.051 1.625.155.373.104.713.29 1.019.56.624.575.936 1.481.936 2.717v5.508h-2.46v-5.783c0-.44-.098-.759-.294-.955-.196-.196-.52-.293-.973-.293h-2.13v7.031h-2.46ZM34.41 19H39v8.886c0 .502-.058.949-.174 1.34a2.807 2.807 0 0 1-.505.992 2.161 2.161 0 0 1-1.02.707c-.397.128-.975.193-1.734.193h-4.902v-1.91h4.571c.539 0 .89-.082 1.056-.248.165-.165.248-.505.248-1.019h-2.258c-.723 0-1.32-.076-1.79-.23a3.381 3.381 0 0 1-1.277-.761c-.893-.845-1.34-2.008-1.34-3.488 0-1.69.545-2.92 1.634-3.69a3.806 3.806 0 0 1 1.24-.598c.434-.116.988-.174 1.661-.174Zm2.13 7.032V20.91h-2.038c-1.42 0-2.13.856-2.13 2.57 0 .832.193 1.466.578 1.9.386.435.946.652 1.68.652h1.91Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="12" fill="none" viewBox="0 0 25 12">
<rect width="25" height="11.921" fill="#E3D6EE" rx="4"/>
<path fill="#1D2129" d="M4.968 2H8.4v1.248H5.556c-.328 0-.536.016-.624.048-.184.064-.276.22-.276.468 0 .208.088.368.264.48.096.064.356.096.78.096h.948c.608 0 1.088.128 1.44.384.392.288.588.712.588 1.272 0 .424-.12.812-.36 1.164-.176.28-.39.464-.642.552-.252.088-.674.132-1.266.132H3.084V6.596h2.868c.352 0 .592-.004.72-.012.288-.032.432-.196.432-.492 0-.24-.096-.404-.288-.492-.096-.048-.328-.072-.696-.072h-.972c-.384 0-.678-.024-.882-.072a1.575 1.575 0 0 1-.57-.264 1.542 1.542 0 0 1-.51-.63A2.019 2.019 0 0 1 3 3.704c0-.584.212-1.048.636-1.392.256-.208.7-.312 1.332-.312Zm5.995 0 1.488 4.02L14.083 2h1.704l-2.52 5.844h-1.728L9.21 2h1.752Zm8.322 0h3v5.808c0 .328-.037.62-.113.876a1.834 1.834 0 0 1-.33.648 1.412 1.412 0 0 1-.666.462c-.26.084-.638.126-1.134.126h-3.205V8.672h2.989c.351 0 .581-.054.69-.162.108-.108.162-.33.162-.666H19.2c-.472 0-.862-.05-1.17-.15a2.21 2.21 0 0 1-.834-.498c-.584-.552-.876-1.312-.876-2.28 0-1.104.356-1.908 1.068-2.412.256-.184.526-.314.81-.39.284-.076.646-.114 1.086-.114Zm1.392 4.596V3.248h-1.331c-.929 0-1.393.56-1.393 1.68 0 .544.126.958.378 1.242.252.284.618.426 1.099.426h1.247Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="24" fill="#8552A1" opacity=".2"/>
<circle cx="24" cy="24" r="18" fill="#8552A1"/>
<path fill="#F3EDF7" d="M12.062 18.99H17.4v1.942h-4.425c-.51 0-.834.025-.97.074-.287.1-.43.343-.43.728 0 .324.137.573.41.747.15.1.555.15 1.214.15h1.475c.946 0 1.692.199 2.24.597.61.448.915 1.107.915 1.979 0 .66-.187 1.263-.56 1.81-.274.436-.607.722-.999.86-.392.136-1.048.204-1.97.204H9.13V26.14h4.461c.548 0 .921-.006 1.12-.019.448-.05.672-.305.672-.765 0-.373-.149-.629-.448-.765-.149-.075-.51-.112-1.082-.112h-1.512c-.598 0-1.055-.038-1.373-.112a2.451 2.451 0 0 1-.886-.411 2.399 2.399 0 0 1-.794-.98A3.14 3.14 0 0 1 9 21.64c0-.908.33-1.63.99-2.165.398-.324 1.088-.486 2.072-.486Zm9.325 0 2.314 6.254 2.54-6.254h2.65l-3.92 9.091h-2.688l-3.622-9.09h2.726Zm12.946 0H39v9.036c0 .51-.06.964-.177 1.362-.118.398-.29.734-.514 1.008a2.196 2.196 0 0 1-1.036.719c-.404.13-.992.196-1.764.196h-4.984V29.37h4.648c.548 0 .905-.084 1.073-.252.168-.168.252-.514.252-1.037h-2.296c-.734 0-1.34-.077-1.82-.233a3.438 3.438 0 0 1-1.297-.774c-.909-.86-1.363-2.042-1.363-3.547 0-1.718.554-2.969 1.662-3.753a3.86 3.86 0 0 1 1.26-.606c.442-.119 1.005-.178 1.69-.178Zm2.166 7.15v-5.208h-2.073c-1.443 0-2.165.871-2.165 2.613 0 .847.196 1.49.588 1.932.392.442.961.663 1.708.663h1.941Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="42" fill="none" viewBox="0 0 18 42">
<path fill="#00E8CF" d="M12.723 5.08H3.775v.442c0 .513.14.917.42 1.212.263.28.652.42 1.164.42h7.2c.56 0 .987-.125 1.282-.373.264-.233.42-.59.466-1.072l.07-.955a.771.771 0 0 1 .268-.56.882.882 0 0 1 .594-.21.794.794 0 0 1 .571.28.81.81 0 0 1 .198.606l-.07.955c-.078.948-.42 1.678-1.025 2.19-.606.529-1.39.793-2.354.793h-7.2c-.947 0-1.732-.303-2.353-.909-.606-.606-.909-1.398-.909-2.377V.816c0-.218.082-.408.245-.571A.809.809 0 0 1 2.936 0h9.787c.217 0 .411.082.582.245a.764.764 0 0 1 .256.57v3.45a.754.754 0 0 1-.256.582.842.842 0 0 1-.582.233ZM.839 11.068h10.043v-.979a.78.78 0 0 1 .244-.57.809.809 0 0 1 .595-.245c.217 0 .407.082.57.245a.782.782 0 0 1 .245.57v.98h3.659c.217 0 .407.08.57.244a.782.782 0 0 1 .245.57.808.808 0 0 1-.245.595.782.782 0 0 1-.57.245h-3.659v7.572a.809.809 0 0 1-.245.595.782.782 0 0 1-.57.244.81.81 0 0 1-.595-.244.809.809 0 0 1-.244-.595v-7.572H.839a.809.809 0 0 1-.594-.245.809.809 0 0 1-.245-.594c0-.218.082-.408.245-.571a.809.809 0 0 1 .594-.245Zm11.045-9.414h-8.11V3.45h8.11V1.654ZM6.198 14.866v2.866a.809.809 0 0 1-.245.595.782.782 0 0 1-.57.244.809.809 0 0 1-.595-.244.809.809 0 0 1-.244-.595v-2.866a.81.81 0 0 1 .244-.594.809.809 0 0 1 .595-.245c.217 0 .407.082.57.245a.81.81 0 0 1 .245.594ZM1.266 41.161v-7.456c0-.233.081-.431.244-.594a.809.809 0 0 1 .595-.245c.233 0 .427.081.582.245a.83.83 0 0 1 .233.594v6.64h4.66v-9.6H2.99a.83.83 0 0 1-.594-.233.771.771 0 0 1-.245-.582v-6.268c0-.218.082-.408.245-.571a.809.809 0 0 1 .594-.245c.218 0 .408.082.571.245a.782.782 0 0 1 .245.57v5.453H7.58v-7.2a.81.81 0 0 1 .245-.594.809.809 0 0 1 .594-.245c.218 0 .408.082.571.245a.809.809 0 0 1 .245.594v7.2h3.775v-5.569a.78.78 0 0 1 .244-.57.809.809 0 0 1 .595-.245.78.78 0 0 1 .57.244.782.782 0 0 1 .245.571v6.385a.771.771 0 0 1-.245.582.801.801 0 0 1-.57.233H9.235v9.6h4.66v-6.757a.81.81 0 0 1 .245-.594.782.782 0 0 1 .57-.245.81.81 0 0 1 .595.245.81.81 0 0 1 .245.594v7.573a.809.809 0 0 1-.245.594.81.81 0 0 1-.594.245H2.105a.809.809 0 0 1-.595-.245.809.809 0 0 1-.244-.594Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="10" fill="none" viewBox="0 0 11 10">
<path fill="#FF0D0D" d="M7.549 0C6.645 0 5.807.455 5.25 1.203 4.7.455 3.855 0 2.951 0 1.323 0 0 1.449 0 3.227c0 1.06.473 1.807.856 2.406 1.108 1.742 3.897 3.903 4.017 3.993a.603.603 0 0 0 .754 0c.12-.09 2.904-2.257 4.017-3.993.383-.599.856-1.347.856-2.406C10.5 1.449 9.177 0 7.549 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="17" fill="none" viewBox="0 0 18 17">
<path fill="#fff" stroke="#8552A1" stroke-width=".855" d="M12.94.428c2.52 0 4.632 2.255 4.632 5.103 0 1.68-.743 2.87-1.4 3.895-.92 1.437-2.554 3.077-4.008 4.394a49.45 49.45 0 0 1-2.774 2.34l-.008.006a.607.607 0 0 1-.673.06l-.09-.06-.009-.007-.27-.21a51.872 51.872 0 0 1-2.508-2.125c-1.455-1.316-3.088-2.955-4.004-4.396v-.001l-.249-.394C.997 8.096.428 7.001.428 5.531.428 2.683 2.54.428 5.06.428c1.402 0 2.725.706 3.595 1.888l.343.465.345-.463c.882-1.185 2.196-1.89 3.597-1.89Z"/>
</svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" fill="none" viewBox="0 0 10 10">
<path fill="#0079F5" d="M7.432 9.596H2.164A2.165 2.165 0 0 1 0 7.432V2.164C0 .97.97 0 2.164 0h5.268c1.195 0 2.164.97 2.164 2.164v5.268c0 1.195-.97 2.164-2.164 2.164Z"/>
<path fill="#fff" d="M6.24 2.982a.36.36 0 0 0-.662 0l-1.272 2.98a.24.24 0 1 0 .444.186l.147-.352a.813.813 0 0 1 .75-.501h.524c.328 0 .624.198.75.5l.147.353a.24.24 0 1 0 .444-.187L6.24 2.982Zm.058 1.312a.42.42 0 1 1-.776.324.42.42 0 0 1 .776-.324Zm-3.01-.587a.253.253 0 0 0-.465 0l-.774 1.814a.18.18 0 1 0 .333.14l.08-.194a.495.495 0 0 1 .457-.305h.273c.2 0 .38.12.457.305l.08.194a.18.18 0 1 0 .333-.14l-.774-1.814ZM3.055 4.84a.222.222 0 1 1 0-.443.222.222 0 0 1 0 .443Z"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
<path fill="#5C5C66" d="M10.333 14.667a.333.333 0 0 1-.31-.213L8.55 10.667H2.784L1.31 14.454a.333.333 0 1 1-.622-.242l4.667-12a.333.333 0 0 1 .621 0l4.667 12a.335.335 0 0 1-.155.416.334.334 0 0 1-.156.039ZM3.043 10H8.29L5.667 3.253 3.043 10ZM15 4h-4a.333.333 0 1 1 0-.667h4A.333.333 0 1 1 15 4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#5C5C66" d="M15.5 22a.5.5 0 0 1-.466-.319L12.824 16H4.176l-2.21 5.681a.5.5 0 1 1-.93-.362l7-18a.5.5 0 0 1 .93 0l7 18A.5.5 0 0 1 15.5 22ZM4.564 15h7.872L8.5 4.88 4.564 15ZM19.5 9a.5.5 0 0 1-.5-.5V6h-2.5a.5.5 0 0 1 0-1H19V2.5a.5.5 0 0 1 1 0V5h2.5a.5.5 0 0 1 0 1H20v2.5a.5.5 0 0 1-.5.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="23" fill="none" viewBox="0 0 24 23">
<path fill="#8552A1" d="M.858 2.656V0H24v2.656H.858Zm9.919 7.034V7.034h13.202V9.69H10.777Zm.02 5.464v-2.656h13.165v2.656H10.794h.003ZM.91 22.085v-2.656h23.015v2.656H.91ZM0 9.621l4.109-4.18 4.106 4.182H0V9.62Zm.045 3.601h8.208L4.15 17.356.045 13.222Z"/>
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="186" height="24" fill="none" viewBox="0 0 186 24">
<path fill="#CE7A7A" d="M1.5 18.355v.954c0 1.434.592 3.191 2.033 3.191h16.944c1.43 0 2.023-1.756 2.023-3.19v-.954c0 1.43-.901 1.81-2.334 1.81H3.834c-1.442 0-2.334-.378-2.334-1.81Zm0-14.832v11.969c0 1.431.893 2.341 2.334 2.341h16.334c1.432 0 2.334-.91 2.334-2.341V3.523C22.5 2.092 21.907 1.5 20.477 1.5h-1.794v2.166a1.908 1.908 0 0 1-.954 3.563c-1.05 0-1.91-.859-1.91-1.912 0-.706.381-1.315.954-1.651V1.5H7.227v2.166c.571.336.954.945.954 1.651 0 1.052-.86 1.912-1.91 1.912a1.915 1.915 0 0 1-1.909-1.912c0-.706.381-1.315.954-1.651V1.5H3.531C2.092 1.5 1.5 2.091 1.5 3.523Z"/>
<path fill="#606060" d="M37.365 1.11h9.837v.02c0 .446-.191 1.186-.574 2.22a11.62 11.62 0 0 1-1.187 2.047c-.268.357-.606.766-1.014 1.225h.402v1.359c0 2.807-.007 6.399-.02 10.775l.498 1.167h-7.521l.478-1.186v-8.268c-1.659.523-3.253.785-4.785.785v-.02c.013-1.046.02-1.626.02-1.741a7.038 7.038 0 0 0 1.454-.594c.434-.216.989-.644 1.665-1.282.651-.753 1.091-1.384 1.32-1.895.588-1.199.881-2.303.881-3.31v-.039l.02-.096h-1.474V1.11Zm16.306.02h5.646c.025 0 .038.012.038.037.013.843.02 1.455.02 1.838l.018.019h2.68c.064 0 1.365-.268 3.904-.804l4.21 1.818v.02c-.012 0-.452.248-1.32.746H46.8c-.025 0-.038-.013-.038-.038V3.024h7.847l.038-.02a3.83 3.83 0 0 1-.02-.286V2.22h-.956V1.13Zm11.35 4.286a530.051 530.051 0 0 0 4.229 1.818c-.038.051-.37.243-.995.575-.192.127-.307.19-.345.19H47.432V6.221h13.741c.115 0 1.397-.268 3.847-.804Zm.018 3.196 4.21 1.819v.019c-.88.485-1.332.727-1.358.727h-20.46v-1.76h13.704c.191-.026 1.493-.294 3.904-.805Zm.517 2.929a2721.37 2721.37 0 0 1 4.21 1.818v.038c-.842.447-1.269.683-1.282.708v5.818c0 .026-.012.039-.038.039h-6.488v-.345h-8.306v.345h-6.507c-.013 0-.02-.013-.02-.039V12h6.527v.325h8.076c.039 0 1.314-.261 3.828-.784Zm-11.904 2.602v3.675c0 .013.006.02.019.02h8.249c.025 0 .038-.007.038-.02v-3.675c0-.025-.013-.038-.038-.038h-8.25c-.012 0-.018.013-.018.038ZM86.092.21h7.426c.025.013.038.026.038.039.013.97.02 1.614.02 1.933 0 .038.006.134.018.287h3.426c.051 0 1.531-.447 4.44-1.34 3.28 1.544 4.989 2.348 5.13 2.412-.893.638-1.366.963-1.417.976v7.904H99.24v-.287c0-.013-.006-.02-.019-.02H80.714v.307H74.8V2.22h5.914v.23h6.335c-.013-.268-.02-.466-.02-.594v-.268c0-.063-.005-.159-.018-.287h-.957V.25c.013-.026.025-.039.038-.039Zm-5.378 4.326v.88H99.24v-.88H80.714Zm0 2.335v.861H99.24v-.861H80.714Zm0 2.335v.86h18.507c.013 0 .02-.012.02-.037v-.823H80.713Zm12.842 3.292 2.354 2.43c-1.2.638-2.686.957-4.46.957-1.964 0-3.495-.408-4.592-1.225l-.096-.095c1.097-1.29 1.652-1.933 1.665-1.933 1.059.331 1.914.497 2.565.497.778 0 1.633-.21 2.564-.631Zm11.388.114c.076.651.325 1.264.746 1.838.396.459.734.746 1.014.861.575.306 1.142.46 1.704.46.025 0 .038.006.038.018v2.91c-2.947 0-4.944-.485-5.99-1.455-.064 0-.319-.255-.766-.766a7.21 7.21 0 0 1-.785-1.263 8.234 8.234 0 0 1-.344-.804c.013 0 1.474-.6 4.383-1.799Zm-25.417.192h6.125c.025 0 .038.012.038.038v2.01c0 .561.6 1.008 1.8 1.34.994.255 2.245.382 3.75.382 2.157 0 4.14-.242 5.953-.727.65-.204 1.122-.415 1.416-.632 0-.012.038-.044.115-.095a3.974 3.974 0 0 1-.21-.479c.777-.37 1.18-.555 1.205-.555l1.741 4.02c-.446.28-1.199.58-2.258.899-2.475.638-5.212.957-8.21.957-4.096 0-7.254-.6-9.474-1.8-1.327-.79-1.99-1.754-1.99-2.89v-2.468Zm-4.689.287 4.211 1.722v.02a9.03 9.03 0 0 1-1.053 2.01c-.42.51-.778.854-1.071 1.032a5.1 5.1 0 0 1-1.436.67c-.944.268-2.182.402-3.713.402v-2.832c.498-.128 1.104-.542 1.818-1.244.37-.434.67-.836.9-1.206 0-.025.115-.217.344-.574Zm64.364-11.943 4.919 2.45c-.039.051-.453.35-1.244.9-.09.076-.16.114-.211.114v15.311c0 .026-.013.039-.038.039h-6.469c-.026 0-.038-.013-.038-.039v-.574h-15.369v.574c0 .026-.012.039-.038.039h-6.488V1.837h6.526v.708h13.991a505.53 505.53 0 0 0 4.172-1.32 1.05 1.05 0 0 1 .287-.077Zm-18.45 3.464V9.99h15.369V4.612h-15.369Zm0 7.407v5.301h15.369v-5.3h-15.369ZM166.36 0c2.845 1.174 4.287 1.76 4.325 1.76v.04c-.472.498-.727.772-.765.823v2.373h5.741c.039 0 1.512-.465 4.421-1.397a674.83 674.83 0 0 1 4.919 2.488l-1.435.995H169.92v2.374h3.77c.026 0 1.499-.466 4.421-1.398.459.243 2.099 1.066 4.919 2.47 0 .025-.472.363-1.417 1.014h-29.799c-.012 0-.019-.013-.019-.039v-2.01c0-.025.007-.037.019-.037h11.599V7.08h-15.082V4.995h15.082V1.033A289.299 289.299 0 0 1 166.36 0Zm-.44 11.77c1.276.804 2.462 1.206 3.56 1.206.446 0 .937-.057 1.473-.172 0 .025.574.995 1.723 2.909-.511.153-1.11.23-1.799.23h-.115c-1.876 0-3.675-.453-5.397-1.36-.358-.19-.734-.478-1.13-.86a4.307 4.307 0 0 1-.363-.44c.14-.115.823-.62 2.048-1.513Zm15.579.9h.019v.019c0 .255.134.67.402 1.244a3.433 3.433 0 0 0 1.952 1.684c.319.102.702.166 1.148.192v2.928h-.019c-2.641 0-4.504-.39-5.588-1.168-.97-.638-1.723-1.677-2.259-3.12a569.36 569.36 0 0 1 4.345-1.78Zm-25.091.172h6.43c.026 0 .039.013.039.038v2.03c0 .548.58.994 1.741 1.339.97.23 2.15.344 3.541.344 2.36 0 4.427-.3 6.201-.9.485-.216.797-.401.938-.554a3.73 3.73 0 0 1-.211-.479h.019l1.187-.555c1.161 2.667 1.742 4.013 1.742 4.038-.447.281-1.193.581-2.24.9-2.437.638-5.129.957-8.076.957-4.058 0-7.165-.593-9.321-1.78-1.327-.804-1.99-1.78-1.99-2.928v-2.45Zm-4.689.287 4.21 1.723v.038a8.71 8.71 0 0 1-1.052 1.933c-.498.561-.874.906-1.13 1.034-1.046.752-2.851 1.129-5.416 1.129V16.19c0-.012.204-.108.613-.287 1.046-.587 1.971-1.512 2.775-2.775Z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="9" fill="none" viewBox="0 0 10 9">
<path fill="#606060" d="m9.755 2.404-3.19 4.904c-.696 1.068-2.434 1.068-3.13 0L.246 2.404C-.451 1.336.419 0 1.809 0h6.382c1.39 0 2.26 1.336 1.564 2.404Z"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#C9CDD4" d="M5.6 18.4h12.8V5.6H5.6v12.8ZM24 20.8a3.2 3.2 0 0 1-2.874 3.184L20.8 24H3.2l-.326-.016a3.2 3.2 0 0 1-2.856-2.858L0 20.8V3.2A3.2 3.2 0 0 1 3.2 0h17.6A3.2 3.2 0 0 1 24 3.2v17.6Z"/>
<path fill="#8552A1" d="M17.4 21.6a1.2 1.2 0 1 1 0 2.4H6.6a1.2 1.2 0 1 1 0-2.4h10.8ZM0 17.4V6.6a1.2 1.2 0 1 1 2.4 0v10.8a1.2 1.2 0 1 1-2.4 0Zm21.6 0V6.6a1.2 1.2 0 1 1 2.4 0v10.8a1.2 1.2 0 1 1-2.4 0ZM17.4 0a1.2 1.2 0 1 1 0 2.4H6.6a1.2 1.2 0 0 1 0-2.4h10.8Z"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
<rect width="9.917" height="9.917" x=".875" y=".875" stroke="#C9CDD4" stroke-width=".583" rx="4.958"/>
</svg>

After

Width:  |  Height:  |  Size: 208 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18">
<g clip-path="url(#a)">
<path fill="#8552A1" d="M9 0a9 9 0 0 0-9 9 9 9 0 0 0 9 9 9 9 0 0 0 9-9 9 9 0 0 0-9-9Zm5.934 6.21L8.16 12.988a.843.843 0 0 1-.599.247.844.844 0 0 1-.6-.247L3.066 9.09a.846.846 0 1 1 1.198-1.197L7.56 11.19l6.177-6.177a.847.847 0 1 1 1.197 1.198Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h18v18H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 488 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#8552A1" rx="10" transform="matrix(-1 0 0 1 32 0)"/>
<path fill="#FEFDFE" d="m24.845 22.204-5.101-5.105-2.64 2.64 5.105 5.104a1.085 1.085 0 0 0 1.527 0l1.108-1.108a1.09 1.09 0 0 0 0-1.531ZM17.22 18.6l1.382-1.382-1.576-1.576a5.512 5.512 0 0 0-.63-7.032 5.51 5.51 0 0 0-7.785 0c-2.15 2.146-2.146 5.635 0 7.785a5.512 5.512 0 0 0 7.033.63l1.575 1.576Zm-7.541-3.288a3.983 3.983 0 0 1 0-5.635 3.983 3.983 0 0 1 5.635 0 3.983 3.983 0 0 1 0 5.635 3.983 3.983 0 0 1-5.635 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path fill="#8552A1" d="M10.114 9.583 7.966 7.61l-.716.658 2.864 2.633L16.25 5.26l-.716-.659-5.42 4.983Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M10.114 9.583 7.966 7.61l-.716.658 2.864 2.633L16.25 5.26l-.716-.659-5.42 4.983Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20">
<path fill="#8552A1" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path fill="#8552A1" d="m15.523 4.546-.806-.705-3.194 2.795L8.328 3.84l-.805.705 3.194 2.795-3.194 2.795.805.705 3.195-2.795 3.194 2.795.806-.705-3.195-2.795 3.195-2.795Z"/>
<path stroke="#8552A1" stroke-width=".5" d="M15.608 15.302a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.525.218H4.326a1.78 1.78 0 0 1-1.303-.542 1.78 1.78 0 0 1-.543-1.303V3.963c0-.207.073-.383.218-.525a.728.728 0 0 1 .525-.218c.203 0 .382.072.524.218a.728.728 0 0 1 .218.524v10.015c0 .303.11.56.327.78.219.217.473.326.776.326h10.015c.206 0 .38.073.525.219Zm-2.97 2.227a.717.717 0 0 1 .218.524.712.712 0 0 1-.218.524.729.729 0 0 1-.524.218H2.098a1.78 1.78 0 0 1-1.303-.542 1.785 1.785 0 0 1-.545-1.306V6.932c0-.206.073-.382.218-.524a.728.728 0 0 1 .524-.219c.203 0 .382.073.525.219a.728.728 0 0 1 .218.524v9.272c0 .304.109.561.327.78.218.217.476.327.779.327h9.273c.203 0 .378.072.524.218ZM18.153.892c.427.43.643.952.643 1.567v9.67c0 .615-.216 1.14-.643 1.566a2.136 2.136 0 0 1-1.567.643H6.914c-.616 0-1.14-.215-1.567-.643a2.129 2.129 0 0 1-.642-1.563V2.459c0-.615.215-1.14.642-1.567A2.136 2.136 0 0 1 6.914.25h9.67c.617 0 1.139.215 1.569.642Zm-.842 2.164c0-.364-.13-.673-.388-.933a1.263 1.263 0 0 0-.934-.388H7.514c-.364 0-.673.13-.934.388a1.27 1.27 0 0 0-.39.933v8.476c0 .364.13.672.387.933.258.26.57.388.934.388h8.475c.364 0 .673-.13.934-.388.26-.257.388-.57.388-.933V3.056h.003Z"/>
<path stroke="#8552A1" stroke-width=".5" d="m15.523 4.546-.806-.705-3.194 2.795L8.328 3.84l-.805.705 3.194 2.795-3.194 2.795.805.705 3.195-2.795 3.194 2.795.806-.705-3.195-2.795 3.195-2.795Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none" viewBox="0 0 40 31">
<path fill="#2420A8" d="M40 24.68a5.17 5.17 0 0 1-.34 1.88 5.179 5.179 0 0 1-.96 1.58c-.413.467-.893.86-1.44 1.18-.547.32-1.14.533-1.78.64l.04.04H8.12c-.133.027-.347.04-.64.04-1.04 0-2.013-.2-2.92-.6-.907-.4-1.7-.94-2.38-1.62a7.48 7.48 0 0 1-1.6-2.38A7.365 7.365 0 0 1 0 22.52c0-1.04.193-2.013.58-2.92a7.48 7.48 0 0 1 1.6-2.38 7.763 7.763 0 0 1 2.38-1.62c.907-.4 1.88-.6 2.92-.6h.04a4.85 4.85 0 0 1-.04-.6v-.64c0-1.893.36-3.68 1.08-5.36.72-1.68 1.707-3.14 2.96-4.38a14.132 14.132 0 0 1 4.38-2.94C17.567.36 19.347 0 21.24 0c1.893 0 3.68.36 5.36 1.08 1.68.72 3.147 1.7 4.4 2.94 1.253 1.24 2.24 2.7 2.96 4.38.72 1.68 1.08 3.467 1.08 5.36 0 .987-.107 1.953-.32 2.9a14.878 14.878 0 0 1-.88 2.7 4.66 4.66 0 0 1 .84-.08c.72 0 1.407.14 2.06.42.653.28 1.22.667 1.7 1.16.48.493.86 1.067 1.14 1.72.28.653.42 1.353.42 2.1Zm-14-4.76c.533 0 .88-.06 1.04-.18.16-.12.053-.407-.32-.86-.373-.48-.787-1.107-1.24-1.88-.453-.773-.927-1.56-1.42-2.36-.493-.8-.993-1.547-1.5-2.24-.507-.693-1-1.2-1.48-1.52-.48-.347-.833-.533-1.06-.56-.227-.027-.553.133-.98.48-.48.4-.947.907-1.4 1.52a54.547 54.547 0 0 0-1.38 1.96c-.467.693-.933 1.373-1.4 2.04a15.38 15.38 0 0 1-1.42 1.76c-.48.48-.653.907-.52 1.28s.467.56 1 .56h.64c.24 0 .5.007.78.02.28.013.553.02.82.02h.68c.373.027.613.153.72.38.107.227.16.54.16.94 0 .453-.007.853-.02 1.2-.013.347-.02.68-.02 1 0 .32-.007.653-.02 1-.013.347-.02.747-.02 1.2 0 .187.02.387.06.6.04.213.113.413.22.6s.267.34.48.46c.213.12.493.18.84.18h.92c.24 0 .6.013 1.08.04.64 0 1.007-.16 1.1-.48.093-.32.14-.827.14-1.52 0-.613.007-1.167.02-1.66.013-.493.02-1.167.02-2.02 0-.613.06-1.087.18-1.42.12-.333.38-.5.78-.5.293 0 .713-.007 1.26-.02.547-.013.967-.02 1.26-.02Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="none" viewBox="0 0 15 15">
<path fill="#000" d="M15 7.5a7.5 7.5 0 1 1-15 0 7.5 7.5 0 0 1 15 0Z"/>
<path fill="#fff" d="M3 7.75A.75.75 0 0 1 3.75 7h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 7.75Z"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="33" fill="none" viewBox="0 0 360 33">
<g clip-path="url(#a)">
<path fill="#8552A1" d="M7.305 15.333h7.898l-.856 1.51h8.49l.593-2.883H5.594L7.832 2.7 5.857.87h50.874L54.1 13.96H36.724l-.592 2.883h17.572l-.856 4.12H35.276l-.395 1.968h17.507l-.79 3.936H34.09l-.263 1.373h20.468l-.79 3.89H0l.79-3.89h19.744l.263-1.373H4.87l.79-3.936h15.927l.395-1.968h-9.938l-.724 1.373H3.16l4.145-7.003ZM20.007 5.63H43.24l.33-1.373H20.336l-.329 1.373Zm22.311 4.668.263-1.327H19.35l-.263 1.327h23.232Zm51.795 10.894.527-2.793h-9.872l.79-3.982h31.327l-.79 3.982h-10.004l-.526 2.793h8.753l-.855 4.165h-8.754l-.724 3.432h10.465l-.856 4.028H81.477l.856-4.028h10.2l.725-3.433H84.57l.856-4.165h8.687ZM85.36 4.622l-4.804.321-.856 4.303h5.66l-.855 4.21H78.12l-3.95 19.498h-8.687l3.95-19.498H62.72l.856-4.21h5.989l.79-3.662-5.66.366.855-4.21L86.216.411l-.856 4.21ZM89.572.87h29.09l-2.369 11.762h-29.09L89.573.87ZM59.101 31.307l4.212-16.111h5.594l-4.212 16.11H59.1Zm16.65 0 2.37-16.111h5.594l-2.37 16.11h-5.594ZM107.21 8.65l.724-3.8h-9.345l-.724 3.8h9.345Zm35.276 11.031.395-1.785 14.545-3.112h-28.958l.921-4.44h43.766l-1.053 5.31-17.111 4.027h21.258l-.856 4.44h-21.324l-1.118 5.72c-.176.886-.757 1.626-1.744 2.22-.988.596-2.117.893-3.39.893h-12.965l5.857-4.302.922-4.531h-21.258l.856-4.44h21.257ZM163.81 6.957h-25.009l-.461 2.472h-13.755l1.382-6.82h19.942l.197-.915L144.066 0h15.927l-.593 2.61h19.086l-1.382 6.82h-13.755l.461-2.472Zm48.044 2.197-4.41 13.044h2.962l2.896-14.371h-11.847l.856-4.348h11.846l.395-1.74L212.446 0h14.413l-.724 3.479h11.847l-.856 4.348H225.28l-2.962 14.371h3.028l.855-13.044h8.161l-1.58 23.022h-8.095l.329-5.63h-3.553l-1.317 6.408h-11.912l1.25-6.407h-3.488l-1.908 5.63h-8.161l7.766-23.023h8.161Zm-12.176-4.806-.197.961-5.594 27.645h-11.913l5.266-25.951-3.818.503 1.053-5.217L200.534.092l-.856 4.256Zm79.503 19.361 1.514 1.053 5.396-3.021H268.98l3.554-6.088h-4.081l.856-4.302h5.726l1.974-3.387h-4.607l.856-4.21h6.186l1.514-2.61L279.313 0h12.504l-2.04 3.753h9.872l-.856 4.211h-11.32l-1.776 3.387h14.018l-.856 4.302h-15.532l-1.119 2.106h14.216l-1.185 5.446-9.148 5.264 6.516 4.44h-11.846l-12.702-9.2h11.122Zm-33.565-9.566 3.751-6.545h-3.356l.856-4.257h4.936l1.118-1.922-1.316-1.373h9.346l-1.909 3.295h11.846l-.855 4.257h-13.426l-3.883 6.82h2.962l1.118-5.31h8.161l-1.053 5.31h3.686l-.79 4.073h-3.751l-.724 3.524 4.936-.457-.922 4.577-4.936.457-1.25 6.27h-9.872l1.053-5.309-9.938.916.921-4.577 9.938-.915.922-4.486h-8.424l.855-4.348Zm78.252 5.127 2.041-9.933h16.914l3.422-3.25h-6.581l-2.304 1.831h-11.122l8.687-6.82-1.25-1.052h13.623l-2.633 2.06h13.69l-.79 4.119-3.357 3.112h5.66l-2.04 9.932H360l-.856 4.12h-12.702L356.907 33h-10.596l-6.845-6.957-9.016 6.774h-11.715l13.097-9.429h-10.991l.856-4.119h2.171ZM311.956 5.446l.856-4.165L311.232 0h12.505l-1.119 5.447h2.567l-.856 4.348h-2.632l-.856 4.394 2.698-.55-.987 4.76-2.698.55-2.172 10.847c-.176.855-.735 1.572-1.678 2.151-.944.58-2.052.87-3.324.87h-10.53l5.133-4.12 1.514-7.46-4.278.87.987-4.806 4.212-.87 1.383-6.636h-4.147l.856-4.348h4.146Zm34.75 7.231-1.317 6.5h4.081l1.316-6.5h-4.08Zm-13.097.138-1.316 6.453h4.014l1.316-6.453h-4.014Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h360v33H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,12 +1,419 @@
const { parseTableByFileName, buildSankeyData } = require('../../utils/sankey');
/**
* 将表头标准化,便于做中英文别名匹配。
*/
function normalizeHeaderName(header) {
return String(header || '')
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, '');
}
/**
* 根据候选别名查找列索引。
*/
function findHeaderIndex(headers, aliases) {
const aliasSet = {};
aliases.forEach((item) => {
aliasSet[normalizeHeaderName(item)] = true;
});
for (let i = 0; i < headers.length; i += 1) {
if (aliasSet[normalizeHeaderName(headers[i])]) {
return i;
}
}
return -1;
}
/**
* 判断文本是否可作为数值。
*/
function isNumericCell(text) {
const normalized = String(text || '')
.replace(/,/g, '')
.trim();
if (!normalized) {
return false;
}
return !Number.isNaN(Number(normalized));
}
/**
* 取第二行中的首个数字列索引。
*/
function findNumericColumnFromSecondRow(rows) {
const secondRow = rows[0] || [];
for (let i = 0; i < secondRow.length; i += 1) {
if (isNumericCell(secondRow[i])) {
return i;
}
}
return -1;
}
/**
* 从文件名中提取后缀(不含点)。
*/
function getFileExtension(fileName) {
const lowerName = String(fileName || '').toLowerCase();
const lastDotIndex = lowerName.lastIndexOf('.');
if (lastDotIndex < 0) {
return '';
}
return lowerName.slice(lastDotIndex + 1);
}
/**
* 节点配色:与当前画布渲染保持一致,双色交替。
*/
function getNodeColor(index) {
return index % 2 === 0 ? '#9b6bc2' : '#7e95f7';
}
/**
* 标签纵向位置:小节点也能保持可读,不贴边。
*/
function getNodeLabelCenterY(nodeHeight) {
return Math.min(12, Math.max(8, nodeHeight / 2));
}
/**
* 构建导出文件时间戳,格式与 Web 保持一致。
*/
function formatFileTimestamp() {
const now = new Date();
const pad = (value) => String(value).padStart(2, '0');
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(
now.getMinutes()
)}${pad(now.getSeconds())}`;
}
/**
* SVG 文本转义,避免标签名包含特殊字符导致 XML 非法。
*/
function escapeSvgText(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* SVG 数值输出做轻量格式化,避免太长小数。
*/
function formatSvgNumber(value) {
const normalized = Number(value || 0);
if (!Number.isFinite(normalized)) {
return '0';
}
return String(Number(normalized.toFixed(2)));
}
/**
* 下拉选项:与 Web 手机端保持一致。
*/
const GAP_OPTIONS = [0, 5, 10, 15, 20, 25, 30];
const PADDING_OPTIONS = [0, 10, 20, 30, 40, 50, 60, 70, 80];
const LABEL_POSITION_OPTIONS = [
{ value: 'inner', label: '内' },
{ value: 'outer', label: '外' },
{ value: 'left', label: '左' },
{ value: 'right', label: '右' }
];
const TARGET_ALIGN_OPTIONS = [
{ value: 'between', label: '两端' },
{ value: 'middle', label: '中间' },
{ value: 'top', label: '顶部' },
{ value: 'bottom', label: '底部' }
];
/**
* 数值限制,避免 UI 参数导致布局异常。
*/
function clampNumber(value, min, max, fallback) {
const normalized = Number(value);
if (!Number.isFinite(normalized)) {
return fallback;
}
return Math.min(max, Math.max(min, normalized));
}
/**
* 方向切换target->source 时对连线做镜像翻转。
*/
function applyDirection(links, direction) {
if (!Array.isArray(links)) {
return [];
}
if (direction !== 'target-to-source') {
return links;
}
return links.map((link) => ({
source: link.target,
target: link.source,
value: link.value
}));
}
/**
* 根据标签位置模式返回 Canvas 文本绘制参数。
*/
function getCanvasLabelPlacement(layout, isSource, labelPositionMode) {
const sourceInnerX = layout.leftX + layout.nodeWidth + 4;
const sourceOuterX = Math.max(2, layout.leftX - 4);
const targetInnerX = Math.max(2, layout.rightX - 4);
const targetOuterX = layout.rightX + layout.nodeWidth + 4;
if (labelPositionMode === 'outer') {
return isSource
? { x: sourceOuterX, textAlign: 'right' }
: { x: targetOuterX, textAlign: 'left' };
}
if (labelPositionMode === 'left') {
return isSource
? { x: sourceOuterX, textAlign: 'right' }
: { x: targetInnerX, textAlign: 'right' };
}
if (labelPositionMode === 'right') {
return isSource
? { x: sourceInnerX, textAlign: 'left' }
: { x: targetOuterX, textAlign: 'left' };
}
return isSource
? { x: sourceInnerX, textAlign: 'left' }
: { x: targetInnerX, textAlign: 'right' };
}
/**
* 根据标签位置模式返回 SVG 文本锚点参数。
*/
function getSvgLabelPlacement(layout, isSource, labelPositionMode) {
const canvasPlacement = getCanvasLabelPlacement(layout, isSource, labelPositionMode);
return {
x: canvasPlacement.x,
textAnchor: canvasPlacement.textAlign === 'right' ? 'end' : 'start'
};
}
/**
* 统一生成桑基图布局数据,供 canvas 渲染与 SVG 导出共用。
*/
function buildSankeyLayout(links, width, height, renderOptions) {
if (!Array.isArray(links) || links.length === 0) {
return null;
}
const nodeGap = clampNumber(renderOptions && renderOptions.nodeGap, 0, 60, 8);
const padding = clampNumber(renderOptions && renderOptions.chartPadding, 0, 120, 16);
const targetAlignMode = (renderOptions && renderOptions.targetAlignMode) || 'between';
const nodeWidth = 10;
const leftX = padding;
const rightX = Math.max(padding + nodeWidth + 80, width - padding - nodeWidth);
const sourceValueMap = {};
const targetValueMap = {};
links.forEach((link) => {
sourceValueMap[link.source] = (sourceValueMap[link.source] || 0) + Number(link.value || 0);
targetValueMap[link.target] = (targetValueMap[link.target] || 0) + Number(link.value || 0);
});
const sourceNames = Object.keys(sourceValueMap);
const targetNames = Object.keys(targetValueMap);
const totalValue = sourceNames.reduce((sum, name) => sum + sourceValueMap[name], 0);
if (totalValue <= 0) {
return null;
}
const sourceGapCount = Math.max(0, sourceNames.length - 1);
const sourceContentHeight = Math.max(10, height - padding * 2 - sourceGapCount * nodeGap);
const sourceUnitHeight = sourceContentHeight / totalValue;
const sourcePos = {};
let sourceCursorY = padding;
sourceNames.forEach((name) => {
const nodeHeight = Math.max(2, sourceValueMap[name] * sourceUnitHeight);
sourcePos[name] = { y: sourceCursorY, h: nodeHeight };
sourceCursorY += nodeHeight + nodeGap;
});
const targetGapCount = Math.max(0, targetNames.length - 1);
const targetTotalValue = targetNames.reduce((sum, name) => sum + targetValueMap[name], 0);
const targetUnitHeight = targetTotalValue > 0 ? sourceUnitHeight : 0;
const targetTotalNodeHeight = targetTotalValue * targetUnitHeight;
const targetBlockHeight = targetTotalNodeHeight + targetGapCount * nodeGap;
const layoutHeight = Math.max(10, height - padding * 2);
let targetStartY = padding;
if (targetAlignMode === 'middle') {
targetStartY = padding + Math.max(0, (layoutHeight - targetBlockHeight) / 2);
} else if (targetAlignMode === 'bottom') {
targetStartY = padding + Math.max(0, layoutHeight - targetBlockHeight);
} else if (targetAlignMode === 'top') {
targetStartY = padding;
}
const targetPos = {};
let targetCursorY = targetStartY;
targetNames.forEach((name) => {
const nodeHeight = Math.max(2, targetValueMap[name] * targetUnitHeight);
targetPos[name] = { y: targetCursorY, h: nodeHeight };
targetCursorY += nodeHeight + nodeGap;
});
const sourceOffset = {};
const targetOffset = {};
sourceNames.forEach((name) => {
sourceOffset[name] = 0;
});
targetNames.forEach((name) => {
targetOffset[name] = 0;
});
const linkSegments = [];
links.forEach((link) => {
const sourceNode = sourcePos[link.source];
const targetNode = targetPos[link.target];
if (!sourceNode || !targetNode) {
return;
}
const linkHeight = Math.max(1, Number(link.value || 0) * sourceUnitHeight);
const sy = sourceNode.y + sourceOffset[link.source] + linkHeight / 2;
const ty = targetNode.y + targetOffset[link.target] + linkHeight / 2;
sourceOffset[link.source] += linkHeight;
targetOffset[link.target] += linkHeight;
const startX = leftX + nodeWidth;
const endX = rightX;
const controlX = (startX + endX) / 2;
linkSegments.push({
startX,
endX,
controlX,
sy,
ty,
linkHeight
});
});
return {
leftX,
rightX,
nodeWidth,
sourceNames,
targetNames,
sourcePos,
targetPos,
linkSegments
};
}
/**
* 基于布局结果构建可下载的 SVG 字符串。
*/
function buildSankeySvgText(links, width, height, renderOptions) {
const layout = buildSankeyLayout(links, width, height, renderOptions);
if (!layout) {
return '';
}
const labelPositionMode = (renderOptions && renderOptions.labelPositionMode) || 'inner';
const segments = [];
segments.push(
`<svg xmlns="http://www.w3.org/2000/svg" width="${formatSvgNumber(width)}" height="${formatSvgNumber(
height
)}" viewBox="0 0 ${formatSvgNumber(width)} ${formatSvgNumber(height)}">`
);
segments.push(
`<rect x="0" y="0" width="${formatSvgNumber(width)}" height="${formatSvgNumber(height)}" fill="#f7f8fa" />`
);
layout.linkSegments.forEach((segment) => {
const pathData = `M ${formatSvgNumber(segment.startX)} ${formatSvgNumber(segment.sy)} C ${formatSvgNumber(
segment.controlX
)} ${formatSvgNumber(segment.sy)} ${formatSvgNumber(segment.controlX)} ${formatSvgNumber(segment.ty)} ${formatSvgNumber(
segment.endX
)} ${formatSvgNumber(segment.ty)}`;
segments.push(
`<path d="${pathData}" fill="none" stroke="#9b6bc2" stroke-opacity="0.35" stroke-width="${formatSvgNumber(
segment.linkHeight
)}" stroke-linecap="round" />`
);
});
layout.sourceNames.forEach((name, index) => {
const node = layout.sourcePos[name];
const textY = node.y + getNodeLabelCenterY(node.h);
const textPlacement = getSvgLabelPlacement(layout, true, labelPositionMode);
segments.push(
`<rect x="${formatSvgNumber(layout.leftX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
layout.nodeWidth
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index)}" />`
);
segments.push(
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
textPlacement.textAnchor
}" dominant-baseline="middle">${escapeSvgText(name)}</text>`
);
});
layout.targetNames.forEach((name, index) => {
const node = layout.targetPos[name];
const textY = node.y + getNodeLabelCenterY(node.h);
const textPlacement = getSvgLabelPlacement(layout, false, labelPositionMode);
segments.push(
`<rect x="${formatSvgNumber(layout.rightX)}" y="${formatSvgNumber(node.y)}" width="${formatSvgNumber(
layout.nodeWidth
)}" height="${formatSvgNumber(node.h)}" fill="${getNodeColor(index)}" />`
);
segments.push(
`<text x="${formatSvgNumber(textPlacement.x)}" y="${formatSvgNumber(textY)}" fill="#4e5969" font-size="10" text-anchor="${
textPlacement.textAnchor
}" dominant-baseline="middle">${escapeSvgText(name)}</text>`
);
});
segments.push('</svg>');
return segments.join('\n');
}
Page({
data: {
selectedThemeIndex: 1,
sourceColumns: ['列1', '列2'],
targetColumns: ['列1', '列2'],
sourceDataIndex: 1,
sourceDescChecked: [1],
targetDescChecked: [1],
showThemeSheet: false
showThemeSheet: false,
uploadMessage: '点击上传或将csv/xls文件拖到这里上传',
parseError: '',
buildError: '',
columnHeaders: ['列1', '列2', '列3'],
tableRows: [],
sourceDataColumn: null,
sourceDescriptionColumns: [],
targetDescriptionColumns: [],
nodesCount: 0,
linksCount: 0,
droppedRows: 0,
buildWarnings: [],
infoLogs: ['解析信息: 尚未加载数据文件'],
sankeyLinks: [],
sankeyNodes: [],
gapOptions: GAP_OPTIONS,
gapOptionIndex: 1,
nodeGap: GAP_OPTIONS[1],
paddingOptions: PADDING_OPTIONS,
paddingOptionIndex: 2,
chartPadding: PADDING_OPTIONS[2],
direction: 'source-to-target',
labelPositionOptionLabels: LABEL_POSITION_OPTIONS.map((item) => item.label),
labelPositionValues: LABEL_POSITION_OPTIONS.map((item) => item.value),
labelPositionIndex: 0,
labelPositionMode: LABEL_POSITION_OPTIONS[0].value,
targetAlignOptionLabels: TARGET_ALIGN_OPTIONS.map((item) => item.label),
targetAlignValues: TARGET_ALIGN_OPTIONS.map((item) => item.value),
targetAlignIndex: 0,
targetAlignMode: TARGET_ALIGN_OPTIONS[0].value
},
/**
@@ -21,5 +428,538 @@ Page({
*/
onCloseThemeSheet() {
this.setData({ showThemeSheet: false });
},
/**
* 更新“节点间距”下拉值并立即重绘。
*/
onChangeGap(e) {
const pickedIndex = Number(e.detail && e.detail.value);
const safeIndex = Number.isFinite(pickedIndex) ? pickedIndex : 0;
const nextValue = this.data.gapOptions[safeIndex];
this.setData(
{
gapOptionIndex: safeIndex,
nodeGap: Number.isFinite(nextValue) ? nextValue : this.data.nodeGap
},
() => {
this.drawSankey();
}
);
},
/**
* 更新“预览边距”下拉值并立即重绘。
*/
onChangePadding(e) {
const pickedIndex = Number(e.detail && e.detail.value);
const safeIndex = Number.isFinite(pickedIndex) ? pickedIndex : 0;
const nextValue = this.data.paddingOptions[safeIndex];
this.setData(
{
paddingOptionIndex: safeIndex,
chartPadding: Number.isFinite(nextValue) ? nextValue : this.data.chartPadding
},
() => {
this.drawSankey();
}
);
},
/**
* 切换连线方向source->target / target->source
*/
onToggleDirection() {
this.setData(
{
direction:
this.data.direction === 'source-to-target' ? 'target-to-source' : 'source-to-target'
},
() => {
this.drawSankey();
}
);
},
/**
* 更新标签位置模式并重绘。
*/
onChangeLabelPosition(e) {
const pickedIndex = Number(e.detail && e.detail.value);
const safeIndex = Number.isFinite(pickedIndex) ? pickedIndex : 0;
const nextMode = this.data.labelPositionValues[safeIndex];
this.setData(
{
labelPositionIndex: safeIndex,
labelPositionMode: nextMode || 'inner'
},
() => {
this.drawSankey();
}
);
},
/**
* 更新汇聚对齐模式并重绘。
*/
onChangeTargetAlign(e) {
const pickedIndex = Number(e.detail && e.detail.value);
const safeIndex = Number.isFinite(pickedIndex) ? pickedIndex : 0;
const nextMode = this.data.targetAlignValues[safeIndex];
this.setData(
{
targetAlignIndex: safeIndex,
targetAlignMode: nextMode || 'between'
},
() => {
this.drawSankey();
}
);
},
/**
* 页面 ready 后绘制一次空白画布,避免首次显示延迟。
*/
onReady() {
this.drawSankey();
},
/**
* 上传数据文件并解析为表头与数据行。
*/
onChooseFile() {
const that = this;
wx.chooseMessageFile({
count: 1,
type: 'file',
extension: ['csv', 'xls', 'xlsx'],
success(res) {
const picked = res.tempFiles && res.tempFiles[0];
if (!picked) {
return;
}
const filePath = picked.path;
const fileName = picked.name || 'unknown.csv';
const extension = getFileExtension(fileName);
const isCsvFile = extension === 'csv';
const readOptions = {
filePath,
success(readRes) {
try {
const filePayload = isCsvFile ? String(readRes.data || '') : readRes.data;
const table = parseTableByFileName(fileName, filePayload);
that.applyParsedTable(table, fileName);
} catch (error) {
that.setData({
parseError: error && error.message ? error.message : '文件解析失败'
});
that.refreshInfoLogs();
}
},
fail(err) {
that.setData({
parseError: `读取文件失败: ${err && err.errMsg ? err.errMsg : '未知错误'}`
});
that.refreshInfoLogs();
}
};
if (isCsvFile) {
readOptions.encoding = 'utf8';
}
wx.getFileSystemManager().readFile(readOptions);
},
fail(err) {
if (err && String(err.errMsg || '').indexOf('cancel') >= 0) {
return;
}
that.setData({
parseError: `选择文件失败: ${err && err.errMsg ? err.errMsg : '未知错误'}`
});
that.refreshInfoLogs();
}
});
},
/**
* 应用解析结果,并按 Web 规则设置默认映射后触发重建。
*/
applyParsedTable(table, fileName) {
const headers = table.headers || [];
const rows = table.rows || [];
const sourceByName = findHeaderIndex(headers, ['data', 'value', '数据', '值']);
const sourceBySecondRow = findNumericColumnFromSecondRow(rows);
const sourceDescByName = findHeaderIndex(headers, ['source', '源']);
const targetDescByName = findHeaderIndex(headers, ['target', '目标']);
const sourceDataColumn =
sourceByName >= 0 ? sourceByName : sourceBySecondRow >= 0 ? sourceBySecondRow : null;
const sourceDescriptionColumns = sourceDescByName >= 0 ? [sourceDescByName] : [];
const targetDescriptionColumns = targetDescByName >= 0 ? [targetDescByName] : [];
this.setData(
{
uploadMessage: `已加载: ${fileName}${rows.length} 行)`,
parseError: '',
buildError: '',
columnHeaders: headers.length > 0 ? headers : ['列1', '列2', '列3'],
tableRows: rows,
sourceDataColumn,
sourceDescriptionColumns,
targetDescriptionColumns
},
() => {
this.rebuildSankey();
}
);
},
onSelectSourceData(e) {
const index = Number(e.currentTarget.dataset.index);
this.setData(
{
sourceDataColumn: Number.isNaN(index) ? null : index
},
() => {
this.rebuildSankey();
}
);
},
onToggleSourceDesc(e) {
const index = Number(e.currentTarget.dataset.index);
if (Number.isNaN(index)) {
return;
}
const current = this.data.sourceDescriptionColumns || [];
const exists = current.indexOf(index) >= 0;
const next = exists
? current.filter((item) => item !== index)
: current.concat(index).sort((a, b) => a - b);
this.setData(
{
sourceDescriptionColumns: next
},
() => {
this.rebuildSankey();
}
);
},
onToggleTargetDesc(e) {
const index = Number(e.currentTarget.dataset.index);
if (Number.isNaN(index)) {
return;
}
const current = this.data.targetDescriptionColumns || [];
const exists = current.indexOf(index) >= 0;
const next = exists
? current.filter((item) => item !== index)
: current.concat(index).sort((a, b) => a - b);
this.setData(
{
targetDescriptionColumns: next
},
() => {
this.rebuildSankey();
}
);
},
/**
* 根据当前映射重建聚合结果,输出统计与告警。
*/
rebuildSankey() {
const headers = this.data.columnHeaders || [];
const rows = this.data.tableRows || [];
if (rows.length === 0) {
this.setData({
nodesCount: 0,
linksCount: 0,
droppedRows: 0,
buildWarnings: [],
buildError: '',
sankeyLinks: [],
sankeyNodes: []
});
this.refreshInfoLogs();
this.drawSankey();
return;
}
try {
const result = buildSankeyData(
{
headers,
rows
},
{
sourceDataColumn: this.data.sourceDataColumn,
sourceDescriptionColumns: this.data.sourceDescriptionColumns || [],
targetDescriptionColumns: this.data.targetDescriptionColumns || [],
delimiter: '-'
}
);
this.setData({
nodesCount: result.nodes.length,
linksCount: result.links.length,
droppedRows: result.meta.droppedRows,
buildWarnings: (result.meta.warnings || []).slice(0, 8),
buildError: '',
sankeyLinks: result.links || [],
sankeyNodes: result.nodes || []
});
} catch (error) {
this.setData({
nodesCount: 0,
linksCount: 0,
droppedRows: 0,
buildWarnings: [],
buildError: error && error.message ? error.message : '聚合失败',
sankeyLinks: [],
sankeyNodes: []
});
}
this.refreshInfoLogs();
this.drawSankey();
},
/**
* 汇总日志到“信息日志”区域。
*/
refreshInfoLogs() {
const logs = [];
const rows = this.data.tableRows || [];
const headers = this.data.columnHeaders || [];
if (rows.length > 0) {
logs.push(`解析信息: 已加载 ${rows.length} 行,${headers.length}`);
logs.push(`解析信息: 已生成 ${this.data.nodesCount} 个节点,${this.data.linksCount} 条连线`);
} else {
logs.push('解析信息: 尚未加载数据文件');
}
if (this.data.parseError) {
logs.push(`错误: ${this.data.parseError}`);
}
if (this.data.buildError) {
logs.push(`错误: ${this.data.buildError}`);
}
if (this.data.droppedRows > 0) {
logs.push(`告警: 已跳过 ${this.data.droppedRows} 行异常数据`);
}
(this.data.buildWarnings || []).forEach((warning) => {
logs.push(`告警: ${warning}`);
});
this.setData({
infoLogs: logs.length > 0 ? logs : ['暂无日志']
});
},
/**
* 在小程序 canvas 上绘制简化版桑基图。
* 说明:
* - 不依赖第三方图表库,保证小程序端零额外依赖可运行
* - 布局规则尽量对齐 Web左 source、右 target、按值缩放高度
*/
drawSankey() {
const rawLinks = this.data.sankeyLinks || [];
const links = applyDirection(rawLinks, this.data.direction);
const query = wx.createSelectorQuery().in(this);
query.select('#sankeyCanvas').boundingClientRect();
query.exec((res) => {
const rect = res && res[0];
if (!rect || !rect.width || !rect.height) {
return;
}
const width = rect.width;
const height = rect.height;
const ctx = wx.createCanvasContext('sankeyCanvas', this);
ctx.clearRect(0, 0, width, height);
ctx.setFillStyle('#f7f8fa');
ctx.fillRect(0, 0, width, height);
const layout = buildSankeyLayout(links, width, height, {
nodeGap: this.data.nodeGap,
chartPadding: this.data.chartPadding,
targetAlignMode: this.data.targetAlignMode
});
if (!layout) {
ctx.setFillStyle('#86909c');
ctx.setFontSize(12);
ctx.fillText('暂无可预览数据', 12, 24);
ctx.draw();
return;
}
layout.linkSegments.forEach((segment) => {
ctx.setStrokeStyle('rgba(155,107,194,0.35)');
ctx.setLineWidth(segment.linkHeight);
ctx.setLineCap('round');
ctx.beginPath();
ctx.moveTo(segment.startX, segment.sy);
ctx.bezierCurveTo(
segment.controlX,
segment.sy,
segment.controlX,
segment.ty,
segment.endX,
segment.ty
);
ctx.stroke();
});
layout.sourceNames.forEach((name, index) => {
const node = layout.sourcePos[name];
const labelPlacement = getCanvasLabelPlacement(layout, true, this.data.labelPositionMode);
ctx.setFillStyle(getNodeColor(index));
ctx.fillRect(layout.leftX, node.y, layout.nodeWidth, node.h);
ctx.setFillStyle('#4e5969');
ctx.setFontSize(10);
ctx.setTextAlign(labelPlacement.textAlign);
ctx.setTextBaseline('middle');
ctx.fillText(name, labelPlacement.x, node.y + getNodeLabelCenterY(node.h));
});
layout.targetNames.forEach((name, index) => {
const node = layout.targetPos[name];
const labelPlacement = getCanvasLabelPlacement(layout, false, this.data.labelPositionMode);
ctx.setFillStyle(getNodeColor(index));
ctx.fillRect(layout.rightX, node.y, layout.nodeWidth, node.h);
ctx.setFillStyle('#4e5969');
ctx.setFontSize(10);
ctx.setTextAlign(labelPlacement.textAlign);
ctx.setTextBaseline('middle');
ctx.fillText(name, labelPlacement.x, node.y + getNodeLabelCenterY(node.h));
});
ctx.draw();
});
},
/**
* 小程序端目前仅支持 PNG 导出(保存到相册)。
*/
onExportPng() {
const that = this;
wx.canvasToTempFilePath(
{
canvasId: 'sankeyCanvas',
fileType: 'png',
success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success() {
wx.showToast({
title: 'PNG 已保存到相册',
icon: 'success'
});
},
fail(err) {
wx.showToast({
title: err && err.errMsg ? '保存失败,请检查相册权限' : '保存失败',
icon: 'none'
});
}
});
},
fail() {
wx.showToast({
title: '导出 PNG 失败',
icon: 'none'
});
}
},
that
);
},
/**
* 小程序 SVG 导出:
* - 使用与画布一致的布局算法生成 SVG 字符串
* - 写入用户数据目录,支持后续转发/分享
*/
onExportSvg() {
const rawLinks = this.data.sankeyLinks || [];
const links = applyDirection(rawLinks, this.data.direction);
if (!Array.isArray(links) || links.length === 0) {
wx.showToast({
title: '暂无可导出的数据',
icon: 'none'
});
return;
}
const query = wx.createSelectorQuery().in(this);
query.select('#sankeyCanvas').boundingClientRect();
query.exec((res) => {
const rect = res && res[0];
if (!rect || !rect.width || !rect.height) {
wx.showToast({
title: '导出 SVG 失败:画布尺寸无效',
icon: 'none'
});
return;
}
const svgText = buildSankeySvgText(links, rect.width, rect.height, {
nodeGap: this.data.nodeGap,
chartPadding: this.data.chartPadding,
labelPositionMode: this.data.labelPositionMode,
targetAlignMode: this.data.targetAlignMode
});
if (!svgText) {
wx.showToast({
title: '暂无可导出的数据',
icon: 'none'
});
return;
}
const svgFilePath = `${wx.env.USER_DATA_PATH}/sankey_${formatFileTimestamp()}.svg`;
wx.getFileSystemManager().writeFile({
filePath: svgFilePath,
data: svgText,
encoding: 'utf8',
success() {
wx.openDocument({
filePath: svgFilePath,
showMenu: true,
success() {
wx.showToast({
title: 'SVG 已导出',
icon: 'success'
});
},
fail() {
wx.setClipboardData({
data: svgFilePath,
success() {
wx.showToast({
title: 'SVG 已导出,路径已复制',
icon: 'none'
});
},
fail() {
wx.showToast({
title: 'SVG 已导出',
icon: 'success'
});
}
});
}
});
},
fail(err) {
wx.showToast({
title: `导出 SVG 失败: ${err && err.errMsg ? err.errMsg : '未知错误'}`,
icon: 'none'
});
}
});
});
}
});

View File

@@ -1,85 +1,149 @@
<view class="page">
<view class="header">
<image class="logo" src="/assets/icons/webicon.png" mode="aspectFill" />
<image class="title" src="/assets/icons/星程桑基图.svg" mode="widthFix" />
<image class="logo" src="../../assets/icons/webicon.png" mode="aspectFill" />
<image class="title" src="../../assets/icons/星程桑基图.svg" mode="widthFix" />
</view>
<view class="toolbar">
<view class="tool-item" bindtap="onToggleThemeSheet">
<text>选择主题</text>
<image class="tool-icon" src="/assets/icons/choose-color.svg" mode="aspectFit" />
<image class="tool-icon" src="../../assets/icons/choose-color.svg" mode="aspectFit" bindtap="onToggleThemeSheet" />
<image class="tool-icon upload-trigger" src="../../assets/icons/upload.svg" mode="aspectFit" bindtap="onChooseFile" />
<view class="upload-box" bindtap="onChooseFile">
<text class="upload-text">{{uploadMessage}}</text>
</view>
<view class="tool-item">
<image class="tiny-icon" src="/assets/icons/content.svg" mode="aspectFit" />
<text>文件上传</text>
<image class="tool-icon" src="/assets/icons/upload.svg" mode="aspectFit" />
</view>
<view class="toolbar-spacer" />
<view class="export-box">
<image class="export-main" src="/assets/icons/export.svg" mode="aspectFit" />
<image class="export-icon" src="/assets/icons/export-svg.svg" mode="aspectFit" />
<image class="export-icon" src="/assets/icons/export-png.svg" mode="aspectFit" />
<image class="export-main" src="../../assets/icons/export.svg" mode="aspectFit" />
<image class="export-icon" src="../../assets/icons/export-svg.svg" mode="aspectFit" bindtap="onExportSvg" />
<image class="export-icon" src="../../assets/icons/export-png.svg" mode="aspectFit" bindtap="onExportPng" />
</view>
</view>
<view class="preview-block">
<text class="block-title">效果预览</text>
<view class="preview-canvas" />
<view class="preview-panel">
<view class="preview-head">
<image class="preview-title" src="../../assets/icons/sankeyview.svg" mode="widthFix" />
<view class="preview-controls">
<view class="control-item">
<image class="control-icon" src="../../assets/icons/gap.svg" mode="aspectFit" />
<picker mode="selector" range="{{gapOptions}}" value="{{gapOptionIndex}}" bindchange="onChangeGap">
<view class="select-pill">
<text>{{gapOptions[gapOptionIndex]}}</text>
<image class="select-arrow" src="../../assets/icons/list.svg" mode="aspectFit" />
</view>
</picker>
</view>
<view class="bottom-grid">
<view class="block">
<text class="block-title">源数据</text>
<view class="field">
<view class="control-item">
<image class="control-icon" src="../../assets/icons/padding.svg" mode="aspectFit" />
<picker mode="selector" range="{{paddingOptions}}" value="{{paddingOptionIndex}}" bindchange="onChangePadding">
<view class="select-pill">
<text>{{paddingOptions[paddingOptionIndex]}}</text>
<image class="select-arrow" src="../../assets/icons/list.svg" mode="aspectFit" />
</view>
</picker>
</view>
<view class="direction-item" bindtap="onToggleDirection">
<view class="vertical-label">
<text>方</text>
<text>向</text>
</view>
<view class="direction-switch {{direction === 'source-to-target' ? 'on' : ''}}">
<text class="direction-text">{{direction === 'source-to-target' ? 'source' : 'target'}}</text>
<view class="direction-thumb" />
</view>
</view>
<view class="picker-item">
<view class="vertical-label">
<text>位</text>
<text>置</text>
</view>
<picker mode="selector" range="{{labelPositionOptionLabels}}" value="{{labelPositionIndex}}" bindchange="onChangeLabelPosition">
<view class="select-pill">
<text>{{labelPositionOptionLabels[labelPositionIndex]}}</text>
<image class="select-arrow" src="../../assets/icons/list.svg" mode="aspectFit" />
</view>
</picker>
</view>
<view class="picker-item">
<view class="vertical-label">
<text>对</text>
<text>齐</text>
</view>
<picker mode="selector" range="{{targetAlignOptionLabels}}" value="{{targetAlignIndex}}" bindchange="onChangeTargetAlign">
<view class="select-pill wide">
<text>{{targetAlignOptionLabels[targetAlignIndex]}}</text>
<image class="select-arrow" src="../../assets/icons/list.svg" mode="aspectFit" />
</view>
</picker>
</view>
</view>
</view>
<text wx:if="{{buildError}}" class="error-text">{{buildError}}</text>
<text wx:if="{{parseError}}" class="error-text">{{parseError}}</text>
<canvas class="preview-canvas" canvas-id="sankeyCanvas" id="sankeyCanvas" />
</view>
<view class="bottom-panels">
<view class="panel data-panel">
<image class="panel-title" src="../../assets/icons/data-select.svg" mode="widthFix" />
<view class="field-group">
<view class="field-title">
<image src="/assets/icons/expand.svg" mode="aspectFit" />
<text>数据</text>
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
<text>数据(link value)</text>
</view>
<view class="row" wx:for="{{sourceColumns}}" wx:key="*this">
<image src="/assets/icons/data.svg" mode="aspectFit" />
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onSelectSourceData">
<image src="../../assets/icons/data.svg" mode="aspectFit" />
<text class="label">{{item}}</text>
<image
src="{{sourceDataIndex === index ? '/assets/icons/radiobutton.svg' : '/assets/icons/radiobutton-no.svg'}}"
src="{{sourceDataColumn === index ? '../../assets/icons/radiobutton.svg' : '../../assets/icons/radiobutton-no.svg'}}"
mode="aspectFit"
/>
</view>
</view>
<view class="field">
<view class="field-group">
<view class="field-title">
<image src="/assets/icons/expand.svg" mode="aspectFit" />
<text>描述列</text>
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
<text>源标签(Source label)</text>
</view>
<view class="row" wx:for="{{sourceColumns}}" wx:key="*this">
<image src="/assets/icons/description.svg" mode="aspectFit" />
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleSourceDesc">
<image src="../../assets/icons/description.svg" mode="aspectFit" />
<text class="label">{{item}}</text>
<image
src="{{sourceDescChecked.indexOf(index) > -1 ? '/assets/icons/checkbox.svg' : '/assets/icons/checkbox-no.svg'}}"
src="{{sourceDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
mode="aspectFit"
/>
</view>
</view>
<view class="field-group">
<view class="field-title">
<image src="../../assets/icons/expand.svg" mode="aspectFit" />
<text>目标标签(target label)</text>
</view>
<view class="row" wx:for="{{columnHeaders}}" wx:key="*this" data-index="{{index}}" bindtap="onToggleTargetDesc">
<image src="../../assets/icons/description.svg" mode="aspectFit" />
<text class="label">{{item}}</text>
<image
src="{{targetDescriptionColumns.indexOf(index) > -1 ? '../../assets/icons/checkbox.svg' : '../../assets/icons/checkbox-no.svg'}}"
mode="aspectFit"
/>
</view>
</view>
</view>
<view class="block">
<text class="block-title">目标数据</text>
<view class="field">
<view class="field-title">
<image src="/assets/icons/expand.svg" mode="aspectFit" />
<text>描述列</text>
</view>
<view class="row" wx:for="{{targetColumns}}" wx:key="*this">
<image src="/assets/icons/description.svg" mode="aspectFit" />
<text class="label">{{item}}</text>
<image
src="{{targetDescChecked.indexOf(index) > -1 ? '/assets/icons/checkbox.svg' : '/assets/icons/checkbox-no.svg'}}"
mode="aspectFit"
/>
</view>
<view class="panel log-panel">
<image class="panel-title panel-title-log" src="../../assets/icons/information.svg" mode="widthFix" />
<view class="log-list">
<text class="log-item" wx:for="{{infoLogs}}" wx:key="index">{{item}}</text>
</view>
</view>
</view>
@@ -91,7 +155,7 @@
<text class="theme-title">选择配色主题</text>
<view class="theme-row" wx:for="{{4}}" wx:key="index">
<image
src="{{selectedThemeIndex === index ? '/assets/icons/radiobutton.svg' : '/assets/icons/radiobutton-no.svg'}}"
src="{{selectedThemeIndex === index ? '../../assets/icons/radiobutton.svg' : '../../assets/icons/radiobutton-no.svg'}}"
mode="aspectFit"
/>
<view class="theme-bar" />

View File

@@ -1,5 +1,8 @@
.page {
min-height: 100vh;
padding: 8px;
background: #f3f4f6;
box-sizing: border-box;
}
.header {
@@ -13,50 +16,77 @@
width: 48px;
height: 48px;
border-radius: 12px;
flex-shrink: 0;
}
.title {
width: 160px;
height: auto;
}
.toolbar {
margin-top: 8px;
height: 38px;
display: flex;
align-items: center;
gap: 6px;
}
.tool-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
padding-left: 6px;
}
.tool-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.tiny-icon {
width: 10px;
height: 10px;
.upload-trigger {
width: 32px;
}
.upload-box {
flex: 1;
min-width: 0;
height: 28px;
border-radius: 6px;
border: 0.5px solid #4e5969;
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
box-sizing: border-box;
}
.upload-text {
width: 100%;
color: #4e5969;
font-size: 10px;
line-height: 1;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toolbar-spacer {
flex: 1;
min-width: 0;
}
.export-box {
margin-left: auto;
background: #fff;
border: 1px solid #e5e6eb;
border: 0.5px solid #e5e6eb;
border-radius: 4px;
padding: 2px 4px;
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.export-main {
width: 10px;
height: 20px;
width: 9px;
height: 21px;
}
.export-icon {
@@ -64,44 +94,193 @@
height: 24px;
}
.preview-block,
.block {
.preview-panel {
margin-top: 8px;
border: 1px solid #fbaca3;
border-radius: 8px;
border: 0.4px solid #f99595;
border-radius: 6.4px;
background: #fff;
padding: 4px;
padding: 3.2px;
box-sizing: border-box;
}
.block-title {
font-size: 16px;
.preview-head {
display: flex;
align-items: center;
gap: 4px;
}
.preview-title {
width: 166px;
height: auto;
flex-shrink: 0;
}
.preview-controls {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 4px;
overflow-x: auto;
overflow-y: hidden;
}
.preview-controls::-webkit-scrollbar {
display: none;
}
.control-item,
.picker-item,
.direction-item {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.control-icon {
width: 14px;
height: 14px;
}
.select-pill {
height: 14px;
border: 0.5px solid #c9aee0;
border-radius: 4px;
padding: 0 2px;
display: flex;
align-items: center;
gap: 2px;
color: #606060;
font-size: 12px;
line-height: 1;
box-sizing: border-box;
}
.select-pill.wide {
min-width: 40px;
}
.select-arrow {
width: 10px;
height: 8px;
}
.vertical-label {
display: flex;
flex-direction: column;
color: #000;
font-size: 12px;
font-weight: 600;
line-height: 1;
}
.direction-switch {
min-width: 43px;
height: 14px;
border-radius: 58px;
background: #c9cdd4;
padding: 1px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1px;
}
.direction-switch.on {
background: #9b6bc2;
}
.direction-text {
color: #4e5969;
font-size: 8px;
line-height: 1;
padding: 0 1px;
}
.direction-switch.on .direction-text {
color: #fff;
}
.direction-thumb {
width: 11.67px;
height: 11.67px;
border-radius: 999px;
background: #fff;
}
.direction-switch.on .direction-thumb {
margin-left: 0;
}
.direction-switch:not(.on) {
flex-direction: row-reverse;
}
.error-text {
margin-top: 4px;
color: #cb272d;
font-size: 12px;
}
.preview-canvas {
margin-top: 4px;
min-height: 300px;
background: #f7f8fa;
margin-top: 3px;
width: 100%;
height: 371px;
border-radius: 4px;
background: #f7f8fa;
}
.bottom-grid {
.bottom-panels {
margin-top: 8px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.field {
margin-top: 8px;
.panel {
border: 1px solid #f7e0e0;
border-radius: 16px;
background: #fff;
padding: 8px;
box-sizing: border-box;
}
.data-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.log-panel {
display: flex;
flex-direction: column;
gap: 8px;
}
.panel-title {
width: 128px;
height: auto;
}
.panel-title-log {
width: 123px;
}
.field-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-title {
display: flex;
align-items: center;
gap: 4px;
color: #1d2129;
font-size: 14px;
font-weight: 600;
font-weight: 500;
}
.field-title image {
@@ -114,32 +293,54 @@
border-bottom: 1px solid #c9cdd4;
display: flex;
align-items: center;
gap: 6px;
padding-bottom: 6px;
margin-top: 4px;
gap: 8px;
padding-bottom: 8px;
box-sizing: border-box;
}
.row image {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.label {
flex: 1;
min-width: 0;
color: #86909c;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.log-list {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 6px;
overflow: auto;
}
.log-item {
color: #1d2129;
font-size: 12px;
line-height: 1.4;
}
.footer {
margin-top: 8px;
margin-top: 6px;
color: #86909c;
font-size: 14px;
line-height: 1.3;
}
.theme-sheet-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.2);
z-index: 10;
}
.theme-sheet {
@@ -150,6 +351,7 @@
background: #fff;
border-radius: 16px 16px 0 0;
padding: 12px;
z-index: 11;
}
.theme-title {

View File

@@ -1,4 +1,9 @@
{
"desc": "星程桑基图小程序",
"rules": []
"rules": [
{
"action": "allow",
"page": "*"
}
]
}

320
miniapp/utils/sankey.js Normal file
View File

@@ -0,0 +1,320 @@
/**
* 统一清洗文本,避免空格导致节点重复。
*/
function normalizeText(value) {
return String(value || '').trim();
}
/**
* 尝试懒加载 xlsx 解析库。
* 说明:
* - 小程序若未完成 npm 构建,此处会拿不到模块
* - 使用缓存避免每次解析都重复 require
*/
let cachedXlsxModule;
function getXlsxModule() {
if (cachedXlsxModule !== undefined) {
return cachedXlsxModule;
}
try {
cachedXlsxModule = require('xlsx');
} catch (error) {
cachedXlsxModule = null;
}
return cachedXlsxModule;
}
/**
* 将二维数组统一整理为 headers + rows 结构。
* 约定第一行为表头,后续行为数据行。
*/
function toRawTable(rows) {
if (!Array.isArray(rows) || rows.length === 0) {
return { headers: [], rows: [] };
}
const firstRow = Array.isArray(rows[0]) ? rows[0] : [];
const maxColumns = rows.reduce((max, row) => {
const length = Array.isArray(row) ? row.length : 0;
return Math.max(max, length);
}, firstRow.length);
const headers = Array.from({ length: maxColumns }, (_, index) => {
const header = normalizeText(firstRow[index] || '');
return header || `${index + 1}`;
});
const dataRows = rows.slice(1).map((row) => {
const safeRow = Array.isArray(row) ? row : [];
return Array.from({ length: maxColumns }, (_, index) => normalizeText(safeRow[index] || ''));
});
return { headers, rows: dataRows };
}
/**
* 解析数字,支持千分位。
*/
function parseNumericValue(text) {
const normalized = normalizeText(text).replace(/,/g, '');
if (!normalized) {
return null;
}
const parsed = Number(normalized);
if (Number.isNaN(parsed)) {
return null;
}
return parsed;
}
/**
* 将单元格值格式化为日志可读文本。
*/
function formatCellValueForWarning(value) {
const text = String(value || '');
return text.length > 0 ? text : '(空)';
}
/**
* 组装“列位置 + 列名 + 原始值”的调试文本。
*/
function buildColumnDebugText(row, headers, columns) {
if (!Array.isArray(columns) || columns.length === 0) {
return '未选择列';
}
return columns
.map((columnIndex) => {
const headerName = headers[columnIndex] || `${columnIndex + 1}`;
const rawValue = row[columnIndex] || '';
return `${columnIndex + 1} 列(${headerName}="${formatCellValueForWarning(rawValue)}"`;
})
.join('');
}
/**
* 简单 CSV 解析(支持双引号与双引号转义)。
*/
function parseCsvText(csvText) {
const text = String(csvText || '').replace(/^\uFEFF/, '');
const rows = [];
let row = [];
let cell = '';
let inQuotes = false;
for (let i = 0; i < text.length; i += 1) {
const ch = text[i];
const next = text[i + 1];
if (ch === '"') {
if (inQuotes && next === '"') {
cell += '"';
i += 1;
} else {
inQuotes = !inQuotes;
}
continue;
}
if (!inQuotes && ch === ',') {
row.push(cell);
cell = '';
continue;
}
if (!inQuotes && (ch === '\n' || ch === '\r')) {
if (ch === '\r' && next === '\n') {
i += 1;
}
row.push(cell);
cell = '';
rows.push(row);
row = [];
continue;
}
cell += ch;
}
row.push(cell);
rows.push(row);
const normalizedRows = rows
.map((items) => items.map((item) => normalizeText(item)))
.filter((items) => items.some((item) => item.length > 0));
return toRawTable(normalizedRows);
}
/**
* 解析 xls/xlsx 二进制内容。
*/
function parseXlsxBuffer(buffer) {
const xlsx = getXlsxModule();
if (!xlsx) {
throw new Error('当前环境未启用 xlsx 解析,请先在开发者工具执行“构建 npm”');
}
const workbook = xlsx.read(buffer, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
if (!firstSheetName) {
throw new Error('Excel 文件中没有工作表');
}
const sheet = workbook.Sheets[firstSheetName];
const rows = xlsx.utils.sheet_to_json(sheet, {
header: 1,
raw: false,
defval: ''
});
return toRawTable(rows);
}
/**
* 按文件名后缀自动分流解析器。
*/
function parseTableByFileName(fileName, payload) {
const lowerName = String(fileName || '').toLowerCase();
if (lowerName.endsWith('.csv')) {
return parseCsvText(String(payload || ''));
}
if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) {
return parseXlsxBuffer(payload);
}
throw new Error('仅支持 .csv / .xlsx / .xls 文件');
}
/**
* 构建 source 名称。
*/
function buildSourceName(row, config) {
if (!Array.isArray(config.sourceDescriptionColumns) || config.sourceDescriptionColumns.length === 0) {
return normalizeText(row[config.sourceDataColumn] || '');
}
const parts = config.sourceDescriptionColumns
.map((column) => normalizeText(row[column] || ''))
.filter((item) => item.length > 0);
return parts.join(config.delimiter || '-');
}
/**
* 构建 target 名称,支持向下补全。
*/
function buildTargetName(row, config, lastNonEmptyTargetValueByColumn) {
const parts = (config.targetDescriptionColumns || [])
.map((column) => {
const raw = normalizeText(row[column] || '');
if (raw.length > 0) {
lastNonEmptyTargetValueByColumn[column] = raw;
return raw;
}
return lastNonEmptyTargetValueByColumn[column] || '';
})
.filter((item) => item.length > 0);
return parts.join(config.delimiter || '-');
}
/**
* 与 Web 端保持一致的聚合规则。
*/
function buildSankeyData(table, config) {
if (config.sourceDataColumn === null || config.sourceDataColumn === undefined) {
throw new Error('必须选择源数据列');
}
if (!Array.isArray(config.targetDescriptionColumns) || config.targetDescriptionColumns.length === 0) {
throw new Error('必须至少选择一个目标描述列');
}
const sourceDataColumnIndex = config.sourceDataColumn;
const sourceDataColumnName = table.headers[sourceDataColumnIndex] || `${sourceDataColumnIndex + 1}`;
const linkValueMap = {};
const warnings = [];
let droppedRows = 0;
const lastNonEmptyTargetValueByColumn = {};
(table.rows || []).forEach((row, rowIndex) => {
const excelRow = rowIndex + 2;
const sourceCellRaw = row[sourceDataColumnIndex] || '';
const sourceValue = parseNumericValue(sourceCellRaw);
if (sourceValue === null) {
warnings.push(
`${excelRow} 行, 第 ${sourceDataColumnIndex + 1} 列(${sourceDataColumnName}: 源数据不是有效数字,原始值="${formatCellValueForWarning(sourceCellRaw)}",已跳过`
);
droppedRows += 1;
return;
}
const sourceName = buildSourceName(row, config);
if (!sourceName) {
warnings.push(
`${excelRow} 行: 源描述为空,字段=${buildColumnDebugText(
row,
table.headers || [],
config.sourceDescriptionColumns || []
)},已跳过`
);
droppedRows += 1;
return;
}
const targetName = buildTargetName(row, config, lastNonEmptyTargetValueByColumn);
if (!targetName) {
warnings.push(
`${excelRow} 行: 目标描述为空,字段=${buildColumnDebugText(
row,
table.headers || [],
config.targetDescriptionColumns || []
)},且无可继承的上方值,已跳过`
);
droppedRows += 1;
return;
}
const key = `${sourceName}@@${targetName}`;
linkValueMap[key] = (linkValueMap[key] || 0) + sourceValue;
});
const links = [];
const sourceSet = {};
const targetSet = {};
Object.keys(linkValueMap).forEach((key) => {
const pair = key.split('@@');
const source = pair[0];
const target = pair[1];
if (!source || !target) {
return;
}
sourceSet[source] = true;
targetSet[target] = true;
links.push({
source,
target,
value: linkValueMap[key]
});
});
const nodes = [];
Object.keys(sourceSet).forEach((name) => {
nodes.push({ name, kind: 'source' });
});
Object.keys(targetSet).forEach((name) => {
if (!sourceSet[name]) {
nodes.push({ name, kind: 'target' });
}
});
return {
nodes,
links,
meta: {
droppedRows,
warnings
}
};
}
module.exports = {
parseCsvText,
parseXlsxBuffer,
parseTableByFileName,
buildSankeyData
};

View File

@@ -1,4 +1,4 @@
[更新时间] 2026-02-13次更新)
[更新时间] 2026-02-13次更新)
[项目] 星程桑基图
一、已完成Done
@@ -13,7 +13,7 @@
5. 已实现导出PNG/SVG带时间戳命名
6. 已实现默认样例加载:页面首次进入自动读取 `data/example0.xlsx`。
7. 已有核心单测parser + sankey 聚合 + xlsx 读取)。
8. 小程序端已完成视觉骨架(非完整业务)。
8. 小程序端已视觉骨架升级为可用流程(上传 -> 映射 -> 预览 -> 导出)。
9. 已实现本地持久化:用户上传文件、映射配置与预览选项会写入 localStorage刷新后自动恢复。
10. 已新增“汇聚对齐”配置Between/Middle/Top/Bottom可控制 target 侧对齐,且 gap 作为源侧基准。
11. 已优化“无配置初始化映射”:优先按表头别名自动匹配,缺失时按第二行首个数字列兜底。
@@ -31,6 +31,19 @@
20. 已完成手机端控件压缩:隐藏上传文本框;工具条单行滚动显示;预览区 gap/padding 改为下拉0-30 步长 5 / 0-80 步长 10方向改为“源/目标”;标签位置改为“内/外/左/右”;汇聚对齐改为“两端/中间/顶部/底部”;控件尽量单行展示。
21. 已强制手机端仅使用下拉控件模式:手机视口不渲染 gap/padding 滑动条与方向拨杆,仅渲染下拉选择,避免样式条件失效时回退到滑动条模式。
22. 已扩展到平板单行模式:`<=1024px` 下工具条与预览控件均单行展示(不换行,超出横向滚动),并压缩滑动条宽度以提升一行容纳能力。
23. 已按 Figma(3764:138) 调整 switch 与下拉样式switch 改为紧凑胶囊尺寸;下拉统一替换 `list.svg` 箭头并改为 hug/fit-content 宽度,控件宽度随内容变化。
24. 已恢复手机端上传文本框显示,并缩窄为单行可容纳宽度(固定窄幅 + 文本省略),保持工具条一行布局。
25. 已进一步压缩手机工具条以避免整体溢出:隐藏工具标签文本、缩小主题/上传按钮与导出按钮、上传框改为弹性窄宽度,工具条取消横向滚动并保持单行。
26. 已继续收紧 switch/下拉宽度:移除 switch 最小宽度与手机下拉最小宽度统一按内容自适应hug显示。
27. 已恢复手机端 `export.svg` 主图标显示,并缩小尺寸以兼顾单行工具条布局。
28. 已为顶部工具栏所有 SVG 图标补充悬停提示title与按钮 aria-label鼠标悬停可显示操作含义。
29. 已为预览框区域 SVG预览标题、gap/padding 图标)补充悬停提示,并为相关控件补充 aria-label。
30. 已统一 pad 与手机的 gap/padding 调节方式:`<=1024px` 统一使用下拉框(与手机一致),仅桌面保留滑动条。
31. 已启动小程序可用化第一阶段:支持 CSV 上传、默认列自动映射、列选择交互、聚合统计与信息日志展示;聚合规则与 Web 端核心逻辑对齐。
32. 已完成小程序第二阶段(基础渲染与导出):接入原生 canvas 桑基图绘制、PNG 导出到相册。
33. 已完成小程序第三阶段(格式与导出补齐):
- 上传解析支持 `csv/xls/xlsx` 三种格式。
- 小程序端接入真实 SVG 导出:按当前布局生成 SVG 文件并写入用户目录,优先尝试打开,失败时复制文件路径。
二、当前状态In Progress
1. 无进行中的代码重构任务。
@@ -46,12 +59,12 @@
1. 本地持久化基于 localStorage受浏览器容量限制超大文件可能无法完整保存。
2. Vite 开发配置依赖本机 HTTPS 证书路径,换机器可能无法直接启动。
3. 当前“目标数据”无独立数值列,数值始终来自 source data 列;若未来业务需要需先改 PRD。
4. 小程序仅骨架,尚未接入真实解析、渲染与导出
4. 小程序 xlsx 解析依赖开发者工具“构建 npm”若未构建将无法读取 xls/xlsx 文件
5. 信息日志当前仅展示最近告警(前 8 条),若后续需要完整历史需引入分页或虚拟滚动。
四、下一步建议Next
1. 决策是否引入“目标数值列”能力(先更新 PRD 后实现)。
2. 小程序由骨架升级为可用版本(优先复用 `src/core`)。
2. 小程序下一阶段:补充持久化恢复(上传文件与映射配置刷新不丢)。
3. 补充更多异常用例测试(空文件、超大文件、乱码表头、极端数值)。
4. 评估并处理 dev HTTPS 证书本地耦合问题,降低新环境接入成本。
5. 评估“信息日志”是否需要支持导出或清空操作。

19
project.config.json Normal file
View File

@@ -0,0 +1,19 @@
{
"description": "sankey miniapp project config",
"packOptions": {
"ignore": []
},
"setting": {
"es6": true,
"enhance": true,
"postcss": true,
"minified": true
},
"compileType": "miniprogram",
"libVersion": "trial",
"appid": "wxcf0f89b6eb65759e",
"projectname": "sankey",
"miniprogramRoot": "miniapp/",
"srcMiniprogramRoot": "miniapp/",
"condition": {}
}

View File

@@ -9,8 +9,14 @@
<div class="toolbar">
<div ref="themeTriggerRef" class="tool-item theme-trigger">
<span class="tool-label">选择主题</span>
<button class="icon-btn" type="button" @click.stop="toggleThemePicker">
<img :src="iconChooseColor" alt="choose-color" />
<button
class="icon-btn"
type="button"
title="选择主题"
aria-label="选择主题"
@click.stop="toggleThemePicker"
>
<img :src="iconChooseColor" alt="choose-color" title="选择主题" />
</button>
<div
@@ -56,8 +62,14 @@
<div class="tool-item">
<span class="tool-label">文件上传</span>
<button class="icon-btn" type="button" @click="openFileDialog">
<img :src="iconUpload" alt="upload" />
<button
class="icon-btn"
type="button"
title="上传文件"
aria-label="上传文件"
@click="openFileDialog"
>
<img :src="iconUpload" alt="upload" title="上传文件" />
</button>
</div>
@@ -79,12 +91,24 @@
</label>
<div class="export-box">
<img :src="iconExport" alt="export" class="export-main" />
<button class="icon-btn export-item" type="button" @click="exportSvg">
<img :src="iconExportSvg" alt="export-svg" />
<img :src="iconExport" alt="export" class="export-main" title="导出" />
<button
class="icon-btn export-item"
type="button"
title="导出 SVG"
aria-label="导出 SVG"
@click="exportSvg"
>
<img :src="iconExportSvg" alt="export-svg" title="导出 SVG" />
</button>
<button class="icon-btn export-item" type="button" @click="exportPng">
<img :src="iconExportPng" alt="export-png" />
<button
class="icon-btn export-item"
type="button"
title="导出 PNG"
aria-label="导出 PNG"
@click="exportPng"
>
<img :src="iconExportPng" alt="export-png" title="导出 PNG" />
</button>
</div>
</div>
@@ -215,21 +239,26 @@
<section class="panel preview-panel">
<div class="preview-head">
<img :src="iconSankeyViewTitle" alt="桑基图预览" class="panel-title-svg panel-title-preview" />
<img
:src="iconSankeyViewTitle"
alt="桑基图预览"
title="桑基图预览"
class="panel-title-svg panel-title-preview"
/>
<div class="preview-controls">
<template v-if="isPhoneViewport">
<label class="compact-control mobile-only">
<img :src="iconGap" alt="间距" class="slider-icon" />
<select v-model.number="nodeGap" class="compact-select">
<template v-if="isNarrowViewport">
<label class="compact-control">
<img :src="iconGap" alt="间距" title="间距" class="slider-icon" />
<select v-model.number="nodeGap" class="compact-select" aria-label="间距">
<option v-for="value in mobileGapOptions" :key="`gap-${value}`" :value="value">
{{ value }}
</option>
</select>
</label>
<label class="compact-control mobile-only">
<img :src="iconPadding" alt="边距" class="slider-icon" />
<select v-model.number="chartPadding" class="compact-select">
<label class="compact-control">
<img :src="iconPadding" alt="边距" title="边距" class="slider-icon" />
<select v-model.number="chartPadding" class="compact-select" aria-label="边距">
<option
v-for="value in mobilePaddingOptions"
:key="`padding-${value}`"
@@ -239,22 +268,10 @@
</option>
</select>
</label>
<label class="compact-control mobile-only">
<span class="compact-label">方向</span>
<select v-model="direction" class="compact-select">
<option
v-for="option in directionOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
</template>
<template v-else>
<label class="slider-label desktop-only">
<img :src="iconGap" alt="间距" class="slider-icon" />
<img :src="iconGap" alt="间距" title="间距" class="slider-icon" />
<div class="slider-track-wrap">
<span class="slider-value" :style="getSliderValueStyle(nodeGap, 0, 30)">{{
nodeGap
@@ -266,11 +283,12 @@
type="range"
min="0"
max="30"
aria-label="间距"
/>
</div>
</label>
<label class="slider-label desktop-only">
<img :src="iconPadding" alt="边距" class="slider-icon" />
<img :src="iconPadding" alt="边距" title="边距" class="slider-icon" />
<div class="slider-track-wrap">
<span class="slider-value" :style="getSliderValueStyle(chartPadding, 0, 80)">
{{ chartPadding }}
@@ -282,9 +300,26 @@
type="range"
min="0"
max="80"
aria-label="边距"
/>
</div>
</label>
</template>
<template v-if="isPhoneViewport">
<label class="compact-control mobile-only">
<span class="compact-label">方向</span>
<select v-model="direction" class="compact-select" aria-label="方向">
<option
v-for="option in directionOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</label>
</template>
<template v-else>
<button class="direction-control desktop-only" type="button" @click="toggleDirection">
<span class="direction-label">方向</span>
<span class="direction-switch" :class="{ on: direction === 'source-to-target' }">
@@ -440,6 +475,7 @@ const buildResult = ref<SankeyBuildResult | null>(null);
const uploadedFileSnapshot = ref<PersistedUploadedFile | null>(null);
const isRestoringWorkspace = ref(false);
const isPhoneViewport = ref(false);
const isNarrowViewport = ref(false);
const mapping = reactive<MappingConfig>({
sourceDataColumn: null,
@@ -900,6 +936,7 @@ function updatePhoneViewportFlag(): void {
}
isPhoneViewport.value = window.matchMedia('(max-width: 640px)').matches;
isNarrowViewport.value = window.matchMedia('(max-width: 1024px)').matches;
}
/**

View File

@@ -548,11 +548,12 @@ body {
.direction-control {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 4px;
border: 0;
background: transparent;
padding: 0;
cursor: pointer;
flex-shrink: 0;
}
.direction-label {
@@ -565,29 +566,31 @@ body {
.direction-switch {
display: inline-flex;
align-items: center;
gap: 2px;
width: 80px;
height: 24px;
padding: 2px;
border-radius: 999px;
gap: 1px;
min-width: 0;
width: auto;
height: 14px;
padding: 1px;
border-radius: 58.333px;
background: var(--fill-3);
justify-content: space-between;
}
.direction-switch-text {
font-size: 14px;
font-size: 8px;
line-height: 1;
padding: 0 4px;
padding: 0 1px;
white-space: nowrap;
color: var(--text-4);
}
.direction-switch-thumb {
width: 20px;
height: 20px;
width: 11.667px;
height: 11.667px;
border-radius: 50%;
background: #fff;
border: 1px solid var(--fill-3);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
border: 0;
box-shadow: none;
}
.direction-switch.on {
@@ -617,15 +620,7 @@ body {
}
.label-position-select {
min-width: 64px;
height: 24px;
border: 1px solid #c9aee0;
border-radius: 2px;
background: #fff;
color: #1d2129;
font-size: 14px;
padding: 0 20px 0 6px;
line-height: 22px;
min-width: 0;
}
.target-align-control {
@@ -643,15 +638,7 @@ body {
}
.target-align-select {
min-width: 88px;
height: 24px;
border: 1px solid #c9aee0;
border-radius: 2px;
background: #fff;
color: #1d2129;
font-size: 14px;
padding: 0 20px 0 6px;
line-height: 22px;
min-width: 0;
}
.compact-control {
@@ -669,15 +656,29 @@ body {
}
.compact-select {
height: 24px;
min-width: 52px;
border: 1px solid #c9aee0;
border-radius: 2px;
min-width: 0;
}
.label-position-select,
.target-align-select,
.compact-select {
width: auto;
inline-size: max-content;
height: 14px;
border: 0.5px solid #c9aee0;
border-radius: 4px;
background: #fff;
color: #1d2129;
font-size: 12px;
padding: 0 16px 0 4px;
line-height: 22px;
line-height: 1;
padding: 0 14px 0 4px;
appearance: none;
-webkit-appearance: none;
background-image: url('../assets/icons/list.svg');
background-repeat: no-repeat;
background-position: right 2px center;
background-size: 10px 8px;
flex-shrink: 0;
}
.example-line {
@@ -859,7 +860,19 @@ body {
}
.upload-area {
display: none;
display: flex;
min-width: 92px;
max-width: none;
flex: 1 1 auto;
min-height: 28px;
padding: 2px 5px;
}
.upload-text {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.top-bar {
@@ -870,14 +883,12 @@ body {
.toolbar {
width: 100%;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
overflow: hidden;
justify-content: flex-start;
gap: 6px;
scrollbar-width: none;
gap: 4px;
}
.toolbar::-webkit-scrollbar {
.tool-item .tool-label {
display: none;
}
@@ -886,6 +897,27 @@ body {
flex-shrink: 0;
}
.tool-item .icon-btn {
width: 28px;
height: 28px;
}
.export-box {
padding: 1px 4px;
gap: 4px;
}
.export-main {
display: block;
width: 12px;
height: 24px;
}
.export-item {
width: 28px;
height: 28px;
}
.content {
gap: 6px;
}
@@ -948,13 +980,13 @@ body {
}
.label-position-select {
min-width: 46px;
min-width: 0;
font-size: 12px;
padding: 0 14px 0 4px;
}
.target-align-select {
min-width: 58px;
min-width: 0;
font-size: 12px;
padding: 0 14px 0 4px;
}

44
tests/miniapp.spec.ts Normal file
View File

@@ -0,0 +1,44 @@
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
const require = createRequire(import.meta.url);
const { parseTableByFileName, parseXlsxBuffer } = require('../miniapp/utils/sankey.js') as {
parseTableByFileName: (fileName: string, payload: string | ArrayBuffer) => {
headers: string[];
rows: string[][];
};
parseXlsxBuffer: (buffer: ArrayBuffer) => {
headers: string[];
rows: string[][];
};
};
describe('miniapp utils sankey', () => {
it('按文件后缀分流解析 csv', () => {
const table = parseTableByFileName('demo.csv', 'source,value,target\nA,12,T1');
expect(table.headers).toEqual(['source', 'value', 'target']);
expect(table.rows).toEqual([['A', '12', 'T1']]);
});
it('按文件后缀分流解析 xlsx', () => {
const filePath = resolve(process.cwd(), 'data/example0.xlsx');
const buffer = readFileSync(filePath);
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
const table = parseTableByFileName('demo.xlsx', arrayBuffer);
expect(table.headers.length).toBeGreaterThan(1);
expect(table.rows.length).toBeGreaterThan(0);
});
it('直接解析 xlsx buffer', () => {
const filePath = resolve(process.cwd(), 'data/example0.xlsx');
const buffer = readFileSync(filePath);
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
const table = parseXlsxBuffer(arrayBuffer);
expect(table.headers.length).toBeGreaterThan(1);
expect(table.rows.length).toBeGreaterThan(0);
});
});