PiranhaCMS 中的 DevExpress Web 仪表板

Posted

技术标签:

【中文标题】PiranhaCMS 中的 DevExpress Web 仪表板【英文标题】:DevExpress Web Dashboard in PiranhaCMS 【发布时间】:2021-09-30 14:32:38 【问题描述】:

我目前正在开发一个基于名为 PiranhaCMS 的 CMS 框架的 .NET Core 应用程序。这个框架允许定义可配置的“块”,基本上是小部件,可以由用户在他们的页面上添加。块的配置页面实现为 Vue.js 组件,然后通过 gulp 以标准 JS 格式编译代码(从 .vue 文件到 Vue.component(...) 语法),供 Piranha 框架使用阅读和渲染。 Piranha 的作者确认这是定义新区块的唯一方法。

在我们的一个自定义块中,我们正在尝试实现 DevExpress Web Dashboard。我尝试按照https://docs.devexpress.com/Dashboard/401150/web-dashboard/dashboard-component-for-vue 中概述的步骤进行操作,但无济于事,因为编译器会抛出异常,指出***声明应该是导出默认值 ... ,而不是导入语句。

我想出了一个解决方法,我在组件的 created() 方法上动态加载所需的脚本和样式,然后以与经典 javascript 案例相同的方式定义仪表板 (https://docs.devexpress.com/Dashboard/119158/web-dashboard/dashboard-control-for-javascript-applications-jquery-knockout-etc/add-web-dashboard-to-a-javascript-application) ;;) 然而,我确信有一个更优雅的解决方案来解决这个问题。

下面是与问题相关的代码。这是自定义块itools-dashboard.vue:

<template>
    <div class="form-group block-body">
        <div :id="'dashboard-designer-' + uid" class="dashboard-designer">
            <div :id="'dashboard_' + uid" style="height: 100%;">
            </div>
        </div>
        <div class="row">
            <div class="col-sm-6" style="padding:10px; margin-top: 0px;vertical-align: top;">
                <fieldset>
                    <legend>Dashboard</legend>
                    <div class="form-group">
                        <label>Dashboard name</label>
                        <select class="form-control small" :id="'dashboard-names-' + uid" v-model="model.dashboardName.value">
                            <option v-for="dash in dashboardNames"> dash </option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label>Update time</label>
                        <input class="form-control small" type="number" v-model="model.updateTime.value">
                    </div>
                    <div class="form-group">
                        <label>Width</label>
                        <input class="form-control small" type="text" v-model="model.width.value">
                    </div>
                    <div class="form-group">
                        <label>Height</label>
                        <input class="form-control small" type="text" v-model="model.height.value">
                    </div>
                </fieldset>
            </div>
            <div class="col-sm-6" style="padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;">
                <itools-base :model="model"></itools-base>
            </div>
        </div>
    </div>
</template>
<script>
    export default 
        props: ["uid", "toolbar", "model"],
        data: function () 
            return 
                dashboardNames: [],
                dahsboardConfig: null,
                updateModes: ["period", "realtime"],
                basePath: "../../../../assets/",
                // define all the css and js files paths
                cssResources: [
                    "devextreme/dist/css/light.css",
                    "@devexpress/analytics-core/dist/css/dx-analytics.common.css",
                    "@devexpress/analytics-core/dist/css/dx-analytics.light.css",
                    "@devexpress/analytics-core/dist/css/dx-querybuilder.css",
                    "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"
                ],
                jsResources: [
                    "js/jquery/jquery-3.3.1.min.js",
                    "jquery-ui-dist/jquery-ui.js",
                    "knockout/build/output/knockout-latest.js",
                    "ace-builds/src-min-noconflict/ace.js",
                    "ace-builds/src-min-noconflict/ext-language_tools.js",
                    "ace-builds/src-min-noconflict/theme-dreamweaver.js",
                    "ace-builds/src-min-noconflict/theme-ambiance.js",
                    "devextreme/dist/js/dx.all.js",
                    "devextreme/dist/js/dx.aspnet.mvc.js",
                    "devextreme-aspnet-data/js/dx.aspnet.data.js",
                    "@devexpress/analytics-core/dist/js/dx-analytics-core.min.js",
                    "@devexpress/analytics-core/dist/js/dx-querybuilder.min.js",
                    "devexpress-dashboard/dist/js/dx-dashboard.min.js"
                ]
            
        ,
        created: function () 
            // dynamically add the required css
            this.cssResources.forEach(x => 
                let link = document.createElement("link");
                link.setAttribute("href", this.basePath + x);
                link.setAttribute("rel", "stylesheet");
                document.head.appendChild(link);
            );
            // dynamically add the js files. 
            // It needs to be a synchronous ajax call so that the exports are visible in the code
            // (eg the new DevExpress call)
            this.jsResources.forEach(x => 
                $.ajax(
                    async: false,
                    url: this.basePath + x,
                    dataType: "script"
                )
            );
            this.model.width.value = this.model.width.value || "100%";
            this.model.height.value = this.model.height.value || "300";
            this.model.updateTime.value = this.model.updateTime.value || 5000;

        ,
        mounted: function () 
            var h = document.getElementById("dashboard-designer-" + this.uid).clientHeight;

            DevExpress.Dashboard.ResourceManager.embedBundledResources();
            var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), 
                endpoint: "/api/dashboard",
                workingMode: "Designer",
                width: "100%",
                height: "100%",
                initialDashboardId: this.model.dashboardName.value,
            );

            dashboardControl.render();
        ,
        beforeCreate: function () 
            fetch("/api/Dashboards/GetDashboardNames")
                .then(response => response.json())
                .then(data => 
                    this.dashboardNames = data;
                );
        ,
    
