├── vue.config.js ├── cypress.json ├── screenshot ├── 1.png ├── 2.png └── 3.png ├── babel.config.js ├── public ├── chartfun.png ├── favicon.ico └── index.html ├── src ├── assets │ ├── img │ │ ├── bg.png │ │ ├── profile.jpg │ │ ├── borders │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ └── charts │ │ │ ├── bar.png │ │ │ ├── line.png │ │ │ ├── pie.png │ │ │ ├── ring.png │ │ │ ├── text.png │ │ │ ├── border.png │ │ │ ├── candle.png │ │ │ ├── funnel.png │ │ │ ├── gauge.png │ │ │ ├── image.png │ │ │ ├── radar.png │ │ │ ├── sankey.png │ │ │ ├── histogram.png │ │ │ ├── map-china.png │ │ │ ├── map-world.png │ │ │ ├── scatter.png │ │ │ ├── wordcloud.png │ │ │ └── liquidfill.png │ └── font │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css │ │ └── iconfont.svg ├── store.js ├── App.vue ├── components │ ├── Console │ │ ├── Index.vue │ │ ├── PageHeader.vue │ │ └── NavMenu.vue │ └── Editor │ │ ├── Topbar.vue │ │ ├── ScaleBar.vue │ │ ├── Toolbar.vue │ │ ├── Index.vue │ │ ├── SidePanel.vue │ │ └── Config.vue ├── main.js ├── http.js ├── router.js └── views │ ├── Console │ ├── Data.vue │ ├── DataAdd.vue │ └── Chart.vue │ ├── Index.vue │ ├── Viewer │ └── Canvas.vue │ └── Editor │ └── Canvas.vue ├── .editorconfig ├── tests └── e2e │ ├── .eslintrc.js │ ├── specs │ └── test.js │ ├── support │ ├── index.js │ └── commands.js │ └── plugins │ └── index.js ├── mock ├── index.js └── data │ ├── charts.json │ └── charts-1234.json ├── server ├── models │ ├── user.js │ ├── connect.js │ └── chart.js ├── test.http ├── routes │ ├── api.js │ ├── demo.js │ ├── user.js │ ├── connect.js │ └── chart.js └── app.js ├── .gitignore ├── LICENSE ├── .circleci └── config.yml ├── README.md └── package.json /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: './', 3 | }; 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /screenshot/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/screenshot/1.png -------------------------------------------------------------------------------- /screenshot/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/screenshot/2.png -------------------------------------------------------------------------------- /screenshot/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/screenshot/3.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /public/chartfun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/public/chartfun.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/bg.png -------------------------------------------------------------------------------- /src/assets/img/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/profile.jpg -------------------------------------------------------------------------------- /src/assets/font/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/font/iconfont.eot -------------------------------------------------------------------------------- /src/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/font/iconfont.woff -------------------------------------------------------------------------------- /src/assets/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/img/borders/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/borders/1.png -------------------------------------------------------------------------------- /src/assets/img/borders/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/borders/2.png -------------------------------------------------------------------------------- /src/assets/img/borders/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/borders/3.png -------------------------------------------------------------------------------- /src/assets/img/charts/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/bar.png -------------------------------------------------------------------------------- /src/assets/img/charts/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/line.png -------------------------------------------------------------------------------- /src/assets/img/charts/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/pie.png -------------------------------------------------------------------------------- /src/assets/img/charts/ring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/ring.png -------------------------------------------------------------------------------- /src/assets/img/charts/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/text.png -------------------------------------------------------------------------------- /src/assets/img/charts/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/border.png -------------------------------------------------------------------------------- /src/assets/img/charts/candle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/candle.png -------------------------------------------------------------------------------- /src/assets/img/charts/funnel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/funnel.png -------------------------------------------------------------------------------- /src/assets/img/charts/gauge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/gauge.png -------------------------------------------------------------------------------- /src/assets/img/charts/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/image.png -------------------------------------------------------------------------------- /src/assets/img/charts/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/radar.png -------------------------------------------------------------------------------- /src/assets/img/charts/sankey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/sankey.png -------------------------------------------------------------------------------- /src/assets/img/charts/histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/histogram.png -------------------------------------------------------------------------------- /src/assets/img/charts/map-china.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/map-china.png -------------------------------------------------------------------------------- /src/assets/img/charts/map-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/map-world.png -------------------------------------------------------------------------------- /src/assets/img/charts/scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/scatter.png -------------------------------------------------------------------------------- /src/assets/img/charts/wordcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/wordcloud.png -------------------------------------------------------------------------------- /src/assets/img/charts/liquidfill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddiu8081/ChartFun/HEAD/src/assets/img/charts/liquidfill.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress', 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true, 8 | }, 9 | rules: { 10 | strict: 'off', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | 9 | }, 10 | mutations: { 11 | 12 | }, 13 | actions: { 14 | 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import Mock from 'mockjs'; 3 | 4 | export default function () { 5 | Mock.mock(/\/chart$/, require('./data/charts.json')); 6 | Mock.mock(/\/chart\/1234/, require('./data/charts-1234.json')); 7 | } 8 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/'); 6 | cy.contains('h1', 'Welcome to Your Vue.js App'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const mongoose = require('mongoose'); 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = new Schema({ 6 | username: String, 7 | password: String 8 | }, { timestamps: true }); 9 | 10 | module.exports = mongoose.model('user', userSchema); 11 | -------------------------------------------------------------------------------- /server/models/connect.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const mongoose = require('mongoose'); 3 | const Schema = mongoose.Schema; 4 | 5 | const connectSchema = new Schema({ 6 | name: String, 7 | uid: String, 8 | data: Object 9 | }, { timestamps: true }); 10 | 11 | module.exports = mongoose.model('connect', connectSchema); 12 | -------------------------------------------------------------------------------- /server/test.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:3000/chart 2 | 3 | ### 4 | POST http://localhost:3000/chart 5 | Content-Type: application/json 6 | 7 | { 8 | "title": "金融大数据", 9 | "user": "1234" 10 | } 11 | 12 | ### 13 | 14 | GET http://localhost:3000/demo/percent 15 | 16 | ### 17 | 18 | GET http://localhost:3000/demo/pv 19 | -------------------------------------------------------------------------------- /server/models/chart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const mongoose = require('mongoose'); 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = new Schema({ 6 | title: String, 7 | img: String, 8 | uid: String, 9 | view: Number, 10 | chartData: Object 11 | }, { timestamps: true }); 12 | 13 | module.exports = mongoose.model('chart', userSchema); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | 26 | # ChartFun uploads 27 | /server/public/upload/* 28 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const Router = require('koa-router'); 3 | const path = require('path'); 4 | 5 | const router = new Router(); 6 | 7 | router.prefix('/api') 8 | 9 | router.post('/uploadfile', async (ctx, next) => { 10 | const file = ctx.request.files.file; // 获取上传文件 11 | const newFileName = file.path.split('/').pop(); // 取新的文件名 12 | return ctx.body = { 13 | success: true, 14 | url: `http://localhost:3000/upload/${newFileName}`, 15 | }; 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |
4 |
5 | > Make data reports by dragging and dropping :)
6 |
7 | ## 特性 / Features
8 |
9 | * 通过 Excel 导入数据
10 | * 可视化画布
11 | * 图表、图片、文字、边框支持
12 | * 可拖拽和缩放的组件
13 | * 静态数据、GET接口支持
14 | * 生成公开链接
15 |
16 | ## 截图 / Screenshot
17 |
18 | 
19 |
20 | 
21 |
22 | 
23 |
24 | ## 开发 / Develop
25 |
26 | ### 前端部分:Vue.js
27 |
28 | #### Project setup
29 |
30 | ```
31 | npm install
32 | ```
33 |
34 | #### Compiles and hot-reloads for development
35 |
36 | ```
37 | npm run serve
38 | ```
39 |
40 | #### Compiles and minifies for production
41 |
42 | ```
43 | npm run build
44 | ```
45 |
46 | ### 后端部分:Node.js + Koa + MongoDB
47 |
48 | 准备工作:配置并运行 MongoDB 数据库,新建一个空数据库并命名为`chartfun`。无需手动配置表结构,它们会被自动创建。
49 |
50 | #### Run web-service
51 |
52 | ```
53 | node ./server/app.js
54 | ```
55 |
56 |
57 |
58 | ## 鸣谢 / Thanks
59 |
60 | 本项目使用了 Vue.js 及以下第三方库:
61 |
62 | * [ElemeFE / element](https://github.com/ElemeFE/element)
63 | * [ElemeFE / v-charts](https://github.com/ElemeFE/v-charts)
64 | * [josdejong / jsoneditor](https://github.com/josdejong/jsoneditor)
65 | * [SortableJS / Vue.Draggable](https://github.com/SortableJS/Vue.Draggable)
66 | * [mauricius / vue-draggable-resizable](https://github.com/mauricius/vue-draggable-resizable)
67 | * [kirillmurashov / vue-drag-resize](https://github.com/kirillmurashov/vue-drag-resize)
68 | * [koajs / koa](https://github.com/koajs/koa)
69 |
70 | ## LICENSE
71 |
72 | MIT
73 |
--------------------------------------------------------------------------------
/src/components/Editor/Topbar.vue:
--------------------------------------------------------------------------------
1 |
2 | .header
3 | .filename {{$parent.title}}
4 | i.btn.iconfont.icon-preview(
5 | :class="{active: $parent.preview}"
6 | @click="$parent.preview = !$parent.preview")
7 | .publish-btn(@click="saveChartData") 发布
8 |
9 |
10 |
27 |
28 |
82 |
--------------------------------------------------------------------------------
/mock/data/charts.json:
--------------------------------------------------------------------------------
1 | {
2 | "errno": 0,
3 | "errmsg": "ok",
4 | "data": {
5 | "chartList": [
6 | {
7 | "id": 1,
8 | "title": "某某电商可视化平台",
9 | "img": "https://image.ddiu.site/img/20190603211609.png",
10 | "timestamp": "1234123412"
11 | }, {
12 | "id": 2,
13 | "title": "某某金融大数据",
14 | "img": "https://image.ddiu.site/img/20190603221132.png",
15 | "timestamp": "1234123412"
16 | }, {
17 | "id": 3,
18 | "title": "金融数据概览",
19 | "img": "https://image.ddiu.site/img/20190603221218.png",
20 | "timestamp": "1234123412"
21 | }, {
22 | "id": 4,
23 | "title": "集团数据监控平台",
24 | "img": "https://image.ddiu.site/img/20190603221708.png",
25 | "timestamp": "1234123412"
26 | }, {
27 | "id": 5,
28 | "title": "校园无线大数据监控平台",
29 | "img": "https://image.ddiu.site/img/20190603221800.png",
30 | "timestamp": "1234123412"
31 | }, {
32 | "id": 6,
33 | "title": "大屏展示方案",
34 | "img": "https://image.ddiu.site/img/20190603221850.png",
35 | "timestamp": "1234123412"
36 | }, {
37 | "id": 7,
38 | "title": "我国大数据市场产值图",
39 | "img": "https://image.ddiu.site/img/20190603221316.png",
40 | "timestamp": "1234123412"
41 | }, {
42 | "id": 8,
43 | "title": "某某产品用户分布图",
44 | "img": "https://image.ddiu.site/img/20190603221455.png",
45 | "timestamp": "1234123412"
46 | }, {
47 | "id": 9,
48 | "title": "金融数据概览",
49 | "img": "https://image.ddiu.site/img/20190603221218.png",
50 | "timestamp": "1234123412"
51 | }
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/Console/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 | .header
3 | .page-title
4 | span {{pageTitle}}
5 | .icon-row
6 | //- el-badge.icon.item(is-dot :hidden="true")
7 | i.el-icon-bell
8 | el-dropdown
9 | .avatar-image
10 | el-dropdown-menu(slot="dropdown")
11 | el-dropdown-item(disabled) {{ user.username }}
12 | el-dropdown-item(divided @click.native="logout") 退出
13 |
14 |
15 |
44 |
45 |
77 |
--------------------------------------------------------------------------------
/server/routes/connect.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const Router = require('koa-router');
3 | const connectModel = require('../models/connect');
4 |
5 | const router = new Router();
6 | router.prefix('/connect');
7 |
8 | // 获取全部数据源列表
9 | router.get('/', async (ctx, next) => {
10 | const rows = await connectModel.find({ 'uid': ctx.request.query.uid }).select('-data');
11 | ctx.body = {
12 | errno: 0,
13 | data: {
14 | connectList: rows
15 | }
16 | }
17 | });
18 |
19 | // 获取某一数据源详情
20 | router.get('/:id', async (ctx, next) => {
21 | const item = await connectModel.findById(ctx.params.id);
22 | ctx.body = {
23 | errno: 0,
24 | data: item
25 | }
26 | });
27 |
28 | // 新增数据源
29 | router.post('/', async (ctx, next) => {
30 | const body = ctx.request.body;
31 |
32 | if (!body.name || !body.data) {
33 | ctx.body = {
34 | errno: 1,
35 | errmsg: '格式错误'
36 | }
37 | return;
38 | }
39 |
40 | const result = await connectModel.create({
41 | name: body.name,
42 | data: body.data,
43 | uid: body.uid,
44 | });
45 |
46 | ctx.body = {
47 | errno: 0,
48 | data: result
49 | }
50 | });
51 |
52 | // 更新可视化图表
53 | router.put('/:id', async (ctx, next) => {
54 | const body = ctx.request.body;
55 |
56 | const item = await connectModel.findById(ctx.params.id);
57 | if (body.name) {
58 | item.name = body.name;
59 | }
60 | item.save();
61 |
62 | ctx.body = {
63 | errno: 0,
64 | }
65 | });
66 |
67 | // 删除可视化图表
68 | router.delete('/:id', async (ctx, next) => {
69 | const body = ctx.request.body;
70 |
71 | const item = await connectModel.findById(ctx.params.id);
72 | item.remove();
73 |
74 | ctx.body = {
75 | errno: 0,
76 | }
77 | });
78 |
79 | module.exports = router;
80 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const path = require('path');
3 | const Koa = require('koa');
4 | const Router = require('koa-router');
5 | const koaBody = require('koa-body');
6 | const cors = require('koa2-cors');
7 | const mongoose = require('mongoose');
8 | const koaStatic = require('koa-static');
9 |
10 | // 路由文件
11 | const apiRouter = require('./routes/api');
12 | const userRouter = require('./routes/user');
13 | const chartRouter = require('./routes/chart');
14 | const connectRouter = require('./routes/connect');
15 | const demoRouter = require('./routes/demo');
16 |
17 | const app = new Koa();
18 | // 解析 POST 请求
19 | app.use(koaBody({
20 | multipart: true, // 支持文件上传
21 | formidable: {
22 | uploadDir: path.join(__dirname, './public/upload/'), // 设置文件上传目录
23 | keepExtensions: true, // 保持文件的后缀
24 | maxFieldsSize: 2 * 1024 * 1024, // 文件上传大小
25 | },
26 | }));
27 |
28 | // 连接数据库
29 | mongoose.connect("mongodb://localhost:27017/chartfun", { useNewUrlParser: true }, err => {
30 | if (err) {
31 | console.log('[server] MongoDB connect error: ' + err);
32 | } else {
33 | console.log('[server] MongoDB connected!');
34 | }
35 | });
36 |
37 | const router = new Router();
38 |
39 | app.use(koaStatic(
40 | path.join(__dirname, './public')
41 | ))
42 | app.use(cors());
43 |
44 | router.get('/', (ctx, next) => {
45 | // ctx.router available
46 | ctx.body = 'Hello ChartFun!';
47 | });
48 |
49 | app.use(apiRouter.routes()).use(apiRouter.allowedMethods());
50 | app.use(userRouter.routes()).use(userRouter.allowedMethods());
51 | app.use(chartRouter.routes()).use(chartRouter.allowedMethods());
52 | app.use(connectRouter.routes()).use(connectRouter.allowedMethods());
53 | app.use(demoRouter.routes()).use(demoRouter.allowedMethods());
54 | app.use(router.routes()).use(router.allowedMethods());
55 | app.listen(3000);
56 |
--------------------------------------------------------------------------------
/mock/data/charts-1234.json:
--------------------------------------------------------------------------------
1 | {
2 | "errno": 0,
3 | "errmsg": "ok",
4 | "data": {
5 | "title": "我国大数据市场产值图",
6 | "chartData": {
7 | "w": 1200,
8 | "h": 800,
9 | "bgcolor": "#eeeeee",
10 | "bgimage": "",
11 | "bgimagesize": "cover",
12 | "elements": [
13 | {
14 | "name": "keykey1",
15 | "x": 10,
16 | "y": 100,
17 | "w": 492,
18 | "h": 308,
19 | "bgcolor": "rgba(19, 206, 102, 0.8)",
20 | "active": false,
21 | "data": {
22 | "type": "chart",
23 | "settings": {
24 | "type": "line"
25 | },
26 | "data": {
27 | "columns": ["日期", "访问用户"],
28 | "rows": [
29 | { "日期": "1月1日", "访问用户": 1523 },
30 | { "日期": "1月2日", "访问用户": 1223 },
31 | { "日期": "1月3日", "访问用户": 2123 },
32 | { "日期": "1月4日", "访问用户": 4123 },
33 | { "日期": "1月5日", "访问用户": 3123 },
34 | { "日期": "1月6日", "访问用户": 7123 }
35 | ]
36 | }
37 | }
38 | }, {
39 | "name": "keykey2",
40 | "x": 300,
41 | "y": 320,
42 | "w": 400,
43 | "h": 160,
44 | "bgcolor": "rgba(0, 206, 255, 0.3)",
45 | "active": false,
46 | "data": {
47 | "type": "chart",
48 | "settings": {
49 | "type": "histogram"
50 | },
51 | "data": {
52 | "columns": ["日期", "访问用户"],
53 | "rows": [
54 | { "日期": "1月1日", "访问用户": 1523 },
55 | { "日期": "1月2日", "访问用户": 1223 },
56 | { "日期": "1月3日", "访问用户": 2123 }
57 | ]
58 | }
59 | }
60 | }
61 | ]
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 |
4 | Vue.use(Router);
5 |
6 | const router = new Router({
7 | // mode: 'history',
8 | base: process.env.BASE_URL,
9 | routes: [
10 | {
11 | path: '/',
12 | name: 'index',
13 | component: () => import('./views/Index.vue'),
14 | meta: {
15 | title: 'ChartFun | 一站式数据大屏制作平台',
16 | },
17 | },
18 | {
19 | path: '/console',
20 | component: () => import('./components/Console/Index.vue'),
21 | children: [
22 | {
23 | path: 'data',
24 | component: () => import('./views/Console/Data.vue'),
25 | meta: {
26 | title: '数据管理',
27 | },
28 | },
29 | {
30 | path: 'data/add',
31 | component: () => import('./views/Console/DataAdd.vue'),
32 | meta: {
33 | title: '添加数据源',
34 | },
35 | },
36 | {
37 | path: 'chart',
38 | component: () => import('./views/Console/Chart.vue'),
39 | meta: {
40 | title: '我的可视化',
41 | },
42 | },
43 | {
44 | path: '',
45 | redirect: 'chart',
46 | },
47 | ],
48 | },
49 | {
50 | path: '/edit',
51 | component: () => import('./components/Editor/Index.vue'),
52 | children: [
53 | {
54 | path: ':id',
55 | component: () => import('./views/Editor/Canvas.vue'),
56 | meta: {
57 | title: '大屏编辑',
58 | },
59 | },
60 | ],
61 | },
62 | {
63 | path: '/view/:id',
64 | name: 'view',
65 | component: () => import('./views/Viewer/Canvas.vue'),
66 | meta: {
67 | title: '大屏查看 | ChartFun',
68 | },
69 | },
70 | ],
71 | });
72 |
73 | router.beforeEach((to, from, next) => {
74 | /* 路由发生变化修改页面title */
75 | if (to.meta.title) {
76 | document.title = to.meta.title;
77 | }
78 | next();
79 | });
80 |
81 | export default router;
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartfun",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "test:e2e": "vue-cli-service test:e2e"
10 | },
11 | "dependencies": {
12 | "axios": "^0.21.2",
13 | "dayjs": "^1.8.14",
14 | "echarts": "^5.4.0",
15 | "element-ui": "^2.9.1",
16 | "html2canvas": "^1.0.0-rc.3",
17 | "js-md5": "^0.7.3",
18 | "jsoneditor": "^9.5.6",
19 | "koa": "^2.7.0",
20 | "koa-body": "^4.1.0",
21 | "koa-router": "^7.4.0",
22 | "koa-static": "^5.0.0",
23 | "koa2-cors": "^2.0.6",
24 | "mockjs": "^1.0.1-beta3",
25 | "mongoose": "^5.13.15",
26 | "v-charts": "^1.19.0",
27 | "vue": "^2.6.6",
28 | "vue-drag-resize": "^1.3.2",
29 | "vue-draggable-resizable": "^2.0.0-rc1",
30 | "vue-json-editor": "^1.3.1",
31 | "vue-router": "^3.0.1",
32 | "vuedraggable": "^2.20.0",
33 | "vuex": "^3.0.1",
34 | "xlsx": "^0.17.0"
35 | },
36 | "devDependencies": {
37 | "@vue/cli-plugin-babel": "^3.5.0",
38 | "@vue/cli-plugin-eslint": "^5.0.8",
39 | "@vue/cli-service": "^5.0.8",
40 | "@vue/eslint-config-airbnb": "^4.0.0",
41 | "babel-eslint": "^10.0.1",
42 | "eslint": "^5.8.0",
43 | "eslint-plugin-vue": "^5.0.0",
44 | "gh-pages": "^2.0.1",
45 | "node-sass": "^7.0.3",
46 | "pug": "^2.0.3",
47 | "pug-plain-loader": "^1.0.0",
48 | "sass-loader": "^7.1.0",
49 | "vue-template-compiler": "^2.5.21"
50 | },
51 | "eslintConfig": {
52 | "root": true,
53 | "env": {
54 | "node": true
55 | },
56 | "extends": [
57 | "plugin:vue/essential",
58 | "@vue/airbnb"
59 | ],
60 | "rules": {},
61 | "parserOptions": {
62 | "parser": "babel-eslint"
63 | }
64 | },
65 | "postcss": {
66 | "plugins": {
67 | "autoprefixer": {}
68 | }
69 | },
70 | "browserslist": [
71 | "> 1%",
72 | "last 2 versions",
73 | "not ie <= 8"
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/Console/NavMenu.vue:
--------------------------------------------------------------------------------
1 |
2 | .sidebar
3 | .logo
4 | router-link(to="/") ChartFun
5 | router-link.sidebar-item(
6 | v-for="item in menuList"
7 | :key="item.name"
8 | :class="{ active: routePath == item.path }"
9 | :to="item.path")
10 | i(:class="item.icon")
11 | span {{item.name}}
12 |
13 |
14 |
46 |
47 |
103 |
--------------------------------------------------------------------------------
/src/components/Editor/ScaleBar.vue:
--------------------------------------------------------------------------------
1 |
2 | .panel
3 | // .canvas-view
4 | vue-draggable-resizable(
5 | :w="100"
6 | :h="80"
7 | :parent="true"
8 | class-name="canvas-box"
9 | class-name-dragging="canvas-box-dragging"
10 | :resizable="false")
11 | .control-bar
12 | i.btn.el-icon-minus(@click="zoomOut")
13 | .scale-mount {{scale * 100}}%
14 | i.btn.el-icon-plus(@click="zoomIn")
15 |
16 |
17 |
43 |
44 |
105 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 | .toolbar
3 | .toolbox
4 | .tool-list
5 | .btn(
6 | v-for="item in btnList"
7 | :class="{active: panelKey === item.key}"
8 | @click="showPanel(item.key)")
9 | i.iconfont(:class="'icon-' + item.key")
10 | .btn(:class="{active: panelKey === 'layers'}" @click="showPanel('layers')")
11 | i.iconfont.icon-layer
12 | .collapse-panel(v-show="panelKey")
13 | SidePanel(:panelKey="panelKey")
14 |
15 |
16 |
60 |
61 |
112 |
--------------------------------------------------------------------------------
/src/assets/font/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {font-family: "iconfont";
2 | src: url('iconfont.eot?t=1554565840275'); /* IE9 */
3 | src: url('iconfont.eot?t=1554565840275#iefix') format('embedded-opentype'), /* IE6-IE8 */
4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAVAAAsAAAAACqQAAATzAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDXgqHeIY1ATYCJAMcCxAABCAFhG0HWhsHCSMRJnyySPaXB/SURsePOhZxN7TtUtb1wKUl4ElKNslS+1OHHvpDKek5z8ED/qa9hEBpaNl2YqaciCtn/ndrAAxuk1VU8z3+qdIfkniFeiqexRrZ+tUSsJLU+ub8Ahu1qjIka9H3gw/MJbz39qbUjtTW1xgcwOTP30/tx/imwuCVXb15BXwFpAGhUMv04pcJRQRqQndCzWi9h2wC00da0adDAJoUZEOUq1CtHjYKYT0BiG6dO7bGdnlQMRyBbbdCdlWImZjYcpK8CszQf198Jo+wQWIaCJvW6lC+HaXeF/B3GF7Ea6YS6MoLAbe9wACyAQrE+NCIh2Gas+lYhy2tqAe42EissmqbwM/nt/Fn+DsiEdyDhZkCV/wHDwwUJhYCCcICcYixjVwWI6mTlTDAT6CEAj+fEib4bZSwwJ+hhAB/hweJdGBtF4gPogCIscDmiyhM7CqQ2gmFokPCIyjLUioYHQzGiDcsOfRPo4WJH81ouqTV5PsNHjzlXv27NpGc9e4ZyHX9hvdrtP92ihWPgvtuyZdeh7rmfF6brLK4KEcQ9sRRNb4Q1bdpg5Yjhq20bVCvPm3VVm0atmef2qRt61ZtG9ZnACEb2JPZdvBAfG0pKq9qRcWNYvr1eMomLdlleq4euDPq9uSb3q2h7uRESxFHKTwjB6bz9Xq21GJx3dYOWP2ucNZUHqeDErIuFvZVr6P5JkcB5FgrHA+2yAK5cNrV73p4c+HQZBuIKYJnkVFFvU8nHAdxbosv4BeDFY1xSr5InGsJau+hLcimGNlXaeVoPp9gPjvXtiKvFk/LWkryHTqkNfr/LYaV8ESkWn4Zixb39gwbtq2KaNNGVCFEamWQ3TL5j29E1LcftCgFvU47e1mtCrWydgLxI1XIalHrjWd6EbmWQ3QpphT9i/llSFLmQ+IPE0Tpxdl2ls/U5yFinbJVzrajfMY+jxChu7z2IzoME1r++vU1SpYstXDa9IVJJhYoYPcdWWplXl1K512plxY/D6dJQopJY8Ijvfp1jkSu0yezuTHJYtx//1icpGnCDf6FXT+GJ8dClUIDblbWjq5ZfeXOGtWTOLryzQHJO7g1x87MFaF8bCl4bA23Y/IBNze/LJcuWCm0q0b1oK5RbfjXLtbEwKbARKuiYKX/iomT156Q9mRFc6LVw13SpXrvuDL2jktGpw8P2R/omeQ03cKtpsTkilSIzXXyTKo389cxAuEOxmF5HsDw5EsAo6/8PQOjlZubTZ6MdZZfjt+/oUyP2XNbRhX7azsKgIfXP9/UPYr8NnuLWUTJmqNVl3KS+hoEb0C9R5bXRAAB+mDCtp1Ho4FjNBGH5+XZ4qfGXBC2+DZIAiQHA5t0pGKzgYlDPrCwKQmarFTc2yEufTAQKghkYRyAIJodIHE5AQbRXCMV+wxMEvAVLKKFAZoGIu4hHVYdRXs4G3KikuIrbamTk/w8GO0fqJ9Dbk6DzvuHhTYvaZR46++YYGVsEV51RjpxpqPc4GwYBhVv2qFk1JD+FMcu7IWiUsfZgbMhJyqh+AptqZNTZ+fBV/sH6ueQW8ywH9V/WGgXLqREJCnIO55SDduWpcOrziAdW8uZjsINQxjEigo+fKUOJSOaHCV/IqbpXFoc1W8bl/McQCN8X0Ck0sY6H9tehleL97pscuN6yL+wrW9LPg0r4sM1VYcwmwEA') format('woff2'),
5 | url('iconfont.woff?t=1554565840275') format('woff'),
6 | url('iconfont.ttf?t=1554565840275') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7 | url('iconfont.svg?t=1554565840275#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family: "iconfont" !important;
12 | font-size: 16px;
13 | font-style: normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-preview:before {
19 | content: "\e78f";
20 | }
21 |
22 | .icon-chart:before {
23 | content: "\e7af";
24 | }
25 |
26 | .icon-layer:before {
27 | content: "\e636";
28 | }
29 |
30 | .icon-picture:before {
31 | content: "\e716";
32 | }
33 |
34 | .icon-text:before {
35 | content: "\e734";
36 | }
37 |
38 | .icon-tools:before {
39 | content: "\e762";
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/server/routes/chart.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const Router = require('koa-router');
3 | const chartModel = require('../models/chart');
4 |
5 | const router = new Router();
6 | router.prefix('/chart');
7 |
8 | // 获取全部实例列表
9 | router.get('/', async (ctx, next) => {
10 | const rows = await chartModel.find({ 'uid': ctx.request.query.uid }).select('-chartData');
11 | ctx.body = {
12 | errno: 0,
13 | data: {
14 | chartList: rows
15 | }
16 | }
17 | });
18 |
19 | // 获取某一实例列表
20 | router.get('/:id', async (ctx, next) => {
21 | const item = await chartModel.findById(ctx.params.id);
22 |
23 | ctx.body = {
24 | errno: 0,
25 | data: item
26 | }
27 | });
28 |
29 | // 大屏访问量增1
30 | router.get('/view/:id', async (ctx, next) => {
31 | const item = await chartModel.findById(ctx.params.id);
32 |
33 | item.view = item.view + 1;
34 |
35 | item.save();
36 |
37 | ctx.body = {
38 | errno: 0,
39 | data: item
40 | }
41 | });
42 |
43 | // 新建可视化图表
44 | router.post('/', async (ctx, next) => {
45 | const body = ctx.request.body;
46 |
47 | if (!body.title || typeof body.title != 'string') {
48 | ctx.body = {
49 | errno: 1,
50 | errmsg: '格式错误'
51 | }
52 | return;
53 | }
54 |
55 | const result = await chartModel.create({
56 | title: body.title,
57 | img: '',
58 | uid: body.uid,
59 | view: 0,
60 | chartData: {
61 | "w": 1200,
62 | "h": 800,
63 | "bgcolor": "#999999",
64 | "bgimage": "",
65 | "bgimagesize": "cover",
66 | "elements": []
67 | }
68 | });
69 |
70 | ctx.body = {
71 | errno: 0,
72 | data: result
73 | }
74 | });
75 |
76 | // 更新可视化图表
77 | router.put('/:id', async (ctx, next) => {
78 | const body = ctx.request.body;
79 |
80 | const item = await chartModel.findById(ctx.params.id);
81 | if (body.title) {
82 | item.title = body.title;
83 | } else if (body.chartData) {
84 | item.chartData = body.chartData;
85 | item.img = body.img;
86 | }
87 | item.save();
88 |
89 | ctx.body = {
90 | errno: 0,
91 | }
92 | });
93 |
94 | // 删除可视化图表
95 | router.delete('/:id', async (ctx, next) => {
96 | const body = ctx.request.body;
97 |
98 | const item = await chartModel.findById(ctx.params.id);
99 | item.remove();
100 |
101 | ctx.body = {
102 | errno: 0,
103 | }
104 | });
105 |
106 | // 复制、导入可视化图表
107 | router.post('/import/:id', async (ctx, next) => {
108 | const body = ctx.request.body;
109 |
110 | const originItem = await chartModel.findById(ctx.params.id);
111 |
112 | const newItem = await chartModel.create({
113 | title: body.title ? body.title : originItem.title + '_导入',
114 | img: originItem.img,
115 | uid: body.uid,
116 | view: 0,
117 | chartData: originItem.chartData
118 | });
119 |
120 | ctx.body = {
121 | errno: 0
122 | }
123 | });
124 |
125 | module.exports = router;
126 |
--------------------------------------------------------------------------------
/src/views/Console/Data.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | el-row(style="margin-bottom: 20px;")
4 | el-button(type="primary" @click="addData") 新增数据源
5 | el-table(:data="connectList")
6 | el-table-column(prop="_id" label="id")
7 | template(slot-scope="scope")
8 | span {{ scope.row._id | simplifyID }}
9 | el-table-column(prop="name" label="名称")
10 | el-table-column(prop="createdAt" label="上传时间")
11 | template(slot-scope="scope")
12 | span {{ $dayjs(scope.row.createdAt).format('YYYY-MM-DD HH:mm') }}
13 | el-table-column(label="操作")
14 | template(slot-scope="scope")
15 | el-button(type="text" size="small" @click="renameData(scope.row)") 重命名
16 | el-button(type="text" size="small" @click="deleteData(scope.row._id)") 删除
17 |
18 |
19 |
105 |
106 |
108 |
--------------------------------------------------------------------------------
/src/views/Index.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | .screen
4 | .logo ChartFun
5 | .desc 一站式数据大屏制作平台
6 | transition(name="slide-fade")
7 | .login-box(v-if="show")
8 | .radio-group
9 | .radio-btn(:class="{active: tab == 'login'}" @click="tab = 'login'") 登录
10 | .radio-btn(:class="{active: tab == 'reg'}" @click="tab = 'reg'") 注册
11 | el-input(placeholder="请输入用户名" v-model="form.user")
12 | el-input(
13 | placeholder="请输入密码"
14 | :type="tab == 'login' ? 'password' : 'text'"
15 | v-model="form.password"
16 | style="margin-top: 10px;")
17 | .btn-wrapper
18 | span.btn(@click="handleClick") 进入系统
19 |
20 |
21 |
74 |
75 |
164 |
--------------------------------------------------------------------------------
/src/assets/font/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
45 |
--------------------------------------------------------------------------------
/src/components/Editor/Index.vue:
--------------------------------------------------------------------------------
1 |
2 | .editor-view
3 | .topbar-view
4 | Topbar
5 | .toolbar-view(v-show="!preview")
6 | Toolbar
7 | .config-view(v-show="!preview")
8 | Config
9 | .scale-view(:class="{preview: preview}")
10 | ScaleBar(@update:scale="changeScale")
11 | .main-view
12 | router-view(:scale="scale" ref="screenContainer")
13 |
14 |
15 |
146 |
147 |
195 |
--------------------------------------------------------------------------------
/src/views/Console/DataAdd.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | el-row(:gutter="20")
4 | el-col(:span="12")
5 | el-form(:model="form" :rules="rules" ref="ruleForm" label-width="100px" :hide-required-asterisk="true")
6 | el-form-item(label="名称" prop="name")
7 | el-input(v-model="form.name" placeholder="请输入数据源名称" style="width:220px;")
8 | el-form-item(label="数据文件")
9 | el-upload(class="upload" ref="fileUpload" drag action="/" :on-change="importExcel" :on-exceed="onFileExceed" :on-remove="onFileRemove" :auto-upload="false" :limit="1")
10 | i.el-icon-upload
11 | .el-upload__text 将文件拖到此处,或点击上传
12 | .el-upload__tip(slot="tip") 请上传 Excel 表格文件,其中第一行为表头
13 | el-form-item(label="选择维度" prop="dimension")
14 | el-select(v-model="form.dimension" placeholder="请选择" @change="form.metrics = []")
15 | el-option(v-for="item in form.columnsRaw" :label="item" :value="item")
16 | el-form-item(label="选择指标" prop="metrics")
17 | el-checkbox-group(v-model="form.metrics")
18 | el-checkbox(
19 | v-for="item in form.columnsRaw"
20 | :label="item"
21 | :disabled="item == form.dimension")
22 | el-form-item
23 | el-button(type="primary" @click="onSubmit('ruleForm')") 提交
24 |
25 |
26 |
170 |
171 |
189 |
--------------------------------------------------------------------------------
/src/views/Console/Chart.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | el-dialog(title="数据统计" :visible.sync="analyseVisible" width="400px")
4 | el-table(:data="analyseData" :show-header="false")
5 | el-table-column(property="key")
6 | el-table-column(property="value")
7 | el-row(:gutter="36")
8 | el-col(:span="6" v-for="item in chartList" :key="item._id")
9 | el-card(:body-style="{ padding: '0px' }" shadow="hover" @click.native="editChart(item._id)")
10 | img.image(:src="item.img")
11 | div(style="padding: 14px;")
12 | span {{item.title}}
13 | el-dropdown(style="float: right;")
14 | i.el-icon-more
15 | el-dropdown-menu(slot="dropdown")
16 | el-dropdown-item(@click.native="editChart(item._id)") 编辑
17 | el-dropdown-item(@click.native="renameChart(item)") 重命名
18 | el-dropdown-item(@click.native="copyChart(item)") 复制
19 | el-dropdown-item(@click.native="deleteChart(item._id)") 删除
20 | el-dropdown-item(@click.native="viewChart(item._id)" divided) 访问
21 | el-dropdown-item(@click.native="openChartAnalyse(item)") 查看统计
22 | el-col(:span="6")
23 | el-card(:body-style="{ padding: '0px' }" shadow="hover" @click.native="addNewChart")
24 | .add-card
25 | i.el-icon-circle-plus
26 |
27 |
28 |
181 |
182 |
225 |
--------------------------------------------------------------------------------
/src/views/Viewer/Canvas.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | .enter-fullscreen(@click="toggleFullscreen(document)") 切换全屏状态
4 | .screen(:style="screenStyle" @click.self="handleActivated(-1)" ref="screen")
5 | .component(
6 | v-for="(item, index) in chartData.elements"
7 | :key="index"
8 | :style="{width: item.w + 'px', height: item.h + 'px', left: item.x + 'px', top: item.y + 'px', zIndex: chartData.elements.length - index}")
9 | div.filler(
10 | v-if="item.data.type == 'chart'"
11 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
12 | ve-map(
13 | v-if="item.data.settings.type=='map'"
14 | :width="item.w + 'px'"
15 | :height="item.h + 'px'"
16 | :data="item.data.generated"
17 | :settings="item.data.settings"
18 | @ready-once="generateData(item)")
19 | ve-liquidfill(
20 | v-else-if="item.data.settings.type=='liquidfill'"
21 | :width="item.w + 'px'"
22 | :height="item.h + 'px'"
23 | :data="item.data.generated"
24 | @ready-once="generateData(item)")
25 | ve-chart(
26 | v-else
27 | :width="item.w + 'px'"
28 | :height="item.h + 'px'"
29 | :data="item.data.generated"
30 | :settings="item.data.settings"
31 | @ready-once="generateData(item)")
32 | div.filler(
33 | v-if="item.data.type == 'text'"
34 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
35 | div.textcontainer(
36 | :style="{fontFamily: item.data.datacon.fontFamily, fontWeight: item.data.datacon.bold ? 'bold' : 'normal', fontStyle: item.data.datacon.italic ? 'italic' : 'normal', color: item.data.datacon.color, fontSize: item.data.datacon.fontSize + 'px', textStroke: item.data.datacon.stroke ? item.data.datacon.strokeSize+'px '+item.data.datacon.strokeColor : '0', textShadow: item.data.datacon.shadow ? '5px 5px '+item.data.datacon.shadowBlur+'px '+item.data.datacon.shadowColor : 'none'}"
37 | v-text="item.data.datacon.text")
38 | div.filler(
39 | v-if="item.data.type == 'image'"
40 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
41 | div.imagecontainer(
42 | :style="{backgroundImage: `url(${item.data.datacon.img})`, backgroundSize: item.data.datacon.imgSize, opacity: item.data.datacon.opacity}")
43 | .placeholder(v-show="!item.data.datacon.img")
44 | div.filler(
45 | v-if="item.data.type == 'border'"
46 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
47 | div.bordercontainer(
48 | :class="'border' + item.data.datacon.borderId"
49 | :style="{opacity: item.data.datacon.opacity}")
50 |
51 |
52 |
141 |
142 |
203 |
--------------------------------------------------------------------------------
/src/views/Editor/Canvas.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | el-dialog(title="发布" :visible.sync="$parent.publishPopVisible" width="50%")
4 | div(style="margin-bottom: 16px;") 发布成功!当前图表的公开链接为:
5 | el-input(v-model="publicUrl" readonly)
6 | span(slot="footer")
7 | el-button(type="primary" @click="$parent.publishPopVisible = false") 确 定
8 | .edit-view(
9 | tabindex="0"
10 | @keydown.space.prevent="handleSpaceDown"
11 | @keyup.space.prevent="handleSpaceUp"
12 | @click.self="handleActivated(-1)")
13 | vue-draggable-resizable(
14 | :style="wrapStyle"
15 | :x="100"
16 | :y="50"
17 | :w="chartData.w"
18 | :h="chartData.h"
19 | class-name="screen-box"
20 | class-name-draggable="screen-box-draggable"
21 | :draggable="screenDraggable"
22 | :resizable="false")
23 | .screen(:style="screenStyle" @click.self="handleActivated(-1)" ref="screen")
24 | vue-drag-resize(
25 | v-for="(item, index) in chartData.elements"
26 | :key="index"
27 | :isActive="item.active && !$parent.preview"
28 | :parentScaleX="scale"
29 | :parentScaleY="scale"
30 | :x="item.x"
31 | :y="item.y"
32 | :w="item.w"
33 | :h="item.h"
34 | :parentLimitation="true"
35 | :parentW="chartData.w"
36 | :parentH="chartData.h"
37 | :aspectRatio="false"
38 | :minw="20"
39 | :minh="20"
40 | :z="chartData.elements.length - index"
41 | :isDraggable="!$parent.preview"
42 | :isResizable="!$parent.preview"
43 | @activated="handleActivated(index)"
44 | @resizing="handleResize(item, arguments[0])"
45 | @dragging="handleDrag(item, arguments[0])")
46 | div.filler(
47 | v-if="item.data.type == 'chart'"
48 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
49 | ve-map(
50 | v-if="item.data.settings.type=='map'"
51 | :width="item.w + 'px'"
52 | :height="item.h + 'px'"
53 | :data="item.data.generated"
54 | :settings="item.data.settings"
55 | @ready-once="generateData(item)")
56 | ve-liquidfill(
57 | v-else-if="item.data.settings.type=='liquidfill'"
58 | :width="item.w + 'px'"
59 | :height="item.h + 'px'"
60 | :data="item.data.generated"
61 | @ready-once="generateData(item)")
62 | ve-chart(
63 | v-else
64 | :width="item.w + 'px'"
65 | :height="item.h + 'px'"
66 | :data="item.data.generated"
67 | :settings="item.data.settings"
68 | @ready-once="generateData(item)")
69 | div.filler(
70 | v-if="item.data.type == 'text'"
71 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
72 | div.textcontainer(
73 | :style="{fontFamily: item.data.datacon.fontFamily, fontWeight: item.data.datacon.bold ? 'bold' : 'normal', fontStyle: item.data.datacon.italic ? 'italic' : 'normal', color: item.data.datacon.color, fontSize: item.data.datacon.fontSize + 'px', textStroke: item.data.datacon.stroke ? item.data.datacon.strokeSize+'px '+item.data.datacon.strokeColor : '0', textShadow: item.data.datacon.shadow ? '5px 5px '+item.data.datacon.shadowBlur+'px '+item.data.datacon.shadowColor : 'none'}"
74 | v-text="item.data.datacon.text")
75 | div.filler(
76 | v-if="item.data.type == 'image'"
77 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
78 | div.imagecontainer(
79 | :style="{backgroundImage: `url(${item.data.datacon.img})`, backgroundSize: item.data.datacon.imgSize, opacity: item.data.datacon.opacity}")
80 | .placeholder(v-show="!item.data.datacon.img")
81 | div.filler(
82 | v-if="item.data.type == 'border'"
83 | :style="{width: '100%', height: '100%', backgroundColor: item.bgcolor}")
84 | div.bordercontainer(
85 | :class="'border' + item.data.datacon.borderId"
86 | :style="{opacity: item.data.datacon.opacity}")
87 | .mock(:class="{front: screenDraggable}")
88 |
89 |
90 |
147 |
148 |
226 |
--------------------------------------------------------------------------------
/src/components/Editor/SidePanel.vue:
--------------------------------------------------------------------------------
1 |
2 | .panel
3 | .title(v-if="panelKey === 'layers'")
4 | span 图层 ({{chartData.elements.length}})
5 | .title(v-else-if="panelKey !== ''")
6 | span {{componentList[panelKey].name}} ({{componentList[panelKey].children.length}})
7 | .layer-list(v-if="panelKey === 'layers'")
8 | draggable(
9 | v-model="chartData.elements"
10 | @start="handleLayerListDragStart"
11 | @end="handleLayerListDragEnd"
12 | ghost-class="ghost")
13 | transition-group(type="transition" :name="!drag ? 'flip-list' : null")
14 | .list-item(
15 | v-for="(item, index) in chartData.elements"
16 | :key="item.name"
17 | @click="$parent.$parent.setActiveComponentByIndex(index)"
18 | :class="{active: index === $parent.$parent.currentElementIndex}")
19 | .name {{item.name}}
20 | i.el-icon-delete.icon(@click="handleDeleteComponent(index)")
21 | .component-list(v-else-if="panelKey !== ''")
22 | .list-item(v-for="item in componentList[panelKey].children" @click="handleAddComponent(item)")
23 | .img-wrapper
24 | img(:src="item.img")
25 | .name {{item.name}}
26 |
27 |
28 |
260 |
261 |
382 |
--------------------------------------------------------------------------------
/src/components/Editor/Config.vue:
--------------------------------------------------------------------------------
1 |
2 | .config
3 | .public-config(v-if="!currentElement.w")
4 | .config-box
5 | .title 画布大小
6 | el-row(:gutter="20")
7 | el-col(:span="12")
8 | el-input.num-input(v-model.number="chartData.w")
9 | template(slot="prepend") w
10 | el-col(:span="12")
11 | el-input(v-model.number="chartData.h")
12 | template(slot="prepend") h
13 | .config-box
14 | .title 背景配置
15 | el-select(v-model="editorSettings.parentBg" placeholder="请选择" style="width: 100%")
16 | el-option(label="背景颜色" :value="0")
17 | el-option(label="背景图片" :value="1")
18 | el-row(:gutter="20" style="margin-top: 12px;" v-show="editorSettings.parentBg === 0")
19 | el-col(:span="4")
20 | el-color-picker(v-model="chartData.bgcolor")
21 | el-col(:span="20")
22 | el-input(v-model="chartData.bgcolor" readonly)
23 | el-row(:gutter="20" style="margin-top: 12px;" v-show="editorSettings.parentBg === 1")
24 | el-col(:span="24")
25 | el-upload(
26 | class="bg-uploader"
27 | action="http://localhost:3000/api/uploadfile/"
28 | :show-file-list="false"
29 | :on-success="handleScreenBgUploadSuccess"
30 | :before-upload="beforeUpload")
31 | .bg-preview-wrapper(v-if="chartData.bgimage")
32 | img.bg-preview(:src="chartData.bgimage")
33 | i.el-icon-plus.avatar-uploader-icon(v-else)
34 | el-col(:span="24" v-show="chartData.bgimage")
35 | el-select(v-model="chartData.bgimagesize" placeholder="请选择" style="width: 100%")
36 | el-option(label="覆盖" value="cover")
37 | el-option(label="平铺" value="contain")
38 | el-option(label="拉伸" value="100% 100%")
39 | el-col(:span="24" v-show="chartData.bgimage" style="margin-top: 16px")
40 | el-button(type="danger" plain @click="handleScreenBgDelete" style="width: 100%") 删除
41 | .component-config(v-if="currentElement.w")
42 | .panel-selector
43 | .radio-group
44 | .radio-btn(
45 | @click="thisKey='general'"
46 | :class="{active: thisKey=='general'}") 基础
47 | .radio-btn(
48 | @click="thisKey='data'"
49 | v-show="currentElement.data.type == 'chart'"
50 | :class="{active: thisKey=='data'}") 数据
51 | .radio-btn(
52 | @click="thisKey='data'"
53 | v-show="currentElement.data.type == 'text'"
54 | :class="{active: thisKey=='data'}") 文字
55 | .radio-btn(
56 | @click="thisKey='data'"
57 | v-show="currentElement.data.type == 'image'"
58 | :class="{active: thisKey=='data'}") 图片
59 | .radio-btn(
60 | @click="thisKey='data'"
61 | v-show="currentElement.data.type == 'border'"
62 | :class="{active: thisKey=='data'}") 边框
63 | .panel(v-show="thisKey=='general'")
64 | .config-box
65 | .title 控件名称
66 | el-input(v-model="currentElement.name")
67 | .config-box
68 | .title 组件位置
69 | el-row(:gutter="20")
70 | el-col(:span="12")
71 | el-input(v-model.number="currentElement.x")
72 | template(slot="prepend") x
73 | el-col(:span="12")
74 | el-input(v-model.number="currentElement.y")
75 | template(slot="prepend") y
76 | el-row(:gutter="20" style="margin-top: 4px;")
77 | el-col(:span="12")
78 | el-input(v-model.number="currentElement.w")
79 | template(slot="prepend") w
80 | el-col(:span="12")
81 | el-input(v-model.number="currentElement.h")
82 | template(slot="prepend") h
83 | .config-box
84 | .title 背景颜色
85 | el-row(:gutter="20")
86 | el-col(:span="4")
87 | el-color-picker(v-model="currentElement.bgcolor" show-alpha)
88 | el-col(:span="20")
89 | el-input(v-model="currentElement.bgcolor" readonly)
90 | .config-box
91 | .title Settings.json
92 | pre.code-box(v-html="formatedJSON")
93 | .panel(v-show="thisKey=='data' && currentElement.data.type == 'chart'")
94 | .config-box
95 | .title 数据配置
96 | el-select(
97 | v-model="currentElement.data.datacon.type"
98 | placeholder="请选择"
99 | @change="handleChartDataChange"
100 | style="width: 100%; margin-bottom: 10px;")
101 | el-option(label="静态JSON" value="raw")
102 | el-option(label="我的数据源" value="connect")
103 | //- el-option(label="表格数据" value="table")
104 | el-option(label="GET接口" value="get")
105 | //- el-input(
106 | v-model="currentElement.data.datacon.data"
107 | type="textarea"
108 | :rows="10"
109 | placeholder="请插入标准 JSON 文件"
110 | v-show="currentElement.data.datacon.type == 'raw'")
111 | vue-json-editor(
112 | v-if="currentElement.data.datacon.type == 'raw'"
113 | v-model="currentElement.data.datacon.data"
114 | mode="code"
115 | :show-btns="true"
116 | @json-save="handleChartDataChange")
117 | el-select(
118 | v-if="currentElement.data.datacon.type == 'connect'"
119 | v-model="currentElement.data.datacon.connectId"
120 | placeholder="请选择"
121 | @change="handleChartDataChange"
122 | style="width: 100%; margin-bottom: 10px;")
123 | el-option(v-for="item in connectList" :label="item.name" :value="item._id")
124 | el-input(
125 | v-if="currentElement.data.datacon.type == 'get'"
126 | v-model="currentElement.data.datacon.getUrl"
127 | type="textarea"
128 | :rows="5"
129 | style="margin-bottom: 10px;")
130 | el-row(v-if="currentElement.data.datacon.type == 'get'")
131 | el-col(:span="8")
132 | p(style="margin-top: 8px;") 刷新时间
133 | el-col(:span="16")
134 | el-input-number(
135 | v-model="currentElement.data.datacon.interval"
136 | :min="1"
137 | :max="10"
138 | @change="handleChartDataChange"
139 | style="width: 100%;")
140 | .panel(v-show="thisKey=='data' && currentElement.data.type == 'text'")
141 | .config-box
142 | .title 输入文本
143 | el-input(
144 | v-model="currentElement.data.datacon.text"
145 | type="textarea"
146 | :rows="5"
147 | style="margin-bottom: 10px;")
148 | .config-box
149 | .title 字体字号
150 | el-select(
151 | v-model="currentElement.data.datacon.fontFamily"
152 | placeholder="请选择"
153 | style="width: 100%; margin-bottom: 10px;")
154 | el-option-group(label="英文字体")
155 | el-option(label="Molengo" value="Molengo")
156 | span(:style="{fontFamily: 'Molengo'}") Molengo
157 | el-option(label="Lobster" value="Lobster")
158 | span(:style="{fontFamily: 'Lobster'}") Lobster
159 | el-option-group(label="中文字体")
160 | el-option(label="思源黑体" value="Noto Sans SC")
161 | span(:style="{fontFamily: 'Noto Sans SC'}") 思源黑体
162 | el-option(label="思源宋体" value="Noto Serif SC")
163 | span(:style="{fontFamily: 'Noto Serif SC'}") 思源宋体
164 | el-option(label="站酷庆科黄油体" value="ZCOOL QingKe HuangYou")
165 | span(:style="{fontFamily: 'ZCOOL QingKe HuangYou'}") 站酷庆科黄油体
166 | el-option(label="站酷小薇体" value="ZCOOL XiaoWei")
167 | span(:style="{fontFamily: 'ZCOOL XiaoWei'}") 站酷小薇体
168 | el-row(:gutter="20")
169 | el-col(:span="4")
170 | el-color-picker(v-model="currentElement.data.datacon.color" show-alpha)
171 | el-col(:span="20")
172 | el-input(v-model="currentElement.data.datacon.fontSize")
173 | template(slot="append") px
174 | el-row(:gutter="20")
175 | el-col(:span="24")
176 | .btn(:class="{active: currentElement.data.datacon.bold}" @click="currentElement.data.datacon.bold = !currentElement.data.datacon.bold")
177 | i.iconfont.icon-bold
178 | .btn(:class="{active: currentElement.data.datacon.italic}" @click="currentElement.data.datacon.italic = !currentElement.data.datacon.italic")
179 | i.iconfont.icon-italic
180 | .config-box
181 | .title 描边
182 | el-switch(v-model="currentElement.data.datacon.stroke" style="float: right;")
183 | el-row(:gutter="20" v-show="currentElement.data.datacon.stroke")
184 | el-col(:span="4")
185 | el-color-picker(v-model="currentElement.data.datacon.strokeColor")
186 | el-col(:span="20")
187 | el-input(v-model="currentElement.data.datacon.strokeSize")
188 | template(slot="append") px
189 | .config-box
190 | .title 阴影
191 | el-switch(v-model="currentElement.data.datacon.shadow" style="float: right;")
192 | el-row(:gutter="20" v-show="currentElement.data.datacon.shadow")
193 | el-col(:span="4")
194 | el-color-picker(v-model="currentElement.data.datacon.shadowColor")
195 | el-col(:span="20")
196 | el-input(v-model="currentElement.data.datacon.shadowBlur")
197 | template(slot="append") px
198 | .panel(v-show="thisKey=='data' && currentElement.data.type == 'image'")
199 | .config-box
200 | .title 上传图片
201 | el-upload(
202 | class="bg-uploader"
203 | action="http://localhost:3000/api/uploadfile/"
204 | :show-file-list="false"
205 | :on-success="handleImageUploadSuccess"
206 | :before-upload="beforeUpload")
207 | .bg-preview-wrapper(v-if="this.currentElement.data.datacon.img")
208 | img.bg-preview(:src="this.currentElement.data.datacon.img")
209 | i.el-icon-plus.avatar-uploader-icon(v-else)
210 | el-row
211 | el-col(:span="24" v-show="this.currentElement.data.datacon.img")
212 | el-select(v-model="currentElement.data.datacon.imgSize" placeholder="请选择" style="width: 100%")
213 | el-option(label="覆盖" value="cover")
214 | el-option(label="平铺" value="contain")
215 | el-option(label="拉伸" value="100% 100%")
216 | .config-box
217 | .title 透明度
218 | el-slider(v-model="currentElement.data.datacon.opacity" :max="1" :step="0.01" show-input)
219 | .panel(v-show="thisKey=='data' && currentElement.data.type == 'border'")
220 | .config-box
221 | .title 边框样式
222 | el-select(
223 | v-model="currentElement.data.datacon.borderId"
224 | placeholder="请选择"
225 | style="width: 100%; margin-bottom: 10px;")
226 | el-option(label="古典-棕" :value="1")
227 | el-option(label="古典-白" :value="2")
228 | el-option(label="科技" :value="3")
229 | .config-box
230 | .title 透明度
231 | el-slider(v-model="currentElement.data.datacon.opacity" :max="1" :step="0.01" show-input)
232 |
233 |
234 |
312 |
313 |
436 |
--------------------------------------------------------------------------------