├── resource ├── aippt.gif ├── step0.png ├── step1.png ├── step2.png ├── step3.png ├── step4.png ├── step5.png ├── ppt2json.png ├── contact_me_qr.png ├── ppt2json_edit.png └── ppt2json_download.png ├── static ├── esm-js │ └── README.md ├── base64js.js ├── cover.js ├── sse.js ├── i18n.js ├── element.js ├── jsoneditor.min.css ├── img │ └── jsoneditor-icons.svg └── chart.js ├── .gitignore ├── server └── README.md ├── README.md ├── README_EN.md ├── ppt2json.html └── index.html /resource/aippt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/aippt.gif -------------------------------------------------------------------------------- /resource/step0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step0.png -------------------------------------------------------------------------------- /resource/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step1.png -------------------------------------------------------------------------------- /resource/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step2.png -------------------------------------------------------------------------------- /resource/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step3.png -------------------------------------------------------------------------------- /resource/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step4.png -------------------------------------------------------------------------------- /resource/step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/step5.png -------------------------------------------------------------------------------- /resource/ppt2json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/ppt2json.png -------------------------------------------------------------------------------- /resource/contact_me_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/contact_me_qr.png -------------------------------------------------------------------------------- /resource/ppt2json_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/ppt2json_edit.png -------------------------------------------------------------------------------- /resource/ppt2json_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veasion/AiPPT/HEAD/resource/ppt2json_download.png -------------------------------------------------------------------------------- /static/esm-js/README.md: -------------------------------------------------------------------------------- 1 | # js 模块化 2 | 3 | 4 | 5 | aippt 项目是纯 js 代码写的(非模块化),这里专门针对模块化改写了 js 文件。 6 | 7 | 8 | 9 | 依赖外部模块如下: 10 | 11 | ```json 12 | { 13 | "dependencies": { 14 | "@types/pako": "^2.0.3", 15 | "pako": "^2.1.0", 16 | "base64-js": "^1.5.1", 17 | "marked": "^13.0.2" 18 | } 19 | } 20 | ``` 21 | 22 | 23 | 24 | 在这个基础上我通过 vue 和 react 重写了 aippt 项目作为参考: 25 | 26 | vue: 27 | 28 | https://github.com/veasion/aippt-vue 29 | 30 | react: 31 | 32 | https://github.com/veasion/aippt-react 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/ 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # 关于服务端代码 2 | 3 | 目前该项目仅开源了前端PPT渲染引擎代码,服务端代码暂未开放 4 | 5 | 可以先通过开放平台API/UI方式接入,后续我们会在B站上录制讲解课程以及开放相关技术路线,敬请期待! 6 | 7 | 8 | 9 | 我们支持私有化部署 10 | 11 | 官网在线体验(开放API):https://docmee.cn 12 | 13 | 开放平台(API/UI 接入): https://docmee.cn/open-platform 14 | 15 | 16 | 17 | API SDK DEMO: 18 | 19 | * iframe demo 20 | 21 | https://github.com/veasion/aippt-ui-iframe 22 | 23 | * js demo 24 | 25 | https://github.com/veasion/aippt 26 | 27 | * vue demo 28 | 29 | https://github.com/veasion/aippt-vue 30 | 31 | * react demo 32 | 33 | https://github.com/veasion/aippt-react 34 | 35 | * java demo 36 | 37 | https://github.com/veasion/aippt-api-java-demo 38 | 39 | * python demo 40 | 41 | https://github.com/veasion/aippt-api-python-demo 42 | 43 | * go demo 44 | 45 | https://github.com/veasion/aippt-api-go-demo 46 | 47 | * .net / c# demo 48 | 49 | https://github.com/veasion/aippt-api-dotnet-demo 50 | 51 | * php demo 52 | 53 | https://github.com/veasion/aippt-api-php-demo 54 | 55 | * c++ demo 56 | 57 | https://github.com/veasion/aippt-api-cpp-demo 58 | 59 | 60 | 61 | 进群交流(文多多AiPPT技术交流群): 62 | 63 | ![qrcode](https://metasign-public.oss-cn-shanghai.aliyuncs.com/github/contact_me_qr.png) 64 | -------------------------------------------------------------------------------- /static/base64js.js: -------------------------------------------------------------------------------- 1 | (function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"==typeof window?"undefined"==typeof global?"undefined"==typeof self?this:self:global:window,b.base64js=a()}})(function(){return function(){function b(d,e,g){function a(j,i){if(!e[j]){if(!d[j]){var f="function"==typeof require&&require;if(!i&&f)return f(j,!0);if(h)return h(j,!0);var c=new Error("Cannot find module '"+j+"'");throw c.code="MODULE_NOT_FOUND",c}var k=e[j]={exports:{}};d[j][0].call(k.exports,function(b){var c=d[j][1][b];return a(c||b)},k,k.exports,b,d,e,g)}return e[j].exports}for(var h="function"==typeof require&&require,c=0;c>16,j[k++]=255&b>>8,j[k++]=255&b;return 2===h&&(b=l[a.charCodeAt(c)]<<2|l[a.charCodeAt(c+1)]>>4,j[k++]=255&b),1===h&&(b=l[a.charCodeAt(c)]<<10|l[a.charCodeAt(c+1)]<<4|l[a.charCodeAt(c+2)]>>2,j[k++]=255&b>>8,j[k++]=255&b),j}function g(a){return k[63&a>>18]+k[63&a>>12]+k[63&a>>6]+k[63&a]}function h(a,b,c){for(var d,e=[],f=b;fj?j:g+f));return 1===d?(b=a[c-1],e.push(k[b>>2]+k[63&b<<4]+"==")):2===d&&(b=(a[c-2]<<8)+a[c-1],e.push(k[b>>10]+k[63&b>>4]+k[63&b<<2]+"=")),e.join("")}c.byteLength=function(a){var b=d(a),c=b[0],e=b[1];return 3*(c+e)/4-e},c.toByteArray=f,c.fromByteArray=j;for(var k=[],l=[],m="undefined"==typeof Uint8Array?Array:Uint8Array,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",o=0,p=n.length;ologo

2 |

文多多 AiPPT

3 |

4 | 简体中文 | English 5 |

6 |

7 | 🔗在线演示 8 |   •   9 | 📝PPT转JSON 10 |   •   11 | 🌏官方网站 12 |   •   13 | 💬合作交流 14 |

15 | 16 | 17 | 18 | 19 | 20 | # 🤖 AI 生成 PPT 21 | 22 | 商用级 AI 生成 PPT 项目,包含以下功能: 23 | 24 | * AI 生成 PPT 25 | * PPT 解析成 JSON 26 | * JSON 反渲染为 PPT 27 | 28 | 29 | 30 | # ✨ AiPPT 31 | 32 | 在线体验:https://veasion.github.io/AiPPT 33 | 34 | 35 | [演示视频](https://metasign-public.oss-cn-shanghai.aliyuncs.com/github/aippt.mp4) 36 | 37 | https://github.com/veasion/aippt/assets/24989778/24d5654b-09f3-4554-a732-dbffc1073a1d 38 | 39 | 40 | 41 | # ✨ PPT 解析成 JSON 42 | 43 | 支持上传PPT并渲染,在线编辑,编辑后下载 ppt 文件。 44 | 45 | 在线体验:https://veasion.github.io/AiPPT/ppt2json.html 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | # 🤝 商业合作 55 | 56 | 针对上面技术,我们开发了一套可商用 aippt 软件,支持代理 & 私有化部署! 57 | 58 | 我们的优势,支持定制化行业解决方案,支持原生图表、动画、3D特效等复杂PPT的解析和渲染,支持用户自定义模板,支持智能添加动画,技术方案行业领先,价格行业最低。 59 | 60 | 官网地址(开放API): 61 | https://docmee.cn 62 | 63 | 开放平台(API/UI 接入): 64 | https://docmee.cn/open-platform 65 | 66 | 商业合作 & 进群交流: 67 | 68 | ![qrcode](https://metasign-public.oss-cn-shanghai.aliyuncs.com/github/contact_me_qr.png) 69 | 70 | 71 | 72 | # 🌟 Star History 73 | 74 | 75 | 76 | 77 | 78 | Star History Chart 79 | 80 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 |

logo

2 |

Docmee AiPPT

3 |

4 | English | 简体中文 5 |

6 |

7 | 🔗Demo 8 |   •   9 | 📝PPT to JSON 10 |   •   11 | 🌏Official website 12 |   •   13 | 💬Business cooperation 14 |

