├── static └── .gitkeep ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── babel.config.js ├── src ├── assets │ ├── logo.png │ ├── xlsx.js │ └── value.js ├── router │ ├── index.js │ ├── caseDetail.vue │ └── home.vue ├── store │ └── index.js ├── components │ ├── edit-div.vue │ ├── end-diaolog.vue │ ├── input-select.vue │ ├── state-fields.vue │ ├── event-item.vue │ └── data-field.vue ├── main.js ├── Main.vue └── mixins │ └── data-field-edit.js ├── autoScript ├── img │ └── icon.png ├── devtools.html ├── background.html ├── create-panels.js ├── manifest.json ├── js │ ├── highlight.js │ └── bind-unbind.js └── background.js ├── .editorconfig ├── .gitignore ├── .babelrc ├── .postcssrc.js ├── index.html ├── README.md ├── gulpfile.js ├── package.json └── test.html /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/chrome-extensions-auto-script/master/src/assets/logo.png -------------------------------------------------------------------------------- /autoScript/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjj5855/chrome-extensions-auto-script/master/autoScript/img/icon.png -------------------------------------------------------------------------------- /autoScript/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /autoScript/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, World! 5 | 6 | 7 |

Hello, World!

8 | 9 | 10 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /autoScript/devtool 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | 16 | *.zip 17 | -------------------------------------------------------------------------------- /autoScript/create-panels.js: -------------------------------------------------------------------------------- 1 | // 创建自定义面板,同一个插件可以创建多个自定义面板 2 | // 几个参数依次为:panel标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调 3 | chrome.devtools.panels.create('autoScript', 'img/icon.png', 'devtool/index.html', function(panel) { 4 | console.log('create devtool panel!'); // 注意这个log一般看不到 5 | }); 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | chrome-dispatch 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import home from './home' 4 | import caseDetail from './caseDetail' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'home', 13 | component: home, 14 | meta: {title: '首页'} 15 | }, 16 | { 17 | path: '/case/:index', 18 | name: 'caseDetail', 19 | component: caseDetail, 20 | meta: {title: '脚本详情'} 21 | }, 22 | ] 23 | }) 24 | -------------------------------------------------------------------------------- /autoScript/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto script", 3 | "version": "1.4", 4 | "manifest_version": 3, 5 | "action": {}, 6 | "permissions": [ 7 | "storage", 8 | "tabs" 9 | ], 10 | "host_permissions": [ "http://*/", "https://*/" ], 11 | "background": { 12 | "service_worker": "background.js" 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [""], 17 | "js": [ 18 | "js/jquery-1.8.3.js", 19 | "js/bind-unbind.js", 20 | "js/highlight.js" 21 | ] 22 | } 23 | ], 24 | "devtools_page": "devtools.html", 25 | "externally_connectable": { 26 | "matches": [ 27 | "http://localhost:63342/*", 28 | "http://localhost:8089/*", 29 | "https://isp.ibanbu.com/*" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-dispatch 2 | 3 | > 只编译devtool页面, 其他都是拷贝过去 4 | 5 | > bash 进入linux命令行 6 | 7 | > 监听文件并打包命令 gulp 8 | 9 | ## Build Setup 10 | 11 | ``` bash 12 | # install dependencies 13 | npm install 14 | 15 | # serve with hot reload at localhost:8080 16 | npm run dev 17 | 18 | # build for production with minification 19 | npm run build 20 | 21 | # build for production and view the bundle analyzer report 22 | npm run build --report 23 | ``` 24 | ## 配置说明 25 | `externally_connectable` 可以直接在网页中使用`chrome.runtime` api发送消息的网页规则 26 | 27 | 28 | ## 实现的功能 29 | 1. 全屏点击事件 √ 30 | 1. \,\输入事件 √ 31 | 1. 滚动事件 √ 32 | 1. \,\自定义key √ 33 | 1. \选择事件 √ 34 | 1. 一层\中的上述事件 √ 35 | 1. 脚本和事件可编辑 √ 36 | 1. 脚本和事件的可视化 √ 37 | 1. 导入导出 √ 38 | 1. 批量导入导出 √ 39 | 1. tab传参调用另一tab中的脚本 √ 40 | 41 | 42 | ## 还未实现的功能 43 | 1. 导出测试结果 × 44 | 45 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | Vue.use(Vuex) 4 | const store = new Vuex.Store({ 5 | state: { 6 | backgroundPageConnection: null, 7 | current: 0, 8 | caseList: [], 9 | disconnect: true 10 | }, 11 | actions: {}, 12 | mutations: { 13 | connect (state) { 14 | state.backgroundPageConnection = chrome.runtime.connect({ 15 | name: "panel" 16 | }) 17 | }, 18 | setCaseList (state, list) { 19 | state.caseList = list 20 | }, 21 | deleteCase (state, index){ 22 | state.caseList.splice(index, 1) 23 | }, 24 | setDisConnect (state, bool) { 25 | state.disconnect = bool 26 | }, 27 | setCurrent (state, index) { 28 | state.current = index 29 | } 30 | }, 31 | getters: { 32 | backgroundPageConnection: state => state.backgroundPageConnection, 33 | current: state => state.current, 34 | caseList: state => state.caseList, 35 | disconnect: state => state.disconnect 36 | } 37 | }) 38 | 39 | export default store 40 | -------------------------------------------------------------------------------- /src/components/edit-div.vue: -------------------------------------------------------------------------------- 1 | 10 | 43 | 49 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./build/check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('./config') 12 | const webpackConfig = require('./build/webpack.prod.conf') 13 | const gulp = require('gulp'); 14 | 15 | const spinner = ora('building for production...') 16 | spinner.start() 17 | 18 | 19 | function build (callback) { 20 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 21 | if (err) throw err 22 | webpack(webpackConfig, (err, stats) => { 23 | spinner.stop() 24 | if (err) throw err 25 | callback() 26 | }) 27 | }) 28 | } 29 | 30 | function clean(cb) { 31 | // body omitted 32 | cb(); 33 | } 34 | 35 | // gulp.task('watch', function () { 36 | // return gulp.watch('src/**', gulp.series(clean, build)) 37 | // }) 38 | 39 | gulp.task('default', function () { 40 | gulp.watch('src/**', gulp.series(clean, build)) 41 | }) 42 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | import { sync } from 'vuex-router-sync' 4 | import router from './router' 5 | import store from './store' 6 | import App from './Main.vue' 7 | import 'element-ui/lib/theme-chalk/index.css' 8 | 9 | Vue.config.productionTip = false 10 | sync(store, router) 11 | Vue.use(ElementUI, { size: 'small', zIndex: 3000 }) 12 | 13 | Vue.prototype.$EventBus = new Vue() 14 | 15 | Vue.prototype.$getHost = getHost 16 | Vue.prototype.$getUrlPath = getUrlPath 17 | Vue.prototype.$getOnlyUrl = getOnlyUrl 18 | function getHost (url) { 19 | // 必须是http开头或者https开头,结尾为'/' 20 | let reg = /^http(s)?:\/\/(.*?)\// 21 | let host = reg.exec(url)[2] 22 | return host 23 | } 24 | function getUrlPath (url) { 25 | let index = url.indexOf('?') 26 | if (index >= 0) { 27 | url = url.substr(0, index) 28 | } 29 | return url 30 | } 31 | function getOnlyUrl (url) { 32 | let urlPath = getUrlPath(url) 33 | let host = getHost(url) 34 | return urlPath.replace(new RegExp('^http(s)?:\/\/' + host), '') 35 | } 36 | 37 | /* eslint-disable no-new */ 38 | let app = new Vue({ 39 | el: '#app', 40 | router, 41 | store, 42 | render: h => h(App) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/end-diaolog.vue: -------------------------------------------------------------------------------- 1 | 14 | 17 | 64 | -------------------------------------------------------------------------------- /src/components/input-select.vue: -------------------------------------------------------------------------------- 1 | 12 | 15 | 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-dispatch", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "element-ui": "^2.15.2", 14 | "vue": "^2.5.2", 15 | "vue-router": "^3.5.1", 16 | "vuex": "^3.6.2", 17 | "vuex-router-sync": "^5.0.0", 18 | "xlsx": "^0.17.0" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^7.1.2", 22 | "babel-core": "^6.22.1", 23 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 24 | "babel-loader": "^7.1.1", 25 | "babel-plugin-syntax-jsx": "^6.18.0", 26 | "babel-plugin-transform-runtime": "^6.22.0", 27 | "babel-plugin-transform-vue-jsx": "^3.5.0", 28 | "babel-preset-env": "^1.3.2", 29 | "babel-preset-stage-2": "^6.22.0", 30 | "chalk": "^2.0.1", 31 | "copy-webpack-plugin": "^4.0.1", 32 | "css-loader": "^0.28.0", 33 | "extract-text-webpack-plugin": "^3.0.0", 34 | "file-loader": "^1.1.4", 35 | "friendly-errors-webpack-plugin": "^1.6.1", 36 | "gulp": "^4.0.2", 37 | "html-webpack-plugin": "^2.30.1", 38 | "node-notifier": "^5.1.2", 39 | "optimize-css-assets-webpack-plugin": "^3.2.0", 40 | "ora": "^1.2.0", 41 | "portfinder": "^1.0.13", 42 | "postcss-import": "^11.0.0", 43 | "postcss-loader": "^2.0.8", 44 | "postcss-url": "^7.2.1", 45 | "rimraf": "^2.6.0", 46 | "semver": "^5.3.0", 47 | "shelljs": "^0.7.6", 48 | "uglifyjs-webpack-plugin": "^1.1.1", 49 | "url-loader": "^0.5.8", 50 | "vue-loader": "^13.3.0", 51 | "vue-style-loader": "^3.0.1", 52 | "vue-template-compiler": "^2.5.2", 53 | "webpack": "^3.6.0", 54 | "webpack-bundle-analyzer": "^2.9.0", 55 | "webpack-dev-server": "^2.9.1", 56 | "webpack-merge": "^4.1.0" 57 | }, 58 | "engines": { 59 | "node": ">= 6.0.0", 60 | "npm": ">= 3.0.0" 61 | }, 62 | "browserslist": [ 63 | "> 1%", 64 | "last 2 versions", 65 | "not ie <= 8" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 9080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | 24 | /** 25 | * Source Maps 26 | */ 27 | 28 | // https://webpack.js.org/configuration/devtool/#development 29 | devtool: 'cheap-module-eval-source-map', 30 | 31 | // If you have problems debugging vue-files in devtools, 32 | // set this to false - it *may* help 33 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 34 | cacheBusting: true, 35 | 36 | cssSourceMap: true 37 | }, 38 | 39 | build: { 40 | // Template for index.html 41 | index: path.resolve(__dirname, '../autoScript/devtool/index.html'), 42 | 43 | // Paths 44 | assetsRoot: path.resolve(__dirname, '../autoScript'), 45 | assetsSubDirectory: 'devtool/static', 46 | assetsPublicPath: '/', 47 | 48 | /** 49 | * Source Maps 50 | */ 51 | 52 | productionSourceMap: true, 53 | // https://webpack.js.org/configuration/devtool/#production 54 | devtool: '#source-map', 55 | 56 | // Gzip off by default as many popular static hosts such as 57 | // Surge or Netlify already gzip all static assets for you. 58 | // Before setting to `true`, make sure to: 59 | // npm install --save-dev compression-webpack-plugin 60 | productionGzip: false, 61 | productionGzipExtensions: ['js', 'css'], 62 | 63 | // Run the build command with an extra argument to 64 | // View the bundle analyzer report after build finishes: 65 | // `npm run build --report` 66 | // Set to `true` or `false` to always turn it on or off 67 | bundleAnalyzerReport: process.env.npm_config_report 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/xlsx.js: -------------------------------------------------------------------------------- 1 | import XLSX from 'xlsx' 2 | // 将workbook装化成blob对象 3 | export function workbook2blob(workbook) { 4 | // 生成excel的配置项 5 | var wopts = { 6 | // 要生成的文件类型 7 | bookType: 'xlsx', 8 | // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性 9 | bookSST: false, 10 | // 二进制类型 11 | type: 'binary' 12 | } 13 | var wbout = XLSX.write(workbook, wopts) 14 | var blob = new Blob([s2ab(wbout)], { 15 | type: 'application/octet-stream' 16 | }) 17 | return blob 18 | } 19 | 20 | // 将字符串转ArrayBuffer 21 | function s2ab(s) { 22 | var buf = new ArrayBuffer(s.length) 23 | var view = new Uint8Array(buf) 24 | for (var i = 0; i != s.length; ++i) { 25 | view[i] = s.charCodeAt(i) & 0xff 26 | } 27 | return buf 28 | } 29 | 30 | // 将blob对象创建bloburl,然后用a标签实现弹出下载框 31 | export function openDownloadDialog(blob, fileName) { 32 | if (typeof blob == 'object' && blob instanceof Blob) { 33 | blob = URL.createObjectURL(blob) // 创建blob地址 34 | } 35 | var aLink = document.createElement('a') 36 | aLink.href = blob 37 | // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,有时候 file:///模式下不会生效 38 | aLink.download = fileName || '' 39 | var event 40 | if (window.MouseEvent) event = new MouseEvent('click') 41 | // 移动端 42 | else { 43 | event = document.createEvent('MouseEvents') 44 | event.initMouseEvent( 45 | 'click', 46 | true, 47 | false, 48 | window, 49 | 0, 50 | 0, 51 | 0, 52 | 0, 53 | 0, 54 | false, 55 | false, 56 | false, 57 | false, 58 | 0, 59 | null 60 | ) 61 | } 62 | aLink.dispatchEvent(event) 63 | URL.revokeObjectURL(blob) 64 | } 65 | 66 | // 读取本地excel文件 67 | export function readWorkbookFromLocalFile(file, callback) { 68 | var reader = new FileReader(); 69 | reader.onload = function(e) { 70 | var data = e.target.result; 71 | // 读取二进制的excel 72 | var workbook = XLSX.read(data, {type: 'binary'}); 73 | if(callback) callback(workbook); 74 | }; 75 | reader.readAsBinaryString(file); 76 | } 77 | 78 | // 获取excel第一行的内容 79 | export function getHeaderKeyList (sheet) { 80 | var wbData = sheet; // 读取的excel单元格内容 81 | var re = /^[A-Z]*1$/; // 匹配excel第一行的内容 82 | var arr1 = []; 83 | for (var key in wbData) { // excel第一行内容赋值给数组 84 | if (wbData.hasOwnProperty(key)) { 85 | if (re.test(key)) { 86 | arr1.push(wbData[key].h); 87 | } 88 | } 89 | } 90 | return arr1; 91 | } 92 | -------------------------------------------------------------------------------- /src/components/state-fields.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 98 | 99 | 102 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 本地测试页面 6 | 7 | 8 |
9 | 10 |
11 |
12 |