</script>

然后通过 gulp 任务编译到

Vue.component("itools-dashboard", 
  props: ["uid", "toolbar", "model"],
  data: function () 
    return 
      dashboardNames: [],
      dahsboardConfig: null,
      updateModes: ["period", "realtime"],
      basePath: "../../../../assets/",
      cssResources: ["devextreme/dist/css/light.css", "@devexpress/analytics-core/dist/css/dx-analytics.common.css", "@devexpress/analytics-core/dist/css/dx-analytics.light.css", "@devexpress/analytics-core/dist/css/dx-querybuilder.css", "devexpress-dashboard/dist/css/dx-dashboard.light.min.css"],
      jsResources: ["js/jquery/jquery-3.3.1.min.js", "jquery-ui-dist/jquery-ui.js", "knockout/build/output/knockout-latest.js", "ace-builds/src-min-noconflict/ace.js", "ace-builds/src-min-noconflict/ext-language_tools.js", "ace-builds/src-min-noconflict/theme-dreamweaver.js", "ace-builds/src-min-noconflict/theme-ambiance.js", "devextreme/dist/js/dx.all.js", "devextreme/dist/js/dx.aspnet.mvc.js", "devextreme-aspnet-data/js/dx.aspnet.data.js", "@devexpress/analytics-core/dist/js/dx-analytics-core.min.js", "@devexpress/analytics-core/dist/js/dx-querybuilder.min.js", "devexpress-dashboard/dist/js/dx-dashboard.min.js"]
    ;
  ,
  created: function () 
    this.cssResources.forEach(x => 
      let link = document.createElement("link");
      link.setAttribute("href", this.basePath + x);
      link.setAttribute("rel", "stylesheet");
      document.head.appendChild(link);
    );
    this.jsResources.forEach(x => 
      $.ajax(
        async: false,
        url: this.basePath + x,
        dataType: "script"
      );
    );
    this.model.width.value = this.model.width.value || "100%";
    this.model.height.value = this.model.height.value || "300";
    this.model.updateTime.value = this.model.updateTime.value || 5000;
  ,
  mounted: function () 
    DevExpress.Dashboard.ResourceManager.embedBundledResources();
    var dashboardControl = new DevExpress.Dashboard.DashboardControl(document.getElementById("dashboard_" + this.uid), 
      endpoint: "/api/dashboard",
      workingMode: "Designer",
      width: "100%",
      height: "100%",
      initialDashboardId: this.model.dashboardName.value
    );
    dashboardControl.render();
  ,
  beforeCreate: function () 
    fetch("/api/Dashboards/GetDashboardNames").then(response => response.json()).then(data => 
      this.dashboardNames = data;
    );
  ,
  template: "\n<div class=\"form-group block-body\">\n    <div :id=\"'dashboard-designer-' + uid\" class=\"dashboard-designer\">\n        <div :id=\"'dashboard_' + uid\" style=\"height: 100%;\">\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px;vertical-align: top;\">\n            <fieldset>\n                <legend>Dashboard</legend>\n                <div class=\"form-group\">\n                    <label>Dashboard name</label>\n                    <select class=\"form-control small\" :id=\"'dashboard-names-' + uid\" v-model=\"model.dashboardName.value\">\n                        <option v-for=\"dash in dashboardNames\"> dash </option>\n                    </select>\n                </div>\n                <div class=\"form-group\">\n                    <label>Update time</label>\n                    <input class=\"form-control small\" type=\"number\" v-model=\"model.updateTime.value\">\n                </div>\n                <div class=\"form-group\">\n                    <label>Width</label>\n                    <input class=\"form-control small\" type=\"text\" v-model=\"model.width.value\">\n                </div>\n                <div class=\"form-group\">\n                    <label>Height</label>\n                    <input class=\"form-control small\" type=\"text\" v-model=\"model.height.value\">\n                </div>\n            </fieldset>\n        </div>\n        <div class=\"col-sm-6\" style=\"padding:10px; margin-top: 0px; background-color: #fcfcfc; border:1px dotted lightgray; vertical-align: top;\">\n            <itools-base :model=\"model\"></itools-base>\n        </div>\n    </div>\n</div>\n"
);  

由 Piranha 定义的负责编译的 gulp 任务是:

var gulp = require('gulp'),
    sass = require('gulp-sass'),
    cssmin = require("gulp-cssmin"),
    uglifyes = require('uglify-es'),
    composer = require('gulp-uglify/composer'),
    uglify = composer(uglifyes, console),
    rename = require("gulp-rename"),
    concat = require("gulp-concat");

var path = require('path'),
    vueCompiler = require('vue-template-compiler'),
    babel = require("@babel/core"),
    babelTemplate = require("@babel/template").default,
    codeFrameColumns = require('@babel/code-frame').codeFrameColumns,
    babelTypes = require("@babel/types"),
    through2 = require('through2');

function vueCompile() 
    return through2.obj(function (file, _, callback) 
        var relativeFile = path.relative(file.cwd, file.path);
        console.log(relativeFile);
        var ext = path.extname(file.path);
        if (ext === '.vue') 
            var getComponent;
            getComponent = function (ast, sourceCode) 
                const ta = ast.program.body[0]
                if (!babelTypes.isExportDefaultDeclaration(ta)) 
                    var msg = 'Top level declaration in file ' + relativeFile + ' must be "export default " \n' + codeFrameColumns(sourceCode,  start: ta.loc.start ,  highlightCode: true );
                    throw msg;
                
                return ta.declaration;
            

            var compile;
            compile = function (componentName, content) 
                var component = vueCompiler.parseComponent(content, []);
                if (component.styles.length > 0) 
                    component.styles.forEach(s => 
                        const linesToStyle = content.substr(0, s.start).split(/\r?\n/).length;
                        var msg = 'WARNING: <style> tag in ' + relativeFile + ' is ignored\n' + codeFrameColumns(content,  start:  line: linesToStyle  ,  highlightCode: true );
                        console.warn(msg);
                    );
                

                var ast = babel.parse(component.script.content, 
                    parserOpts: 
                        sourceFilename: file.path
                    
                );

                var vueComponent = getComponent(ast, component.script.content);
                vueComponent.properties.push(babelTypes.objectProperty(babelTypes.identifier('template'), babelTypes.stringLiteral(component.template.content)))

                var wrapInComponent = babelTemplate("Vue.component(NAME, COMPONENT);");
                var componentAst = wrapInComponent(
                    NAME: babelTypes.stringLiteral(componentName),
                    COMPONENT: vueComponent
                )

                ast.program.body = [componentAst]

                babel.transformFromAst(ast, null, null, function (err, result) 
                    if (err) 
                        callback(err, null)
                    
                    else 
                        file.contents = Buffer.from(result.code);
                        callback(null, file)
                    
                );
            
            var componentName = path.basename(file.path, ext);
            if (file.isBuffer()) 
                compile(componentName, file.contents.toString());
            
            else if (file.isStream()) 
                var chunks = [];
                file.contents.on('data', function (chunk) 
                    chunks.push(chunk);
                );
                file.contents.on('end', function () 
                    compile(componentName, Buffer.concat(chunks).toString());
                );
            
         else 
            callback(null, file);
        
    );


var js = 
    name: "itools-blocks.js",
    path: "wwwroot/assets/js/blocks/*.vue"


//
// Compile & minimize js files
//
gulp.task("min:js", function (done) 
    gulp.src(js.path,  base: "." )
        .pipe(vueCompile())
        .pipe(concat("wwwroot/assets/js/blocks/" + js.name))
        .pipe(gulp.dest("."))
        .pipe(uglify().on('error', function (e) 
            console.log(e);
        ))
        .pipe(rename(
            suffix: ".min"
        ))
        .pipe(gulp.dest("."));
    done();
);

非常感谢任何形式的帮助

【问题讨论】:

【参考方案1】:

您所指的带有“vueCompile”方法的 gulpfile 是专门为满足我们在框架中提供的内部组件的需求而编写的,它绝不是所有 Vue 组件编译的灵丹妙药。但是我理解您的问题,在编写此代码之前,我们拼命搜索了现有的 npm-packages,它们可以为我们提供所需的功能,但这并不容易找到,因为我们只使用了 Vue.js 中可用功能的子集

我们非常乐意收到有关如何完成此操作的反馈或更多信息,因此我们将关注此线程??

【讨论】:

嗨 Håkan,我从版本 6 开始就一直在使用 piranha,在我看来,VUE 的引入已经“加强”了框架,如果没有创建对象的可能性不是很有用吗?绑定到 VUE? 由于我们想要构建的编辑界面几乎不可能仅使用服务器端呈现的 asp.net 创建,因此我们不得不选择一个 JavaScript 框架来添加客户端功能。可以使用 MVC 或带有或不带有 Vue.js 的 Razor Pages 将自定义页面添加到管理器,但是当添加应该与现有编辑视图集成的组件时,例如字段或自定义块,有必要使用 Vue 构建这些。

以上是关于PiranhaCMS 中的 DevExpress Web 仪表板的主要内容,如果未能解决你的问题,请参考以下文章

在 PiranhaCMS 中使用块

PiranhaCMS - 获取当前 Windows 用户

Devexpress常见问题

Piranha CMS 和 Azure 存储

如何给DevExpress中的GridControl添加列

如何给DevExpress中的GridControl添加列