15 | 16 | 17 | 18 | 19 | 20 | # 🤖 AI Generate PPT 21 | 22 | Commercial level AI generated PPT project, including the following features: 23 | 24 | * AI generated PPT 25 | * PPT parsed into JSON 26 | * JSON re-rendered as PPT 27 | 28 | 29 | 30 | # ✨ AiPPT 31 | 32 | Demo: https://veasion.github.io/AiPPT 33 | 34 | [Demo Video](https://metasign-public.oss-cn-shanghai.aliyuncs.com/github/aippt.mp4) 35 | 36 | https://github.com/veasion/aippt/assets/24989778/24d5654b-09f3-4554-a732-dbffc1073a1d 37 | 38 | 39 | 40 | # ✨ PPT to JSON 41 | 42 | Support uploading PPT and rendering, online editing, and download the edited PPT file. 43 | 44 | Demo: https://veasion.github.io/AiPPT/ppt2json.html 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | # 🤝 Business cooperation 54 | 55 | We have developed a commercially available aippt software that supports proxy and private deployment for the above technology! 56 | 57 | Our advantages include supporting customized industry solutions, supporting complex PPT analysis and rendering such as native charts and animations, supporting user-defined templates, leading technology solutions, and the lowest prices in the industry. 58 | 59 | Official website (Open API): 60 | https://docmee.cn 61 | 62 | Open platform (API / UI): 63 | https://docmee.cn/open-platform 64 | 65 | 66 | WeChat group qrcode: 67 | 68 | ![qrcode](https://metasign-public.oss-cn-shanghai.aliyuncs.com/github/contact_me_qr.png) 69 | 70 | 71 | 72 | # 🌟 Star History 73 | 74 | 75 | 76 | 77 | 78 | Star History Chart 79 | 80 | -------------------------------------------------------------------------------- /static/cover.js: -------------------------------------------------------------------------------- 1 | // import { Ppt2Canvas } from './ppt2canvas.js' 2 | 3 | async function drawPptxFun(pptxObj, idx) { 4 | let canvas = document.createElement('canvas') 5 | let width = pptxObj.width 6 | let height = pptxObj.height 7 | canvas.width = width * 2 8 | canvas.height = height * 2 9 | canvas.style.width = width + 'px' 10 | canvas.style.height = height + 'px' 11 | let ctx = canvas.getContext('2d') 12 | ctx.scale(2, 2) 13 | ctx.imageSmoothingEnabled = true 14 | ctx.imageSmoothingQuality = 'high' 15 | let ppt2Canvas = new Ppt2Canvas(canvas, 'anonymous') 16 | await ppt2Canvas.drawPptx(pptxObj, idx) 17 | return ppt2Canvas.getCanvas() 18 | } 19 | 20 | // drawPptxFun = async (pptxObj, idx) => return canvas 21 | async function drawCover(pptxObj, drawPptxFun, grayScale) { 22 | let pages = pptxObj.pages 23 | if (pages.length == 1) { 24 | let _canvas = await drawPptxFun(pptxObj, 0, pptxObj.width, pptxObj.height) 25 | return _canvas.toDataURL() 26 | } 27 | async function drawPptxImage(pptxObj, idx) { 28 | let _canvas = await drawPptxFun(pptxObj, idx) 29 | let imgSrc = _canvas.toDataURL() 30 | return new Promise(resolve => { 31 | let image = new Image() 32 | image.src = imgSrc 33 | image.onload = async function() { 34 | resolve(image) 35 | } 36 | }) 37 | } 38 | // let width = 1200, height = 676 39 | let width = 600, height = 338 40 | let canvas = document.createElement('canvas') 41 | canvas.width = width 42 | canvas.height = height 43 | let ctx = canvas.getContext('2d') 44 | ctx.imageSmoothingEnabled = true 45 | ctx.imageSmoothingQuality = 'high' 46 | 47 | ctx.fillStyle = 'rgb(212, 212, 212)' 48 | ctx.fillRect(0, 0, width, height) 49 | 50 | let size = 0 51 | let w = width / 3.3, h = height / 3.3 52 | for (let i = 1; i < pages.length; i++) { 53 | size++ 54 | let x = 0 55 | let y = 0 56 | if (size == 2 || size == 7) { 57 | x = w + (width - w * 3) / 2 58 | } else if (size == 3 || size == 5 || size == 8) { 59 | x = width - w 60 | } 61 | if (size == 4 || size == 5) { 62 | y = h + (height - h * 3) / 2 63 | } else if (size > 5) { 64 | y = height - h 65 | } 66 | let image = await drawPptxImage(pptxObj, i) 67 | ctx.drawImage(image, x, y, w, h) 68 | if (size == 8) { 69 | break 70 | } 71 | } 72 | w = width * 0.65 73 | h = height * 0.65 74 | let image = await drawPptxImage(pptxObj, 0) 75 | ctx.drawImage(image, (width - w) / 2, (height - h) / 2, w, h) 76 | ctx.rect((width - w) / 2, (height - h) / 2, w, h) 77 | ctx.lineWidth = 1 78 | ctx.strokeStyle = 'rgb(212, 212, 212)' 79 | ctx.stroke() 80 | if (grayScale) { 81 | // 灰度 82 | let imageData = ctx.getImageData(0, 0, width, height) 83 | let data = imageData.data 84 | for (let i = 0; i < data.length; i += 4) { 85 | let luminance = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114 86 | data[i] = luminance 87 | data[i + 1] = luminance 88 | data[i + 2] = luminance 89 | } 90 | ctx.putImageData(imageData, 0, 0) 91 | } 92 | return canvas.toDataURL() 93 | } 94 | 95 | // export { drawPptxFun, drawCover } -------------------------------------------------------------------------------- /static/sse.js: -------------------------------------------------------------------------------- 1 | function SSE(url, options) { 2 | this.INITIALIZING = -1; 3 | this.CONNECTING = 0; 4 | this.OPEN = 1; 5 | this.CLOSED = 2; 6 | 7 | this.url = url; 8 | 9 | options = options || {}; 10 | this.headers = options.headers || {}; 11 | this.payload = options.payload !== undefined ? options.payload : ''; 12 | this.method = options.method || (this.payload && 'POST') || 'GET'; 13 | this.withCredentials = !!options.withCredentials; 14 | 15 | this.FIELD_SEPARATOR = ':'; 16 | this.listeners = {}; 17 | 18 | this.xhr = null; 19 | this.readyState = this.INITIALIZING; 20 | this.progress = 0; 21 | this.chunk = ''; 22 | 23 | this.addEventListener = function (type, listener) { 24 | if (this.listeners[type] === undefined) { 25 | this.listeners[type] = []; 26 | } 27 | 28 | if (this.listeners[type].indexOf(listener) === -1) { 29 | this.listeners[type].push(listener); 30 | } 31 | }; 32 | 33 | this.removeEventListener = function (type, listener) { 34 | if (this.listeners[type] === undefined) { 35 | return; 36 | } 37 | 38 | var filtered = []; 39 | this.listeners[type].forEach(function (element) { 40 | if (element !== listener) { 41 | filtered.push(element); 42 | } 43 | }); 44 | if (filtered.length === 0) { 45 | delete this.listeners[type]; 46 | } else { 47 | this.listeners[type] = filtered; 48 | } 49 | }; 50 | 51 | this.dispatchEvent = function (e) { 52 | if (!e) { 53 | return true; 54 | } 55 | 56 | e.source = this; 57 | 58 | var onHandler = 'on' + e.type; 59 | if (this.hasOwnProperty(onHandler)) { 60 | this[onHandler].call(this, e); 61 | if (e.defaultPrevented) { 62 | return false; 63 | } 64 | } 65 | 66 | if (this.listeners[e.type]) { 67 | return this.listeners[e.type].every(function (callback) { 68 | callback(e); 69 | return !e.defaultPrevented; 70 | }); 71 | } 72 | 73 | return true; 74 | }; 75 | 76 | this._setReadyState = function (state) { 77 | var event = new CustomEvent('readystatechange'); 78 | event.readyState = state; 79 | this.readyState = state; 80 | this.dispatchEvent(event); 81 | }; 82 | 83 | this._onStreamFailure = function (e) { 84 | var event = new CustomEvent('error'); 85 | event.data = e.currentTarget.response; 86 | this.dispatchEvent(event); 87 | this.close(); 88 | }; 89 | 90 | this._onStreamAbort = function (e) { 91 | this.dispatchEvent(new CustomEvent('abort')); 92 | this.close(); 93 | }; 94 | 95 | this._onStreamProgress = function (e) { 96 | if (!this.xhr) { 97 | return; 98 | } 99 | 100 | if (this.xhr.status !== 200) { 101 | this._onStreamFailure(e); 102 | return; 103 | } 104 | 105 | if (this.readyState == this.CONNECTING) { 106 | this.dispatchEvent(new CustomEvent('open')); 107 | this._setReadyState(this.OPEN); 108 | } 109 | 110 | var data = this.xhr.responseText.substring(this.progress); 111 | this.progress += data.length; 112 | data.split(/(\r\n|\r|\n){2}/g).forEach( 113 | function (part) { 114 | if (part.trim().length === 0) { 115 | this.dispatchEvent(this._parseEventChunk(this.chunk.trim())); 116 | this.chunk = ''; 117 | } else { 118 | this.chunk += part; 119 | } 120 | }.bind(this), 121 | ); 122 | }; 123 | 124 | this._onStreamLoaded = function (e) { 125 | this._onStreamProgress(e); 126 | 127 | // Parse the last chunk. 128 | this.dispatchEvent(this._parseEventChunk(this.chunk)); 129 | this.chunk = ''; 130 | }; 131 | 132 | /** 133 | * Parse a received SSE event chunk into a constructed event object. 134 | */ 135 | this._parseEventChunk = function (chunk) { 136 | if (!chunk || chunk.length === 0) { 137 | return null; 138 | } 139 | 140 | var e = { id: null, retry: null, data: '', event: 'message' }; 141 | chunk.split(/\n|\r\n|\r/).forEach( 142 | function (line) { 143 | line = line.trimRight(); 144 | var index = line.indexOf(this.FIELD_SEPARATOR); 145 | if (index <= 0) { 146 | // Line was either empty, or started with a separator and is a comment. 147 | // Either way, ignore. 148 | return; 149 | } 150 | 151 | var field = line.substring(0, index); 152 | if (!(field in e)) { 153 | return; 154 | } 155 | 156 | var value = line.substring(index + 1).trimLeft(); 157 | if (field === 'data') { 158 | e[field] += value; 159 | } else { 160 | e[field] = value; 161 | } 162 | }.bind(this), 163 | ); 164 | 165 | var event = new CustomEvent(e.event); 166 | event.data = e.data; 167 | event.id = e.id; 168 | return event; 169 | }; 170 | 171 | this._checkStreamClosed = function () { 172 | if (!this.xhr) { 173 | return; 174 | } 175 | 176 | if (this.xhr.readyState === XMLHttpRequest.DONE) { 177 | this._setReadyState(this.CLOSED); 178 | var event = new CustomEvent('end'); 179 | event.data = this.xhr.responseText; 180 | this.dispatchEvent(event); 181 | } 182 | }; 183 | 184 | this.stream = function () { 185 | this._setReadyState(this.CONNECTING); 186 | 187 | this.xhr = new XMLHttpRequest(); 188 | this.xhr.addEventListener('progress', this._onStreamProgress.bind(this)); 189 | this.xhr.addEventListener('load', this._onStreamLoaded.bind(this)); 190 | this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this)); 191 | this.xhr.addEventListener('error', this._onStreamFailure.bind(this)); 192 | this.xhr.addEventListener('abort', this._onStreamAbort.bind(this)); 193 | this.xhr.open(this.method, this.url); 194 | for (var header in this.headers) { 195 | this.xhr.setRequestHeader(header, this.headers[header]); 196 | } 197 | this.xhr.withCredentials = this.withCredentials; 198 | this.xhr.send(this.payload); 199 | }; 200 | 201 | this.close = function () { 202 | if (this.readyState === this.CLOSED) { 203 | return; 204 | } 205 | 206 | this.xhr.abort(); 207 | this.xhr = null; 208 | this._setReadyState(this.CLOSED); 209 | }; 210 | }; 211 | 212 | // export { SSE } 213 | 214 | /* 215 | const url = 'https://xxx/chat'; 216 | var source = new SSE(url, { 217 | method: 'POST', 218 | // withCredentials: true, 219 | headers: { 220 | 'Content-Type': 'application/json', 221 | 'Cache-Control': 'no-cache' 222 | }, 223 | payload: JSON.stringify({ prompt: 'xxx' }), 224 | }); 225 | source.onmessage = function (data) { 226 | console.log('chunk => ' + data.data) 227 | }; 228 | source.onend = function (data) { 229 | console.log('结束'); 230 | }; 231 | source.onerror = function (err) { 232 | console.error('异常', err); 233 | }; 234 | source.stream(); 235 | */ -------------------------------------------------------------------------------- /static/i18n.js: -------------------------------------------------------------------------------- 1 | // ==================== 国际化配置 ==================== 2 | const translations = { 3 | 'zh': { 4 | brand_title: '文多多 AiPPT', 5 | page1_title: '🤖 AI智能生成PPT演示文稿', 6 | page1_steps: '生成大纲 ---> 挑选模板 ---> 实时生成PPT', 7 | subject_label: '主题:', 8 | subject_placeholder: '请输入PPT主题', 9 | generate_outline_btn: '生成大纲', 10 | next_to_template_btn: '下一步:选择模板', 11 | select_template_title: '---- 选择模板 ----', 12 | next_to_generate_btn: '下一步:生成PPT', 13 | templates_loading: '模板加载中...', 14 | generating_msg: '正在生成中,请稍后...', 15 | generating_progress: '正在生成中,进度', 16 | insert_element_title: '-- 插入元素 --', 17 | insert_title1: '插入大标题', 18 | insert_title2: '插入副标题', 19 | insert_content: '插入正文文本', 20 | insert_image: '插入图片', 21 | insert_geometry: '插入随机形状', 22 | insert_table: '插入表格', 23 | insert_bar_chart: '插入柱状图', 24 | insert_pie_chart: '插入饼图', 25 | insert_doughnut_chart: '插入环形图', 26 | insert_line_chart: '插入折线图', 27 | download_options_title: '-- 下载选项 --', 28 | download_no_animation: '不添加动画', 29 | download_with_animation: '智能添加动画', 30 | download_pptx_btn: '渲染pptx并下载', 31 | rendering: '正在渲染中...', 32 | load_more_templates: '换一批', 33 | alert_empty_subject: '请输入主题', 34 | alert_subject_too_short: '主题太短', 35 | alert_subject_too_long: '主题太长,不能超过30字', 36 | alert_outline_error: '生成大纲异常:', 37 | alert_ppt_error: '生成PPT异常:', 38 | alert_download_error: '下载失败:', 39 | seconds: '秒', 40 | // ppt2json.html 页面翻译 41 | ppt2json_title: 'ppt2json', 42 | refresh_json: '刷新', 43 | refresh_json_title: '刷新JSON数据', 44 | download_json: '下载json文件', 45 | download_json_title: '下载json数据', 46 | render_download_pptx: '渲染pptx并下载', 47 | render_download_pptx_title: '渲染pptx并下载', 48 | edit_mode_label: '编辑模式:', 49 | parsing_msg: '解析中,请稍后...', 50 | please_select_file: '请选择文件', 51 | file_too_large: '文件不能超过50M', 52 | read_error: '读取错误: ', 53 | rendering_pptx: '正在渲染中...', 54 | http_status: 'http status: ', 55 | // 示例数据翻译 56 | table_cell: '第{row}行{col}列', 57 | chart_title: '图表标题', 58 | chart_sales: '销售额', 59 | chart_q1: '第一季度', 60 | chart_q2: '第二季度', 61 | chart_q3: '第三季度', 62 | chart_q4: '第四季度', 63 | chart_series: '系列 {n}', 64 | chart_category: '类别 {n}' 65 | }, 66 | 'en': { 67 | brand_title: 'Docmee AI PPT', 68 | page1_title: '🤖 AI-Powered PPT Generation', 69 | page1_steps: 'Generate Outline ---> Choose Template ---> Generate PPT', 70 | subject_label: 'Topic:', 71 | subject_placeholder: 'Enter PPT topic', 72 | generate_outline_btn: 'Generate Outline', 73 | next_to_template_btn: 'Next: Choose Template', 74 | select_template_title: '---- Select Template ----', 75 | next_to_generate_btn: 'Next: Generate PPT', 76 | templates_loading: 'Loading templates...', 77 | generating_msg: 'Generating, please wait...', 78 | generating_progress: 'Generating, progress', 79 | insert_element_title: '-- Insert Element --', 80 | insert_title1: 'Insert Main Title', 81 | insert_title2: 'Insert Subtitle', 82 | insert_content: 'Insert Text Content', 83 | insert_image: 'Insert Image', 84 | insert_geometry: 'Insert Random Shape', 85 | insert_table: 'Insert Table', 86 | insert_bar_chart: 'Insert Bar Chart', 87 | insert_pie_chart: 'Insert Pie Chart', 88 | insert_doughnut_chart: 'Insert Doughnut Chart', 89 | insert_line_chart: 'Insert Line Chart', 90 | download_options_title: '-- Download Options --', 91 | download_no_animation: 'No Animation', 92 | download_with_animation: 'Smart Animation', 93 | download_pptx_btn: 'Render & Download PPTX', 94 | rendering: 'Rendering...', 95 | load_more_templates: 'Load More', 96 | alert_empty_subject: 'Please enter a topic', 97 | alert_subject_too_short: 'Topic is too short', 98 | alert_subject_too_long: 'Topic is too long, max 30 characters', 99 | alert_outline_error: 'Outline generation error: ', 100 | alert_ppt_error: 'PPT generation error: ', 101 | alert_download_error: 'Download failed: ', 102 | seconds: 's', 103 | // ppt2json.html page translations 104 | ppt2json_title: 'ppt2json', 105 | refresh_json: 'Refresh', 106 | refresh_json_title: 'Refresh JSON data', 107 | download_json: 'Download JSON', 108 | download_json_title: 'Download JSON data', 109 | render_download_pptx: 'Render & Download PPTX', 110 | render_download_pptx_title: 'Render and download PPTX', 111 | edit_mode_label: 'Edit Mode:', 112 | parsing_msg: 'Parsing, please wait...', 113 | please_select_file: 'Please select a file', 114 | file_too_large: 'File size cannot exceed 50MB', 115 | read_error: 'Read error: ', 116 | rendering_pptx: 'Rendering...', 117 | http_status: 'HTTP status: ', 118 | // Sample data translations 119 | table_cell: 'Row {row} Col {col}', 120 | chart_title: 'Chart Title', 121 | chart_sales: 'Sales', 122 | chart_q1: 'Q1', 123 | chart_q2: 'Q2', 124 | chart_q3: 'Q3', 125 | chart_q4: 'Q4', 126 | chart_series: 'Series {n}', 127 | chart_category: 'Category {n}' 128 | } 129 | } 130 | 131 | // 当前语言 132 | let currentLanguage = detectLanguage() 133 | 134 | /** 135 | * 检测浏览器语言 136 | */ 137 | function detectLanguage() { 138 | const savedLang = localStorage.getItem('ppt_language') 139 | if (savedLang && (savedLang === 'zh' || savedLang === 'en')) { 140 | return savedLang 141 | } 142 | const browserLang = navigator.language || navigator.userLanguage 143 | return browserLang.toLowerCase().startsWith('zh') ? 'zh' : 'en' 144 | } 145 | 146 | /** 147 | * 切换语言 148 | */ 149 | function toggleLanguage() { 150 | currentLanguage = currentLanguage === 'zh' ? 'en' : 'zh' 151 | localStorage.setItem('ppt_language', currentLanguage) 152 | applyLanguage() 153 | } 154 | 155 | /** 156 | * 获取当前语言 157 | */ 158 | function getCurrentLanguage() { 159 | return currentLanguage 160 | } 161 | 162 | /** 163 | * 设置语言 164 | */ 165 | function setLanguage(lang) { 166 | if (lang === 'zh' || lang === 'en') { 167 | currentLanguage = lang 168 | localStorage.setItem('ppt_language', currentLanguage) 169 | applyLanguage() 170 | } 171 | } 172 | 173 | /** 174 | * 翻译文本 175 | */ 176 | function t(key) { 177 | return translations[currentLanguage][key] || translations['zh'][key] || key 178 | } 179 | 180 | /** 181 | * 应用语言到页面 182 | */ 183 | function applyLanguage() { 184 | document.documentElement.lang = currentLanguage 185 | 186 | // 更新语言切换按钮 187 | const langText = document.getElementById('lang-text') 188 | if (langText) { 189 | langText.textContent = currentLanguage === 'zh' ? 'EN' : '中文' 190 | } 191 | 192 | // 更新所有带 data-i18n 属性的元素 193 | document.querySelectorAll('[data-i18n]').forEach(element => { 194 | const key = element.getAttribute('data-i18n') 195 | if (element.tagName === 'INPUT') { 196 | element.placeholder = t(key) 197 | } else if (element.tagName === 'OPTION') { 198 | element.textContent = t(key) 199 | } else { 200 | element.innerHTML = t(key) 201 | } 202 | }) 203 | 204 | // 更新带 data-i18n-placeholder 的元素 205 | document.querySelectorAll('[data-i18n-placeholder]').forEach(element => { 206 | const key = element.getAttribute('data-i18n-placeholder') 207 | element.placeholder = t(key) 208 | }) 209 | 210 | // 更新带 data-i18n-title 的元素 211 | document.querySelectorAll('[data-i18n-title]').forEach(element => { 212 | const key = element.getAttribute('data-i18n-title') 213 | element.title = t(key) 214 | }) 215 | } 216 | 217 | -------------------------------------------------------------------------------- /ppt2json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ppt2json 6 | 7 | 8 | 78 | 79 | 80 | 文多多 AiPPT 81 |
82 | EN 83 |
84 |
85 | 86 | 刷新 87 | 88 | 89 |
90 |
91 |
92 |
    93 | 编辑模式: 94 | 95 | 108 |
    109 |
    110 | 111 |
    112 | 113 | 114 | 115 | 116 | 117 | 118 | 351 | 352 | -------------------------------------------------------------------------------- /static/element.js: -------------------------------------------------------------------------------- 1 | // 创建空白页 2 | function createPage(page, width, height) { 3 | return { 4 | page: page || 1, 5 | extInfo: { 6 | background: { 7 | realType: 'Background', 8 | anchor: [0, 0, width || 960, height || 540], 9 | fillStyle: { 10 | type: 'color', 11 | color: { 12 | color: -1, 13 | realColor: -1 14 | } 15 | } 16 | }, 17 | slideMasterIdx: 0, 18 | slideLayoutIdx: 0 19 | }, 20 | children: [] 21 | } 22 | } 23 | 24 | // 创建文本框 25 | // @param subType 类型 => title1 标题; title2 副标题; content 正文内容 26 | // @param fontFamily 字体(默认 null) 27 | // @param fontColor 文字颜色(默认 null) 28 | function createTextBox(subType, fontFamily, fontColor) { 29 | const fontSize = (subType == 'title1' ? 70 : (subType == 'title2' ? 35 : 18)) 30 | const text = (subType == 'title1' || subType == 'title2' ? '输入标题' : '请输入内容') 31 | const textWordWrap = (subType == 'title1' || subType == 'title2' ? false : true) 32 | const textAlign = (subType == 'title1' || subType == 'title2' ? 'CENTER' : 'LEFT') 33 | const width = (text.length + 1) * fontSize 34 | const anchor = [(960 - width) / 2, 540 / 2 - fontSize, width, fontSize] 35 | if (textAlign == 'CENTER') { 36 | anchor[0] = (960 - width) / 2 37 | } else { 38 | anchor[0] = 960 / 2 - width 39 | } 40 | const id = 'txt' + Math.floor((Math.random() * 100000) + 100000) 41 | if (!fontColor && subType == 'title1') { 42 | // 标题默认颜色 43 | fontColor = { 44 | type: 'gradient', 45 | gradient: { 46 | angle: 90, 47 | colors: [ 48 | {color: -7614, realColor: -7614, alpha: 100000}, 49 | {color: -1838, realColor: -1838, alpha: 100000}, 50 | {color: -7614, realColor: -7614, alpha: 100000}, 51 | {color: -25569, realColor: -25569, alpha: 100000}, 52 | {color: -7614, realColor: -7614, alpha: 100000} 53 | ], 54 | fractions: [0.06, 0.26, 0.5, 0.71, 0.89], 55 | gradientType: 'linear' 56 | } 57 | } 58 | } else if (!fontColor) { 59 | // 正文默认颜色 60 | fontColor = { 61 | type: 'color', 62 | color: { 63 | scheme: null, 64 | realColor: -16777216, 65 | color: -16777216 66 | }, 67 | gradient: null 68 | } 69 | } 70 | return { 71 | id: `${id}`, 72 | type: 'text', 73 | depth: 1, 74 | point: [...anchor], 75 | extInfo: { 76 | property: { 77 | realType: 'TextBox', 78 | shapeType: 'rect', 79 | anchor: [...anchor], 80 | fillStyle: { 81 | type: 'noFill' 82 | }, 83 | geometry: { 84 | name: 'rect' 85 | }, 86 | textAutofit: 'SHAPE', 87 | textDirection: 'HORIZONTAL', 88 | textVerticalAlignment: 'TOP', 89 | textWordWrap: textWordWrap, 90 | textInsets: [3.6,7.2,3.6,7.2] 91 | } 92 | }, 93 | children: [ 94 | { 95 | id: `${id}_p`, 96 | pid: `${id}`, 97 | type: 'p', 98 | depth: 2, 99 | extInfo: { 100 | property: { 101 | textAlign: textAlign, 102 | leftMargin: 0 103 | } 104 | }, 105 | children: [ 106 | { 107 | id: `${id}_p_r`, 108 | pid: `${id}_p`, 109 | type: 'r', 110 | text: text, 111 | depth: 3, 112 | extInfo: { 113 | property: { 114 | fontSize: fontSize, 115 | bold: null, 116 | fontFamily: fontFamily, 117 | fontColor: {...fontColor}, 118 | line: null, 119 | lang: 'zh-CN' 120 | } 121 | } 122 | } 123 | ] 124 | } 125 | ] 126 | } 127 | } 128 | 129 | // 创建形状 130 | // @param geometryName 形状名称 => geometryMap key 131 | // @param fillStyle 填充 132 | // @param strokeStylePaint 画笔填充 133 | function createGeometry(geometryName, fillStyle, strokeStylePaint) { 134 | const width = 200 135 | const height = 200 136 | const anchor = [(960 - width) / 2, (540 - height) / 2, width, height] 137 | const id = 'txt' + Math.floor((Math.random() * 100000) + 100000) 138 | if (!fillStyle) { 139 | fillStyle = { 140 | type: 'color', 141 | color: { 142 | scheme: null, 143 | color: -10773547, 144 | realColor: -10773547 145 | } 146 | } 147 | } 148 | if (!strokeStylePaint) { 149 | strokeStylePaint = { 150 | type: 'color', 151 | color: { 152 | scheme: null, 153 | color: -10773547, 154 | realColor: -14532775, 155 | shade: 15000 156 | } 157 | } 158 | } 159 | return { 160 | id: `${id}`, 161 | type: 'text', 162 | depth: 1, 163 | point: [...anchor], 164 | extInfo: { 165 | property: { 166 | realType: 'Auto', 167 | shapeType: geometryName, 168 | anchor: [...anchor], 169 | fillStyle: {...fillStyle}, 170 | strokeStyle: { 171 | paint: {...strokeStylePaint}, 172 | lineWidth: 1, 173 | lineCap: 'FLAT', 174 | lineDash: 'SOLID', 175 | lineCompound: 'SINGLE' 176 | }, 177 | geometry: { 178 | name: geometryName, 179 | data: null, 180 | avLst: null 181 | }, 182 | textAutofit: 'NORMAL', 183 | textDirection: 'HORIZONTAL', 184 | textVerticalAlignment: 'MIDDLE', 185 | textInsets: [3.6, 7.2, 3.6, 7.2] 186 | } 187 | }, 188 | children: [ 189 | { 190 | id: `${id}_p`, 191 | pid: `${id}`, 192 | type: 'p', 193 | depth: 2, 194 | extInfo: { 195 | property: { 196 | textAlign: 'CENTER', 197 | leftMargin: 0 198 | } 199 | }, 200 | children: [] 201 | } 202 | ] 203 | } 204 | } 205 | 206 | // 创建图片 207 | // @param src 图片src (url/base64) 208 | function createImage(src, width, height) { 209 | if (!width) { 210 | width = 200 211 | } 212 | if (!height) { 213 | height = 200 214 | } 215 | const extension = src.indexOf('.png') > -1 || src.indexOf('image/png') ? '.png' : '.jpg' 216 | const id = 'img' + Math.floor((Math.random() * 100000) + 100000) 217 | const anchor = [(960 - width) / 2, (540 - height) / 2, width, height] 218 | const contentType = extension == '.png' ? 'image/png' : 'image/jpeg' 219 | return { 220 | id: id, 221 | depth: 1, 222 | type: 'image', 223 | point: [...anchor], 224 | extInfo: { 225 | property: { 226 | image: src, 227 | anchor: [...anchor], 228 | extension: extension, 229 | fileName: 'image' + extension, 230 | contentType: contentType, 231 | fillStyle: { 232 | type: 'texture', 233 | texture: { 234 | imageData: '$image', 235 | flipMode: 'NONE', 236 | insets: [0, 0, 0, 0], 237 | stretch: [0, 0, 0, 0], 238 | contentType: contentType 239 | } 240 | }, 241 | flipHorizontal: false, 242 | flipVertical: false, 243 | realType: 'Picture', 244 | geometry: { 245 | name: 'rect' 246 | } 247 | } 248 | }, 249 | children: [] 250 | } 251 | } 252 | 253 | // 创建表格 254 | // @param rowColumnDataList 表格数据: [['第1行1列', '第1行2列', '第1行3列'], ['第2行1列', '第2行2列', '第2行3列'], ['第3行1列', '第3行2列', '第3行3列']] 255 | // @param rowFillStyles 填充色(按行循环交替) 256 | // @param borderColor 边框颜色 257 | // @param fontColor 字体颜色 258 | function createTable(rowColumnDataList, rowFillStyles, borderColor, fontColor) { 259 | const rowNum = rowColumnDataList.length 260 | const columnNum = rowColumnDataList[0].length 261 | const lineWidth = 1 262 | const fontSize = 16 263 | const textAlign = 'LEFT' 264 | const rowHeight = 65, colWidth = 80 265 | const width = rowHeight * rowNum + lineWidth * (rowNum + 1) 266 | const height = colWidth * columnNum + lineWidth * (columnNum + 1) 267 | const tableAnchor = [(960 - width) / 2, (540 - height) / 2, width, height] 268 | const id = 'tab' + Math.floor((Math.random() * 100000) + 100000) 269 | const rows = [] 270 | if (!rowFillStyles) { 271 | rowFillStyles = [ 272 | { 273 | type: 'color', 274 | color: { 275 | color: -7555288, 276 | realColor: -1378864, 277 | lumMod: 20000, 278 | lumOff: 80000 279 | } 280 | }, 281 | { 282 | type: 'color', 283 | color: { 284 | color: -7555288, 285 | realColor: -2823519, 286 | lumMod: 40000, 287 | lumOff: 60000 288 | } 289 | } 290 | ] 291 | } 292 | if (!fontColor) { 293 | fontColor = { 294 | type: 'color', 295 | color: { 296 | color: -16777216, 297 | realColor: -16777216, 298 | alpha: 100000 299 | } 300 | } 301 | } 302 | if (borderColor == null) { 303 | borderColor = -7555288 304 | } 305 | for (let i = 0; i < rowNum; i++) { 306 | const columns = [] 307 | const fillStyle = rowFillStyles[i % rowFillStyles.length] 308 | for (let j = 0; j < columnNum; j++) { 309 | let text = rowColumnDataList[i][j] 310 | columns.push({ 311 | id: `${id}_r${i}_c${j}`, 312 | pid: `${id}_r${i}`, 313 | type: 'tableColumn', 314 | depth: 3, 315 | extInfo: { 316 | property: { 317 | realType: 'TableCell', 318 | anchor: [tableAnchor[0] + colWidth * j + lineWidth * (j + 1), tableAnchor[1] + rowHeight * i + lineWidth * (i + 1), colWidth, rowHeight], 319 | fillStyle: {...fillStyle}, 320 | strokeStyle: {}, 321 | geometry: { 322 | name: 'tableColumn' 323 | }, 324 | textAutofit: 'NORMAL', 325 | textDirection: 'HORIZONTAL', 326 | textVerticalAlignment: 'MIDDLE', 327 | textInsets: [3.6, 7.2, 3.6, 7.2], 328 | columnWidth: colWidth, 329 | borders: [ 330 | { 331 | color: borderColor, 332 | lineWidth: lineWidth, 333 | lineCap: 'FLAT', 334 | lineDash: 'SOLID', 335 | lineCompound: 'SINGLE' 336 | }, 337 | { 338 | color: borderColor, 339 | lineWidth: lineWidth, 340 | lineCap: 'FLAT', 341 | lineDash: 'SOLID', 342 | lineCompound: 'SINGLE' 343 | }, 344 | { 345 | color: borderColor, 346 | lineWidth: lineWidth, 347 | lineCap: 'FLAT', 348 | lineDash: 'SOLID', 349 | lineCompound: 'SINGLE' 350 | }, 351 | { 352 | color: borderColor, 353 | lineWidth: lineWidth, 354 | lineCap: 'FLAT', 355 | lineDash: 'SOLID', 356 | lineCompound: 'SINGLE' 357 | } 358 | ] 359 | } 360 | }, 361 | children: [ 362 | { 363 | id: `${id}_r${i}_c${j}_p`, 364 | pid: `${id}_r${i}_c${j}`, 365 | type: 'p', 366 | depth: 4, 367 | extInfo: { 368 | property: { 369 | textAlign: textAlign, 370 | leftMargin: 0 371 | } 372 | }, 373 | children: [ 374 | { 375 | id: `${id}_r${i}_c${j}_p_r`, 376 | pid: `${id}_r${i}_c${j}_p`, 377 | type: 'r', 378 | text: text, 379 | depth: 5, 380 | extInfo: { 381 | property: { 382 | fontSize: fontSize, 383 | fontColor: {...fontColor}, 384 | lang: 'zh-CN' 385 | } 386 | } 387 | } 388 | ] 389 | } 390 | ] 391 | }) 392 | } 393 | rows.push({ 394 | id: `${id}_r${i}`, 395 | pid: `${id}`, 396 | type: 'tableRow', 397 | depth: 2, 398 | extInfo: { 399 | property: { 400 | rowHeight: rowHeight 401 | } 402 | }, 403 | children: columns 404 | }) 405 | } 406 | return { 407 | id: `${id}`, 408 | pid: null, 409 | type: 'table', 410 | text: null, 411 | depth: 1, 412 | point: [...tableAnchor], 413 | extInfo: { 414 | property: { 415 | anchor: [...tableAnchor], 416 | realType: 'table', 417 | numberOfRows: rowNum, 418 | numberOfColumns: columnNum 419 | } 420 | }, 421 | children: rows 422 | } 423 | } 424 | 425 | // 创建图表 426 | // @param title 图表标题 427 | // @param chartType 图表类型 pie/doughnut/bar/line 428 | // @param rowColumnDataList 表格数据: 429 | // 柱状图、折线图: [[' ','系列 1','系列 2','系列 3'], ['类别 1','4.3','2.4','2'], ['类别 2','2.5','4.4','2'], ['类别 3','3.5','1.8','3'], ['类别 4','4.5','2.8','5']] 430 | // 饼图、环形图: [[' ','销售额'], ['第一季度','8.2'], ['第二季度','3.2'], ['第三季度','1.4'], ['第四季度','1.2']] 431 | // @param colors 颜色 [{type:'color',color:{realColor:-1213135}}] 432 | function createChart(title, chartType, rowColumnDataList, colors) { 433 | if (!colors) { 434 | colors = [ 435 | {type: 'color', color: { color: -478429, realColor: -478429 }}, 436 | {type: 'color', color: { color: -10130855, realColor: -10130855 }}, 437 | {type: 'color', color: { color: -12143947, realColor: -12143947 }}, 438 | {type: 'color', color: { color: -7558530, realColor: -7558530 }}, 439 | {type: 'color', color: { color: -2920600, realColor: -2920600 }}, 440 | {type: 'color', color: { color: -8232330, realColor: -8232330 }} 441 | ] 442 | } 443 | if (chartType == 'pie' || chartType == 'doughnut') { 444 | rowColumnDataList[0][1] = rowColumnDataList[0][1] || title 445 | return createPieChart(rowColumnDataList, chartType == 'doughnut' ? 50 : 0, colors) 446 | } else if (chartType == 'bar' || chartType == 'line') { 447 | return createBarLineChart(title, chartType, rowColumnDataList, colors) 448 | } 449 | return null 450 | } 451 | 452 | function createPieChart(rowColumnDataList, holeSize, colors) { 453 | const width = 400 454 | const height = 220 455 | const anchor = [(960 - width) / 2, (540 - height) / 2, width, height] 456 | const id = 'chart' + Math.floor((Math.random() * 100000) + 100000) 457 | const dataPoint = [] 458 | for (let i = 1; i < rowColumnDataList.length; i++) { 459 | dataPoint.push({ 460 | property: { 461 | anchor: null, 462 | fillStyle: colors[(i - 1) % colors.length], 463 | strokeStyle: { 464 | paint: { 465 | type: 'color', 466 | color: { 467 | scheme: 'lt1', 468 | realColor: -1, 469 | color: -1 470 | } 471 | }, 472 | lineWidth: 1.5 473 | }, 474 | geometry: null, 475 | shadow: null 476 | } 477 | }) 478 | } 479 | const chartData = { 480 | chartType: holeSize ? 'doughnut' : 'pie', 481 | series: [ 482 | { 483 | text: { 484 | formula: 'Sheet1!$B$1', 485 | formatCode: null, 486 | data: [rowColumnDataList[0][1]] 487 | }, 488 | category: { 489 | formula: 'Sheet1!$A$2:$A$' + rowColumnDataList.length, 490 | formatCode: null, 491 | data: rowColumnDataList.map(s => s[0]).splice(1) 492 | }, 493 | value: { 494 | formula: 'Sheet1!$B$2:$B$' + rowColumnDataList.length, 495 | formatCode: 'General', 496 | data: rowColumnDataList.map(s => s[1]).splice(1) 497 | }, 498 | dataPoint: dataPoint, 499 | property: null 500 | } 501 | ], 502 | categoryAxis: null, 503 | valueAxes: null, 504 | extInfo: holeSize ? { holeSize: holeSize } : {} 505 | } 506 | return { 507 | id: id, 508 | type: 'graphicFrame', 509 | depth: 1, 510 | point: [...anchor], 511 | extInfo: { 512 | property: { 513 | anchor: [...anchor], 514 | chart: { 515 | title: '', 516 | legend: { 517 | position: 'BOTTOM', 518 | property: { 519 | anchor: null, 520 | fillStyle: { 521 | type: 'noFill' 522 | }, 523 | strokeStyle: { 524 | paint: { 525 | type: 'noFill' 526 | } 527 | }, 528 | geometry: null, 529 | shadow: null 530 | } 531 | }, 532 | excelData: [ 533 | { 534 | sheetName: 'Sheet1', 535 | rows: rowColumnDataList 536 | } 537 | ], 538 | chartData: [chartData] 539 | }, 540 | realType: 'graphicFrame' 541 | } 542 | }, 543 | children: [] 544 | } 545 | } 546 | 547 | function createBarLineChart(title, chartType, rowColumnDataList, colors) { 548 | const width = 460 549 | const height = 270 550 | const anchor = [(960 - width) / 2, (540 - height) / 2, width, height] 551 | const id = 'chart' + Math.floor((Math.random() * 100000) + 100000) 552 | const series = [] 553 | for (let i = 1; i < rowColumnDataList[0].length; i++) { 554 | const vf = String.fromCharCode(65 + i) 555 | series.push({ 556 | text: { 557 | formula: 'Sheet1!$' + vf + '$1', 558 | formatCode: null, 559 | data: [rowColumnDataList[0][i]] 560 | }, 561 | category: { 562 | formula: 'Sheet1!$A$' + (i + 1) + ':$A$' + rowColumnDataList.length, 563 | formatCode: null, 564 | data: rowColumnDataList.map(s => s[0]).splice(1) 565 | }, 566 | value: { 567 | formula: 'Sheet1!$' + vf + '$' + (i + 1) + ':$' + vf + '$' + rowColumnDataList.length, 568 | formatCode: 'General', 569 | data: rowColumnDataList.map(s => s[i]).splice(1) 570 | }, 571 | dataPoint: [null], 572 | property: { 573 | anchor: null, 574 | fillStyle: chartType == 'line' ? null : colors[(i - 1) % colors.length], 575 | strokeStyle: chartType == 'line' ? { 576 | paint: colors[(i - 1) % colors.length], 577 | lineWidth: 2.25, 578 | lineCap: 'ROUND' 579 | } : { 580 | paint: { 581 | type: 'noFill' 582 | } 583 | }, 584 | geometry: null, 585 | shadow: null 586 | } 587 | }) 588 | } 589 | let extInfo = {} 590 | if (chartType == 'bar') { 591 | extInfo = { 592 | type: 'col', 593 | overlap: '-27', 594 | gapWidth: '219', 595 | majorGridlines: 'true' 596 | } 597 | } else if (chartType == 'line') { 598 | extInfo = { majorGridlines: 'true' } 599 | } 600 | return { 601 | id: id, 602 | type: 'graphicFrame', 603 | depth: 1, 604 | point: [...anchor], 605 | extInfo: { 606 | property: { 607 | anchor: [...anchor], 608 | chart: { 609 | title: title, 610 | legend: { 611 | position: 'BOTTOM', 612 | property: { 613 | anchor: null, 614 | fillStyle: { 615 | type: 'noFill' 616 | }, 617 | strokeStyle: { 618 | paint: { 619 | type: 'noFill' 620 | } 621 | }, 622 | geometry: null, 623 | shadow: null 624 | } 625 | }, 626 | excelData: [ 627 | { 628 | sheetName: 'Sheet1', 629 | rows: rowColumnDataList 630 | } 631 | ], 632 | chartData: [ 633 | { 634 | chartType: chartType, 635 | series: series, 636 | categoryAxis: { 637 | position: 'BOTTOM', 638 | deleted: false, 639 | property: { 640 | anchor: null, 641 | fillStyle: { 642 | type: 'noFill' 643 | }, 644 | strokeStyle: { 645 | paint: { 646 | type: 'color', 647 | color: { 648 | realColor: -2500135, 649 | color: -16777216, 650 | lumMod: 15000, 651 | lumOff: 85000 652 | } 653 | }, 654 | lineWidth: 0.75, 655 | lineCap: 'FLAT', 656 | lineCompound: 'SINGLE' 657 | }, 658 | geometry: null, 659 | shadow: null 660 | } 661 | }, 662 | valueAxes: [ 663 | { 664 | position: 'LEFT', 665 | deleted: false, 666 | property: { 667 | anchor: null, 668 | fillStyle: { 669 | type: 'noFill' 670 | }, 671 | strokeStyle: { 672 | paint: { 673 | type: 'noFill' 674 | } 675 | }, 676 | geometry: null, 677 | shadow: null 678 | } 679 | } 680 | ], 681 | extInfo: extInfo 682 | } 683 | ] 684 | }, 685 | realType: 'graphicFrame' 686 | } 687 | }, 688 | children: [] 689 | } 690 | } 691 | 692 | // export { createPage, createTextBox, createGeometry, createImage, createTable, createChart } 693 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AI PPT 6 | 176 | 177 | 178 | 文多多 AiPPT 179 |
    180 | EN 181 |
    182 | 183 |
    184 |
    185 |

    🤖 AI智能生成PPT演示文稿

    186 |
    生成大纲 ---> 挑选模板 ---> 实时生成PPT
    187 |
    188 | 189 | 190 | 191 | 192 |
    193 |
    194 | 195 |
    196 |
    197 |
    ---- 选择模板 ----
    198 | 199 |
    200 |
    模板加载中...
    201 |
    202 | 203 |
    204 |
    205 | 正在生成中,请稍后... 206 | 207 |
    208 |
    209 |
    210 |
    211 | 217 |
    218 |
    219 |
    220 |
    221 | 234 |
    235 | 240 | 渲染pptx并下载 241 |
    242 |
    243 | 244 | 245 |
    246 |
    247 |
    248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 735 | 736 | -------------------------------------------------------------------------------- /static/jsoneditor.min.css: -------------------------------------------------------------------------------- 1 | .jsoneditor input,.jsoneditor input:not([type]),.jsoneditor input[type=search],.jsoneditor input[type=text],.jsoneditor-modal input,.jsoneditor-modal input:not([type]),.jsoneditor-modal input[type=search],.jsoneditor-modal input[type=text]{height:auto;border:inherit;box-shadow:none;font-size:inherit;box-sizing:inherit;padding:inherit;font-family:inherit;transition:none;line-height:inherit}.jsoneditor input:focus,.jsoneditor input:not([type]):focus,.jsoneditor input[type=search]:focus,.jsoneditor input[type=text]:focus,.jsoneditor-modal input:focus,.jsoneditor-modal input:not([type]):focus,.jsoneditor-modal input[type=search]:focus,.jsoneditor-modal input[type=text]:focus{border:inherit;box-shadow:inherit}.jsoneditor textarea,.jsoneditor-modal textarea{height:inherit}.jsoneditor select,.jsoneditor-modal select{display:inherit;height:inherit}.jsoneditor label,.jsoneditor-modal label{font-size:inherit;font-weight:inherit;color:inherit}.jsoneditor table,.jsoneditor-modal table{border-collapse:collapse;width:auto}.jsoneditor td,.jsoneditor th,.jsoneditor-modal td,.jsoneditor-modal th{padding:0;display:table-cell;text-align:left;vertical-align:inherit;border-radius:inherit}.jsoneditor .autocomplete.dropdown{position:absolute;background:#fff;box-shadow:2px 2px 12px rgba(128,128,128,.3);border:1px solid #d3d3d3;overflow-x:hidden;overflow-y:auto;cursor:default;margin:0;padding:5px;text-align:left;outline:0;font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px}.jsoneditor .autocomplete.dropdown .item{color:#1a1a1a}.jsoneditor .autocomplete.dropdown .item.hover{background-color:#ebebeb}.jsoneditor .autocomplete.hint{color:#a1a1a1;top:4px;left:4px}.jsoneditor-contextmenu-root{position:relative;width:0;height:0}.jsoneditor-contextmenu{position:absolute;box-sizing:content-box;z-index:2}.jsoneditor-contextmenu .jsoneditor-menu{position:relative;left:0;top:0;width:128px;height:auto;background:#fff;border:1px solid #d3d3d3;box-shadow:2px 2px 12px rgba(128,128,128,.3);list-style:none;margin:0;padding:0}.jsoneditor-contextmenu .jsoneditor-menu button{position:relative;padding:0 8px 0 0;margin:0;width:128px;height:auto;border:none;cursor:pointer;color:#4d4d4d;background:0 0;font-size:14px;font-family:arial,sans-serif;box-sizing:border-box;text-align:left}.jsoneditor-contextmenu .jsoneditor-menu button::-moz-focus-inner{padding:0;border:0}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-default{width:96px}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-expand{float:right;width:32px;height:24px;border-left:1px solid #e5e5e5}.jsoneditor-contextmenu .jsoneditor-menu li{overflow:hidden}.jsoneditor-contextmenu .jsoneditor-menu li ul{display:none;position:relative;left:-10px;top:0;border:none;box-shadow:inset 0 0 10px rgba(128,128,128,.5);padding:0 10px;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.jsoneditor-contextmenu .jsoneditor-menu li ul .jsoneditor-icon{margin-left:24px}.jsoneditor-contextmenu .jsoneditor-menu li ul li button{padding-left:24px;animation:all ease-in-out 1s}.jsoneditor-contextmenu .jsoneditor-menu li button .jsoneditor-expand{position:absolute;top:0;right:0;width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:0 -72px}.jsoneditor-contextmenu .jsoneditor-icon{position:absolute;top:0;left:0;width:24px;height:24px;border:none;padding:0;margin:0;background-image:url(./img/jsoneditor-icons.svg)}.jsoneditor-contextmenu .jsoneditor-text{padding:4px 0 4px 24px;word-wrap:break-word}.jsoneditor-contextmenu .jsoneditor-text.jsoneditor-right-margin{padding-right:24px}.jsoneditor-contextmenu .jsoneditor-separator{height:0;border-top:1px solid #e5e5e5;padding-top:5px;margin-top:5px}.jsoneditor-contextmenu button.jsoneditor-remove .jsoneditor-icon{background-position:-24px 0}.jsoneditor-contextmenu button.jsoneditor-append .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-insert .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-duplicate .jsoneditor-icon{background-position:-48px 0}.jsoneditor-contextmenu button.jsoneditor-sort-asc .jsoneditor-icon{background-position:-168px 0}.jsoneditor-contextmenu button.jsoneditor-sort-desc .jsoneditor-icon{background-position:-192px 0}.jsoneditor-contextmenu button.jsoneditor-transform .jsoneditor-icon{background-position:-216px 0}.jsoneditor-contextmenu button.jsoneditor-extract .jsoneditor-icon{background-position:0 -24px}.jsoneditor-contextmenu button.jsoneditor-type-string .jsoneditor-icon{background-position:-144px 0}.jsoneditor-contextmenu button.jsoneditor-type-auto .jsoneditor-icon{background-position:-120px 0}.jsoneditor-contextmenu button.jsoneditor-type-object .jsoneditor-icon{background-position:-72px 0}.jsoneditor-contextmenu button.jsoneditor-type-array .jsoneditor-icon{background-position:-96px 0}.jsoneditor-contextmenu button.jsoneditor-type-modes .jsoneditor-icon{background-image:none;width:6px}.jsoneditor-contextmenu li,.jsoneditor-contextmenu ul{box-sizing:content-box;position:relative}.jsoneditor-contextmenu .jsoneditor-menu button:focus,.jsoneditor-contextmenu .jsoneditor-menu button:hover{color:#1a1a1a;background-color:#f5f5f5;outline:0}.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover{color:#fff;background-color:#ee422e}.jsoneditor-contextmenu .jsoneditor-menu li ul li button:focus,.jsoneditor-contextmenu .jsoneditor-menu li ul li button:hover{background-color:#f5f5f5}.jsoneditor-modal{max-width:95%;border-radius:2px!important;padding:45px 15px 15px 15px!important;box-shadow:2px 2px 12px rgba(128,128,128,.3);color:#4d4d4d;line-height:1.3em}.jsoneditor-modal.jsoneditor-modal-transform{width:600px!important}.jsoneditor-modal .pico-modal-header{position:absolute;box-sizing:border-box;top:0;left:0;width:100%;padding:0 10px;height:30px;line-height:30px;font-family:arial,sans-serif;font-size:11pt;background:#3883fa;color:#fff}.jsoneditor-modal table{width:100%}.jsoneditor-modal table td{padding:3px 0}.jsoneditor-modal table td.jsoneditor-modal-input{text-align:right;padding-right:0;white-space:nowrap}.jsoneditor-modal table td.jsoneditor-modal-actions{padding-top:15px}.jsoneditor-modal table th{vertical-align:middle}.jsoneditor-modal p:first-child{margin-top:0}.jsoneditor-modal a{color:#3883fa}.jsoneditor-modal .jsoneditor-jmespath-block{margin-bottom:10px}.jsoneditor-modal .pico-close{background:0 0!important;font-size:24px!important;top:7px!important;right:7px!important;color:#fff}.jsoneditor-modal input{padding:4px}.jsoneditor-modal input[type=text]{cursor:inherit}.jsoneditor-modal input[disabled]{background:#d3d3d3;color:grey}.jsoneditor-modal .jsoneditor-select-wrapper{position:relative;display:inline-block}.jsoneditor-modal .jsoneditor-select-wrapper:after{content:"";width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:6px solid #666;position:absolute;right:8px;top:14px;pointer-events:none}.jsoneditor-modal select{padding:3px 24px 3px 10px;min-width:180px;max-width:350px;-webkit-appearance:none;-moz-appearance:none;appearance:none;text-indent:0;text-overflow:"";font-size:14px;line-height:1.5em}.jsoneditor-modal select::-ms-expand{display:none}.jsoneditor-modal .jsoneditor-button-group input{padding:4px 10px;margin:0;border-radius:0;border-left-style:none}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-first{border-top-left-radius:3px;border-bottom-left-radius:3px;border-left-style:solid}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-last{border-top-right-radius:3px;border-bottom-right-radius:3px}.jsoneditor-modal .jsoneditor-transform-preview{background:#f5f5f5;height:200px}.jsoneditor-modal .jsoneditor-transform-preview.jsoneditor-error{color:#ee422e}.jsoneditor-modal .jsoneditor-jmespath-wizard{line-height:1.2em;width:100%;padding:0;border-radius:3px}.jsoneditor-modal .jsoneditor-jmespath-label{font-weight:700;color:#1e90ff;margin-top:20px;margin-bottom:5px}.jsoneditor-modal .jsoneditor-jmespath-wizard-table{width:100%;border-collapse:collapse}.jsoneditor-modal .jsoneditor-jmespath-wizard-label{font-style:italic;margin:4px 0 2px 0}.jsoneditor-modal .jsoneditor-inline{position:relative;display:inline-block;width:100%;padding-top:2px;padding-bottom:2px}.jsoneditor-modal .jsoneditor-inline:not(:last-child){padding-right:2px}.jsoneditor-modal .jsoneditor-jmespath-filter{display:flex;flex-wrap:wrap}.jsoneditor-modal .jsoneditor-jmespath-filter-field{width:180px}.jsoneditor-modal .jsoneditor-jmespath-filter-relation{width:100px}.jsoneditor-modal .jsoneditor-jmespath-filter-value{min-width:180px;flex:1}.jsoneditor-modal .jsoneditor-jmespath-sort-field{width:170px}.jsoneditor-modal .jsoneditor-jmespath-sort-order{width:150px}.jsoneditor-modal .jsoneditor-jmespath-select-fields{width:100%}.jsoneditor-modal .selectr-selected{border-color:#d3d3d3;padding:4px 28px 4px 8px}.jsoneditor-modal .selectr-selected .selectr-tag{background-color:#3883fa;border-radius:5px}.jsoneditor-modal table td,.jsoneditor-modal table th{text-align:left;vertical-align:middle;font-weight:400;color:#4d4d4d;border-spacing:0;border-collapse:collapse}.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal input[type=text]:focus,.jsoneditor-modal select,.jsoneditor-modal textarea{background:#fff;border:1px solid #d3d3d3;color:#4d4d4d;border-radius:3px;padding:4px}.jsoneditor-modal #query,.jsoneditor-modal textarea{border-radius:unset}.jsoneditor-modal,.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal option,.jsoneditor-modal select,.jsoneditor-modal table td,.jsoneditor-modal table th,.jsoneditor-modal textarea{font-size:10.5pt;font-family:arial,sans-serif}.jsoneditor-modal #query,.jsoneditor-modal .jsoneditor-transform-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;width:100%;box-sizing:border-box}.jsoneditor-modal input[type=button],.jsoneditor-modal input[type=submit]{background:#f5f5f5;padding:4px 20px}.jsoneditor-modal input,.jsoneditor-modal select{cursor:pointer}.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-asc input.jsoneditor-button-asc,.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-desc input.jsoneditor-button-desc{background:#3883fa;border-color:#3883fa;color:#fff}.jsoneditor{color:#1a1a1a;border:thin solid #3883fa;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;position:relative;padding:0;line-height:100%}div.jsoneditor-default,div.jsoneditor-field,div.jsoneditor-readonly,div.jsoneditor-value{border:1px solid transparent;min-height:16px;min-width:32px;line-height:16px;padding:2px;margin:1px;word-wrap:break-word;float:left}div.jsoneditor-field p,div.jsoneditor-value p{margin:0}div.jsoneditor-value{word-break:break-word}div.jsoneditor-value.jsoneditor-empty::after{content:"value"}div.jsoneditor-value.jsoneditor-string{color:#006000}div.jsoneditor-value.jsoneditor-number{color:#ee422e}div.jsoneditor-value.jsoneditor-boolean{color:#ff8c00}div.jsoneditor-value.jsoneditor-null{color:#004ed0}div.jsoneditor-value.jsoneditor-color-value{color:#1a1a1a}div.jsoneditor-value.jsoneditor-invalid{color:#1a1a1a}div.jsoneditor-readonly{min-width:16px;color:grey}div.jsoneditor-empty{border-color:#d3d3d3;border-style:dashed;border-radius:2px}div.jsoneditor-field.jsoneditor-empty::after{content:"field"}div.jsoneditor td{vertical-align:top}div.jsoneditor td.jsoneditor-separator{padding:3px 0;vertical-align:top;color:grey}div.jsoneditor td.jsoneditor-tree{vertical-align:top}div.jsoneditor.busy pre.jsoneditor-preview{background:#f5f5f5;color:grey}div.jsoneditor.busy div.jsoneditor-busy{display:inherit}div.jsoneditor code.jsoneditor-preview{background:0 0}div.jsoneditor.jsoneditor-mode-preview pre.jsoneditor-preview{width:100%;height:100%;box-sizing:border-box;overflow:auto;padding:2px;margin:0;white-space:pre-wrap;word-break:break-all}div.jsoneditor-default{color:grey;padding-left:10px}div.jsoneditor-tree{width:100%;height:100%;position:relative;overflow:auto;background:#fff}div.jsoneditor-tree button.jsoneditor-button{width:24px;height:24px;padding:0;margin:0;border:none;cursor:pointer;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg)}div.jsoneditor-tree button.jsoneditor-button:focus{background-color:#f5f5f5;outline:#e5e5e5 solid 1px}div.jsoneditor-tree button.jsoneditor-collapsed{background-position:0 -48px}div.jsoneditor-tree button.jsoneditor-expanded{background-position:0 -72px}div.jsoneditor-tree button.jsoneditor-contextmenu-button{background-position:-48px -72px}div.jsoneditor-tree button.jsoneditor-invisible{visibility:hidden;background:0 0}div.jsoneditor-tree button.jsoneditor-dragarea{background-image:url(./img/jsoneditor-icons.svg);background-position:-72px -72px;cursor:move}div.jsoneditor-tree :focus{outline:0}div.jsoneditor-tree div.jsoneditor-show-more{display:inline-block;padding:3px 4px;margin:2px 0;background-color:#e5e5e5;border-radius:3px;color:grey;font-family:arial,sans-serif;font-size:14px}div.jsoneditor-tree div.jsoneditor-show-more a{display:inline-block;color:grey}div.jsoneditor-tree div.jsoneditor-color{display:inline-block;width:12px;height:12px;margin:4px;border:1px solid grey;cursor:pointer}div.jsoneditor-tree div.jsoneditor-color.jsoneditor-color-readonly{cursor:inherit}div.jsoneditor-tree div.jsoneditor-date{background:#a1a1a1;color:#fff;font-family:arial,sans-serif;border-radius:3px;display:inline-block;padding:3px;margin:0 3px}div.jsoneditor-tree table.jsoneditor-tree{border-collapse:collapse;border-spacing:0;width:100%}div.jsoneditor-tree .jsoneditor-button{display:block}div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error{width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}div.jsoneditor-outer{position:static;width:100%;height:100%;margin:0;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}div.jsoneditor-outer.has-nav-bar{margin-top:-26px;padding-top:26px}div.jsoneditor-outer.has-nav-bar.has-main-menu-bar{margin-top:-61px;padding-top:61px}div.jsoneditor-outer.has-status-bar{margin-bottom:-26px;padding-bottom:26px}div.jsoneditor-outer.has-main-menu-bar{margin-top:-35px;padding-top:35px}div.jsoneditor-busy{position:absolute;top:15%;left:0;box-sizing:border-box;width:100%;text-align:center;display:none}div.jsoneditor-busy span{background-color:#ffffab;border:1px solid #fe0;border-radius:3px;padding:5px 15px;box-shadow:0 0 5px rgba(0,0,0,.4)}div.jsoneditor-field.jsoneditor-empty::after,div.jsoneditor-value.jsoneditor-empty::after{pointer-events:none;color:#d3d3d3;font-size:8pt}a.jsoneditor-value.jsoneditor-url,div.jsoneditor-value.jsoneditor-url{color:#006000;text-decoration:underline}a.jsoneditor-value.jsoneditor-url{display:inline-block;padding:2px;margin:2px}a.jsoneditor-value.jsoneditor-url:focus,a.jsoneditor-value.jsoneditor-url:hover{color:#ee422e}div.jsoneditor-field.jsoneditor-highlight,div.jsoneditor-field[contenteditable=true]:focus,div.jsoneditor-field[contenteditable=true]:hover,div.jsoneditor-value.jsoneditor-highlight,div.jsoneditor-value[contenteditable=true]:focus,div.jsoneditor-value[contenteditable=true]:hover{background-color:#ffffab;border:1px solid #fe0;border-radius:2px}div.jsoneditor-field.jsoneditor-highlight-active,div.jsoneditor-field.jsoneditor-highlight-active:focus,div.jsoneditor-field.jsoneditor-highlight-active:hover,div.jsoneditor-value.jsoneditor-highlight-active,div.jsoneditor-value.jsoneditor-highlight-active:focus,div.jsoneditor-value.jsoneditor-highlight-active:hover{background-color:#fe0;border:1px solid #ffc700;border-radius:2px}div.jsoneditor-value.jsoneditor-array,div.jsoneditor-value.jsoneditor-object{min-width:16px}div.jsoneditor-tree button.jsoneditor-contextmenu-button.jsoneditor-selected,div.jsoneditor-tree button.jsoneditor-contextmenu-button:focus,div.jsoneditor-tree button.jsoneditor-contextmenu-button:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button{background-position:-48px -48px}div.jsoneditor-tree div.jsoneditor-show-more a:focus,div.jsoneditor-tree div.jsoneditor-show-more a:hover{color:#ee422e}.ace-jsoneditor,textarea.jsoneditor-text{min-height:150px}.ace-jsoneditor.ace_editor,textarea.jsoneditor-text.ace_editor{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace}textarea.jsoneditor-text{width:100%;height:100%;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;outline-width:0;border:none;background-color:#fff;resize:none}tr.jsoneditor-highlight,tr.jsoneditor-selected{background-color:#d3d3d3}tr.jsoneditor-selected button.jsoneditor-contextmenu-button,tr.jsoneditor-selected button.jsoneditor-dragarea{visibility:hidden}tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{visibility:visible}div.jsoneditor-tree button.jsoneditor-dragarea:focus,div.jsoneditor-tree button.jsoneditor-dragarea:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{background-position:-72px -48px}div.jsoneditor td,div.jsoneditor th,div.jsoneditor tr{padding:0;margin:0}.jsoneditor-popover,.jsoneditor-schema-error,div.jsoneditor td,div.jsoneditor textarea,div.jsoneditor th,div.jsoneditor-field,div.jsoneditor-value,pre.jsoneditor-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;color:#1a1a1a}.jsoneditor-schema-error{cursor:default;display:inline-block;height:24px;line-height:24px;position:relative;text-align:center;width:24px}.jsoneditor-popover{background-color:#4c4c4c;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.4);color:#fff;padding:7px 10px;position:absolute;cursor:auto;width:200px}.jsoneditor-popover.jsoneditor-above{bottom:32px;left:-98px}.jsoneditor-popover.jsoneditor-above:before{border-top:7px solid #4c4c4c;bottom:-7px}.jsoneditor-popover.jsoneditor-below{top:32px;left:-98px}.jsoneditor-popover.jsoneditor-below:before{border-bottom:7px solid #4c4c4c;top:-7px}.jsoneditor-popover.jsoneditor-left{top:-7px;right:32px}.jsoneditor-popover.jsoneditor-left:before{border-left:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;right:-14px;left:inherit;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover.jsoneditor-right{top:-7px;left:32px}.jsoneditor-popover.jsoneditor-right:before{border-right:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;left:-14px;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover:before{border-right:7px solid transparent;border-left:7px solid transparent;content:"";display:block;left:50%;margin-left:-7px;position:absolute}.jsoneditor-text-errors tr.jump-to-line:hover{text-decoration:underline;cursor:pointer}.jsoneditor-schema-error:focus .jsoneditor-popover,.jsoneditor-schema-error:hover .jsoneditor-popover{display:block;animation:fade-in .3s linear 1,move-up .3s linear 1}@keyframes fade-in{from{opacity:0}to{opacity:1}}.jsoneditor .jsoneditor-validation-errors-container{max-height:130px;overflow-y:auto}.jsoneditor .jsoneditor-validation-errors{width:100%;overflow:hidden}.jsoneditor .jsoneditor-additional-errors{position:absolute;margin:auto;bottom:31px;left:calc(50% - 92px);color:grey;background-color:#ebebeb;padding:7px 15px;border-radius:8px}.jsoneditor .jsoneditor-additional-errors.visible{visibility:visible;opacity:1;transition:opacity 2s linear}.jsoneditor .jsoneditor-additional-errors.hidden{visibility:hidden;opacity:0;transition:visibility 0s 2s,opacity 2s linear}.jsoneditor .jsoneditor-text-errors{width:100%;border-collapse:collapse;border-top:1px solid #ffc700}.jsoneditor .jsoneditor-text-errors td{padding:3px 6px;vertical-align:middle}.jsoneditor .jsoneditor-text-errors td pre{margin:0;white-space:pre-wrap}.jsoneditor .jsoneditor-text-errors tr{background-color:#ffffab}.jsoneditor .jsoneditor-text-errors tr.parse-error{background-color:rgba(238,46,46,.4392156863)}.jsoneditor-text-errors .jsoneditor-schema-error{border:none;width:24px;height:24px;padding:0;margin:0 4px 0 0;cursor:pointer}.jsoneditor-text-errors tr .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}.jsoneditor-text-errors tr.parse-error .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0;background-color:transparent}.jsoneditor-anchor{cursor:pointer}.jsoneditor-anchor .picker_wrapper.popup.popup_bottom{top:28px;left:-10px}.fadein{-webkit-animation:fadein .3s;animation:fadein .3s;-moz-animation:fadein .3s;-o-animation:fadein .3s}@keyframes fadein{0%{opacity:0}100%{opacity:1}}.jsoneditor-modal input[type=search].selectr-input{border:1px solid #d3d3d3;width:calc(100% - 4px);margin:2px;padding:4px;box-sizing:border-box}.jsoneditor-modal button.selectr-input-clear{right:8px}.jsoneditor-menu{width:100%;height:35px;padding:2px;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;background-color:#3883fa;border-bottom:1px solid #3883fa}.jsoneditor-menu>.jsoneditor-modes>button,.jsoneditor-menu>button{width:26px;height:26px;margin:2px;padding:0;border-radius:2px;border:1px solid transparent;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg);color:#fff;opacity:.8;font-family:arial,sans-serif;font-size:14px;float:left}.jsoneditor-menu>.jsoneditor-modes>button:hover,.jsoneditor-menu>button:hover{background-color:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4)}.jsoneditor-menu>.jsoneditor-modes>button:active,.jsoneditor-menu>.jsoneditor-modes>button:focus,.jsoneditor-menu>button:active,.jsoneditor-menu>button:focus{background-color:rgba(255,255,255,.3)}.jsoneditor-menu>.jsoneditor-modes>button:disabled,.jsoneditor-menu>button:disabled{opacity:.5;background-color:transparent;border:none}.jsoneditor-menu>button.jsoneditor-collapse-all{background-position:0 -96px}.jsoneditor-menu>button.jsoneditor-expand-all{background-position:0 -120px}.jsoneditor-menu>button.jsoneditor-sort{background-position:-120px -96px}.jsoneditor-menu>button.jsoneditor-transform{background-position:-144px -96px}.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-transform,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-transform{display:none}.jsoneditor-menu>button.jsoneditor-undo{background-position:-24px -96px}.jsoneditor-menu>button.jsoneditor-undo:disabled{background-position:-24px -120px}.jsoneditor-menu>button.jsoneditor-redo{background-position:-48px -96px}.jsoneditor-menu>button.jsoneditor-redo:disabled{background-position:-48px -120px}.jsoneditor-menu>button.jsoneditor-compact{background-position:-72px -96px}.jsoneditor-menu>button.jsoneditor-format{background-position:-72px -120px}.jsoneditor-menu>button.jsoneditor-repair{background-position:-96px -96px}.jsoneditor-menu>.jsoneditor-modes{display:inline-block;float:left}.jsoneditor-menu>.jsoneditor-modes>button{background-image:none;width:auto;padding-left:6px;padding-right:6px}.jsoneditor-menu>.jsoneditor-modes>button.jsoneditor-separator,.jsoneditor-menu>button.jsoneditor-separator{margin-left:10px}.jsoneditor-menu a{font-family:arial,sans-serif;font-size:14px;color:#fff;opacity:.8;vertical-align:middle}.jsoneditor-menu a:hover{opacity:1}.jsoneditor-menu a.jsoneditor-poweredBy{font-size:8pt;position:absolute;right:0;top:0;padding:10px}.jsoneditor-navigation-bar{width:100%;height:26px;line-height:26px;padding:0;margin:0;border-bottom:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:grey;background-color:#ebebeb;overflow:hidden;font-family:arial,sans-serif;font-size:14px}.jsoneditor-search{font-family:arial,sans-serif;position:absolute;right:4px;top:4px;border-collapse:collapse;border-spacing:0;display:flex}.jsoneditor-search input{color:#1a1a1a;width:120px;border:none;outline:0;margin:1px;line-height:20px;font-family:arial,sans-serif}.jsoneditor-search button{width:16px;height:24px;padding:0;margin:0;border:none;background:url(./img/jsoneditor-icons.svg);vertical-align:top}.jsoneditor-search button:hover{background-color:transparent}.jsoneditor-search button.jsoneditor-refresh{width:18px;background-position:-99px -73px}.jsoneditor-search button.jsoneditor-next{cursor:pointer;background-position:-124px -73px}.jsoneditor-search button.jsoneditor-next:hover{background-position:-124px -49px}.jsoneditor-search button.jsoneditor-previous{cursor:pointer;background-position:-148px -73px;margin-right:2px}.jsoneditor-search button.jsoneditor-previous:hover{background-position:-148px -49px}.jsoneditor-results{font-family:arial,sans-serif;color:#fff;padding-right:5px;line-height:26px}.jsoneditor-frame{border:1px solid transparent;background-color:#fff;padding:0 2px;margin:0}.jsoneditor-statusbar{line-height:26px;height:26px;color:grey;background-color:#ebebeb;border-top:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px}.jsoneditor-statusbar>.jsoneditor-curserinfo-val{margin-right:12px}.jsoneditor-statusbar>.jsoneditor-curserinfo-count{margin-left:4px}.jsoneditor-statusbar>.jsoneditor-validation-error-icon{float:right;width:24px;height:24px;padding:0;margin-top:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-validation-error-count{float:right;margin:0 4px 0 0;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-parse-error-icon{float:right;width:24px;height:24px;padding:0;margin:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0}.jsoneditor-statusbar .jsoneditor-array-info a{color:inherit}div.jsoneditor-statusbar>.jsoneditor-curserinfo-label,div.jsoneditor-statusbar>.jsoneditor-size-info{margin:0 4px}.jsoneditor-treepath{padding:0 5px;overflow:hidden;white-space:nowrap;outline:0}.jsoneditor-treepath.show-all{word-wrap:break-word;white-space:normal;position:absolute;background-color:#ebebeb;z-index:1;box-shadow:2px 2px 12px rgba(128,128,128,.3)}.jsoneditor-treepath.show-all span.jsoneditor-treepath-show-all-btn{display:none}.jsoneditor-treepath div.jsoneditor-contextmenu-root{position:absolute;left:0}.jsoneditor-treepath .jsoneditor-treepath-show-all-btn{position:absolute;background-color:#ebebeb;left:0;height:20px;padding:0 3px;cursor:pointer}.jsoneditor-treepath .jsoneditor-treepath-element{margin:1px;font-family:arial,sans-serif;font-size:14px}.jsoneditor-treepath .jsoneditor-treepath-seperator{margin:2px;font-size:9pt;font-family:arial,sans-serif}.jsoneditor-treepath span.jsoneditor-treepath-element:hover,.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover{cursor:pointer;text-decoration:underline}/*! 2 | * Selectr 2.4.13 3 | * http://mobius.ovh/docs/selectr 4 | * 5 | * Released under the MIT license 6 | */.selectr-container{position:relative}.selectr-container li{list-style:none}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0 none}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;background-color:#fff}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:"";-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0 4px;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{list-style:none;position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:medium none;border-radius:10px;background:#acb7bf none repeat scroll 0 0}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:medium none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:" ";background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;background-color:#fff}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input-container.active,.selectr-input-container.active .selectr-clear{display:block}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;list-style:outside none none;cursor:pointer;font-weight:400}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-option.excluded{display:none}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent #999;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-placeholder,.selectr-empty{display:none}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-label{width:auto}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px;width:auto}.selectr-tag-input{border:medium none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:"";-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running selectr-spin;-webkit-animation:.5s linear 0s normal forwards infinite running selectr-spin;animation:.5s linear 0s normal forwards infinite running selectr-spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;background-color:#fff}.selectr-container.inverted .selectr-options-container{top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1} -------------------------------------------------------------------------------- /static/img/jsoneditor-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | JSON Editor Icons 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | JSON Editor Icons 27 | 28 | 29 | 30 | 32 | 56 | 60 | 61 | 62 | 69 | 76 | 83 | 90 | 97 | 100 | 107 | 114 | 115 | 119 | 126 | 133 | 134 | 141 | 148 | 155 | 157 | 164 | 171 | 178 | 179 | 182 | 189 | 196 | 203 | 204 | 211 | 217 | 223 | 230 | 235 | 240 | 247 | 253 | 258 | 265 | 271 | 277 | 284 | 291 | 298 | 305 | 312 | 319 | 326 | 332 | 340 | 346 | 352 | 359 | 367 | 375 | 382 | 389 | 396 | 403 | 410 | 417 | 424 | 431 | 437 | 445 | 451 | 457 | 464 | 472 | 480 | 487 | 494 | 501 | 508 | 515 | 522 | 529 | 536 | 541 | 546 | 551 | 557 | 563 | 568 | 573 | 578 | 583 | 588 | 604 | 621 | 638 | 655 | 661 | 667 | 673 | 679 | 686 | 692 | 695 | 702 | 709 | 716 | 723 | 729 | 730 | 736 | 743 | 749 | 750 | -------------------------------------------------------------------------------- /static/chart.js: -------------------------------------------------------------------------------- 1 | async function drawChart(chart, anchor, canvas, ctx) { 2 | if (!canvas) { 3 | canvas = document.createElement('canvas') 4 | let width = anchor[2] 5 | let height = anchor[3] 6 | canvas.width = width * 2 7 | canvas.height = height * 2 8 | canvas.style.width = width + 'px' 9 | canvas.style.height = height + 'px' 10 | let ctx = canvas.getContext('2d') 11 | ctx.scale(2, 2) 12 | ctx.imageSmoothingEnabled = true 13 | ctx.imageSmoothingQuality = 'high' 14 | } 15 | if (!ctx) { 16 | ctx = canvas.getContext('2d') 17 | } 18 | ctx.imageSmoothingEnabled = true 19 | ctx.imageSmoothingQuality = 'high' 20 | let title = chart.title 21 | let legend = chart.legend 22 | let chartData = chart.chartData[0] 23 | let chartType = chartData.chartType 24 | if (chartType == 'bar') { 25 | // 柱状图、条形图 26 | let type = chartData.extInfo.type 27 | if (type == 'bar') { 28 | // 条形图 29 | await drawBarChartWithBar(title, chartData, legend, anchor, canvas, ctx) 30 | } else { 31 | // 柱状图 32 | await drawBarChartWithCol(title, chartData, legend, anchor, canvas, ctx) 33 | } 34 | } else if (chartType == 'pie' || chartType == 'doughnut') { 35 | // 饼图、圆环图 36 | await drawPieChart(title, chartData, legend, anchor, canvas, ctx) 37 | } else if (chartType == 'line') { 38 | // 折线图 39 | await drawLineChart(title, chartData, legend, anchor, canvas, ctx) 40 | } else { 41 | // 其他: 暂不支持 42 | await drawRect(ctx, { 43 | strokeStyle: { 44 | lineWidth: 0.5, 45 | paint: { type: 'color', color: { color: -2500135 } } 46 | }, 47 | anchor: [anchor[0] + 0.5, anchor[1] + 0.5, anchor[2] - 1, anchor[3] - 1] 48 | }) 49 | let str = '该图表暂不支持渲染' 50 | ctx.font = `${Math.min(anchor[2] * 0.056, 16)}px 等线` 51 | ctx.fillStyle = 'rgb(153, 153, 153)' 52 | let textWidth = ctx.measureText(str).width 53 | ctx.fillText(str, anchor[0] + anchor[2] / 2 - textWidth / 2, anchor[1] + anchor[3] / 2 - 8) 54 | } 55 | return canvas 56 | } 57 | 58 | async function drawBarChartWithBar(title, chartData, legend, anchor, canvas, ctx) { 59 | if (title) { 60 | ctx.font = `${Math.min(anchor[3] * 0.064, 18.5)}px 等线` 61 | ctx.fillStyle = 'rgb(89, 89, 89)' 62 | let textWidth = ctx.measureText(title).width 63 | ctx.fillText(title, anchor[0] + anchor[2] / 2 - textWidth / 2, anchor[1] + anchor[3] * 0.06) 64 | } 65 | let extInfo = chartData.extInfo 66 | let series = chartData.series 67 | let categoryAxis = chartData.categoryAxis 68 | let valueAxes = chartData.valueAxes && chartData.valueAxes.length > 0 ? chartData.valueAxes[0] : null 69 | let minValue = 0, maxValue = 0 70 | for (let i = 0; i < series.length; i++) { 71 | minValue = Math.min(minValue, ...series[i].value.data) 72 | maxValue = Math.max(maxValue, ...series[i].value.data) 73 | } 74 | ctx.lineWidth = 0.5 75 | ctx.font = `${Math.min(anchor[2] * 0.048, 14)}px 等线` 76 | ctx.fillStyle = 'rgb(89, 89, 89)' 77 | ctx.strokeStyle = 'rgb(217, 217, 217)' 78 | let categorys = series[0].category.data 79 | let maxCw = ctx.measureText('00').width 80 | for (let i = 0; i < categorys.length; i++) { 81 | let w = ctx.measureText(categorys[i] + '0').width 82 | if (w > maxCw) { 83 | maxCw = w 84 | } 85 | } 86 | let sGap = valueAxes && !valueAxes.deleted ? maxCw : anchor[2] * 0.15 87 | let x = anchor[0] + sGap 88 | let xWidth = anchor[2] - sGap - anchor[2] * 0.04 89 | let startY = title ? anchor[3] * 0.12 : anchor[3] * 0.05 90 | let y = anchor[1] + startY 91 | let yHeight = anchor[3] - startY - anchor[3] * 0.15 92 | let minTicks = xWidth / 10 > 20 ? 11 : 6 93 | let ticks = calculateTicks(minValue, maxValue, minTicks, 11) 94 | let xGap = xWidth / (ticks.length - 1) 95 | ctx.beginPath() 96 | for (let i = 0; i < ticks.length; i++) { 97 | // x轴数值 98 | if (valueAxes && !valueAxes.deleted) { 99 | ctx.textAlign = 'center' 100 | let v = ticks[i] 101 | let vs = v.toString() 102 | if (vs.indexOf('.') > -1 && vs.split('.')[1].length > 1) { 103 | vs = v.toFixed(1) 104 | } 105 | ctx.fillText(vs, x, y + yHeight + anchor[3] * 0.05) 106 | ctx.textAlign = 'start' 107 | } 108 | if (extInfo.majorGridlines == 'true') { 109 | // 网格线 110 | ctx.moveTo(x, y) 111 | ctx.lineTo(x, y + yHeight) 112 | } 113 | x += xGap 114 | } 115 | ctx.stroke() 116 | let yGap = anchor[3] * 0.04 // y开始 117 | y += yGap 118 | x = anchor[0] + sGap 119 | let vGap = anchor[3] * 0.015 // 柱子间距 120 | let catGap = vGap * 5 // 类目间距 121 | let categoryHeight = (yHeight - catGap * (categorys.length - 1) - yGap * 2) / categorys.length 122 | let vHeight = (categoryHeight - vGap * (series.length - 1)) / series.length 123 | // 绘制类目和柱状图 124 | for (let i = 0; i < categorys.length; i++) { 125 | if (categoryAxis && !categoryAxis.deleted) { 126 | ctx.fillStyle = 'rgb(89, 89, 89)' 127 | let _cy = (categoryHeight + catGap) * i + categoryHeight / 2 + 6 128 | ctx.textAlign = 'right' 129 | ctx.fillText(categorys[categorys.length - 1 - i], x - 4, y + _cy) 130 | ctx.textAlign = 'start' 131 | } 132 | for (let j = 0; j < series.length; j++) { 133 | let seriesData = series[series.length - 1 - j] 134 | let valueData = seriesData.value.data 135 | let vWidth = (valueData[valueData.length - 1 - i] / ticks[ticks.length - 1]) * xWidth 136 | let _x = x 137 | let _y = y + (categoryHeight + catGap) * i + (vHeight + vGap) * j 138 | let _anchor = [_x, _y, vWidth, vHeight] 139 | // 柱状图 140 | await drawRect(ctx, { 141 | fillStyle: seriesData.property.fillStyle, 142 | strokeStyle: seriesData.property.strokeStyle, 143 | anchor: _anchor 144 | }) 145 | } 146 | } 147 | if (legend) { 148 | // 绘制v标签标识 149 | x = anchor[0] + sGap 150 | let vTexts = [] 151 | let vTextWidth = 0 152 | for (let i = 0; i < series.length; i++) { 153 | let s = series[i].text.data[0] 154 | vTexts.push(s) 155 | vTextWidth += ctx.measureText(s).width 156 | } 157 | vGap = anchor[3] * 0.03 158 | let _iw = Math.min(anchor[2] * 0.028, 15) 159 | let _y = y + yHeight + anchor[3] * 0.1 160 | let _x = x + xWidth / 2 - (vTextWidth + vGap * (vTexts.length - 1) + (_iw * 1.25) * vTexts.length) / 2 161 | for (let i = 0; i < vTexts.length; i++) { 162 | await drawRect(ctx, { 163 | fillStyle: series[series.length - 1 - i].property.fillStyle, 164 | strokeStyle: series[series.length - 1 - i].property.strokeStyle, 165 | anchor: [_x, _y - _iw, _iw, _iw] 166 | }) 167 | _x += (_iw * 1.25) 168 | ctx.fillStyle = 'rgb(89, 89, 89)' 169 | let str = vTexts[vTexts.length - 1 - i] 170 | ctx.fillText(str, _x, _y) 171 | _x += ctx.measureText(str).width 172 | _x += vGap 173 | } 174 | } 175 | } 176 | 177 | async function drawBarChartWithCol(title, chartData, legend, anchor, canvas, ctx) { 178 | if (title) { 179 | ctx.font = `${Math.min(anchor[3] * 0.07, 18.5)}px 等线` 180 | ctx.fillStyle = 'rgb(89, 89, 89)' 181 | let textWidth = ctx.measureText(title).width 182 | ctx.fillText(title, anchor[0] + anchor[2] / 2 - textWidth / 2, anchor[1] + anchor[3] * 0.06) 183 | } 184 | let extInfo = chartData.extInfo 185 | let series = chartData.series 186 | let categoryAxis = chartData.categoryAxis 187 | let valueAxes = chartData.valueAxes && chartData.valueAxes.length > 0 ? chartData.valueAxes[0] : null 188 | let minValue = 0, maxValue = 0 189 | for (let i = 0; i < series.length; i++) { 190 | minValue = Math.min(minValue, ...series[i].value.data) 191 | maxValue = Math.max(maxValue, ...series[i].value.data) 192 | } 193 | ctx.lineWidth = 0.5 194 | ctx.font = `${Math.min(anchor[3] * 0.06, 14)}px 等线` 195 | ctx.fillStyle = 'rgb(89, 89, 89)' 196 | ctx.strokeStyle = 'rgb(217, 217, 217)' 197 | let startY = title ? anchor[3] * 0.12 : anchor[3] * 0.05 198 | let y = anchor[1] + startY 199 | let yHeight = anchor[3] - startY - anchor[3] * 0.15 200 | let mixTicks = yHeight / 10 > 20 ? 11 : 6 201 | let ticks = calculateTicks(minValue, maxValue, mixTicks, 11) 202 | let xGap = ctx.measureText(ticks[ticks.length - 1] + '00').width 203 | let x = anchor[0] + xGap 204 | let xWidth = anchor[2] - xGap * 1.5 205 | let yGap = yHeight / (ticks.length - 1) 206 | ctx.beginPath() 207 | for (let i = 0; i < ticks.length; i++) { 208 | if (valueAxes && !valueAxes.deleted) { 209 | // y轴数值 210 | ctx.textAlign = 'right' 211 | let v = ticks[ticks.length - 1 - i] 212 | let vs = v.toString() 213 | if (vs.indexOf('.') > -1 && vs.split('.')[1].length > 1) { 214 | vs = v.toFixed(1) 215 | } 216 | ctx.fillText(vs, x - 4, y + 6) 217 | ctx.textAlign = 'start' 218 | } 219 | if (extInfo.majorGridlines == 'true') { 220 | // 网格线 221 | ctx.moveTo(x, y) 222 | ctx.lineTo(x + xWidth, y) 223 | } 224 | y += yGap 225 | } 226 | ctx.stroke() 227 | x += xGap 228 | y -= yGap 229 | let vGap = anchor[2] * 0.01 // 柱子间距 230 | let catGap = vGap * 6 // 类目间距 231 | let categorys = series[0].category.data 232 | let categoryWidth = (xWidth - catGap * (categorys.length - 1) - xGap * 1.5) / categorys.length 233 | let vWidth = (categoryWidth - vGap * (series.length - 1)) / series.length 234 | // 绘制类目和柱状图 235 | for (let i = 0; i < categorys.length; i++) { 236 | if (categoryAxis && !categoryAxis.deleted) { 237 | let _cx = (categoryWidth + catGap) * i + categoryWidth / 2 - ctx.measureText(categorys[i]).width / 2 238 | ctx.fillStyle = 'rgb(89, 89, 89)' 239 | ctx.fillText(categorys[i], x + _cx, y + anchor[3] * 0.055) 240 | } 241 | for (let j = 0; j < series.length; j++) { 242 | let vHeight = (series[j].value.data[i] / ticks[ticks.length - 1]) * yHeight 243 | let _x = x + (categoryWidth + catGap) * i + (vWidth + vGap) * j 244 | let _y = y - vHeight 245 | let _anchor = [_x, _y, vWidth, vHeight] 246 | // 柱状图 247 | await drawRect(ctx, { 248 | fillStyle: series[j].property.fillStyle, 249 | strokeStyle: series[j].property.strokeStyle, 250 | anchor: _anchor 251 | }) 252 | } 253 | } 254 | if (legend) { 255 | // 绘制v标签标识 256 | x = anchor[0] + xGap 257 | let vTexts = [] 258 | let vTextWidth = 0 259 | for (let i = 0; i < series.length; i++) { 260 | let s = series[i].text.data[0] 261 | vTexts.push(s) 262 | vTextWidth += ctx.measureText(s).width 263 | } 264 | vGap = anchor[2] * 0.02 265 | let _iw = Math.min(anchor[2] * 0.025, 15) 266 | let _y = y + anchor[3] * 0.125 267 | let _x = x + xWidth / 2 - (vTextWidth + vGap * (vTexts.length - 1) + (_iw * 1.25) * vTexts.length) / 2 268 | for (let i = 0; i < vTexts.length; i++) { 269 | await drawRect(ctx, { 270 | fillStyle: series[i].property.fillStyle, 271 | strokeStyle: series[i].property.strokeStyle, 272 | anchor: [_x, _y - _iw, _iw, _iw] 273 | }) 274 | _x += (_iw * 1.25) 275 | ctx.fillStyle = 'rgb(89, 89, 89)' 276 | ctx.fillText(vTexts[i], _x, _y) 277 | _x += ctx.measureText(vTexts[i]).width 278 | _x += vGap 279 | } 280 | } 281 | } 282 | 283 | async function drawPieChart(title, chartData, legend, anchor, canvas, ctx) { 284 | if (title == '') { 285 | title = chartData.series[0].text.data[0] 286 | } 287 | if (title) { 288 | ctx.font = `${Math.min(anchor[3] * 0.07, 18.5)}px 等线` 289 | ctx.fillStyle = 'rgb(89, 89, 89)' 290 | let textWidth = ctx.measureText(title).width 291 | ctx.fillText(title, anchor[0] + anchor[2] / 2 - textWidth / 2, anchor[1] + anchor[3] * 0.06) 292 | } 293 | let extInfo = chartData.extInfo 294 | let holeSize = +(extInfo.holeSize || 0) / 100 295 | let series = chartData.series 296 | let values = series[0].value.data 297 | let dataPoints = series[0].dataPoint 298 | let totalValue = 0 299 | for (let i = 0; i < values.length; i++) { 300 | totalValue += (+values[i]) 301 | } 302 | let xGap = anchor[2] * 0.05 // 开始间距 303 | let x = anchor[0] + xGap 304 | let xWidth = anchor[2] - xGap * 2 305 | let startY = title ? anchor[3] * 0.12 : anchor[3] * 0.05 306 | let y = anchor[1] + startY 307 | let yHeight = anchor[3] - startY - anchor[3] * 0.15 308 | let centerX = xWidth / 2 309 | let centerY = yHeight / 2 310 | let radius = Math.min(centerX, centerY) 311 | ctx.lineWidth = 0.5 312 | ctx.font = `${Math.min(anchor[2] * 0.048, 14)}px 等线` 313 | ctx.fillStyle = 'rgb(89, 89, 89)' 314 | ctx.save() 315 | ctx.translate(x + centerX, y + centerY) 316 | // arc 是以3点钟方向开始,反向旋转-90调整到0点开始 317 | ctx.rotate(-90 * Math.PI / 180) 318 | ctx.translate(-x - centerX, -y - centerY) 319 | let startAngle = 0 320 | if (extInfo.startAngle) { 321 | startAngle = extInfo.startAngle * (Math.PI / 180) 322 | } 323 | for (let i = 0; i < values.length; i++) { 324 | let property = dataPoints[i].property 325 | ctx.fillStyle = await toCtxPaint(ctx, property.fillStyle, property.anchor || anchor) 326 | if (property.strokeStyle) { 327 | ctx.lineWidth = property.strokeStyle.lineWidth || 1.5 328 | let lineCap = property.strokeStyle.lineCap 329 | if (!lineCap || lineCap == 'FLAT') { 330 | lineCap = 'butt' 331 | } 332 | ctx.lineCap = lineCap.toLowerCase() 333 | ctx.strokeStyle = await toCtxPaint(ctx, property.strokeStyle.paint, property.anchor || anchor) 334 | } 335 | let sliceAngle = (2 * Math.PI * values[i]) / totalValue 336 | ctx.beginPath() 337 | ctx.moveTo(x + centerX, y + centerY) 338 | // 实心圆 339 | ctx.arc(x + centerX, y + centerY, radius, startAngle, startAngle + sliceAngle, false) 340 | if (holeSize > 0) { 341 | // 空心圆 342 | let holeRadius = radius * holeSize 343 | ctx.arc(x + centerX, y + centerY, holeRadius, startAngle + sliceAngle, startAngle, true) 344 | ctx.closePath() 345 | ctx.save() 346 | ctx.clip() 347 | ctx.fill() 348 | ctx.stroke() 349 | ctx.restore() 350 | } else { 351 | ctx.closePath() 352 | ctx.fill() 353 | ctx.stroke() 354 | } 355 | startAngle += sliceAngle 356 | } 357 | ctx.restore() 358 | if (legend) { 359 | // 绘制类目标签标识 360 | let vTexts = [] 361 | let vTextWidth = 0 362 | let categorys = series[0].category.data 363 | for (let i = 0; i < categorys.length; i++) { 364 | let s = categorys[i] 365 | vTexts.push(s) 366 | vTextWidth += ctx.measureText(s).width 367 | } 368 | let vGap = anchor[2] * 0.03 369 | let _iw = Math.min(anchor[2] * 0.028, 15) 370 | let _y = y + yHeight + anchor[3] * 0.125 371 | let _x = x + xWidth / 2 - (vTextWidth + vGap * (vTexts.length - 1) + (_iw * 1.25) * vTexts.length) / 2 372 | for (let i = 0; i < vTexts.length; i++) { 373 | let property = dataPoints[i].property 374 | await drawRect(ctx, { 375 | fillStyle: property.fillStyle, 376 | strokeStyle: property.strokeStyle, 377 | anchor: [_x, _y - _iw, _iw, _iw] 378 | }) 379 | _x += (_iw * 1.25) 380 | ctx.fillStyle = 'rgb(89, 89, 89)' 381 | ctx.fillText(vTexts[i], _x, _y) 382 | _x += ctx.measureText(vTexts[i]).width 383 | _x += vGap 384 | } 385 | } 386 | } 387 | 388 | async function drawLineChart(title, chartData, legend, anchor, canvas, ctx) { 389 | if (title) { 390 | ctx.font = `${Math.min(anchor[3] * 0.07, 18.5)}px 等线` 391 | ctx.fillStyle = 'rgb(89, 89, 89)' 392 | let textWidth = ctx.measureText(title).width 393 | ctx.fillText(title, anchor[0] + anchor[2] / 2 - textWidth / 2, anchor[1] + anchor[3] * 0.06) 394 | } 395 | let extInfo = chartData.extInfo 396 | let smooth = extInfo.smooth == 'true' 397 | let series = chartData.series 398 | let categoryAxis = chartData.categoryAxis 399 | let valueAxes = chartData.valueAxes && chartData.valueAxes.length > 0 ? chartData.valueAxes[0] : null 400 | let minValue = 0, maxValue = 0 401 | for (let i = 0; i < series.length; i++) { 402 | minValue = Math.min(minValue, ...series[i].value.data) 403 | maxValue = Math.max(maxValue, ...series[i].value.data) 404 | } 405 | ctx.lineWidth = 0.5 406 | ctx.font = `${Math.min(anchor[3] * 0.06, 14)}px 等线` 407 | ctx.fillStyle = 'rgb(89, 89, 89)' 408 | ctx.strokeStyle = 'rgb(217, 217, 217)' 409 | let ticks = calculateTicks(minValue, maxValue, 5, 10) 410 | let xGap = ctx.measureText(ticks[ticks.length - 1] + '00').width 411 | let x = anchor[0] + xGap 412 | let xWidth = anchor[2] - xGap * 1.5 413 | let startY = title ? anchor[3] * 0.12 : anchor[3] * 0.05 414 | let y = anchor[1] + startY 415 | let yHeight = anchor[3] - startY - anchor[3] * 0.15 416 | let yGap = yHeight / (ticks.length - 1) 417 | ctx.beginPath() 418 | for (let i = 0; i < ticks.length; i++) { 419 | // y轴数值 420 | if (valueAxes && !valueAxes.deleted) { 421 | ctx.textAlign = 'right' 422 | let v = ticks[ticks.length - 1 - i] 423 | let vs = v.toString() 424 | if (vs.indexOf('.') > -1 && vs.split('.')[1].length > 1) { 425 | vs = v.toFixed(1) 426 | } 427 | ctx.fillText(vs, x - 4, y + 6) 428 | ctx.textAlign = 'start' 429 | } 430 | if (extInfo.majorGridlines == 'true') { 431 | // 网格线 432 | ctx.moveTo(x, y) 433 | ctx.lineTo(x + xWidth, y) 434 | } 435 | y += yGap 436 | } 437 | ctx.stroke() 438 | x += xGap 439 | y -= yGap 440 | let catGap = anchor[2] * 0.05 // 类目间距 441 | let categorys = series[0].category.data 442 | let categoryWidth = (xWidth - catGap * (categorys.length - 1) - xGap * 1.5) / categorys.length 443 | let cxList = [] 444 | // 绘制类目 445 | for (let i = 0; i < categorys.length; i++) { 446 | let _cx = (categoryWidth + catGap) * i + categoryWidth / 2 - ctx.measureText(categorys[i]).width / 2 447 | cxList.push(_cx) 448 | if (categoryAxis && !categoryAxis.deleted) { 449 | ctx.textAlign = 'center' 450 | ctx.fillText(categorys[i], x + _cx, y + anchor[3] * 0.05) 451 | ctx.textAlign = 'start' 452 | } 453 | } 454 | // 绘制折线 455 | for (let i = 0; i < series.length; i++) { 456 | let property = series[i].property 457 | if (property.strokeStyle) { 458 | ctx.lineWidth = property.strokeStyle.lineWidth || 1.5 459 | let lineCap = property.strokeStyle.lineCap 460 | if (!lineCap || lineCap == 'FLAT') { 461 | lineCap = 'butt' 462 | } 463 | ctx.lineCap = lineCap.toLowerCase() 464 | ctx.strokeStyle = await toCtxPaint(ctx, property.strokeStyle.paint, property.anchor) 465 | } 466 | ctx.beginPath() 467 | let points = [] 468 | for (let j = 0; j < categorys.length; j++) { 469 | let vHeight = (series[i].value.data[j] / ticks[ticks.length - 1]) * yHeight 470 | let _x = x + cxList[j] 471 | let _y = y - vHeight 472 | if (j == 0) { 473 | ctx.moveTo(_x, _y) 474 | } else if (smooth) { 475 | points.push({ x: _x, y: _y }) 476 | } else { 477 | ctx.lineTo(_x, _y) 478 | } 479 | } 480 | if (smooth) { 481 | let j = 0 482 | for (; j < points.length - 2; j++) { 483 | let xc = (points[j].x + points[j + 1].x) / 2 484 | let yc = (points[j].y + points[j + 1].y) / 2 485 | ctx.quadraticCurveTo(points[j].x, points[j].y, xc, yc) 486 | } 487 | ctx.quadraticCurveTo(points[j].x, points[j].y, points[j + 1].x, points[j + 1].y) 488 | } 489 | ctx.stroke() 490 | } 491 | if (legend) { 492 | // 绘制v标签标识 493 | x = anchor[0] + xGap 494 | let vTexts = [] 495 | let vTextWidth = 0 496 | for (let i = 0; i < series.length; i++) { 497 | let s = series[i].text.data[0] 498 | vTexts.push(s) 499 | vTextWidth += ctx.measureText(s).width 500 | } 501 | let vGap = anchor[2] * 0.02 502 | let _iw = anchor[2] * 0.04 503 | let _y = y + anchor[3] * 0.128 504 | let _x = x + xWidth / 2 - (vTextWidth + vGap * (vTexts.length - 1) + (_iw + 2) * vTexts.length) / 2 505 | for (let i = 0; i < vTexts.length; i++) { 506 | let property = series[i].property 507 | if (property.strokeStyle) { 508 | ctx.lineWidth = property.strokeStyle.lineWidth || 1.5 509 | let lineCap = property.strokeStyle.lineCap 510 | if (!lineCap || lineCap == 'FLAT') { 511 | lineCap = 'butt' 512 | } 513 | ctx.lineCap = lineCap.toLowerCase() 514 | ctx.strokeStyle = await toCtxPaint(ctx, property.strokeStyle.paint, property.anchor) 515 | } 516 | ctx.beginPath() 517 | ctx.moveTo(_x, _y - 4) 518 | ctx.lineTo(_x + _iw, _y - 4) 519 | ctx.stroke() 520 | _x += (_iw + 2) 521 | ctx.fillStyle = 'rgb(89, 89, 89)' 522 | ctx.fillText(vTexts[i], _x, _y) 523 | _x += ctx.measureText(vTexts[i]).width 524 | _x += vGap 525 | } 526 | } 527 | } 528 | 529 | function calculateTicks(minValue, maxValue, minTicks, maxTicks) { 530 | let range = maxValue - minValue 531 | let rawInterval = range / (minTicks - 1) 532 | let magnitude = Math.pow(10, Math.floor(Math.log10(rawInterval))) 533 | let normalizedInterval = rawInterval / magnitude 534 | let adjustedInterval 535 | if (normalizedInterval <= 1.5) { 536 | adjustedInterval = 1 537 | } else if (normalizedInterval <= 3) { 538 | adjustedInterval = 2 539 | } else if (normalizedInterval <= 7) { 540 | adjustedInterval = 5 541 | } else { 542 | adjustedInterval = 10 543 | } 544 | adjustedInterval *= magnitude 545 | let tickEnd = Math.ceil(maxValue / adjustedInterval) * adjustedInterval 546 | let ticks = [] 547 | for (let tick = 0; tick <= tickEnd; tick += adjustedInterval) { 548 | ticks.push(tick) 549 | } 550 | if (ticks.length < minTicks) { 551 | for (let i = ticks.length; i < minTicks; i++) { 552 | ticks.push(i * adjustedInterval) 553 | } 554 | } else { 555 | while (ticks.length > maxTicks) { 556 | adjustedInterval *= 2 557 | ticks = [] 558 | tickEnd = Math.ceil(maxValue / adjustedInterval) * adjustedInterval 559 | for (let tick = 0; tick <= tickEnd; tick += adjustedInterval) { 560 | ticks.push(tick) 561 | } 562 | } 563 | } 564 | return ticks 565 | } 566 | 567 | async function drawRect(ctx, property) { 568 | ctx.fillStyle = await toCtxPaint(ctx, property.fillStyle, property.anchor) 569 | if (property.strokeStyle) { 570 | ctx.lineWidth = property.strokeStyle.lineWidth || 1 571 | ctx.strokeStyle = await toCtxPaint(ctx, property.strokeStyle.paint, property.anchor) 572 | } 573 | ctx.fillRect(property.anchor[0], property.anchor[1], property.anchor[2], property.anchor[3]) 574 | } 575 | 576 | function toCtxPaint(ctx, paint, anchor, isBackground, defaultColor) { 577 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 578 | return new Promise((resolve, reject) => { 579 | if (!paint) { 580 | resolve(defaultColor || 'transparent') 581 | } else if (paint.type == 'noFill') { 582 | // 无填充 583 | resolve('transparent') 584 | } else if (paint.type == 'color') { 585 | // 颜色 586 | resolve(toColor(paint.color, defaultColor)) 587 | } else if (paint.type == 'bgFill') { 588 | // 背景填充 589 | resolve(ctx.bgFillStyle || defaultColor || 'transparent') 590 | } else if (paint.type == 'groupFill') { 591 | // 组合背景 592 | let groupFillStyle = paint.parentGroupFillStyle || ctx.groupFillStyle 593 | if (groupFillStyle) { 594 | toCtxPaint(ctx, groupFillStyle, anchor || groupFillStyle.groupAnchor, false, defaultColor).then(res => { 595 | resolve(res) 596 | }) 597 | } else { 598 | resolve(defaultColor || 'transparent') 599 | } 600 | } else if (paint.type == 'gradient') { 601 | // 渐变 602 | let gradient = paint.gradient 603 | let x = anchor[0], y = anchor[1], width = anchor[2], height = anchor[3] 604 | let centerX = x + width / 2 605 | let centerY = y + height / 2 606 | let gradientObj 607 | // linear,circular,rectangular,shape 608 | if (gradient.gradientType == 'circular') { 609 | // 射线 610 | let radius = Math.sqrt(width * width + height * height) * (gradient.insets[1] == 0.5 ? 0.5 : 1) 611 | let cx = centerX + width * (gradient.insets[1] - gradient.insets[3]) / 2 612 | let cy = centerY + height * (gradient.insets[0] - gradient.insets[2]) / 2 613 | gradientObj = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius) 614 | } else { 615 | // 线性 616 | let startX = x 617 | let startY = centerY 618 | let endX = x + width 619 | let endY = centerY 620 | if (gradient.angle) { 621 | let radians = gradient.angle * Math.PI / 180 622 | let midX = (startX + endX) / 2 623 | let midY = (startY + endY) / 2 624 | let newStartX = midX + (startX - midX) * Math.cos(radians) - (startY - midY) * Math.sin(radians) 625 | let newStartY = midY + (startX - midX) * Math.sin(radians) + (startY - midY) * Math.cos(radians) 626 | let newEndX = midX + (endX - midX) * Math.cos(radians) - (endY - midY) * Math.sin(radians) 627 | let newEndY = midY + (endX - midX) * Math.sin(radians) + (endY - midY) * Math.cos(radians) 628 | startX = newStartX 629 | startY = newStartY 630 | endX = newEndX 631 | endY = newEndY 632 | } 633 | gradientObj = ctx.createLinearGradient(startX, startY, endX, endY) 634 | } 635 | for (let i = 0; i < gradient.colors.length; i++) { 636 | let color = gradient.colors[i] 637 | gradientObj.addColorStop(gradient.fractions[i], toColor(color)) 638 | } 639 | resolve(gradientObj) 640 | } else if (anchor && anchor[2] == 0 && anchor[3] == 0) { 641 | resolve('transparent') 642 | } else if (paint.type == 'texture') { 643 | // 图片或纹理 644 | let texture = paint.texture 645 | loadChartImage(texture.imageData).then(img => { 646 | let pat = createCtxTexturePattern(ctx, img, texture, anchor) 647 | resolve(pat) 648 | }) 649 | } else if (paint.type == 'pattern') { 650 | // 图案 651 | let pattern = paint.pattern 652 | // let prst = pattern.prst 653 | let fgColor = pattern.fgColor.realColor 654 | let bgColor = pattern.bgColor.realColor 655 | let width = anchor[2], height = anchor[3] 656 | let imgCanvas = document.createElement('canvas') 657 | imgCanvas.width = width 658 | imgCanvas.height = height 659 | let imgCtx = imgCanvas.getContext('2d') 660 | imgCtx.imageSmoothingEnabled = true 661 | imgCtx.imageSmoothingQuality = 'high' 662 | let imgData = imgCtx.createImageData(width, height) 663 | let line = 0 664 | for (let i = 0; i < imgData.data.length; i += 4) { 665 | if (++line % 16 == 0) { 666 | // 前景 667 | imgData.data[i + 0] = (fgColor >> 16) & 255 668 | imgData.data[i + 1] = (fgColor >> 8) & 255 669 | imgData.data[i + 2] = (fgColor >> 0) & 255 670 | imgData.data[i + 3] = (fgColor >> 24) & 255 671 | } else { 672 | // 背景 673 | imgData.data[i + 0] = (bgColor >> 16) & 255 674 | imgData.data[i + 1] = (bgColor >> 8) & 255 675 | imgData.data[i + 2] = (bgColor >> 0) & 255 676 | imgData.data[i + 3] = (bgColor >> 24) & 255 677 | } 678 | if (i % 400 == 0) { 679 | line += 2 680 | } 681 | } 682 | imgCtx.putImageData(imgData, 0, 0) 683 | let imgSrc = imgCanvas.toDataURL() 684 | let image = new Image() 685 | image.src = imgSrc 686 | image.onload = async function() { 687 | resolve(ctx.createPattern(image, 'no-repeat')) 688 | } 689 | } 690 | }) 691 | } 692 | 693 | function createCtxTexturePattern(ctx, img, texture, anchor) { 694 | let width = anchor[2] 695 | let height = anchor[3] 696 | let mode = texture.alignment || !texture.stretch ? 'repeat' : 'no-repeat' 697 | if (width < 1 && height < 1 || isNaN(width) || isNaN(height)) { 698 | return ctx.createPattern(img, mode) 699 | } 700 | if (texture.alignment || !texture.stretch) { 701 | width = img.width 702 | height = img.height 703 | } 704 | let patternCanvas = document.createElement('canvas') 705 | patternCanvas.width = Math.max(1, width) 706 | patternCanvas.height = Math.max(1, height) 707 | let patternCtx = patternCanvas.getContext('2d') 708 | patternCtx.imageSmoothingEnabled = true 709 | patternCtx.imageSmoothingQuality = 'high' 710 | if (texture.alpha >= 0 && texture.alpha < 100000) { 711 | patternCtx.globalAlpha = texture.alpha / 100000 712 | } 713 | let imgInsets = null 714 | if (texture.insets) { 715 | let top = texture.insets[0] / 100000 716 | let left = texture.insets[1] / 100000 717 | let bottom = texture.insets[2] / 100000 718 | let right = texture.insets[3] / 100000 719 | let x = img.width * left 720 | let y = img.height * top 721 | let w = img.width * (1 - left - right) 722 | let h = img.height * (1 - top - bottom) 723 | imgInsets = [x, y, w, h] 724 | } 725 | if (texture.stretch) { 726 | let top = texture.stretch[0] / 100000 727 | let left = texture.stretch[1] / 100000 728 | let bottom = texture.stretch[2] / 100000 729 | let right = texture.stretch[3] / 100000 730 | let x = width * left 731 | let y = height * top 732 | let w = width * (1 - left - right) 733 | let h = height * (1 - top - bottom) 734 | if (imgInsets) { 735 | patternCtx.drawImage(img, imgInsets[0], imgInsets[1], imgInsets[2], imgInsets[3], x, y, w, h) 736 | } else { 737 | patternCtx.drawImage(img, x, y, w, h) 738 | } 739 | } else if (texture.alignment) { 740 | let x = 0, y = 0 741 | if (texture.alignment == 'CENTER') { 742 | if (width > anchor[2]) { 743 | x = (width - anchor[2]) / 2 744 | } 745 | if (height > anchor[3]) { 746 | y = (height - anchor[3]) / 2 747 | } 748 | } 749 | patternCtx.drawImage(img, x, y, width, height, 0, 0, width, height) 750 | } else { 751 | if (imgInsets) { 752 | patternCtx.drawImage(img, imgInsets[0], imgInsets[1], imgInsets[2], imgInsets[3], 0, 0, width, height) 753 | } else { 754 | patternCtx.drawImage(img, 0, 0, width, height) 755 | } 756 | } 757 | if (texture.duoTone && texture.duoTone.length > 0 && texture.duoTonePrst) { 758 | // 重新着色 759 | let color = texture.duoTone[0].realColor 760 | let r = (color >> 16) & 255 761 | let g = (color >> 8) & 255 762 | let b = (color >> 0) & 255 763 | let imageData = patternCtx.getImageData(0, 0, patternCanvas.width, patternCanvas.height) 764 | let data = imageData.data 765 | for(var i = 0; i < data.length; i += 4) { 766 | let gray = (data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11) / 255 767 | // black / white 768 | let prst = texture.duoTonePrst == 'white' ? 255 : 0 769 | data[i] = gray * r + (1 - gray) * prst 770 | data[i + 1] = gray * g + (1 - gray) * prst 771 | data[i + 2] = gray * b + (1 - gray) * prst 772 | } 773 | patternCtx.putImageData(imageData, 0, 0) 774 | } 775 | return ctx.createPattern(patternCanvas, mode) 776 | } 777 | 778 | function toColor(colorObj, defaultColor) { 779 | if (colorObj == null || (colorObj.color == null && colorObj.realColor == null)) { 780 | return defaultColor || 'transparent' 781 | } 782 | let color = colorObj.realColor != null ? colorObj.realColor : colorObj.color 783 | let r = (color >> 16) & 255 784 | let g = (color >> 8) & 255 785 | let b = (color >> 0) & 255 786 | let a = ((color >> 24) & 255) / 255 787 | if (colorObj.realColor == null) { 788 | if (colorObj.alpha != null && colorObj.alpha != -1) { 789 | if (colorObj.alpha > 1000) { 790 | a = colorObj.alpha / 100000 791 | } else { 792 | a = (colorObj.alpha > 0 && colorObj.alpha < 1) ? colorObj.alpha : colorObj.alpha / 255 793 | } 794 | a = Math.min(1, Math.max(0, a)) 795 | } 796 | if (colorObj.lumMod && colorObj.lumMod > 0) { 797 | let value = colorObj.lumMod / 100000 798 | r = r * value 799 | g = g * value 800 | b = b * value 801 | } 802 | if (colorObj.lumOff && colorObj.lumOff > 0) { 803 | let value = colorObj.lumOff / 100000 804 | r += 255 * value 805 | g += 255 * value 806 | b += 255 * value 807 | } 808 | } 809 | return `rgba(${r}, ${g}, ${b}, ${a})` 810 | } 811 | 812 | function loadChartImage(src) { 813 | return new Promise(resolve => { 814 | if (!src) { 815 | resolve() 816 | return 817 | } 818 | let img = new Image() 819 | let eqOrigin = src.startsWith('data:') || src.startsWith(document.location.origin) || (src.startsWith('//') && (document.location.protocol + src).startsWith(document.location.origin)) 820 | if (!eqOrigin) { 821 | img.crossOrigin = 'anonymous' 822 | } 823 | img.src = src 824 | img.onload = function() { 825 | resolve(img) 826 | } 827 | img.onerror = function (e) { 828 | resolve() 829 | console.log('图片加载失败: ', src, e) 830 | } 831 | }) 832 | } 833 | 834 | // export { drawChart } --------------------------------------------------------------------------------