{{item.tab.title}} tabId: {{item.tab.id}}

13 |
14 | {{i + 1}}. {{caseDetail.name}} 15 |
16 |
17 |
18 | 19 |
20 | tabId 脚本名称 21 |
22 |
23 |
自定义字段
24 |
25 | {{key}} 26 |
27 |
28 | 29 | 30 | 31 |
32 | 执行结果

 33 |   
34 |
35 | 36 | 37 | 38 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/assets/value.js: -------------------------------------------------------------------------------- 1 | const UNDEFINED = '__vue_devtool_undefined__' 2 | const INFINITY = '__vue_devtool_infinity__' 3 | const NEGATIVE_INFINITY = '__vue_devtool_negative_infinity__' 4 | const NAN = '__vue_devtool_nan__' 5 | 6 | const rawTypeRE = /^\[object (\w+)]$/ 7 | const specialTypeRE = /^\[native (\w+) (.*?)(<>((.|\s)*))?\]$/ 8 | 9 | export function isPlainObject (obj) { 10 | return Object.prototype.toString.call(obj) === '[object Object]' 11 | } 12 | 13 | export function specialTokenToString (value) { 14 | if (value === null) { 15 | return 'null' 16 | } else if (value === UNDEFINED) { 17 | return 'undefined' 18 | } else if (value === NAN) { 19 | return 'NaN' 20 | } else if (value === INFINITY) { 21 | return 'Infinity' 22 | } else if (value === NEGATIVE_INFINITY) { 23 | return '-Infinity' 24 | } 25 | return false 26 | } 27 | 28 | 29 | export function valueType (value) { 30 | const type = typeof value 31 | if (value == null || value === UNDEFINED) { 32 | return 'null' 33 | } else if ( 34 | type === 'boolean' || 35 | type === 'number' || 36 | value === INFINITY || 37 | value === NEGATIVE_INFINITY || 38 | value === NAN 39 | ) { 40 | return 'literal' 41 | } else if (value && value._custom) { 42 | return 'custom' 43 | } else if (type === 'string') { 44 | const typeMatch = specialTypeRE.exec(value) 45 | if (typeMatch) { 46 | const [, type] = typeMatch 47 | return `native ${type}` 48 | } else { 49 | return 'string' 50 | } 51 | } else if (Array.isArray(value) || (value && value._isArray)) { 52 | return 'array' 53 | } else if (isPlainObject(value)) { 54 | return 'plain-object' 55 | } else { 56 | return 'unknown' 57 | } 58 | } 59 | const ESC = { 60 | '<': '<', 61 | '>': '>', 62 | '"': '"', 63 | '&': '&' 64 | } 65 | function escapeChar (a) { 66 | return ESC[a] || a 67 | } 68 | function escape (s) { 69 | return s.replace(/[<>"&]/g, escapeChar) 70 | } 71 | 72 | export function formattedValue (value, quotes = true) { 73 | let result 74 | const type = valueType(value) 75 | if ((result = specialTokenToString(value))) { 76 | return result 77 | } else if (type === 'custom') { 78 | return value._custom.display 79 | } else if (type === 'array') { 80 | return 'Array[' + value.length + ']' 81 | } else if (type === 'plain-object') { 82 | return 'Object' + (Object.keys(value).length ? '' : ' (empty)') 83 | } else if (type.includes('native')) { 84 | return escape(specialTypeRE.exec(value)[2]) 85 | } else if (typeof value === 'string') { 86 | const typeMatch = value.match(rawTypeRE) 87 | if (typeMatch) { 88 | value = escape(typeMatch[1]) 89 | } else if (quotes) { 90 | value = `"${escape(value)}"` 91 | } else { 92 | value = escape(value) 93 | } 94 | value = value.replace(/ /g, ' ') 95 | .replace(/\n/g, '\\n') 96 | } 97 | return value 98 | } 99 | 100 | export function sortByKey (state) { 101 | return state && state.slice().sort((a, b) => { 102 | if (a.key < b.key) return -1 103 | if (a.key > b.key) return 1 104 | return 0 105 | }) 106 | } 107 | 108 | export function set (object, path, value, cb = null) { 109 | const sections = Array.isArray(path) ? path : path.split('.') 110 | while (sections.length > 1) { 111 | object = object[sections.shift()] 112 | } 113 | const field = sections[0] 114 | if (cb) { 115 | cb(object, field, value) 116 | } else { 117 | object[field] = value 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/event-item.vue: -------------------------------------------------------------------------------- 1 | 21 | 32 | 125 | -------------------------------------------------------------------------------- /src/Main.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 134 | 135 | 146 | -------------------------------------------------------------------------------- /autoScript/js/highlight.js: -------------------------------------------------------------------------------- 1 | 2 | let overlay 3 | let overlayContent 4 | let currentInstance 5 | let contentWindow = window 6 | let contentDocument = document 7 | 8 | function createOverlay() { 9 | if (overlay) return 10 | overlay = contentDocument.createElement('div') 11 | overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)' 12 | overlay.style.position = 'fixed' 13 | overlay.style.zIndex = '99999999999998' 14 | overlay.style.pointerEvents = 'none' 15 | overlay.style.borderRadius = '3px' 16 | overlayContent = contentDocument.createElement('div') 17 | overlayContent.style.position = 'fixed' 18 | overlayContent.style.zIndex = '99999999999999' 19 | overlayContent.style.pointerEvents = 'none' 20 | overlayContent.style.backgroundColor = 'white' 21 | overlayContent.style.fontFamily = 'monospace' 22 | overlayContent.style.fontSize = '11px' 23 | overlayContent.style.padding = '4px 8px' 24 | overlayContent.style.borderRadius = '3px' 25 | overlayContent.style.color = '#333' 26 | overlayContent.style.textAlign = 'center' 27 | overlayContent.style.border = 'rgba(65, 184, 131, 0.5) 1px solid' 28 | overlayContent.style.backgroundClip = 'padding-box' 29 | } 30 | 31 | function showOverlay (bounds, children) { 32 | if (!children.length) return 33 | 34 | positionOverlay(bounds) 35 | contentDocument.body.appendChild(overlay) 36 | 37 | overlayContent.innerHTML = '' 38 | children.forEach(child => overlayContent.appendChild(child)) 39 | contentDocument.body.appendChild(overlayContent) 40 | 41 | positionOverlayContent(bounds) 42 | } 43 | 44 | function getComponentBounds (el = null) { 45 | if (!el) { 46 | return { 47 | width: contentWindow.innerWidth, 48 | height: contentWindow.innerHeight, 49 | top: 0, 50 | left: 0 51 | } 52 | } 53 | let bounds = el.getBoundingClientRect() 54 | return bounds 55 | } 56 | function positionOverlay ({ width = 0, height = 0, top = 0, left = 0 }) { 57 | overlay.style.width = Math.round(width) + 'px' 58 | overlay.style.height = Math.round(height) + 'px' 59 | overlay.style.left = Math.round(left) + 'px' 60 | overlay.style.top = Math.round(top) + 'px' 61 | } 62 | 63 | function positionOverlayContent ({ wifth = 0, height = 0, top = 0, left = 0 }) { 64 | // Content position (prevents overflow) 65 | const contentWidth = overlayContent.offsetWidth 66 | const contentHeight = overlayContent.offsetHeight 67 | let contentLeft = left 68 | if (contentLeft < 0) { 69 | contentLeft = 0 70 | } else if (contentLeft + contentWidth > contentWindow.innerWidth) { 71 | contentLeft = contentWindow.innerWidth - contentWidth 72 | } 73 | let contentTop = top - contentHeight - 2 74 | if (contentTop < 0) { 75 | contentTop = top + height + 2 76 | } 77 | if (contentTop < 0) { 78 | contentTop = 0 79 | } else if (contentTop + contentHeight > contentWindow.innerHeight) { 80 | contentTop = contentWindow.innerHeight - contentHeight 81 | } 82 | overlayContent.style.left = ~~contentLeft + 'px' 83 | overlayContent.style.top = ~~contentTop + 'px' 84 | } 85 | 86 | function highlight (data) { 87 | // 判断是不是iframe中的元素 88 | let el = document.elementFromPoint(data.x, data.y) 89 | if (el.tagName === 'IFRAME') { 90 | contentWindow = el.contentWindow 91 | contentDocument = contentWindow.document 92 | el = contentDocument.elementFromPoint(data.clientX, data.clientY) 93 | } else { 94 | contentWindow = window 95 | contentDocument = document 96 | } 97 | 98 | let bounds = getComponentBounds(el) 99 | createOverlay() 100 | // Name 101 | const name = data.content || 'Anonymous' 102 | const pre = contentDocument.createElement('span') 103 | pre.style.opacity = '0.6' 104 | pre.innerText = '<' 105 | const text = contentDocument.createElement('span') 106 | text.style.fontWeight = 'bold' 107 | text.style.color = '#09ab56' 108 | text.innerText = name 109 | const post = contentDocument.createElement('span') 110 | post.style.opacity = '0.6' 111 | post.innerText = '>' 112 | 113 | // Size 114 | const size = contentDocument.createElement('span') 115 | size.style.opacity = '0.5' 116 | size.style.marginLeft = '6px' 117 | size.appendChild(contentDocument.createTextNode((Math.round(bounds.width * 100) / 100).toString())) 118 | const multiply = contentDocument.createElement('span') 119 | multiply.style.marginLeft = multiply.style.marginRight = '2px' 120 | multiply.innerText = '×' 121 | size.appendChild(multiply) 122 | size.appendChild(contentDocument.createTextNode((Math.round(bounds.height * 100) / 100).toString())) 123 | 124 | currentInstance = el 125 | 126 | showOverlay(bounds, [pre, text, post, size]) 127 | 128 | startUpdateTimer() 129 | } 130 | 131 | function unHighlight () { 132 | overlay?.parentNode?.removeChild(overlay) 133 | overlayContent?.parentNode?.removeChild(overlayContent) 134 | currentInstance = null 135 | 136 | stopUpdateTimer() 137 | } 138 | 139 | function updateOverlay () { 140 | if (currentInstance) { 141 | const bounds = getComponentBounds(currentInstance) 142 | if (bounds) { 143 | const sizeEl = overlayContent.children.item(3) 144 | const widthEl = sizeEl.childNodes[0] 145 | widthEl.textContent = (Math.round(bounds.width * 100) / 100).toString() 146 | const heightEl = sizeEl.childNodes[2] 147 | heightEl.textContent = (Math.round(bounds.height * 100) / 100).toString() 148 | 149 | positionOverlay(bounds) 150 | positionOverlayContent(bounds) 151 | } 152 | } 153 | } 154 | 155 | let updateTimer 156 | 157 | function startUpdateTimer () { 158 | stopUpdateTimer() 159 | updateTimer = setInterval(() => { 160 | updateOverlay() 161 | }, 1000 / 30) // 30fps 162 | } 163 | 164 | function stopUpdateTimer () { 165 | clearInterval(updateTimer) 166 | } 167 | 168 | chrome.runtime.onMessage.addListener( 169 | function(request, sender, sendResponse) { 170 | // console.log('highlight 收到消息', request) 171 | switch (request.function) { 172 | case "highlight": 173 | highlight(request.highlightData) 174 | break 175 | case "unHighlight": 176 | unHighlight() 177 | break 178 | } 179 | } 180 | ); 181 | console.log('highlight.js 已运行') 182 | -------------------------------------------------------------------------------- /autoScript/background.js: -------------------------------------------------------------------------------- 1 | 2 | // 总连接池 3 | var connections = {} 4 | var externalConnections = {} 5 | var runningTabId = null 6 | 7 | chrome.runtime.onInstalled.addListener((detail) => { 8 | console.log('bg onInstalled', detail) 9 | }) 10 | 11 | chrome.runtime.onConnect.addListener(function (port) { 12 | console.log('onConnect.addListener') 13 | var extensionListener = function (message, sender, sendResponse) { 14 | // The original connection event doesn't include the tab ID of the 15 | // DevTools page, so we need to send it explicitly. 16 | if (message.name == "init") { 17 | console.log('init', message) 18 | connections[message.tabId] = port; 19 | connections[message.tabId].postMessage({ 20 | type: 'connected' 21 | }) 22 | return; 23 | } 24 | console.log('other message', message) 25 | // other message handling 26 | switch (message.type) { 27 | // case 'executeScript': 28 | // chrome.scripting.executeScript({ 29 | // target: {tabId: message.tabId, allFrames: true}, 30 | // files: [message.scriptToInject] 31 | // }); 32 | // break 33 | case 'init-caseList': 34 | // 从 storage 中获取当前host的case发送给tab 35 | chrome.tabs.get(message.tabId, function (tab) { 36 | let host = getHost(tab.url) 37 | if (host) { 38 | chrome.storage.sync.get([host], function (result) { 39 | connections[message.tabId].postMessage({ 40 | type: 'init-caseList', 41 | data: result[host] || [] 42 | }) 43 | }) 44 | } 45 | }) 46 | break 47 | case 'change-window': 48 | focusTab(message.tabId, message.width, message.height) 49 | break 50 | case 'bind': 51 | focusTab(message.tabId, message.width, message.height) 52 | chrome.tabs.sendMessage(message.tabId, {function: 'bind'}); 53 | break 54 | case 'unbind': 55 | chrome.tabs.sendMessage(message.tabId, {function: 'unbind'}); 56 | break 57 | case 'save-case': 58 | chrome.tabs.get(message.tabId, function (tab) { 59 | let host = getHost(tab.url) 60 | if (host) { 61 | chrome.storage.sync.set({ 62 | [host]: message.data 63 | }) 64 | } 65 | }) 66 | break 67 | case 'run-one-case': 68 | runningTabId = message.tabId 69 | focusTab(runningTabId) 70 | chrome.tabs.sendMessage(message.tabId, { 71 | function: 'runOneCase', 72 | case: message.case, 73 | index: message.index 74 | }); 75 | break 76 | case 'dom-highlight': 77 | chrome.tabs.sendMessage(message.tabId, { 78 | function: 'highlight', 79 | highlightData: message.highlightData 80 | }); 81 | break 82 | case 'dom-unhighlight': 83 | chrome.tabs.sendMessage(message.tabId, { 84 | function: 'unHighlight' 85 | }); 86 | break 87 | case 'remove-tab': 88 | chrome.tabs.remove(message.tabId) 89 | break 90 | case 'get-caseList': 91 | if (message.notifyTabId && externalConnections[message.notifyTabId]) { 92 | externalConnections[message.notifyTabId].postMessage(message) 93 | } 94 | break 95 | } 96 | } 97 | 98 | // Listen to messages sent from the DevTools page 99 | port.onMessage.addListener(extensionListener); 100 | 101 | port.onDisconnect.addListener(function(port) { 102 | port.onMessage.removeListener(extensionListener); 103 | 104 | var tabs = Object.keys(connections); 105 | for (var i=0, len=tabs.length; i < len; i++) { 106 | if (connections[tabs[i]] == port) { 107 | delete connections[tabs[i]] 108 | break; 109 | } 110 | } 111 | }); 112 | }); 113 | 114 | // 监听测试脚本的测试结果是打开新页面的情况 115 | chrome.tabs.onActivated.addListener(function (activeInfo) { 116 | if (runningTabId && connections[runningTabId]) { 117 | chrome.tabs.get(activeInfo.tabId, function (tab) { 118 | connections[runningTabId].postMessage({ 119 | type: 'tab-activated', 120 | tab: tab 121 | }) 122 | }) 123 | } 124 | }) 125 | 126 | // Receive message from content script and relay to the devTools page for the 127 | // current tab 128 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 129 | // Messages from content scripts should have sender.tab set 130 | if (request.type === 'change-url') { 131 | let tabId = request.tabId; 132 | chrome.tabs.sendMessage(request.tabId, { 133 | function: 'changeUrl', 134 | urlPath: request.urlPath 135 | }, 136 | function (data) { 137 | sendResponse(data) 138 | }) 139 | } else if (sender.tab) { 140 | let tabId = sender.tab.id; 141 | if (tabId in connections) { 142 | connections[tabId].postMessage(request); 143 | } else { 144 | console.log("Tab not found in connection list."); 145 | } 146 | } else { 147 | console.log("sender.tab not defined."); 148 | } 149 | return true; 150 | }); 151 | 152 | 153 | // 监听externally_connectable配置的正常网页发送的消息 154 | chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) { 155 | if (request.tabId in connections) { 156 | // 不使用长连接发送消息 原因是不能设置回调 157 | // connections[request.tabId].postMessage(request.data) 158 | 159 | chrome.runtime.sendMessage(request, response => { 160 | sendResponse(response) 161 | }) 162 | return true // 返回 true 表示是异步回调 163 | } else { 164 | sendResponse({error: 'onMessageExternal Tab not found in connection list.'}) 165 | } 166 | }) 167 | 168 | // 监听externally_connectable配置的正常网页发送的长连接 169 | chrome.runtime.onConnectExternal.addListener(function(port) { 170 | let tabId = port.sender.tab.id 171 | if (tabId) { 172 | externalConnections[tabId] = port 173 | } 174 | let extensionListener = function (msg, sender) { 175 | switch (msg.type) { 176 | case 'init': 177 | port.postMessage({type: 'init', tabId: sender.sender.tab.id}) 178 | break 179 | // 给每个连接都发送消息, devtool发送到bg.js 接收一个发送一个 180 | case 'get-connected-caseList': 181 | for (let id in connections) { 182 | connections[id].postMessage({ 183 | type: 'get-caseList', 184 | notifyTabId: msg.notifyTabId 185 | }) 186 | } 187 | break 188 | } 189 | } 190 | 191 | // Listen to messages sent from the externally_connectable page 192 | port.onMessage.addListener(extensionListener); 193 | 194 | port.onDisconnect.addListener(function(port) { 195 | port.onMessage.removeListener(extensionListener); 196 | 197 | var tabs = Object.keys(externalConnections); 198 | for (var i=0, len=tabs.length; i < len; i++) { 199 | if (externalConnections[tabs[i]] == port) { 200 | delete externalConnections[tabs[i]] 201 | break; 202 | } 203 | } 204 | }); 205 | }) 206 | 207 | function getHost (url) { 208 | // 必须是http开头或者https开头,结尾为'/' 209 | let reg = /^http(s)?:\/\/(.*?)\// 210 | let host = reg.exec(url)[2] 211 | return host 212 | } 213 | 214 | /** 215 | * 高亮聚焦需要录制的tab 216 | * @param tabId 217 | * @param width 218 | * @param height 219 | */ 220 | function focusTab (tabId, width, height) { 221 | chrome.tabs.get(tabId, function (tab) { 222 | chrome.tabs.highlight({windowId: tab.windowId, tabs: tab.index}) 223 | let config = { 224 | state: 'normal', // 最大化时改变不了大小 225 | focused: true 226 | } 227 | if (width && height) { 228 | config.width = width 229 | config.height = height 230 | } 231 | chrome.windows.update(tab.windowId, config) 232 | }) 233 | } 234 | -------------------------------------------------------------------------------- /src/router/caseDetail.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 269 | 270 | 272 | -------------------------------------------------------------------------------- /src/mixins/data-field-edit.js: -------------------------------------------------------------------------------- 1 | 2 | const UNDEFINED = '__vue_devtool_undefined__' 3 | const INFINITY = '__vue_devtool_infinity__' 4 | const NEGATIVE_INFINITY = '__vue_devtool_negative_infinity__' 5 | const NAN = '__vue_devtool_nan__' 6 | 7 | const SPECIAL_TOKENS = { 8 | true: true, 9 | false: false, 10 | undefined: UNDEFINED, 11 | null: null, 12 | '-Infinity': NEGATIVE_INFINITY, 13 | Infinity: INFINITY, 14 | NaN: NAN 15 | } 16 | function decode (list, reviver) { 17 | let i = list.length 18 | let j, k, data, key, value, proto 19 | while (i--) { 20 | data = list[i] 21 | proto = Object.prototype.toString.call(data) 22 | if (proto === '[object Object]') { 23 | const keys = Object.keys(data) 24 | for (j = 0, k = keys.length; j < k; j++) { 25 | key = keys[j] 26 | value = list[data[key]] 27 | if (reviver) value = reviver.call(data, key, value) 28 | data[key] = value 29 | } 30 | } else if (proto === '[object Array]') { 31 | for (j = 0, k = data.length; j < k; j++) { 32 | value = list[data[j]] 33 | if (reviver) value = reviver.call(data, j, value) 34 | data[j] = value 35 | } 36 | } 37 | } 38 | } 39 | function parseCircularAutoChunks (data, reviver = null) { 40 | if (Array.isArray(data)) { 41 | data = data.join('') 42 | } 43 | const hasCircular = /^\s/.test(data) 44 | if (!hasCircular) { 45 | return arguments.length === 1 46 | ? JSON.parse(data) 47 | : JSON.parse(data, reviver) 48 | } else { 49 | const list = JSON.parse(data) 50 | decode(list, reviver) 51 | return list[0] 52 | } 53 | } 54 | function parse (data, revive = false) { 55 | return revive 56 | ? parseCircularAutoChunks(data, reviver) 57 | : parseCircularAutoChunks(data) 58 | } 59 | 60 | function reviver (key, val) { 61 | return revive(val) 62 | } 63 | 64 | const specialTypeRE = /^\[native (\w+) (.*?)(<>((.|\s)*))?\]$/ 65 | const symbolRE = /^\[native Symbol Symbol\((.*)\)\]$/ 66 | 67 | export function revive (val) { 68 | if (val === UNDEFINED) { 69 | return undefined 70 | } else if (val === INFINITY) { 71 | return Infinity 72 | } else if (val === NEGATIVE_INFINITY) { 73 | return -Infinity 74 | } else if (val === NAN) { 75 | return NaN 76 | } else if (symbolRE.test(val)) { 77 | const [, string] = symbolRE.exec(val) 78 | return Symbol.for(string) 79 | } else if (specialTypeRE.test(val)) { 80 | const [, type, string,, details] = specialTypeRE.exec(val) 81 | const result = new window[type](string) 82 | if (type === 'Error' && details) { 83 | result.stack = details 84 | } 85 | return result 86 | } else { 87 | return val 88 | } 89 | } 90 | 91 | 92 | let currentEditedField = null 93 | 94 | function numberQuickEditMod (event) { 95 | let mod = 1 96 | if (event.ctrlKey || event.metaKey) { 97 | mod *= 5 98 | } 99 | if (event.shiftKey) { 100 | mod *= 10 101 | } 102 | if (event.altKey) { 103 | mod *= 100 104 | } 105 | return mod 106 | } 107 | 108 | export default { 109 | inject: { 110 | InspectorInjection: { 111 | default: null 112 | } 113 | }, 114 | 115 | props: { 116 | editable: { 117 | type: Boolean, 118 | default: true 119 | }, 120 | removable: { 121 | type: Boolean, 122 | default: false 123 | }, 124 | renamable: { 125 | type: Boolean, 126 | default: false 127 | } 128 | }, 129 | 130 | data () { 131 | return { 132 | editing: false, 133 | editedValue: null, 134 | editedKey: null, 135 | addingValue: false, 136 | newField: null 137 | } 138 | }, 139 | 140 | computed: { 141 | cssClass () { 142 | return { 143 | editing: this.editing 144 | } 145 | }, 146 | 147 | isEditable () { 148 | // if (this.InspectorInjection && !this.InspectorInjection.editable) return false 149 | return this.editable && 150 | !this.fieldOptions.abstract && 151 | !this.fieldOptions.readOnly && 152 | ( 153 | typeof this.field.key !== 'string' || 154 | this.field.key.charAt(0) !== '$' 155 | ) 156 | }, 157 | 158 | isValueEditable () { 159 | const type = this.valueType 160 | return this.isEditable && 161 | ( 162 | type === 'null' || 163 | type === 'literal' || 164 | type === 'string' || 165 | type === 'array' || 166 | type === 'plain-object' 167 | ) 168 | }, 169 | 170 | isSubfieldsEditable () { 171 | return this.isEditable && (this.valueType === 'array' || this.valueType === 'plain-object') 172 | }, 173 | 174 | valueValid () { 175 | try { 176 | parse(this.transformSpecialTokens(this.editedValue, false)) 177 | return true 178 | } catch (e) { 179 | return false 180 | } 181 | }, 182 | 183 | duplicateKey () { 184 | return this.parentField && this.parentField.value.hasOwnProperty(this.editedKey) 185 | }, 186 | 187 | keyValid () { 188 | return this.editedKey && (this.editedKey === this.field.key || !this.duplicateKey) 189 | }, 190 | 191 | editValid () { 192 | return this.valueValid && (!this.renamable || this.keyValid) 193 | }, 194 | 195 | quickEdits () { 196 | if (this.isValueEditable) { 197 | const value = this.field.value 198 | const type = typeof value 199 | if (type === 'boolean') { 200 | return [ 201 | { 202 | icon: value ? 'check_box' : 'check_box_outline_blank', 203 | newValue: !value 204 | } 205 | ] 206 | } else if (type === 'number') { 207 | return [ 208 | { 209 | icon: 'remove', 210 | class: 'big', 211 | title: this.quickEditNumberTooltip('-'), 212 | newValue: event => value - numberQuickEditMod(event) 213 | }, 214 | { 215 | icon: 'add', 216 | class: 'big', 217 | title: this.quickEditNumberTooltip('+'), 218 | newValue: event => value + numberQuickEditMod(event) 219 | } 220 | ] 221 | } 222 | } 223 | return null 224 | } 225 | }, 226 | 227 | methods: { 228 | openEdit (focusKey = false) { 229 | if (this.isValueEditable) { 230 | if (currentEditedField && currentEditedField !== this) { 231 | currentEditedField.cancelEdit() 232 | } 233 | this.editedValue = this.transformSpecialTokens(JSON.stringify(this.field.value), true) 234 | console.log('openEdit', this.editedValue) 235 | this.editedKey = this.field.key 236 | this.editing = true 237 | currentEditedField = this 238 | this.$nextTick(() => { 239 | const el = this.$refs[focusKey && this.renamable ? 'keyInput' : 'editInput'] 240 | el.focus() 241 | el.setSelectionRange(0, el.value.length) 242 | }) 243 | } 244 | }, 245 | 246 | cancelEdit () { 247 | this.editing = false 248 | this.$emit('cancel-edit') 249 | currentEditedField = null 250 | }, 251 | 252 | submitEdit () { 253 | if (this.editValid) { 254 | this.editing = false 255 | const value = this.transformSpecialTokens(this.editedValue, false) 256 | const newKey = this.editedKey !== this.field.key ? this.editedKey : undefined 257 | this.sendEdit({ 258 | value: value != null ? parse(value, true) : value, 259 | newKey 260 | }) 261 | this.$emit('submit-edit') 262 | } 263 | }, 264 | 265 | sendEdit (payload) { 266 | this.$emit('edit-state', this.path, payload) 267 | }, 268 | 269 | transformSpecialTokens (str, display) { 270 | Object.keys(SPECIAL_TOKENS).forEach(key => { 271 | const value = JSON.stringify(SPECIAL_TOKENS[key]) 272 | let search 273 | let replace 274 | if (display) { 275 | search = value 276 | replace = key 277 | } else { 278 | search = key 279 | replace = value 280 | } 281 | str = str.replace(new RegExp(search, 'g'), replace) 282 | }) 283 | return str 284 | }, 285 | 286 | quickEdit (info, event) { 287 | let newValue 288 | if (typeof info.newValue === 'function') { 289 | newValue = info.newValue(event) 290 | } else { 291 | newValue = info.newValue 292 | } 293 | this.sendEdit({ value: JSON.stringify(newValue) }) 294 | }, 295 | 296 | removeField () { 297 | this.sendEdit({ remove: true }) 298 | }, 299 | 300 | addNewValue () { 301 | let key 302 | if (this.valueType === 'array') { 303 | key = this.field.value.length 304 | } else if (this.valueType === 'plain-object') { 305 | let i = 1 306 | while (this.field.value.hasOwnProperty(key = `prop${i}`)) i++ 307 | } 308 | this.newField = { key, value: UNDEFINED } 309 | this.expanded = true 310 | this.addingValue = true 311 | this.$nextTick(() => { 312 | this.$refs.newField.openEdit(true) 313 | }) 314 | }, 315 | 316 | containsEdition () { 317 | return currentEditedField && currentEditedField.path.indexOf(this.path) === 0 318 | }, 319 | 320 | cancelCurrentEdition () { 321 | this.containsEdition() && currentEditedField.cancelEdit() 322 | }, 323 | 324 | quickEditNumberTooltip (operator) { 325 | return this.$t('DataField.quickEdit.number.tooltip', { 326 | operator 327 | }) 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/components/data-field.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 292 | 293 | 400 | -------------------------------------------------------------------------------- /autoScript/js/bind-unbind.js: -------------------------------------------------------------------------------- 1 | // 通用节流方法 2 | function throttle (cb, delay = 100) { 3 | let timer = null; 4 | return (ev) => { 5 | if (timer) { 6 | clearTimeout(timer) 7 | } 8 | timer = setTimeout(() => { 9 | cb && cb.bind(this)(ev); 10 | }, delay) 11 | }; 12 | } 13 | 14 | /** 15 | * 获取最底层iframe页面中鼠标点击的坐标 16 | */ 17 | function getPosition_Iframe (event = {}, contentWindow) { 18 | var parentWindow = contentWindow.parent; 19 | var tmpLocation = contentWindow.location; 20 | var target = null; 21 | var left = 0; 22 | var top = 0; 23 | while (parentWindow != null && typeof (parentWindow) != 'undefined' && tmpLocation.pathname != parentWindow.location.pathname) { 24 | for (var x = 0; x < parentWindow.frames.length; x++) { 25 | if (tmpLocation.pathname == parentWindow.frames[x].location.pathname) { 26 | target = parentWindow.frames[x].frameElement; 27 | break; 28 | } 29 | } 30 | do { 31 | left += target.offsetLeft || 0; 32 | top += target.offsetTop || 0; 33 | target = target.offsetParent; 34 | } while (target) 35 | tmpLocation = parentWindow.location; 36 | parentWindow = parentWindow.parent; 37 | } 38 | let xy = {x: left + (event.clientX || 0), y: top + (event.clientY || 0)} 39 | return xy 40 | } 41 | 42 | /** 43 | * this = contentWindow 44 | * @param ev 45 | */ 46 | function onclick (ev) { 47 | let {x, y} = getPosition_Iframe(ev, this) 48 | let delay = new Date().getTime() - window.startTime 49 | if (delay > 5 && !window.running) { 50 | window.startTime += delay 51 | 52 | let event 53 | 54 | // 原生