iamkun преди 6 години
родител
ревизия
f29f49a17d

+ 2 - 1
.eslintrc

@@ -1,6 +1,7 @@
 {
   "globals": {
-    "ga": true
+    "ga": true,
+    "chrome": true
   },
   "plugins": ["html", "json"],
   "extends": "elemefe",

+ 33 - 0
build/webpack.extension.js

@@ -0,0 +1,33 @@
+const path = require('path');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const demoConfig = require('./webpack.demo');
+const webpack = require('webpack');
+const ProgressBarPlugin = require('progress-bar-webpack-plugin');
+const VueLoaderPlugin = require('vue-loader/lib/plugin');
+
+demoConfig.entry = {
+  background: path.join(process.cwd(), './examples/extension/src/background'),
+  entry: path.join(process.cwd(), './examples/extension/src/entry')
+};
+demoConfig.output = {
+  path: path.join(process.cwd(), './examples/extension/dist'),
+  filename: '[name].js'
+};
+demoConfig.plugins = [
+  new CopyWebpackPlugin([
+    { from: 'examples/extension/src/manifest.json' },
+    { from: 'examples/extension/src/icon.png' }
+  ]),
+  new VueLoaderPlugin(),
+  new ProgressBarPlugin(),
+  new webpack.LoaderOptionsPlugin({
+    vue: {
+      compilerOptions: {
+        preserveWhitespace: false
+      }
+    }
+  }),
+  new webpack.HotModuleReplacementPlugin()
+];
+demoConfig.module.rules.find(a => a.loader === 'url-loader').query = {};
+module.exports = demoConfig;

+ 1 - 1
examples/components/theme-configurator/editor/borderRadius.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/editor/boxShadow.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
       <el-button 

+ 1 - 1
examples/components/theme-configurator/editor/color.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/editor/fontLineHeight.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/editor/fontSize.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/editor/fontWeight.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/editor/simpleText.vue

@@ -1,7 +1,7 @@
 <template>
   <section class="config" :key="displayName">
     <div class="config-label">
-      <el-tooltip :content="displayName">
+      <el-tooltip :content="displayName" placement="top">
         <span>{{displayKeyName}}</span>
       </el-tooltip>
     </div>

+ 1 - 1
examples/components/theme-configurator/index.vue

@@ -102,7 +102,7 @@ export default {
             defaultConfig = res;
           })
           .catch(err => {
-            this.onError(err);
+            this.onError && this.onError(err);
           })
           .then(() => {
             setTimeout(() => {

+ 2 - 2
examples/components/theme-configurator/main.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="main" ref="mainPanel">
+  <div class="editor-main" ref="mainPanel">
     <!-- <span>{{configName}}</span> -->
     <div v-for="(config, key) in configByOrder" :key="key">
       <span 
@@ -22,7 +22,7 @@
 </template>
 
 <style>
-.main {
+.editor-main {
   padding: 0 18px 15px;
   overflow-y: auto;
 }

+ 6 - 1
examples/components/theme/theme-card.vue

@@ -260,7 +260,8 @@ export default {
     base: {
       type: String,
       default: ''
-    }
+    },
+    from: String
   },
   data() {
     return {
@@ -308,6 +309,10 @@ export default {
       switch (e) {
         case 'preview':
         case 'edit':
+          if (this.from) {
+            this.$emit('action', e, this.config);
+            return;
+          }
           const { name, theme } = this.config;
           savePreviewToLocal({
             type: this.type,

+ 1 - 0
examples/extension/.gitignore

@@ -0,0 +1 @@
+dist

+ 18 - 0
examples/extension/src/app.js

@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import App from './editor/index';
+import Element from 'main/index.js';
+import 'packages/theme-chalk/src/index.scss';
+
+export default () => {
+  Vue.use(Element, { zIndex: 100000 });
+  const root = document.createElement('div');
+  document.body.appendChild(root);
+
+  window.ga = function() {
+    console.log(arguments);
+  };
+
+  new Vue({ // eslint-disable-line
+    render: h => h(App)
+  }).$mount(root);
+};

+ 6 - 0
examples/extension/src/background.js

@@ -0,0 +1,6 @@
+chrome.browserAction.onClicked.addListener(tab => {
+  chrome.tabs.executeScript(tab.id, {
+    file: 'entry.js'
+  });
+})
+;

+ 212 - 0
examples/extension/src/editor/editor.vue

@@ -0,0 +1,212 @@
+<template>
+  <div class="ext-panel" :class="{moving : moving}" :style="{top: `${top}px`, left: `${left}px`}" ref="editor" >
+    <img class="entrance touch-icon" src="./icon-entrance.png" v-show="!showSidebar" @click="toggleSidebar" />
+    <img class="close touch-icon" src="./icon-close.png" v-show="showSidebar" @click="toggleSidebar" />
+    <div class="editor" :style="{height: `${height}px`}" v-show="showSidebar">
+      <el-tabs v-model="activeTab" @tab-click="onTabClick">
+        <el-tab-pane label="Config" name="config">
+          <theme-configurator
+            :themeConfig="themeConfig"
+            :previewConfig="previewConfig"
+            :onUserConfigUpdate="onUserConfigUpdate"
+            from="extension"
+          ></theme-configurator>
+        </el-tab-pane>
+        <el-tab-pane label="Gallery" name="gallery">
+          <gallery 
+            ref='gallery'
+            :height="height"
+            :width="width - 7"
+            @action="onGalleryAction"
+          />
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+  </div>
+</template>
+
+<script>
+import ThemeConfigurator from '../../../components/theme-configurator';
+import themeLoader from '../../../components/theme/loader';
+import gallery from './gallery';
+import { loadUserThemeFromLocal, saveUserThemeToLocal } from './utils';
+import bus from '../../../bus.js';
+import {
+  ACTION_APPLY_THEME
+} from '../../../components/theme/constant.js';
+
+let initX;
+let initY;
+let leftX = 0;
+let topY = 25;
+export default {
+  mixins: [themeLoader],
+  components: {
+    ThemeConfigurator,
+    gallery
+  },
+  data() {
+    return {
+      showSidebar: true,
+      previewConfig: {},
+      themeConfig: {},
+      top: topY,
+      left: leftX,
+      height: window.innerHeight - 30 * 2,
+      width: 0,
+      moving: false,
+      activeTab: 'config',
+      themeName: '',
+      userTheme: []
+    };
+  },
+  mounted() {
+    const editor = this.$refs.editor;
+    this.width = editor.offsetWidth;
+    leftX = window.innerWidth - 20 - this.width;
+    this.left = leftX;
+    editor.addEventListener('mousedown', e => {
+      initX = e.clientX;
+      initY = e.clientY;
+      leftX = this.left;
+      topY = this.top;
+      document.addEventListener('mousemove', this.moveFunc);
+    });
+    document.addEventListener('mouseup', e => {
+      document.removeEventListener('mousemove', this.moveFunc);
+      setTimeout(() => {this.moving = false;}, 0);
+    });
+    // chrome.storage.local.remove('ELEMENT_THEME_USER_CONFIG');
+    loadUserThemeFromLocal()
+      .then((result) => {
+        if (result) {
+          this.activeTab = 'gallery';
+          this.userTheme = result;
+        }
+      });
+  },
+  methods: {
+    checkLocalThemeConfig() {}, // disable mixin auto loading
+    toggleSidebar() {
+      if (this.moving) return;
+      this.showSidebar = !this.showSidebar;
+      if (!this.showSidebar) {
+        const windowWidth = window.innerWidth;
+        if (this.left + this.width * 0.5 < windowWidth * 0.5) {
+          this.left = 0;
+        } else {
+          this.left = windowWidth - 50;
+        }
+      } else {
+        this.moveEditor(this.left, this.top);
+      }
+    },
+    moveFunc(e) {
+      e.preventDefault();
+      const deltaX = initX - e.clientX;
+      const deltaY = initY - e.clientY;
+      if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
+        this.moving = true;
+      }
+      this.moveEditor(leftX - deltaX, topY - deltaY);
+    },
+    moveEditor(x, y) {
+      const showSidebar = this.showSidebar;
+      let nextTop = y;
+      if (nextTop < 0) nextTop = 0;
+      const maxTop = window.innerHeight - (showSidebar ? (this.height + 5) : 50);
+      if (nextTop > maxTop) nextTop = maxTop;
+      this.top = nextTop;
+      let nextLeft = x;
+      if (nextLeft < 0) nextLeft = 0;
+      const maxLeft = window.innerWidth - (showSidebar ? (this.width + 5) : 50);
+      if (nextLeft > maxLeft) nextLeft = maxLeft;
+      this.left = nextLeft;
+    },
+    onGalleryAction(key, value) {
+      switch (key) {
+        case 'edit':
+          this.themeName = value.name;
+          this.themeConfig = JSON.parse(value.theme);
+          bus.$emit(ACTION_APPLY_THEME, this.themeConfig);
+          this.activeTab = 'config';
+          break;
+        default:
+          return;
+      }
+    },
+    onTabClick(e) {
+      if (e && e.name === 'gallery') {
+        this.$refs.gallery.init();
+      }
+    },
+    onUserConfigUpdate(userConfig) {
+      if (this.themeName) {
+        this.userTheme.forEach((config) => {
+          if (config.name === this.themeName) {
+            config.update = Date.now();
+            config.theme = JSON.stringify(userConfig);
+          }
+        });
+      } else {
+        this.themeName = `Theme-${Date.now()}`;
+        this.userTheme.push({
+          update: Date.now(),
+          name: this.themeName,
+          theme: JSON.stringify(userConfig)
+        });
+      }
+      saveUserThemeToLocal(this.userTheme);
+    }
+  }
+};
+</script>
+<style scoped>
+.ext-panel {
+  position: fixed;
+  background: transparent;
+  user-select: none;
+  z-index: 100000;
+}
+.ext-panel.moving{
+  cursor: move;
+}
+.ext-panel.moving .touch-icon{
+  cursor: move;
+}
+.ext-panel .touch-icon{
+  cursor: pointer;
+}
+.ext-panel .close {
+  position: absolute;
+  right: 0;
+  top: 0;
+  height: 20px;
+  width: 20px;
+  z-index: 100001;
+}
+.ext-panel .entrance {
+  height: 50px;
+  width: 50px;
+}
+.ext-panel .editor {
+  overflow: hidden;
+  background: #f5f7fa;
+  border: 1px solid #ebeef5;
+  border-radius: 5px;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+  margin: 5px 5px 0 0;
+  min-width: 261px;
+}
+</style>
+<style>
+.ext-panel  .editor .el-tabs__content, .ext-panel  .editor .el-tabs--top, .ext-panel  .editor .el-tab-pane {
+  height: 100%;
+}
+.ext-panel .el-tabs__nav-scroll >div {
+  transform: translateX(60px)!important;
+}
+.ext-panel .editor-main {
+  padding: 0 18px 70px;
+}
+</style>

+ 185 - 0
examples/extension/src/editor/gallery.vue

@@ -0,0 +1,185 @@
+<template>
+  <div :style="{ 
+    height: `${height}px`,
+      width: `${width}px`
+      }"
+    class="main"
+    >
+    <ul
+      class="theme-card-list"
+    >
+      <li class="theme-card" v-for="item in userTheme" :key="item.name">
+        <theme-card type="user" :config="item" @action="onAction" from="extension"></theme-card>
+      </li>
+      <li class="theme-card">
+        <theme-card type="upload" :config="{name: 'upload'}" @action="onAction"></theme-card>
+      </li>
+    </ul>
+    <el-dialog :visible.sync="copyDialogVisible" :modal-append-to-body="false">
+      <el-form :model="copyForm" ref="copyForm" :rules="copyFormRule">
+        <el-form-item label="主题名称" prop="name">
+          <el-input v-model="copyForm.name"></el-input>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="closeCopyForm">取消</el-button>
+        <el-button type="primary" @click="copyToUser">确认</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+ul, li {
+  padding: 0;
+  margin: 0;
+}
+.main {
+  overflow: auto;
+}
+.theme-card-list {
+  padding-bottom: 50px;
+  width: 90%;
+  margin: 0 auto;
+}
+.theme-card {
+  display: inline-block;
+  height: 140px;
+  height: 15vw;
+  width: 100%;
+  max-height: 230px;
+  flex: 0 0 24%;
+  cursor: default;
+  vertical-align: bottom;
+}
+</style>
+<style>
+.theme-card .theme-card-item {
+  margin-top: 0;
+}
+.theme-card .theme-card-item.is-upload {
+  height: 80%
+}
+</style>
+
+<script>
+import ThemeCard from '../../../components/theme/theme-card.vue';
+import { loadUserThemeFromLocal, saveUserThemeToLocal } from './utils';
+
+export default {
+  props: {
+    height: Number,
+    width: Number
+  },
+  data() {
+    return {
+      userTheme: [],
+      copyDialogVisible: false,
+      copyForm: {},
+      copyFormRule: {
+        name: [{
+          validator: this.validateCopyName,
+          trigger: 'blur'
+        }]
+      }
+    };
+  },
+  components: {
+    ThemeCard
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      loadUserThemeFromLocal().then(result => {
+        if (!result) return;
+        this.userTheme = result;
+      });
+    },
+    onAction(key, value) {
+      switch (key) {
+        case 'copy':
+          this.openCopyForm(value.theme);
+          break;
+        case 'upload':
+          this.openCopyForm(value);
+          break;
+        case 'delete':
+          this.$confirm('确定要删除这个主题?', '提示', {
+            confirmButtonText: '确认',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }).then(() => {
+            this.deleteUserThemeByName(value.name);
+          }).catch(() => {});
+          break;
+        case 'rename':
+          this.openRenameForm(value.name);
+          break;
+        default:
+          this.$emit('action', key, value);
+          return;
+      }
+    },
+    openCopyForm(theme) {
+      this.copyForm.theme = theme;
+      this.copyDialogVisible = true;
+    },
+    openRenameForm(name) {
+      this.copyForm.oldname = name;
+      this.copyDialogVisible = true;
+    },
+    closeCopyForm() {
+      this.copyDialogVisible = false;
+      this.$nextTick(() => {
+        this.copyForm = {};
+      });
+    },
+    validateCopyName(rule, value, callback) {
+      if (!value) {
+        callback(new Error('主题名称是必填项'));
+      } else if (this.filterUserThemeByName(value).length > 0) {
+        callback(new Error('主题名称重复'));
+      } else {
+        callback();
+      }
+    },
+    copyToUser() {
+      this.$refs.copyForm.validate((valid) => {
+        if (valid) {
+          const { theme, name, oldname } = this.copyForm;
+          if (theme) {
+            // copy
+            this.userTheme.push({
+              update: Date.now(),
+              name,
+              theme
+            });
+          } else {
+            // rename
+            this.userTheme.forEach((config) => {
+              if (config.name === oldname) {
+                config.update = Date.now();
+                config.name = name;
+              }
+            });
+          }
+          this.saveToLocal();
+          this.closeCopyForm();
+        }
+      });
+    },
+    filterUserThemeByName(name, include = true) {
+      return this.userTheme.filter((theme) => (include ? theme.name === name : theme.name !== name));
+    },
+    saveToLocal() {
+      saveUserThemeToLocal(this.userTheme);
+    },
+    deleteUserThemeByName(name) {
+      this.userTheme = this.filterUserThemeByName(name, false);
+      this.saveToLocal();
+    }
+  }
+};
+</script>

BIN
examples/extension/src/editor/icon-close.png


BIN
examples/extension/src/editor/icon-entrance.png


+ 13 - 0
examples/extension/src/editor/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <editor />
+</template>
+
+<script>
+import editor from './editor';
+
+export default {
+  components: {
+    editor
+  }
+};
+</script>

+ 18 - 0
examples/extension/src/editor/utils.js

@@ -0,0 +1,18 @@
+const ELEMENT_THEME_USER_CONFIG = 'ELEMENT_THEME_USER_CONFIG';
+export const loadFromLocal = (key) => {
+  return new window.Promise((resolve) => {
+    chrome.storage.local.get([key], (result) => {
+      resolve(result[key]);
+    });
+  });
+};
+export const saveToLocal = (key, value) => {
+  chrome.storage.local.set({[key]: value});
+};
+
+export const loadUserThemeFromLocal = () => {
+  return loadFromLocal(ELEMENT_THEME_USER_CONFIG);
+};
+export const saveUserThemeToLocal = (value) => {
+  saveToLocal(ELEMENT_THEME_USER_CONFIG, value);
+};

+ 7 - 0
examples/extension/src/entry.js

@@ -0,0 +1,7 @@
+import init from './app';
+
+if (!window.ElementThemeRollerInit) {
+  window.ElementThemeRollerInit = true;
+  init();
+}
+

BIN
examples/extension/src/icon.png


+ 27 - 0
examples/extension/src/manifest.json

@@ -0,0 +1,27 @@
+{
+  "name": "Element Theme Roller",
+  "version": "0.0.9",
+  "description": "Customize all Design Tokens of Element UI and preview in real-time",
+  "web_accessible_resources": ["entry.js"],
+  "background": {
+    "scripts": [
+      "background.js"
+    ],
+    "persistent": true
+  },
+  "icons": {
+    "128": "icon.png"
+  },
+  "browser_action": {
+    "default_icon": "icon.png",
+    "default_title": "Element Theme Roller"
+  },
+  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
+  "manifest_version": 2,
+  "permissions": [
+    "activeTab",
+    "storage",
+    "https://*.ele.me/",
+    "https://*.elenet.me/"
+  ]
+}

+ 2 - 0
package.json

@@ -18,6 +18,8 @@
     "build:umd": "node build/bin/build-locale.js",
     "clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
     "deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",
+    "deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js",
+    "dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js",
     "dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
     "dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js",
     "dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",