diff --git a/build/webpack.config.js b/build/webpack.config.js index e8f00681..e88f5400 100644 --- a/build/webpack.config.js +++ b/build/webpack.config.js @@ -3,8 +3,10 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin'); const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const fs = require('fs'); +const SvgSpritePlugin = require('../tool/plugin-svg-sprite'); const distPath = path.resolve(__dirname, '../public'); +const svgPath = path.resolve(__dirname, '../src/asset/icon'); module.exports = (env, argv) => { @@ -77,7 +79,11 @@ module.exports = (env, argv) => { ] }, { - test: /\.(svg|html)$/, + test: /\.svg$/, + type: 'asset/source' + }, + { + test: /\.html$/, use: [ { loader: 'html-loader', @@ -114,6 +120,10 @@ module.exports = (env, argv) => { vue: 'Vue' }, plugins: [ + new SvgSpritePlugin({ + spriteFilename: '../asset/sprite.svg', + svgPath: svgPath + }), new webpack.DefinePlugin({ // It can be used in the code directly. CONFIG_LOCAL: JSON.stringify(configLocal), diff --git a/src/asset/icon/ui/camera.svg b/src/asset/icon/ui/camera.svg new file mode 100644 index 00000000..d1277869 --- /dev/null +++ b/src/asset/icon/ui/camera.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/codepen.svg b/src/asset/icon/ui/codepen.svg new file mode 100644 index 00000000..6adef6b3 --- /dev/null +++ b/src/asset/icon/ui/codepen.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/asset/icon/ui/codesandbox.svg b/src/asset/icon/ui/codesandbox.svg new file mode 100644 index 00000000..bb95916d --- /dev/null +++ b/src/asset/icon/ui/codesandbox.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/asset/icon/ui/document.svg b/src/asset/icon/ui/document.svg new file mode 100644 index 00000000..b77c278d --- /dev/null +++ b/src/asset/icon/ui/document.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/download.svg b/src/asset/icon/ui/download.svg new file mode 100644 index 00000000..c97acbae --- /dev/null +++ b/src/asset/icon/ui/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/format.svg b/src/asset/icon/ui/format.svg new file mode 100644 index 00000000..e85741c2 --- /dev/null +++ b/src/asset/icon/ui/format.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/github.svg b/src/asset/icon/ui/github.svg new file mode 100644 index 00000000..53ef6d79 --- /dev/null +++ b/src/asset/icon/ui/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/loading.svg b/src/asset/icon/ui/loading.svg new file mode 100644 index 00000000..0b581190 --- /dev/null +++ b/src/asset/icon/ui/loading.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/play.svg b/src/asset/icon/ui/play.svg new file mode 100644 index 00000000..a4e10c39 --- /dev/null +++ b/src/asset/icon/ui/play.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/asset/icon/ui/setting.svg b/src/asset/icon/ui/setting.svg new file mode 100644 index 00000000..d5cfe426 --- /dev/null +++ b/src/asset/icon/ui/setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/icon/ui/share.svg b/src/asset/icon/ui/share.svg new file mode 100644 index 00000000..77fdb94f --- /dev/null +++ b/src/asset/icon/ui/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/SVGIcon.vue b/src/common/SVGIcon.vue new file mode 100644 index 00000000..9452b829 --- /dev/null +++ b/src/common/SVGIcon.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/editor/Editor.vue b/src/editor/Editor.vue index cc627b9d..16b2a536 100644 --- a/src/editor/Editor.vue +++ b/src/editor/Editor.vue @@ -51,12 +51,7 @@ 'editor.pr.tooltip' )}`" > - - - + #{{ shared.prNumber }} - - - + - - - + - - - + - - - - + {{ $t('editor.run') }} @@ -312,11 +254,14 @@ {{ $t('editor.prPreview.review') }} - + > + + {{ $t('editor.prPreview.loadingReview') @@ -378,11 +323,14 @@
{{ $t('editor.prPreview.viewDiff') }} - + > + +
= 0;
+      return (
+        this.exampleConfig &&
+        this.exampleConfig.since &&
+        compareVersions(
+          this.shared.echartsFullVersion,
+          this.exampleConfig.since
+        ) >= 0
+      );
     },
 
     versionSinceBanner() {
-      return this.$t('editor.bannerVersionRequire') + ' v' + this.exampleConfig.since + '+';
-    },
+      return (
+        this.$t('editor.bannerVersionRequire') +
+        ' v' +
+        this.exampleConfig.since +
+        '+'
+      );
+    }
   },
 
   mounted() {
@@ -642,7 +603,7 @@ export default {
               )}`;
           return !isObjOrArray
             ? `${name}`
-            : `${name}`;
+            : `${name}`;
         },
         expandOnCreatedAndUpdated(path) {
           return (
@@ -968,7 +929,7 @@ $handler-width: 15px;
   font-family: 'Source Code Pro', 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas',
     monospace;
 
-  .el-icon-document {
+  .icon-document {
     margin-left: 5px;
     font-size: 1rem;
 
@@ -1173,4 +1134,11 @@ $handler-width: 15px;
 
   background: $clr-bg;
 }
+.fill-none {
+  fill: none;
+}
+
+.stroke-current {
+  stroke: currentColor;
+}
 
diff --git a/src/editor/Preview.vue b/src/editor/Preview.vue
index 6cb204d7..c0e94521 100644
--- a/src/editor/Preview.vue
+++ b/src/editor/Preview.vue
@@ -43,12 +43,14 @@
               :inactive-text="''"
             />
           
-          
-            
-              {{ $t('editor.renderCfgTitle')
-              }}
-            
-          
+          
         
         
         
@@ -161,6 +160,7 @@ import debounce from 'lodash/debounce';
 import { download } from './downloadExample';
 import { gotoURL, getURL } from '../common/route';
 import { gt, rcompare } from 'semver';
+import SVGIcon from '../common/SVGIcon.vue';
 
 const example = getExampleConfig();
 const isGL = 'gl' in URL_PARAMS || isGLExample();
@@ -353,6 +353,9 @@ function run(recreateInstance) {
 }
 
 export default {
+  components: {
+    SVGIcon
+  },
   props: {
     inEditor: {
       type: Boolean
@@ -845,4 +848,17 @@ export default {
     }
   }
 }
+
+.icon {
+  width: 12px;
+  height: 12px;
+}
+
+.ml-4 {
+  margin-left: 4px;
+}
+
+.mr-4 {
+  margin-right: 4px;
+}
 
diff --git a/src/explore/Explore.vue b/src/explore/Explore.vue
index b3a21ef3..2f3e5af8 100644
--- a/src/explore/Explore.vue
+++ b/src/explore/Explore.vue
@@ -20,7 +20,9 @@
                 :id="'left-chart-nav-' + category"
                 :href="'#chart-type-' + category"
               >
-                
+                
+                  
+                
                 {{
                   $t('chartTypes.' + category)
                 }}
@@ -71,6 +73,7 @@ import CHART_LIST_GL from '../data/chart-list-data-gl';
 import { EXAMPLE_CATEGORIES, BLACK_MAP } from '../common/config';
 import { store } from '../common/store';
 import ExampleCard from './ExampleCard.vue';
+import SVGIcon from '../common/SVGIcon.vue';
 import LazyLoad from 'vanilla-lazyload/dist/lazyload.esm';
 
 const icons = {};
@@ -106,10 +109,9 @@ const icons = {};
   'rich',
   'graphic'
 ].forEach(function (category) {
-  icons[category] = require('../asset/icon/' + category + '.svg');
+  icons[category] = category;
 });
 
-const glIcon = require('../asset/icon/gl.svg');
 [
   'globe',
   'bar3D',
@@ -124,14 +126,15 @@ const glIcon = require('../asset/icon/gl.svg');
   'graphGL',
   'geo3D'
 ].forEach(function (category) {
-  icons[category] = glIcon;
+  icons[category] = 'gl';
 });
 
 const LAZY_LOADED_CLASS = 'ec-shot-loaded';
 
 export default {
   components: {
-    ExampleCard
+    ExampleCard,
+    SVGIcon
   },
 
   data() {
@@ -305,7 +308,7 @@ export default {
 @import '../style/config.xl.scss';
 
 $chart-nav-width: 200px;
-$chart-icon-width: 25px;
+$chart-icon-size: 20px;
 $chart-icon-border: 1px;
 
 $toolbar-height: 30px;
@@ -454,15 +457,15 @@ $pd-lg: 20px;
       }
 
       .chart-icon {
-        content: '';
-        width: 20px;
+        width: $chart-icon-size;
+        height: $chart-icon-size;
         display: inline-block;
         border-radius: 50%;
         vertical-align: middle;
 
         svg {
-          width: 100% !important;
-          height: auto !important;
+          width: 100%;
+          height: 100%;
         }
       }
 
@@ -470,8 +473,8 @@ $pd-lg: 20px;
         background-color: $nav-active-bg;
         color: #fff;
 
-        .chart-icon * {
-          fill: #fff;
+        .chart-icon use {
+          filter: invert(1) grayscale(1) brightness(200%);
         }
       }
 
diff --git a/tool/plugin-svg-sprite.js b/tool/plugin-svg-sprite.js
new file mode 100644
index 00000000..df071faf
--- /dev/null
+++ b/tool/plugin-svg-sprite.js
@@ -0,0 +1,94 @@
+const { RawSource } = require('webpack-sources');
+const path = require('path');
+const fs = require('fs');
+
+class SvgSpritePlugin {
+  constructor(options = {}) {
+    this.spriteFilename = options.spriteFilename || 'sprite.svg';
+    this.svgPath = options.svgPath;
+  }
+
+  // Helper to process raw SVG string into a 
+  processSvg(filePath, rawSvg) {
+    const id = path.basename(filePath, '.svg').replace(/[^\w0-9_-]/g, '-');
+
+    // Remove XML declaration and comments
+    let svg = rawSvg.replace(/<\?xml.*?\?>/g, '').replace(//gs, '');
+
+    // Extract viewBox (crucial for icon scaling)
+    const viewBoxMatch = svg.match(/viewBox="([^"]*)"/);
+    if (!viewBoxMatch) {
+      console.warn(
+        `Warning: SVG file '${filePath}' is missing a viewBox attribute. Skipping.`
+      );
+      return null;
+    }
+    const viewBox = viewBoxMatch[1];
+
+    // WARNING: Simple regex is used here. For production, a reliable XML parser is safer.
+    const symbolContent = svg
+      .replace(/<\/?svg[^>]*>/g, '') // Remove opening/closing  tags
+      .replace(/xlink:href/g, 'href') // xlink:href causes parse error and icons do not load
+      .replace(/\s+/g, ' ') // Minify: collapse whitespace
+      .replace(/>\s+<') // Minify: remove spaces between tags
+      .trim();
+
+    return `${symbolContent}`;
+  }
+
+  // Recursively get all .svg files
+  getSvgFiles(dir) {
+    let files = [];
+    const items = fs.readdirSync(dir);
+    for (const item of items) {
+      const fullPath = path.join(dir, item);
+      const stat = fs.statSync(fullPath);
+      if (stat.isDirectory()) {
+        files = files.concat(this.getSvgFiles(fullPath));
+      } else if (item.endsWith('.svg')) {
+        files.push(fullPath);
+      }
+    }
+    return files;
+  }
+
+  apply(compiler) {
+    compiler.hooks.compilation.tap('SvgSpritePlugin', (compilation) => {
+      compilation.hooks.processAssets.tap(
+        {
+          name: 'SvgSpritePlugin',
+          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
+        },
+        (assets) => {
+          if (!this.svgPath || !fs.existsSync(this.svgPath)) {
+            return;
+          }
+
+          const svgFiles = this.getSvgFiles(this.svgPath);
+          let symbols = [];
+
+          for (const filePath of svgFiles) {
+            const rawSvg = fs.readFileSync(filePath, 'utf-8');
+            const symbol = this.processSvg(filePath, rawSvg);
+            if (symbol) {
+              symbols.push(symbol);
+            }
+          }
+
+          // 4. Assemble the final sprite content
+          const spriteContent = `${symbols.join(
+            '\n'
+          )}`;
+
+          // 5. Emit the single sprite file to the build directory
+          compilation.emitAsset(
+            this.spriteFilename,
+            new RawSource(spriteContent)
+          );
+        }
+      );
+    });
+  }
+}
+
+module.exports = SvgSpritePlugin;