├── .npmrc
├── tests
├── api
│ ├── data
│ │ ├── response.txt
│ │ ├── response.gif
│ │ ├── response.jpg
│ │ ├── response.png
│ │ ├── response.css
│ │ ├── response.json
│ │ ├── response.svg
│ │ ├── response.html
│ │ └── response.js
│ └── index.js
├── unit
│ ├── .eslintrc
│ └── HelloWorld.spec.js
└── e2e
│ ├── .eslintrc
│ ├── specs
│ └── test.js
│ ├── plugins
│ └── index.js
│ └── support
│ ├── index.js
│ └── commands.js
├── .npmignore
├── .postcssrc
├── cypress.json
├── src
├── constants
│ ├── index.js
│ └── PanelType.js
├── components
│ ├── VJSONViewer
│ │ ├── index.js
│ │ ├── VJSONViewer.vue
│ │ ├── JSONTextInlineBlock.vue
│ │ └── JSONTextBlock.vue
│ ├── index.js
│ ├── VTabBar.vue
│ ├── VTabBarItem.vue
│ ├── VFootBar.vue
│ ├── VIcon.vue
│ └── VHighlightView.vue
├── assets
│ └── icons
│ │ ├── 2xx.png
│ │ ├── 3xx.png
│ │ ├── 4xx.png
│ │ ├── 5xx.png
│ │ ├── add.png
│ │ ├── ban.png
│ │ ├── edit.png
│ │ ├── save.png
│ │ ├── close.png
│ │ ├── expand.png
│ │ ├── collapse.png
│ │ ├── refresh.png
│ │ ├── setting.png
│ │ ├── add-disable.png
│ │ ├── ban_disable.png
│ │ ├── chrome_logo.png
│ │ ├── close-disable.png
│ │ ├── edit-disable.png
│ │ ├── save-disable.png
│ │ └── transparent_bg.png
├── panels
│ ├── network
│ │ ├── RequestType.js
│ │ ├── TabResponse.vue
│ │ ├── HttpStatus.js
│ │ ├── TabPreview.vue
│ │ ├── NetworkRequest.vue
│ │ └── TabHeaders.vue
│ ├── index.js
│ ├── element
│ │ ├── StyleColorValue.vue
│ │ ├── NodeLink.vue
│ │ ├── Tag.vue
│ │ ├── StyleProperty.vue
│ │ ├── TabStyles.vue
│ │ ├── BoxModel.vue
│ │ ├── StyleRule.vue
│ │ ├── ElementPanel.vue
│ │ ├── NodeView.vue
│ │ └── TabComputed.vue
│ ├── application
│ │ ├── ApplicationPanel.vue
│ │ └── XStorage.js
│ └── console
│ │ ├── TextInlineBlock.vue
│ │ └── ConsolePanel.vue
├── utils
│ ├── filters.js
│ ├── index.js
│ ├── Logger.js
│ ├── EventBus.js
│ ├── consoleHooks.js
│ ├── TaskScheduler.js
│ ├── miscs.js
│ └── style.js
├── plugins
│ ├── index.js
│ ├── pluginEvents.js
│ ├── pluginManager.js
│ └── Plugin.js
├── styles
│ ├── _triangles.scss
│ ├── _mixins.scss
│ ├── _variables.scss
│ └── _global.scss
├── polyfill.js
├── main.js
└── App.vue
├── docs
├── snapshot.png
├── snapshot_console.png
├── snapshot_element.png
├── snapshot_network.png
├── snapshot_plugin.png
├── snapshot_element2.png
├── snapshot_element3.png
├── snapshot_application.png
├── snapshot_plugin_settings1.png
├── snapshot_plugin_settings2.png
├── snapshot.md
├── depoly.md
└── plugin.md
├── public
├── favicon.ico
├── style_link.css
├── style_import2.css
├── style_import1.css
├── index_cdn.html
├── test_application.js
├── test_network.js
├── test_plugin.js
└── test_console.js
├── tsconfig.json
├── .babelrc
├── .eslintrc
├── .gitignore
├── jest.config.js
├── gulpfile.js
├── vue.config.js
├── package.json
└── readme.md
/.npmrc:
--------------------------------------------------------------------------------
1 | git-tag-version=false
--------------------------------------------------------------------------------
/tests/api/data/response.txt:
--------------------------------------------------------------------------------
1 | plain text data
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | */
3 | !dist/*.js
4 | !package*.json
--------------------------------------------------------------------------------
/.postcssrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "autoprefixer": {}
4 | }
5 | }
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "pluginsFile": "tests/e2e/plugins/index.js"
3 | }
4 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export { default as PanelType } from "./PanelType";
2 |
--------------------------------------------------------------------------------
/docs/snapshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot.png
--------------------------------------------------------------------------------
/src/components/VJSONViewer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from "./VJSONViewer.vue";
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/docs/snapshot_console.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_console.png
--------------------------------------------------------------------------------
/docs/snapshot_element.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_element.png
--------------------------------------------------------------------------------
/docs/snapshot_network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_network.png
--------------------------------------------------------------------------------
/docs/snapshot_plugin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_plugin.png
--------------------------------------------------------------------------------
/src/assets/icons/2xx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/2xx.png
--------------------------------------------------------------------------------
/src/assets/icons/3xx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/3xx.png
--------------------------------------------------------------------------------
/src/assets/icons/4xx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/4xx.png
--------------------------------------------------------------------------------
/src/assets/icons/5xx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/5xx.png
--------------------------------------------------------------------------------
/src/assets/icons/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/add.png
--------------------------------------------------------------------------------
/src/assets/icons/ban.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/ban.png
--------------------------------------------------------------------------------
/src/assets/icons/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/edit.png
--------------------------------------------------------------------------------
/src/assets/icons/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/save.png
--------------------------------------------------------------------------------
/docs/snapshot_element2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_element2.png
--------------------------------------------------------------------------------
/docs/snapshot_element3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_element3.png
--------------------------------------------------------------------------------
/src/assets/icons/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/close.png
--------------------------------------------------------------------------------
/src/assets/icons/expand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/expand.png
--------------------------------------------------------------------------------
/tests/api/data/response.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/tests/api/data/response.gif
--------------------------------------------------------------------------------
/tests/api/data/response.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/tests/api/data/response.jpg
--------------------------------------------------------------------------------
/tests/api/data/response.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/tests/api/data/response.png
--------------------------------------------------------------------------------
/docs/snapshot_application.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_application.png
--------------------------------------------------------------------------------
/src/assets/icons/collapse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/collapse.png
--------------------------------------------------------------------------------
/src/assets/icons/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/refresh.png
--------------------------------------------------------------------------------
/src/assets/icons/setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/setting.png
--------------------------------------------------------------------------------
/src/assets/icons/add-disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/add-disable.png
--------------------------------------------------------------------------------
/src/assets/icons/ban_disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/ban_disable.png
--------------------------------------------------------------------------------
/src/assets/icons/chrome_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/chrome_logo.png
--------------------------------------------------------------------------------
/docs/snapshot_plugin_settings1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_plugin_settings1.png
--------------------------------------------------------------------------------
/docs/snapshot_plugin_settings2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/docs/snapshot_plugin_settings2.png
--------------------------------------------------------------------------------
/src/assets/icons/close-disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/close-disable.png
--------------------------------------------------------------------------------
/src/assets/icons/edit-disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/edit-disable.png
--------------------------------------------------------------------------------
/src/assets/icons/save-disable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/save-disable.png
--------------------------------------------------------------------------------
/src/assets/icons/transparent_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whinc/web-console/HEAD/src/assets/icons/transparent_bg.png
--------------------------------------------------------------------------------
/tests/api/data/response.css:
--------------------------------------------------------------------------------
1 | @import "aa";
2 |
3 | .aa {
4 | color: red;
5 | background: rgb(50%, 50%, 50%);
6 | }
7 |
--------------------------------------------------------------------------------
/src/panels/network/RequestType.js:
--------------------------------------------------------------------------------
1 | // 请求类型
2 | export default Object.freeze({
3 | XHR: "xhr",
4 | FETCH: "fetch"
5 | });
6 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | },
5 | "rules": {
6 | "import/no-extraneous-dependencies": "off"
7 | }
8 | }
--------------------------------------------------------------------------------
/public/style_link.css:
--------------------------------------------------------------------------------
1 | /* 测试元素审查 */
2 | #element {
3 | background-color: white;
4 | }
5 | .xyz {
6 | border: 1px solid red;
7 | padding: 1px;
8 | }
9 |
--------------------------------------------------------------------------------
/public/style_import2.css:
--------------------------------------------------------------------------------
1 | /* 测试元素审查 */
2 | #element {
3 | border-color: yellow;
4 | }
5 | .xyz {
6 | border: 3px dotted yellow;
7 | padding: 3px;
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@": ["./src"],
6 | "@/*": ["./src/*"]
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/utils/filters.js:
--------------------------------------------------------------------------------
1 | export default {
2 | lowerCase: input => {
3 | if (typeof input !== "string") return input;
4 | else return input.toLowerCase();
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | export { default as pluginManager } from "./pluginManager";
2 | export { default as pluginEvents } from "./pluginEvents";
3 | export { default as Plugin } from "./Plugin";
4 |
--------------------------------------------------------------------------------
/public/style_import1.css:
--------------------------------------------------------------------------------
1 | @import url("style_import2.css");
2 |
3 | /* 测试元素审查 */
4 | #element {
5 | border-color: green;
6 | }
7 | .xyz {
8 | border: 2px dashed blue;
9 | padding: 2px;
10 | }
11 |
--------------------------------------------------------------------------------
/tests/e2e/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "cypress"
4 | ],
5 | "env": {
6 | "mocha": true,
7 | "cypress/globals": true
8 | },
9 | "rules": {
10 | "strict": "off"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@vue/app"
4 | ],
5 | "plugins": [
6 | [
7 | "component",
8 | {
9 | "libraryName": "mint-ui",
10 | "style": true
11 | }
12 | ]
13 | ]
14 | }
--------------------------------------------------------------------------------
/tests/e2e/specs/test.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 |
3 | describe('My First Test', () => {
4 | it('Visits the Kitchen Sink', () => {
5 | cy.visit('/')
6 | cy.contains('h1', 'Welcome to Your Vue.js App')
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/tests/api/data/response.json:
--------------------------------------------------------------------------------
1 | {
2 | "a": 1,
3 | "b": true,
4 | "c": "c",
5 | "d": [1.23, true, "c", { "a": 1, "b": 2 }],
6 | "e": {
7 | "a": 1,
8 | "b": true,
9 | "c": "c",
10 | "d": null,
11 | "e1": 1,
12 | "e2": 1,
13 | "e3": 1
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "plugin:vue/essential",
5 | "eslint:recommended"
6 | ],
7 | "env": {
8 | "browser": true,
9 | "node": true
10 | },
11 | "globals": {
12 | },
13 | "rules": {
14 | "no-console": "off",
15 | "no-case-declarations": "off"
16 | }
17 | }
--------------------------------------------------------------------------------
/tests/api/data/response.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as VTabBar } from "./VTabBar";
2 | export { default as VTabBarItem } from "./VTabBarItem";
3 | export { default as VFootBar } from "./VFootBar";
4 | export { default as VJSONViewer } from "./VJSONViewer";
5 | export { default as VIcon } from "./VIcon";
6 | export { default as VHighlightView } from "./VHighlightView";
7 |
--------------------------------------------------------------------------------
/tests/api/data/response.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 | Hello World
11 |
12 |
--------------------------------------------------------------------------------
/docs/snapshot.md:
--------------------------------------------------------------------------------
1 | # Element 面板
2 |
3 | 
4 |
5 | 
6 |
7 | 
8 |
9 | # Console 面板
10 |
11 | 
12 |
13 | # Network 面板
14 |
15 | 
16 |
17 | # Application 面板
18 |
19 | 
20 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { default as Style } from "./style";
2 | export { default as filters } from "./filters";
3 | export { default as Logger } from "./Logger";
4 | export { default as consoleHooks } from "./consoleHooks";
5 | export { default as TaskScheduler } from "./TaskScheduler";
6 | export { default as eventBus, EventBus } from "./eventBus";
7 | export * from "./miscs";
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /demo
5 |
6 | /tests/e2e/videos/
7 | /tests/e2e/screenshots/
8 |
9 | # local env files
10 | .env.local
11 | .env.*.local
12 |
13 | # Log files
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 |
--------------------------------------------------------------------------------
/src/panels/index.js:
--------------------------------------------------------------------------------
1 | export { default as SettingsPanel } from "./settings/SettingsPanel.vue";
2 | export { default as NetworkPanel } from "./network/NetworkPanel.vue";
3 | export { default as ConsolePanel } from "./console/ConsolePanel";
4 | export { default as ApplicationPanel } from "./application/ApplicationPanel";
5 | export { default as ElementPanel } from "./element/ElementPanel.vue";
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue'
7 | ],
8 | transform: {
9 | '^.+\\.vue$': 'vue-jest',
10 | '^.+\\.jsx?$': 'babel-jest'
11 | },
12 | moduleNameMapper: {
13 | '^@/(.*)$': '/src/$1'
14 | },
15 | snapshotSerializers: [
16 | 'jest-serializer-vue'
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/unit/HelloWorld.spec.js:
--------------------------------------------------------------------------------
1 | import { shallow } from '@vue/test-utils'
2 | import HelloWorld from '@/components/HelloWorld.vue'
3 |
4 | describe('HelloWorld.vue', () => {
5 | it('renders props.msg when passed', () => {
6 | const msg = 'new message'
7 | const wrapper = shallow(HelloWorld, {
8 | propsData: { msg }
9 | })
10 | expect(wrapper.text()).toMatch(msg)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/tests/e2e/plugins/index.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/guides/guides/plugins-guide.html
2 |
3 | module.exports = (on, config) => {
4 | return Object.assign({}, config, {
5 | fixturesFolder: 'tests/e2e/fixtures',
6 | integrationFolder: 'tests/e2e/specs',
7 | screenshotsFolder: 'tests/e2e/screenshots',
8 | videosFolder: 'tests/e2e/videos',
9 | supportFile: 'tests/e2e/support/index.js'
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/docs/depoly.md:
--------------------------------------------------------------------------------
1 | ## 部署
2 |
3 | 发布到 npm 仓库:
4 |
5 | ```bash
6 | npm run semantic-release
7 | ```
8 |
9 | 更新 github.io 在线示例:
10 |
11 | ```bash
12 | npm run depoly
13 | ```
14 |
15 | 本地调试
16 |
17 | ```js
18 | const script = document.createElement("script");
19 | script.src = "http://localhost:8081/app.js";
20 | script.onload = function() {
21 | new window.WebConsole();
22 | };
23 | document.body.appendChild(script);
24 | ```
25 |
--------------------------------------------------------------------------------
/tests/api/data/response.js:
--------------------------------------------------------------------------------
1 | function sayHello(params) {
2 | console.log(params);
3 | Array.prototype.every.call(this, 1, 2);
4 | var a = {
5 | 1: 100,
6 | 2: true,
7 | 3: null,
8 | 4: undefined,
9 | 5: [100, 200],
10 | 6: "stringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstringstring",
11 | 7: {
12 | a: 1
13 | },
14 | 8: Symbol("a")
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/public/index_cdn.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 测试CDN下载
7 |
8 |
9 |
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/src/plugins/pluginEvents.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 插件事件
3 | *
4 | * 约定:事件名称与插件的生命周期方法同名
5 | */
6 | export default Object.freeze({
7 | // 插件 DOM 已渲染
8 | WEB_CONSOLE_READY: "onWebConsoleReady",
9 | WEB_CONSOLE_SHOW: "onWebConsoleShow",
10 | WEB_CONSOLE_HIDE: "onWebConsoleHide",
11 | WEB_CONSOLE_TAB_CHANGED: "onWebConsoleTabChanged",
12 | WEB_CONSOLE_SETTINGS_LOADED: "onWebConsoleSettingsLoaded",
13 | WEB_CONSOLE_SETTINGS_CHANGED: "onWebConsoleSettingsChanged"
14 | });
15 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const pkgInfo = require("./package.json");
2 | const child_process = require("child_process");
3 | const process = require("process");
4 | var gulp = require("gulp");
5 |
6 | gulp.task("default", function(cb) {
7 | // 将你的默认的任务代码放在这
8 | });
9 |
10 | let version = "prerelease";
11 |
12 | gulp.task("version", cb => {
13 | const oldVersion = pkgInfo.version;
14 | console.log(oldVersion);
15 | console.log(process.argv, ";", process.argv0);
16 | child_process.exec("npm version " + version, (err, stdout, stderr) => {
17 | if (err) cb(err);
18 | console.log(stdout);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/constants/PanelType.js:
--------------------------------------------------------------------------------
1 | export default Object.freeze(
2 | Object.setPrototypeOf(
3 | {
4 | ELEMENT: "element",
5 | CONSOLE: "console",
6 | APPLICATION: "application",
7 | NETWORK: "network"
8 | },
9 | {
10 | text(type) {
11 | switch (type) {
12 | case this.ELEMENT:
13 | return "Element";
14 | case this.CONSOLE:
15 | return "Console";
16 | case this.APPLICATION:
17 | return "Application";
18 | case this.NETWORK:
19 | return "Network";
20 | default:
21 | return type;
22 | }
23 | }
24 | }
25 | )
26 | );
27 |
--------------------------------------------------------------------------------
/src/panels/element/StyleColorValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{color}}
5 |
6 |
7 |
8 |
19 |
20 |
32 |
--------------------------------------------------------------------------------
/src/styles/_triangles.scss:
--------------------------------------------------------------------------------
1 | @mixin triangles-collapse($border-width: 4px, $color: black) {
2 | display: inline-block;
3 | content: "";
4 | width: 0;
5 | height: 0;
6 | /* 等边三角形,tan(30) 约为 0.5773502691896257 */
7 | border-left: $border-width solid $color;
8 | border-top: $border-width * 0.8 solid transparent;
9 | border-bottom: $border-width * 0.8 solid transparent;
10 | }
11 |
12 | @mixin triangles-expand($border-width: 4px, $color: black) {
13 | display: inline-block;
14 | content: "";
15 | width: 0;
16 | height: 0;
17 | /* 等边三角形,tan(30) 约为 0.5773502691896257 */
18 | border-top: $border-width solid $color;
19 | border-left: $border-width * 0.8 solid transparent;
20 | border-right: $border-width * 0.8 solid transparent;
21 | }
22 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | /** 以 VUE_APP_* 开头的环境变量会替换代码中相应的变量 */
4 | const npmPkg = require("./package.json");
5 | process.env.VUE_APP_NAME = npmPkg.name;
6 | process.env.VUE_APP_VERSION = npmPkg.version;
7 | process.env.VUE_APP_DATE = new Date(Date.now() - new Date().getTimezoneOffset() * 60 * 1000).toISOString();
8 |
9 | module.exports = {
10 | css: {
11 | extract: false
12 | },
13 | // 部署和测试时用,打包成 lib 时不需要
14 | publicPath: process.env.NODE_ENV === "production" ? "./" : "/",
15 | configureWebpack: {
16 | // 将入口的导出作为默认导出
17 | output: {
18 | libraryExport: "default"
19 | },
20 | resolve: {
21 | alias: {
22 | "@": path.resolve(__dirname, "src")
23 | }
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/tests/e2e/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/public/test_application.js:
--------------------------------------------------------------------------------
1 | window.$application = (function() {
2 | function testWriteCookie(n = 1) {
3 | for (var i = 0; i < n; ++i) {
4 | const obj = { timestamp: Date.now() };
5 | document.cookie = i + "=" + JSON.stringify(obj);
6 | }
7 | }
8 |
9 | function testWriteLocalStorage(n = 1) {
10 | for (var i = 0; i < n; ++i) {
11 | const obj = { timestamp: Date.now() };
12 | window.localStorage.setItem(i, JSON.stringify(obj));
13 | }
14 | }
15 |
16 | function testWriteSessionStorage(n = 1) {
17 | for (var i = 0; i < n; ++i) {
18 | const obj = { timestamp: Date.now() };
19 | window.sessionStorage.setItem(i, JSON.stringify(obj));
20 | }
21 | }
22 |
23 | return {
24 | testWriteCookie,
25 | testWriteSessionStorage,
26 | testWriteLocalStorage
27 | };
28 | })();
29 |
--------------------------------------------------------------------------------
/src/components/VJSONViewer/VJSONViewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
35 |
36 |
41 |
--------------------------------------------------------------------------------
/src/polyfill.js:
--------------------------------------------------------------------------------
1 | if (Element.prototype.matches === undefined) {
2 | Element.prototype.matches =
3 | Element.prototype.matchesSelector ||
4 | Element.prototype.mozMatchesSelector ||
5 | Element.prototype.msMatchesSelector ||
6 | Element.prototype.oMatchesSelector ||
7 | Element.prototype.webkitMatchesSelector ||
8 | function(s) {
9 | var matches = (this.document || this.ownerDocument).querySelectorAll(s),
10 | i = matches.length;
11 | while (--i >= 0 && matches.item(i) !== this);
12 | return i > -1;
13 | };
14 | }
15 |
16 | if (Element.prototype.getAttributeNames == undefined) {
17 | Element.prototype.getAttributeNames = function() {
18 | var attributes = this.attributes;
19 | var length = attributes.length;
20 | var result = new Array(length);
21 | for (var i = 0; i < length; i++) {
22 | result[i] = attributes[i].name;
23 | }
24 | return result;
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/tests/e2e/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/src/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | // 隐藏滚动条(刷新后生效)
2 | @mixin hide-scrollbar {
3 | &::-webkit-scrollbar {
4 | display: none;
5 | overflow: -moz-scrollbars-none;
6 | }
7 | }
8 |
9 | // 给当前元素以及后代元素设置属性
10 | @mixin descendant-attr($name, $value) {
11 | #{$name}: $value;
12 | /deep/ * {
13 | #{$name}: $value;
14 | }
15 | }
16 |
17 | // 文本超出父容器时显示省略号
18 | @mixin text-ellipsis {
19 | white-space: nowrap;
20 | overflow: hidden;
21 | text-overflow: ellipsis;
22 | }
23 |
24 | // 文本超出父容器时可滚动
25 | @mixin text-scroll {
26 | white-space: nowrap;
27 | overflow: auto;
28 | }
29 |
30 | // input 输入框样式
31 | @mixin input($height: 1.8em) {
32 | height: $height;
33 | outline: none;
34 | color: #5a5a5a;
35 | background-color: white;
36 | border: 1px solid transparent;
37 | &::placeholder {
38 | color: rgb(128, 128, 128);
39 | }
40 | &:focus {
41 | border: 1px solid #2196f3;
42 | }
43 | }
44 |
45 | // 初始化元素属性,避免宿主页面与选择器同名时受影响
46 | @mixin initial() {
47 | width: initial;
48 | height: initial;
49 | padding: initial;
50 | margin: initial;
51 | border: initial;
52 | position: initial;
53 | }
54 |
--------------------------------------------------------------------------------
/src/utils/Logger.js:
--------------------------------------------------------------------------------
1 | import { isDev, isString } from "./miscs";
2 |
3 | /* 原始的 console 方法 */
4 | const _error = window.console.error;
5 | const _log = window.console.log;
6 | const _warn = window.console.warn;
7 |
8 | /**
9 | * 日志类
10 | * 调用原始的 console 方法打印日志
11 | */
12 | export default class Logger {
13 | constructor(prefix) {
14 | this._prefix = prefix ? prefix + " " : "";
15 | }
16 |
17 | error(...args) {
18 | if (!isDev) return;
19 |
20 | if (isString(args[0])) {
21 | args[0] = this._prefix + args[0];
22 | } else {
23 | args.unshift(this._prefix);
24 | }
25 | _error.apply(this, args);
26 | }
27 | warn(...args) {
28 | if (!isDev) return;
29 |
30 | if (isString(args[0])) {
31 | args[0] = this._prefix + args[0];
32 | } else {
33 | args.unshift(this._prefix);
34 | }
35 | _warn.apply(this, args);
36 | }
37 | log(...args) {
38 | if (!isDev) return;
39 |
40 | if (isString(args[0])) {
41 | args[0] = this._prefix + args[0];
42 | } else {
43 | args.unshift(this._prefix);
44 | }
45 | _log.apply(this, args);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | /* web-console UI structure */
2 | $panel-height: 75vh;
3 |
4 | $footbar-height: 40px;
5 |
6 | $tabbar-height: 40px;
7 |
8 | $list-row-height: 38px;
9 |
10 | /* color */
11 | $default-color: rgb(48, 57, 66);
12 |
13 | $accent-color: #03a9f4;
14 | $accent-color-b: #2196f3;
15 | $accent-color-c: #3e82f7;
16 |
17 | $toolbar-bg-color: rgb(243, 243, 243);
18 | $toolbar-hover-bg-color: #eaeaea;
19 | $toolbar-border-color: rgb(205, 205, 205);
20 |
21 | $selection-fg-color: white;
22 | $selection-bg-color: $accent-color-b;
23 | $selection-inactive-bg-color: #dadada;
24 |
25 | $tab-selected-fg-color: #333;
26 | $tab-selected-bg-color: $toolbar-bg-color;
27 |
28 | $drop-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.2), 0 2px 6px rgba(0, 0, 0, 0.1);
29 |
30 | $divider-color: #d0d0d0;
31 |
32 | $tabbar-bg-color: $toolbar-bg-color;
33 | $tabbar-border-color: $toolbar-border-color;
34 | $tab-fg-color: rgb(90, 90, 90);
35 | $tab-selected-fg-color: rgb(51, 51, 51);
36 |
37 | $dom-tag-name-color: rgb(136, 18, 128);
38 | $dom-attribute-name-color: rgb(153, 69, 0);
39 | $dom-attribute-value-color: rgb(26, 26, 166);
40 | $dom-link-color: rgb(17, 85, 204);
41 |
42 | /* font */
43 | $primary-font-size: 15px;
44 | $secondary-font-size: 13px;
45 | $source-code-font-size: 14px;
46 | $font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
47 |
--------------------------------------------------------------------------------
/src/utils/EventBus.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Logger from "./Logger";
3 |
4 | const logger = new Logger("[EventBus]");
5 | class EventBus {
6 | constructor(events = {}) {
7 | this._vue = new Vue();
8 |
9 | Object.assign(this, events);
10 | // this._eventNameList = Object.keys(events).map(name => events[name]);
11 | }
12 | emit(eventName, ...args) {
13 | // if (!this._eventNameList.some(v => v === eventName)) {
14 | // logger.warn("unregistered event name:", eventName);
15 | // return;
16 | // }
17 |
18 | logger.log('emit: "%s"', eventName, ...args);
19 | // freeze 一下避免 Vue 添加一些额外字段,同时也确保事件数据传递时不被修改
20 | this._vue.$emit(eventName, ...args.map(v => Object.freeze(v)));
21 | }
22 | on(eventName, callback) {
23 | // if (!this._eventNameList.some(v => v === eventName)) {
24 | // logger.warn("unregistered event name:", eventName);
25 | // return;
26 | // }
27 |
28 | // logger.log('on: "%s"', eventName);
29 | this._vue.$on(eventName, callback);
30 | }
31 | }
32 |
33 | export { EventBus };
34 |
35 | /*
36 | * 预定义事件类型
37 | * 目标触发了事件::
38 | * 请求目标触发事件:::
39 | */
40 | export default new EventBus({
41 | POPUP_VISIBILITY_CHANGE: "popup_visibility_change",
42 | SETTINGS_CHANGE: "settings_change",
43 | SETTINGS_LOADED: "settings_loaded",
44 | WEB_CONSOLE_SHOW: "web-console:show",
45 | WEB_CONSOLE_HIDE: "web-console:hide",
46 | REQUEST_WEB_CONSOLE_HIDE: "request:web-console:hide"
47 | });
48 |
--------------------------------------------------------------------------------
/src/utils/consoleHooks.js:
--------------------------------------------------------------------------------
1 | import { uuid, createStack } from "./miscs";
2 |
3 | // hook console
4 | // install 前的 console 接口
5 | const originConsole = {};
6 | // install 后的 console 接口(之后第三方可能会再次重写改接口)
7 | const currentConsole = {};
8 | const names = ["log", "info", "error", "warn", "debug"];
9 | const msgList = [];
10 | let active = false;
11 |
12 | const install = () => {
13 | active = true;
14 | names.forEach(name => {
15 | originConsole[name] = window.console[name];
16 |
17 | currentConsole[name] = function(...args) {
18 | if (active) {
19 | const logArgs = args.map(v => {
20 | if (v instanceof Error) {
21 | createStack(v, currentConsole[name]);
22 | }
23 | return v;
24 | });
25 | const msg = {
26 | id: uuid(),
27 | type: name,
28 | timestamps: Date.now(),
29 | logArgs
30 | };
31 | // 冻结计算结果,避免 Vue 添加额外属性
32 | msgList.push(Object.freeze(msg));
33 | }
34 | originConsole[name].apply(this, args);
35 | };
36 |
37 | window.console[name] = currentConsole[name];
38 | });
39 | };
40 |
41 | // 卸载
42 | const uninstall = () => {
43 | active = false;
44 | // 如果第三方没有重写 console 接口,则还原 console 接口为原生的 console 接口
45 | names.forEach(name => {
46 | if (currentConsole[name] === window.console[name]) {
47 | window.console[name] = originConsole[name];
48 | }
49 | });
50 | };
51 |
52 | const getMsgList = () => msgList;
53 |
54 | export default { install, uninstall, getMsgList };
55 |
--------------------------------------------------------------------------------
/src/components/VTabBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
45 |
46 |
73 |
--------------------------------------------------------------------------------
/src/panels/network/TabResponse.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
This request has no response data available.
11 |
12 |
13 |
14 |
15 |
16 |
53 |
54 |
55 |
76 |
--------------------------------------------------------------------------------
/src/components/VTabBarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
29 |
30 |
72 |
--------------------------------------------------------------------------------
/src/styles/_global.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | // 全局样式
4 | .web-console {
5 | font-size: $primary-font-size;
6 | font-family: $font-family;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | color: $default-color;
10 | .g-source-code,
11 | .source-code {
12 | font-size: $source-code-font-size !important;
13 | font-family: Menlo, Consolas, monospace;
14 | }
15 |
16 | .platform-linux {
17 | color: rgb(48, 57, 66);
18 | font-family: Roboto, Ubuntu, Arial, sans-serif;
19 | }
20 | .platform-mac {
21 | color: rgb(48, 57, 66);
22 | font-family: ".SFNSDisplay-Regular", "Helvetica Neue", "Lucida Grande", sans-serif;
23 | }
24 | .platform-windows {
25 | font-family: "Segoe UI", Tahoma, sans-serif;
26 | }
27 |
28 | // 隐藏滚动条
29 | .g-hide-scrollbar::-webkit-scrollbar {
30 | display: none;
31 | overflow: -moz-scrollbars-none;
32 | }
33 |
34 | // 重置默认样式
35 | * {
36 | box-sizing: border-box;
37 | user-select: auto;
38 | // IOS:移除默认的触摸高亮样式
39 | -webkit-tap-highlight-color: transparent;
40 | &:focus {
41 | outline: none;
42 | }
43 | }
44 |
45 | input,
46 | select,
47 | textarea,
48 | button {
49 | font-size: $primary-font-size;
50 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
51 | -webkit-font-smoothing: antialiased;
52 | -moz-osx-font-smoothing: grayscale;
53 | border: none;
54 | outline: none;
55 | }
56 |
57 | pre {
58 | margin: 0;
59 | }
60 |
61 | // Fix:
标签不换行导致的样式问题(如 Element 面板的盒模型坍塌)
62 | br:before {
63 | content: "\A";
64 | white-space: pre-line;
65 | }
66 | br:after {
67 | white-space: pre-line;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/VFootBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
45 |
46 |
47 |
79 |
--------------------------------------------------------------------------------
/src/plugins/pluginManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 插件管理
3 | */
4 | import { eventBus, EventBus } from "@/utils";
5 | import Plugin from "./Plugin";
6 |
7 | class PluginManager extends EventBus {
8 | constructor(...args) {
9 | super(...args);
10 | /**
11 | * 插件集合
12 | * @type {Array}
13 | */
14 | this._plugins = [];
15 | // 当前最新的设置
16 | this._settings = {};
17 | // 宿主代理
18 | this._hostProxy = null;
19 | }
20 |
21 | // 宿主代理,提供一些操作 web-console 的方法
22 | get hostProxy() {
23 | if (!this._hostProxy) {
24 | this._hostProxy = {
25 | hidePanel: () => eventBus.emit(eventBus.REQUEST_WEB_CONSOLE_HIDE),
26 | getSettings: () => this._settings
27 | };
28 | }
29 | return this._hostProxy;
30 | }
31 |
32 | /**
33 | * 注册插件
34 | * @param {Plugin} plugin
35 | */
36 | addPlugin(plugin) {
37 | if (!(plugin instanceof Plugin)) {
38 | console.warn("Invalid plugin: plugin should inherit WebConsole.Plugin");
39 | return;
40 | }
41 |
42 | if (!plugin.id) {
43 | console.warn(`Empty plugin id: plugin id must not be empty and must be unique among all plugins`);
44 | return;
45 | }
46 |
47 | if (this._plugins.find(v => v.id === plugin.id)) {
48 | console.warn(`Plugin conflict: plugin id "${plugin.id}" has existed`);
49 | return;
50 | }
51 |
52 | plugin.__init__(this);
53 | this._plugins.push(plugin);
54 | }
55 |
56 | getPlugins() {
57 | return this._plugins;
58 | }
59 |
60 | updateSettings(settings) {
61 | this._settings = settings;
62 | }
63 |
64 | getSettings() {
65 | return this._settings;
66 | }
67 |
68 | hasPlugin(pluginId) {
69 | return this._plugins.findIndex(plugin => plugin.id === pluginId) !== -1;
70 | }
71 |
72 | toString() {
73 | return JSON.stringify(this._plugins, null, 4);
74 | }
75 | }
76 |
77 | export default new PluginManager();
78 |
--------------------------------------------------------------------------------
/src/panels/network/HttpStatus.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "100": "Continue",
3 | "101": "Switching Protocols",
4 | "102": "Processing",
5 | "200": "OK",
6 | "201": "Created",
7 | "202": "Accepted",
8 | "203": "Non-Authoritative Information",
9 | "204": "No Content",
10 | "205": "Reset Content",
11 | "206": "Partial Content",
12 | "207": "Multi-Status",
13 | "208": "Already Reported",
14 | "226": "IM Used",
15 | "300": "Multiple Choices",
16 | "301": "Moved Permanently",
17 | "302": "Found",
18 | "303": "See Other",
19 | "304": "Not Modified",
20 | "305": "Use Proxy",
21 | "307": "Temporary Redirect",
22 | "308": "Permanent Redirect",
23 | "400": "Bad Request",
24 | "401": "Unauthorized",
25 | "402": "Payment Required",
26 | "403": "Forbidden",
27 | "404": "Not Found",
28 | "405": "Method Not Allowed",
29 | "406": "Not Acceptable",
30 | "407": "Proxy Authentication Required",
31 | "408": "Request Timeout",
32 | "409": "Conflict",
33 | "410": "Gone",
34 | "411": "Length Required",
35 | "412": "Precondition Failed",
36 | "413": "Payload Too Large",
37 | "414": "URI Too Long",
38 | "415": "Unsupported Media Type",
39 | "416": "Range Not Satisfiable",
40 | "417": "Expectation Failed",
41 | "418": "I'm a teapot",
42 | "421": "Misdirected Request",
43 | "422": "Unprocessable Entity",
44 | "423": "Locked",
45 | "424": "Failed Dependency",
46 | "425": "Unordered Collection",
47 | "426": "Upgrade Required",
48 | "428": "Precondition Required",
49 | "429": "Too Many Requests",
50 | "431": "Request Header Fields Too Large",
51 | "451": "Unavailable For Legal Reasons",
52 | "500": "Internal Server Error",
53 | "501": "Not Implemented",
54 | "502": "Bad Gateway",
55 | "503": "Service Unavailable",
56 | "504": "Gateway Timeout",
57 | "505": "HTTP Version Not Supported",
58 | "506": "Variant Also Negotiates",
59 | "507": "Insufficient Storage",
60 | "508": "Loop Detected",
61 | "509": "Bandwidth Limit Exceeded",
62 | "510": "Not Extended",
63 | "511": "Network Authentication Required"
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/VIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
55 |
56 |
66 |
--------------------------------------------------------------------------------
/src/plugins/Plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 插件基类
3 | */
4 | import { isFunction } from "@/utils";
5 | import { VFootBar, VIcon, VTabBar, VTabBarItem, VHighlightView, VJSONViewer } from "@/components";
6 | import pluginEvents from "./pluginEvents";
7 |
8 | export default class Plugin {
9 | constructor({ id, name, settings, component }) {
10 | this.id = id;
11 | this.name = name || this.id;
12 | this.settings = isFunction(settings) ? settings.call(this) : settings;
13 | this.component = isFunction(component) ? component.call(this) : component;
14 | }
15 |
16 | __init__(pluginMgr) {
17 | // const plugin = this;
18 | const component = this.component;
19 |
20 | this.component = {
21 | ...component,
22 | name: component.name || this.id,
23 | mixins: [
24 | ...(component.mixins || []),
25 | {
26 | components: {
27 | VFootBar,
28 | VHighlightView,
29 | VIcon,
30 | VTabBar,
31 | VTabBarItem,
32 | VJSONViewer
33 | },
34 | created() {
35 | // created 周期函数触发时,Vue 已完成事件部署
36 | const vm = this;
37 | // 注册插件生命周期方法
38 | // 当接收到特定事件时,自动触发相应的插件生命周期方法
39 | Object.keys(pluginEvents)
40 | .map(key => pluginEvents[key])
41 | .forEach(event => {
42 | pluginMgr.on(event, (...args) => {
43 | if (isFunction(vm[event])) {
44 | vm[event].call(vm, pluginMgr.hostProxy, ...args);
45 | }
46 | });
47 | });
48 | } // created
49 | }
50 | ] // mixins
51 | };
52 |
53 | // 暂不支持
54 | // Object.keys(pluginEvents)
55 | // .map(key => pluginEvents[key])
56 | // .forEach(event => {
57 | // pluginMgr.on(event, (...args) => {
58 | // if (isFunction(plugin[event])) {
59 | // plugin[event].call(plugin, pluginMgr.hostProxy, ...args);
60 | // }
61 | // });
62 | // });
63 | }
64 |
65 | // [pluginEvents.WEB_CONSOLE_READY]() {}
66 | // [pluginEvents.WEB_CONSOLE_SHOW]() {}
67 | // [pluginEvents.WEB_CONSOLE_HIDE]() {}
68 | // [pluginEvents.WEB_CONSOLE_TAB_CHANGED]() {}
69 | // [pluginEvents.WEB_CONSOLE_SETTINGS_LOADED]() {}
70 | // [pluginEvents.WEB_CONSOLE_SETTINGS_CHANGED]() {}
71 |
72 | toString() {
73 | return this.id + "(" + this.name + ")";
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/panels/element/NodeLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{id}}
6 | {{tagName}}
7 |
8 |
9 | {{tagName}}
10 | {{id}}
11 | {{className}}
12 |
13 |
14 |
15 | (text)
16 |
17 |
18 | <!-->
19 |
20 |
21 | <!doctype>
22 |
23 |
24 | (unknow)
25 |
26 |
27 |
28 |
29 |
64 |
65 |
91 |
--------------------------------------------------------------------------------
/src/utils/TaskScheduler.js:
--------------------------------------------------------------------------------
1 | import { isFunction } from "./miscs";
2 | // import Logger from "./Logger";
3 |
4 | // const logger = new Logger("[TaskScheduler]");
5 | /**
6 | * 任务调度器
7 | * 用途:按任意速率添加任务,之后这些任务按指定速率按序执行,避免阻塞交互
8 | *
9 | * 第一版实现方式:新增任务放入队列,调度器间隔一定时间执行 1 个任务,这种方式问题在于每次执行的任务数量
10 | * 只有一个,导致任务执行的总时间变长,总时间 = 任务数量 * 间隔时间
11 | *
12 | * T T T T T T T <-- push task
13 | * T -> wait -> T -> wait -> T
14 | *
15 | * 第二版实现方式:新增任务放入队列,调度器间隔一定时间执行 N 个任务,总时间 = 任务数量 / N * 间隔时间,
16 | * 大大缩短了任务执行的总时间
17 | *
18 | * T T T T T T T <-- push task
19 | * T...T -> wait -> T...T -> wait -> T...T
20 | *
21 | * 示例:
22 | * const scheduler = new TaskScheduler(interval)
23 | * scheduler.addAndStart(task1)
24 | * scheduler.addAndStart(task2)
25 | */
26 | export default class TaskScheduler {
27 | constructor({ interval = 200, taskBundleSize = 100 } = {}) {
28 | this._interval = interval;
29 | this._isRunning = false;
30 | this._tasks = [];
31 | this._timerId = null;
32 | this._taskBundleSize = taskBundleSize;
33 | }
34 |
35 | get length() {
36 | return this._tasks.length;
37 | }
38 |
39 | add(task) {
40 | if (!isFunction(task)) return;
41 |
42 | this._tasks.push(task);
43 | }
44 |
45 | addAndStart(task) {
46 | this.add(task);
47 | if (this.length > 0) {
48 | this.start({ immediate: true });
49 | }
50 | }
51 |
52 | /**
53 | * 启动调度
54 | * @param {Object} params
55 | * @param {Boolean} params.immediate 是否立即执行队列任务
56 | */
57 | start({ immediate = true } = {}) {
58 | this._runTask(immediate);
59 | }
60 |
61 | // 停止调度,并清空任务队列
62 | stop() {
63 | if (this._timerId !== null) {
64 | clearTimeout(this._timerId);
65 | this._timerId = null;
66 | }
67 | this._tasks = [];
68 | this._isRunning = false;
69 | }
70 |
71 | _shiftTaskBunlde() {
72 | const count = Math.min(this._tasks.length, this._taskBundleSize);
73 | const taskBundle = this._tasks.slice(0, count);
74 | this._tasks = this._tasks.slice(count);
75 |
76 | // logger.log("_shiftTaskBunlde remove:", count, "rest:", this.length);
77 | return taskBundle;
78 | }
79 |
80 | _runTask(immediate = false) {
81 | if (this.length <= 0 || this._isRunning) return;
82 |
83 | this._isRunning = true;
84 |
85 | if (immediate) {
86 | const task = this._tasks.shift();
87 | task.call(null);
88 | // logger.log("_runTask immediate size:", 1);
89 | }
90 | this._timerId = setTimeout(() => {
91 | const tasks = this._shiftTaskBunlde();
92 | tasks.forEach(task => task.call(null));
93 |
94 | this._isRunning = false;
95 | this._runTask();
96 | }, this._interval);
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/utils/miscs.js:
--------------------------------------------------------------------------------
1 | export { default as cloneDeep } from "lodash.clonedeep";
2 |
3 | export const isNull = v => v === null;
4 | export const isUndefined = v => v === undefined;
5 | export const isString = v => typeof v === "string";
6 | export const isFunction = v => typeof v === "function";
7 | export const isArray = v => Array.isArray(v);
8 | export const isNumber = v => typeof v === "number";
9 | export const isSymbol = v => typeof v === "symbol";
10 | export const isBoolean = v => typeof v === "boolean";
11 | export const isObject = v => typeof v === "object" && v !== null;
12 |
13 | export const isDev = process.env.NODE_ENV !== "production";
14 |
15 | export const noop = function() {};
16 |
17 | export const nextTick = cb => {
18 | if (typeof window.Promise === "function") {
19 | Promise.resolve().then(cb);
20 | } else {
21 | setTimeout(cb, 0);
22 | }
23 | };
24 |
25 | // 将 HTTP content-type 类型转换成 highlight.js 所支持的语法高亮类型
26 | export const mimeType2Language = mimeType => {
27 | switch (true) {
28 | case /javascript/.test(mimeType):
29 | return "javascript";
30 | case /json/.test(mimeType):
31 | return "json";
32 | case /html/.test(mimeType):
33 | return "html";
34 | case /css/.test(mimeType):
35 | return "css";
36 | default:
37 | return "";
38 | }
39 | };
40 |
41 | /**
42 | * generate an uuid
43 | * @returns string
44 | */
45 | export const uuid = () => {
46 | let id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
47 | let r = (Math.random() * 16) | 0;
48 | let v = c === "x" ? r : (r & 0x3) | 0x8;
49 | return v.toString(16);
50 | });
51 | return id;
52 | };
53 |
54 | export const flatMap = (arr, callback) => {
55 | let r = [];
56 | arr.forEach((v, i) => {
57 | const r2 = callback(v, i);
58 | r2.forEach(v2 => r.push(v2));
59 | });
60 | return r;
61 | };
62 |
63 | export const formatFileName = (s = "") => s.replace(/https?:\/\/.*\/(.*)/g, "$1");
64 |
65 | /**
66 | * 在目标对象上创建一个 stack 属性表示调用堆栈
67 | * @param {*} targetObject
68 | * @param {*} constructorOpt 该函数(含自身)以上的堆栈都会被忽略
69 | */
70 | export const createStack = (targetObject, constructorOpt) => {
71 | // Chrome 提供 captureStackTrace 方法记录堆栈信息到目标对象的 stack 属性,参考
72 | if (typeof Error.captureStackTrace === "function") {
73 | Error.captureStackTrace(targetObject, constructorOpt);
74 | }
75 | // safari 创建的 Error 对象包含 stack 属性记录堆栈信息
76 | if (typeof targetObject.stack === "string") {
77 | targetObject.stack = formatFileName(targetObject.stack);
78 | }
79 | };
80 |
81 | // 获取 URL 中的文件名(含扩展名)
82 | export const getURLFileName = url => {
83 | if (typeof url !== "string") return "";
84 |
85 | const index = url.lastIndexOf("/");
86 | return url.slice(index + 1);
87 | };
88 |
--------------------------------------------------------------------------------
/src/panels/network/TabPreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
75 |
76 |
77 |
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@whinc/web-console",
3 | "version": "0.7.2",
4 | "author": "whincwu@163.com",
5 | "license": "MIT",
6 | "keywords": [
7 | "console",
8 | "vConsole",
9 | "web-console",
10 | "webConsole",
11 | "log"
12 | ],
13 | "repository": "https://github.com/whinc/web-console",
14 | "main": "dist/web-console.umd.min.js",
15 | "scripts": {
16 | "serve": "vue-cli-service serve",
17 | "build:lib": "vue-cli-service build --target lib --name web-console --dest dist src/main.js",
18 | "depoly:lib": "npm run lint && npm run build:lib && cross-env NPM_TOKEN=$NPM_TOKEN semantic-release && npm run changelog",
19 | "build:demo": "vue-cli-service build --target app --dest demo",
20 | "depoly:demo": "npm run build:demo && npx gh-pages -d demo",
21 | "lint": "vue-cli-service lint src/**/*.js",
22 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
23 | "report": "vue-cli-service build --report",
24 | "test:unit": "vue-cli-service test:unit",
25 | "test:api": "cd ./tests/api && nodemon index.js",
26 | "e2e": "vue-cli-service e2e"
27 | },
28 | "dependencies": {
29 | "cookie_js": "^1.2.2",
30 | "highlight.js": "^9.14.2",
31 | "lodash.clonedeep": "^4.5.0",
32 | "mint-ui": "^2.2.13",
33 | "specificity": "^0.4.1",
34 | "url-search-params": "^1.1.0",
35 | "vue": "^2.5.22",
36 | "vue-infinite-scroll": "^2.0.2"
37 | },
38 | "devDependencies": {
39 | "@commitlint/cli": "^7.5.0",
40 | "@commitlint/config-conventional": "^7.5.0",
41 | "@vue/cli-plugin-babel": "^3.4.1",
42 | "@vue/cli-plugin-e2e-cypress": "^3.4.1",
43 | "@vue/cli-plugin-eslint": "^3.4.1",
44 | "@vue/cli-plugin-unit-jest": "^3.5.1",
45 | "@vue/cli-service": "^3.4.1",
46 | "@vue/test-utils": "^1.0.0-beta.28",
47 | "babel-core": "^7.0.0-0",
48 | "babel-jest": "^22.4.4",
49 | "babel-plugin-component": "^1.1.1",
50 | "conventional-changelog-angular": "^5.0.3",
51 | "conventional-changelog-cli": "^2.0.12",
52 | "cross-env": "^5.2.0",
53 | "cz-conventional-changelog": "^2.1.0",
54 | "gh-pages": "^1.2.0",
55 | "gulp": "^4.0.0",
56 | "husky": "^1.3.1",
57 | "i": "^0.3.6",
58 | "node-sass": "^4.11.0",
59 | "nodemon": "^1.18.9",
60 | "prettier": "1.14.2",
61 | "pretty-quick": "^1.10.0",
62 | "sass-loader": "^6.0.6",
63 | "semantic-release": "^15.13.3",
64 | "semantic-release-cli": "^4.1.1",
65 | "vue-template-compiler": "^2.5.22"
66 | },
67 | "browserslist": [
68 | "> 1%",
69 | "last 2 versions",
70 | "not ie <= 8"
71 | ],
72 | "husky": {
73 | "hooks": {
74 | "pre-commit": "pretty-quick --staged",
75 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
76 | }
77 | },
78 | "prettier": {
79 | "printWidth": 120
80 | },
81 | "commitlint": {
82 | "extends": [
83 | "@commitlint/config-conventional"
84 | ]
85 | },
86 | "release": {
87 | "ci": false,
88 | "plugins": [
89 | "@semantic-release/commit-analyzer",
90 | "@semantic-release/release-notes-generator",
91 | "@semantic-release/npm"
92 | ]
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/panels/element/Tag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <
5 | {{tagName}}
6 |
7 |
8 | {{name}}
9 | ="
10 | {{value}}
11 | "
12 |
13 | >
14 |
15 |
16 | </
17 | {{tagName}}
18 | >
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
86 |
87 |
113 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # web-console
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 | web-console 是一款基于 H5 开发的移动端 Web 调试工具。其高度还原了 Chrome DevTools 的功能和交互,支持 webpack 打包和`
83 |
84 | ```
85 |
86 | 通过下面代码初始化
87 |
88 | ```js
89 | new WebConsole(config);
90 | ```
91 |
92 | ## API
93 |
94 | `WebConsole`构造参数如下:
95 |
96 | | 字段 | 类型 | 必填 | 备注 |
97 | | --------- | ------------ | ----- | ------------------------------------ |
98 | | panelVisible | bool | false | 是否自动弹窗主面板 |
99 | | activeTab | string | 'console' | 默认激活的 Tab 面板,支持'element', 'console', 'network', 'application', 以及插件的 id |
100 | | entryStyle | string | 'button' | 入口样式,支持两种'button'和'icon' |
101 |
102 | > 后续补充更多的配置参数和 API 接口
103 |
104 | ## 插件开发
105 |
106 | web-console 提供一些开箱即用的功能,如果这些无法满足你的需求,你还可以通过 web-console 提供的插件机制,添加第三方编写的插件来扩展功能。
107 |
108 | 可参考下面资源:
109 |
110 | - [插件开发文档](./docs/plugin.md)
111 | - [插件项目模板](https://github.com/whinc/web-console-plugin)
112 |
113 | ## 更新日志
114 |
115 | [CHANGELOG](CHANGELOG.md)
116 |
117 | ## 相似项目
118 |
119 | **Web**
120 | - [vConsole](https://github.com/Tencent/vConsole) A lightweight, extendable front-end developer tool for mobile web page.
121 | - [eruda](https://github.com/liriliri/eruda) Console for mobile browsers
122 |
123 | **Native**
124 | - [wt-console](https://github.com/WeBankFinTech/wt-console) A lightweight, extendable react-native developer and tester tool
125 |
126 | ## License
127 |
128 | MIT
129 |
--------------------------------------------------------------------------------
/src/panels/network/NetworkRequest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{requestInfo.url}}
5 | {{requestInfo.method}}
6 | {{requestInfo.displayStatus}}
7 | {{requestInfo.type}}
8 |
9 |
10 |
11 |
12 | Preview
13 | Response
14 |
15 |
16 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
80 |
81 |
127 |
--------------------------------------------------------------------------------
/src/panels/element/StyleProperty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{name}}
6 | :
7 |
8 |
9 |
10 |
11 | {{colorValueList[0]}}
12 |
13 | {{colorValueList[2]}}
14 |
15 | {{value}}
16 |
17 | ;
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
94 |
95 |
143 |
--------------------------------------------------------------------------------
/src/panels/application/ApplicationPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 | {{desc}}
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
25 |
109 |
110 |
136 |
--------------------------------------------------------------------------------
/tests/api/index.js:
--------------------------------------------------------------------------------
1 | var http = require("http");
2 | var fs = require("fs");
3 | const url = require("url");
4 |
5 | const routes = {
6 | "/get_data": function(req, res) {
7 | const queryParams = url.parse(req.url, true).query;
8 | const mimeType = queryParams["mime_type"];
9 | const responseHeaders = {
10 | "Access-Control-Allow-Origin": "*",
11 | "Content-Type": mimeType || "text/plain"
12 | };
13 | res.writeHead(200, responseHeaders);
14 |
15 | // 参考 http://devdocs.io/http/basics_of_http/mime_types
16 | switch (mimeType) {
17 | case "application/octet-stream":
18 | break;
19 | case "text/javascript":
20 | case "application/javascript":
21 | fs.readFile("./data/response.js", function(err, data) {
22 | res.end(data);
23 | });
24 | break;
25 | case "application/json":
26 | fs.readFile("./data/response.json", function(err, data) {
27 | res.end(data);
28 | });
29 | break;
30 | case "image/jpeg":
31 | fs.readFile("./data/response.jpg", function(err, data) {
32 | res.end(data);
33 | });
34 | break;
35 | case "image/png":
36 | fs.readFile("./data/response.png", function(err, data) {
37 | res.end(data);
38 | });
39 | break;
40 | case "image/gif":
41 | fs.readFile("./data/response.gif", function(err, data) {
42 | res.end(data);
43 | });
44 | break;
45 | case "image/svg+xml":
46 | fs.readFile("./data/response.svg", function(err, data) {
47 | res.end(data);
48 | });
49 | break;
50 | case "text/html":
51 | fs.readFile("./data/response.html", function(err, data) {
52 | res.end(data);
53 | });
54 | break;
55 | case "text/css":
56 | fs.readFile("./data/response.css", function(err, data) {
57 | res.end(data);
58 | });
59 | break;
60 | case "text/plain":
61 | default:
62 | fs.readFile("./data/response.txt", function(err, data) {
63 | res.end(data);
64 | });
65 | break;
66 | }
67 | },
68 | // 获取不同的 HTTP 状态码
69 | "/get_status/\\d+": function(req, res) {
70 | const pathname = url.parse(req.url).pathname;
71 | const status = new RegExp("/get_status/(\\d+)").exec(pathname)[1];
72 | res.writeHead(status, {
73 | "Access-Control-Allow-Method": "GET,POST,OPTIONS",
74 | "Access-Control-Allow-Headers": "Content-Type",
75 | "Access-Control-Allow-Origin": "*",
76 | "Content-Type": "text/plain; charset=utf-8"
77 | });
78 | res.end("response status: " + status);
79 | },
80 | "/get": function(req, res) {
81 | res.writeHead(200, {
82 | "Access-Control-Allow-Origin": "*",
83 | "Content-Type": "text/plain; charset=utf-8"
84 | });
85 | res.end("get hello");
86 | },
87 | "/post": function(req, res) {
88 | res.writeHead(200, {
89 | "Access-Control-Allow-Method": "GET,POST,OPTIONS",
90 | "Access-Control-Allow-Headers": "Content-Type",
91 | "Access-Control-Allow-Origin": "*",
92 | "Content-Type": "text/plain; charset=utf-8"
93 | });
94 | res.end("post");
95 | }
96 | };
97 |
98 | const httpServer = http
99 | .createServer(function(req, res) {
100 | const urlObj = url.parse(req.url);
101 | const pathname = urlObj.pathname;
102 | console.log("received request:", pathname + "?" + urlObj.query);
103 |
104 | const key = Object.keys(routes).find(route => new RegExp(route).test(pathname));
105 | if (key) {
106 | routes[key](req, res);
107 | } else {
108 | res.writeHead(404, { "Content-Type": "text/html" });
109 | res.end("404 Not Found");
110 | }
111 |
112 | // if (typeof routes[pathname] === 'function') {
113 | // routes[pathname](req, res)
114 | // } else {
115 | // res.writeHead(404, {'Content-Type': 'text/html'})
116 | // res.end('404 Not Found')
117 | // }
118 | })
119 | .listen(8090);
120 |
121 | // 控制台会输出以下信息
122 | console.log("Server running at http://localhost:" + httpServer.address().port);
123 |
--------------------------------------------------------------------------------
/src/components/VJSONViewer/JSONTextInlineBlock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [
5 |
6 |
11 | ,
12 |
13 | ]
14 |
15 |
16 |
17 |
18 | {
19 |
20 |
21 |
22 | {{propName}}:
23 |
28 | ,
29 |
30 | , ...
31 |
32 |
33 | ...
34 | }
35 |
36 |
37 |
38 |
39 | "
40 | {{value}}
41 | "
42 |
43 |
44 | {{value}}
45 |
46 |
47 |
48 | {{formattedValue}}
49 |
50 |
51 |
121 |
122 |
149 |
--------------------------------------------------------------------------------
/docs/plugin.md:
--------------------------------------------------------------------------------
1 | # web-console 插件开发
2 |
3 | 通过插件可以增强 web-console 的能力,扩大使用场景。web-console 插件基于 Vue 组件开发,提供了丰富的 API、生命周期方法、内置组件、自定义偏好设置,同时提供了一个初始的插件开发项目模板方便开发、测试和发布。
4 |
5 | ## 一个简单的插件示例
6 |
7 | 下面是一个简单的插件示例。
8 |
9 | 首先,创建一个文件 MyPlugin.js 用于定义插件。
10 |
11 | ```js
12 | export default function(WebConsole, options) {
13 | return new WebConsole.Plugin({
14 | id: "myPlugin",
15 | name: "我的插件",
16 | component: {
17 | render(h) {
18 | return h("h1", null, "这是我的第一个插件");
19 | }
20 | }
21 | });
22 | }
23 | ```
24 |
25 | 然后通过`WebConsole.use()`方法注册插件,当 web-console 运行起来后,插件会自动加载到主面板。
26 |
27 | ```js
28 | // 使用插件
29 | import WebConsole from "@whinc/web-console";
30 | import MyPlugin from "my-plugin";
31 |
32 | WebConsole.use(MyPlugin);
33 | new WebConsole();
34 | ```
35 |
36 | 运行截图如下:
37 |
38 | 
39 |
40 | ## 创建插件
41 |
42 | web-console 插件是一个返回`WebConsole.Plugin`实例的函数,函数的第一个参数是`WebConsole`对象,第二个参数是传递给插件的参数。
43 |
44 | ```js
45 | function MyPlugin(WebConsole, options) {
46 | return new WebConsole.Plugin({
47 | /*...*/
48 | });
49 | }
50 | ```
51 |
52 | `WebConsole.Plugin`类是所有插件的基类,返回的插件实例必须继承自它,它的构造函数接受一个配置参数,支持的字段及取值如下:
53 |
54 | | 字段 | 类型 | 必填 | 备注 |
55 | | --------- | ------------ | ----- | ------------------------------------ |
56 | | id | string | true | 不能与已安装的插件 id 相同 |
57 | | name | string | false | 插件名称,用于显示,缺省时与 id 相同 |
58 | | component | VueComponent | true | 插件主面板,是一个 Vue 组件 |
59 | | settings | Object | false | 增加到设置面板的设置项 |
60 |
61 | > 参数配置中的 component 注入了一些 web-console 组件,可以直接使用。(后续补充关于这些内置组件的文档)
62 |
63 | ## 注册插件
64 |
65 | 通过`WebConsole.use()`方法注册插件。
66 |
67 | ```js
68 | WebConsole.use(MyPlugin, {/*...*/})
69 |
70 | new WebConsole(})
71 | ```
72 |
73 | > 注意:插件必须在 WebConsole 实例化之前注册,否则不生效。
74 |
75 | ## 插件生命周期
76 |
77 | 目前插件支持如下生命周期方法,这些插件方法在`component`所赋值的组件中可用。
78 |
79 | | 生命周期方法 | 执行时机 | 备注 |
80 | | --------------------------------------------------- | ---------------------------- | ----------------------------- |
81 | | `onWebConsoleReady(hostProxy)` | 主面板首次渲染完成时 | 可访问 DOM 和配置,仅执行一次 |
82 | | `onWebConsoleShow(hostProxy)` | 主面板从不可见变为**可见**时 | |
83 | | `onWebConsoleHide(hostProxy)` | 主面板从可见变为**不可见**时 |
84 | | `onWebConsoleTabChanged(hostProxy, newTab, oldTab)` | 切换不同子面板时 |
85 | | `onWebConsoleSettingsLoaded(hostProxy, settings)` | 偏好设置首次加载时 |
86 | | `onWebConsoleSettingsChanged(hostProxy, settings)` | 偏好设置变化时 |
87 |
88 | 使用示例:
89 |
90 | ```js
91 | export default function(WebConsole, options) {
92 | return new WebConsole.Plugin({
93 | id: "myPlugin",
94 | name: "我的插件",
95 | component: {
96 | render(h) {
97 | return h("h1", null, "这是我的第一个插件");
98 | },
99 | methods: {
100 | onWebConsoleShow() {
101 | console.log("Show");
102 | },
103 | onWebConsoleHide() {
104 | console.log("Hide");
105 | }
106 | }
107 | }
108 | });
109 | }
110 | ```
111 |
112 | ## 插件偏好设置
113 |
114 | web-console 有一个集中的偏好设置界面(点击主面板右上角的齿轮进入),插件可以在其中添加自己的偏好设置项,添加方式是在创建插件时指定`settings`属性,该属性接受一个数组,数组每个元素是一个设置描述对象。
115 |
116 | 支持的设置描述对象如下(还会支持更多):
117 |
118 | ```js
119 | // 复选框
120 | {
121 | type: 'checkbox',
122 | desc: '是否开启',
123 | name: 'isOpen',
124 | value: false
125 | }
126 |
127 | // 下拉选择框
128 | {
129 | type: "select",
130 | desc: "选择颜色",
131 | name: 'color',
132 | value: 'red',
133 | options: [
134 | { text: "红色", value: 'red' },
135 | { text: "绿色", value: 'green' },
136 | { text: "蓝色", value: 'blue' },
137 | ]
138 | }
139 | ```
140 |
141 | 使用示例:
142 |
143 | ```js
144 | export default function(WebConsole, options) {
145 | return new WebConsole.Plugin({
146 | id: "myPlugin",
147 | name: "我的插件",
148 | component: {
149 | render(h) {
150 | return h("h1", null, "这是我的第一个插件");
151 | },
152 | methods: {
153 | onWebConsoleSettingsLoaded(hostProxy, settings) {
154 | console.log("加载设置:", settings);
155 | },
156 | onWebConsoleSettingsChanged(hostProxy, settings) {
157 | console.log("设置变化:", settings);
158 | }
159 | }
160 | },
161 | settings: [
162 | {
163 | type: "checkbox",
164 | desc: "是否选中",
165 | name: "isChecked",
166 | value: false
167 | }
168 | ]
169 | });
170 | }
171 | ```
172 |
173 | 运行截图:
174 |
175 | 
176 |
177 | 
178 |
--------------------------------------------------------------------------------
/src/panels/element/TabStyles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Inherited from
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
137 |
138 |
170 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./App.vue";
3 | import "./polyfill";
4 | import { consoleHooks, filters, isFunction } from "@/utils";
5 | import { pluginManager, Plugin } from "@/plugins";
6 | import { PanelType } from "@/constants";
7 | import InfiniteScroll from "vue-infinite-scroll";
8 | import "./styles/_global.scss";
9 |
10 | // register filters
11 | Object.keys(filters).forEach(name => {
12 | Vue.filter(name, filters[name]);
13 | });
14 |
15 | // Fix: 避免重复注册插件(从 mint-ui 库引入和从 vue-infinite-scroll 引入是同一个插件的两个不同实例)
16 | const installedPlugins = Vue._installedPlugins || [];
17 | const foundIndex = installedPlugins.findIndex(plugin => isFunction(plugin.bind) && isFunction(plugin.unbind));
18 | if (foundIndex === -1) {
19 | Vue.use(InfiniteScroll);
20 | } else {
21 | console.warn('"vue-infinite-scroll" has been registered');
22 | }
23 |
24 | // const logger = new Logger("[main.js]");
25 |
26 | // 放在可滚动容器上,在滚动触顶或触底时,可以阻止背景层滚动
27 | Vue.directive("prevent-bkg-scroll", {
28 | bind(el) {
29 | let preventMove = false;
30 | el.addEventListener("touchstart", function() {
31 | // logger.log('touchstart scrollTop: %s, clientHeight: %s, scrollHeight: %s', el.scrollTop, el.clientHeight, el.scrollHeight)
32 | if (el.scrollTop <= 0) {
33 | // 滚动到顶部时将其设置为 1,避免触顶后不能向下滚动(IOS 上 scrollTop 会出现负数)
34 | el.scrollTop = 1;
35 | // 如果内容不足一屏,则禁用 touchmove
36 | if (el.scrollTop === 0) {
37 | preventMove = true;
38 | }
39 | } else if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
40 | // 滚动到底部时,将滚动距离设置为最大可滚动距离 - 1,避免触底后不能向上滚动
41 | el.scrollTop = el.scrollHeight - el.clientHeight - 1;
42 | // 如果内容不足一屏
43 | if (el.scrollTop + el.clientHeight === el.scrollHeight) {
44 | preventMove = true;
45 | }
46 | }
47 | });
48 | el.addEventListener("touchmove", function(e) {
49 | if (preventMove) {
50 | e.preventDefault();
51 | }
52 | });
53 | el.addEventListener("touchend", function() {
54 | preventMove = false;
55 | });
56 | }
57 | });
58 |
59 | Vue.config.productionTip = false;
60 |
61 | // WebConsole 只能创建一个实例
62 | let _webConsole = null;
63 |
64 | class WebConsole {
65 | // 注册插件
66 | static use(pluginCreator, options) {
67 | if (_webConsole) {
68 | console.warn("Plugin must be registered before creating WebConsole");
69 | return;
70 | }
71 |
72 | if (!isFunction(pluginCreator)) {
73 | console.warn("Invalid plugin:", pluginCreator);
74 | return;
75 | }
76 | const plugin = pluginCreator.call(null, WebConsole, options);
77 | pluginManager.addPlugin(plugin);
78 | }
79 |
80 | constructor(options) {
81 | if (_webConsole) {
82 | console.warn("WebConsole has been initialized, return previous instance: %O", _webConsole);
83 | return _webConsole;
84 | }
85 |
86 | _webConsole = this;
87 | this._isLoaded = false;
88 | this._load(this._processOptions(options));
89 | }
90 |
91 | _processOptions(options) {
92 | const defaultOptions = {
93 | panelVisible: false,
94 | activeTab: "console",
95 | entryStyle: "button"
96 | };
97 |
98 | const supportTab = Object.keys(PanelType).map(key => PanelType[key]);
99 |
100 | let activeTab = options.activeTab;
101 | if (supportTab.indexOf(activeTab) === -1 && !pluginManager.hasPlugin(activeTab)) {
102 | activeTab = defaultOptions.activeTab;
103 | }
104 |
105 | return {
106 | panelVisible: options.panelVisible || defaultOptions.panelVisible,
107 | entryStyle: options.entryStyle || defaultOptions.entryStyle,
108 | activeTab: activeTab
109 | };
110 | }
111 |
112 | _load(options = {}) {
113 | if (!this._isLoaded && (document.readyState === "interactive" || document.readyState === "complete")) {
114 | this._isLoaded = true;
115 | const root = document.createElement("div");
116 | (document.documentElement || document.body).appendChild(root);
117 |
118 | const vm = new Vue({
119 | el: root,
120 | render: h =>
121 | h(App, {
122 | props: {
123 | initPanelVisible: options.panelVisible,
124 | initActiveTab: options.activeTab,
125 | initEntryStyle: options.entryStyle
126 | }
127 | })
128 | });
129 | } else {
130 | document.addEventListener("readystatechange", this._load.bind(this, ...arguments));
131 | }
132 | }
133 | }
134 |
135 | WebConsole.Plugin = Plugin;
136 |
137 | if (!window.WebConsole) {
138 | window.WebConsole = WebConsole;
139 | consoleHooks.install();
140 | } else {
141 | console.error("Inject WebConsole into window failed");
142 | }
143 | export default WebConsole;
144 |
--------------------------------------------------------------------------------
/src/components/VHighlightView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
54 |
55 |
62 |
63 |
221 |
--------------------------------------------------------------------------------
/src/panels/element/BoxModel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | position
6 | {{computedStyle.top | format('position')}}
7 |
8 | {{computedStyle.left | format('position')}}
9 |
10 |
11 |
margin
12 |
{{computedStyle.marginTop | format}}
13 |
14 |
{{computedStyle.marginLeft | format}}
15 |
16 |
border
17 |
{{computedStyle.borderTopWidth | format}}
18 |
19 |
{{computedStyle.borderLeftWidth | format}}
20 |
21 |
padding
22 |
{{computedStyle.paddingTop | format}}
23 |
24 |
{{computedStyle.paddingLeft | format}}
25 |
26 | {{computedStyle.width | format('content')}} x {{computedStyle.height | format('content')}}
27 |
28 |
{{computedStyle.paddingRight | format}}
29 |
30 |
{{computedStyle.paddingBottom | format}}
31 |
32 |
{{computedStyle.borderRightWidth | format}}
33 |
34 |
{{computedStyle.borderBottomWidth | format}}
35 |
36 |
{{computedStyle.marginRight | format}}
37 |
38 |
{{computedStyle.marginBottom | format}}
39 |
40 |
41 | {{computedStyle.right | format('position')}}
42 |
43 | {{computedStyle.bottom | format('position')}}
44 |
45 |
46 |
47 |
48 |
49 |
83 |
84 |
140 |
--------------------------------------------------------------------------------
/public/test_network.js:
--------------------------------------------------------------------------------
1 | window.$network = (function() {
2 | var baseURL = "http://localhost:8090";
3 | var nodeApi = "https://nodejs.org/dist/latest-v8.x/docs/api/index.json";
4 |
5 | function testXMLHttpRequest() {}
6 |
7 | function testFetch() {
8 | if (typeof window.fetch !== "function") {
9 | console.warn("current browser version don't support fetch api");
10 | return;
11 | }
12 |
13 | var url = baseURL + "/get_status/" + 200;
14 |
15 | // 不带选项
16 | var options = {
17 | method: "GET",
18 | headers: {
19 | "Content-Type": "text/plain"
20 | }
21 | };
22 | fetch(url);
23 | fetch(url, options);
24 |
25 | fetch(new Request(url));
26 | fetch(new Request(url, options));
27 |
28 | fetch(new Request(url, options), {
29 | headers: {
30 | "Content-Type": "text/html"
31 | }
32 | });
33 | }
34 |
35 | // 测试 HTTP 状态码
36 | function testHTTPStatus() {
37 | request({ url: nodeApi });
38 | [100, 200, 300, 400, 500].forEach(function(status) {
39 | request({ url: baseURL + "/get_status/" + status });
40 | });
41 | }
42 |
43 | /**
44 | * 测试请求参数
45 | */
46 | function testRequestParams(type = "get") {
47 | var email = "xx@yy.com";
48 | var password = "zz";
49 |
50 | switch (type) {
51 | case "get":
52 | // GET
53 | request({ url: baseURL + "/get?email=" + email + "&password=" + password + "&a=&b" });
54 | break;
55 | case "post:raw":
56 | request({
57 | url: baseURL + "/post",
58 | method: "POST",
59 | data: "hello"
60 | });
61 | break;
62 | case "post:raw:text":
63 | request({
64 | url: baseURL + "/post",
65 | method: "POST",
66 | requestHeaders: {
67 | "Content-Type": "text/plain"
68 | },
69 | data: "hello"
70 | });
71 | break;
72 | case "post:raw:json":
73 | request({
74 | url: baseURL + "/post",
75 | method: "POST",
76 | requestHeaders: {
77 | "Content-Type": "application/json;charset=UTF-8"
78 | },
79 | data: '{"email": aa}'
80 | });
81 | break;
82 | case "post:x-www-form-urlencoded":
83 | request({
84 | url: baseURL + "/post",
85 | method: "POST",
86 | requestHeaders: {
87 | "Content-Type": "application/x-www-form-urlencoded"
88 | },
89 | data: "email=" + encodeURIComponent(email) + "&password=" + encodeURIComponent(password)
90 | });
91 | break;
92 | case "post:form-data":
93 | var formData = new FormData();
94 | formData.append("email", email);
95 | formData.append("password", password);
96 |
97 | request({
98 | url: baseURL + "/post",
99 | method: "POST",
100 | data: formData
101 | });
102 | break;
103 | }
104 | }
105 |
106 | // 测试响应数据类型
107 | // 参考 http://devdocs.io/http/basics_of_http/mime_types
108 | function testResponseData() {
109 | const mimeTypeList = [
110 | "application/json",
111 | "application/javascript",
112 | "text/css",
113 | "text/plain",
114 | "text/html",
115 | "image/jpeg",
116 | "image/png",
117 | "image/gif",
118 | "image/svg+xml"
119 | ];
120 |
121 | mimeTypeList.forEach(mimeType => {
122 | request({ url: baseURL + "/get_data/?mime_type=" + encodeURIComponent(mimeType) });
123 | });
124 | }
125 |
126 | return {
127 | testHTTPStatus: testHTTPStatus,
128 | testResponseData: testResponseData,
129 | testRequestParams: testRequestParams,
130 | testFetch: testFetch,
131 | testXMLHttpRequest: testXMLHttpRequest
132 | };
133 | })();
134 |
135 | function request(options) {
136 | options = options || {};
137 | var url = options.url;
138 | var method = options.method || "GET";
139 | var data = options.data || null;
140 | var requestHeaders = options.requestHeaders || {};
141 |
142 | /* XMLHttpRequest */
143 | var xhr = new window.XMLHttpRequest();
144 | xhr.onreadystatechange = function() {
145 | // console.log("readyState:", this.readyState);
146 | if (this.readyState === XMLHttpRequest.DONE) {
147 | // console.log(this.getResponseHeader("content-type"));
148 | // console.log(this.response);
149 | }
150 | };
151 | xhr.open(method, url);
152 | Object.keys(requestHeaders).forEach(key => {
153 | xhr.setRequestHeader(key, requestHeaders[key]);
154 | });
155 | xhr.send(data);
156 |
157 | /* fetch */
158 | if (typeof window.fetch === "function") {
159 | fetch(url, {
160 | method: method,
161 | body: data,
162 | headers: requestHeaders
163 | });
164 | } else {
165 | console.warn('current brwoser don\'t support "fetch"');
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/panels/element/StyleRule.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{selector}}
5 | {
6 | {{rule.href}}
7 |
8 |
9 |
15 |
16 |
17 | }
18 |
19 |
20 |
21 |
22 |
166 |
167 |
203 |
--------------------------------------------------------------------------------
/src/panels/application/XStorage.js:
--------------------------------------------------------------------------------
1 | import { Logger, cloneDeep } from "@/utils";
2 | import { cookie } from "cookie_js";
3 |
4 | const logger = new Logger("XStorage");
5 |
6 | class CookieStorage {
7 | constructor() {
8 | this._keys = [];
9 | this.refresh();
10 | }
11 |
12 | refresh() {
13 | const cookies = cookie.all();
14 | this._keys = Object.keys(cookies);
15 | }
16 |
17 | get length() {
18 | return this._keys.length;
19 | }
20 |
21 | key(n) {
22 | return this._keys[n];
23 | }
24 |
25 | getItem(key) {
26 | return cookie.get(key);
27 | }
28 |
29 | setItem(key, value) {
30 | cookie.set(key, value);
31 | this.refresh();
32 | }
33 |
34 | removeItem(key) {
35 | cookie.remove(key);
36 | this.refresh();
37 | }
38 |
39 | clear() {
40 | cookie.empty();
41 | this.refresh();
42 | }
43 | }
44 |
45 | /**
46 | * 统一 localStorage/sessionStorage/cookie 三种存储器接口
47 | *
48 | * XStorage
49 | * |
50 | * | <- XStorage.refresh()
51 | * |
52 | * Filtered Data Source
53 | * |
54 | * | <- XStorage._refreshFilteredDataSource()
55 | * |
56 | * Data Source
57 | * |
58 | * | <- XStorage._refreshDataSource()
59 | * |
60 | * CookieStorage | SessionStorage | LocalStorage
61 | * |
62 | * | <- CookieStorage.refresh()
63 | * |
64 | * cookie
65 | */
66 | class XStorage {
67 | // 分页大小
68 | static get PAGE_SIZE() {
69 | return 20;
70 | }
71 |
72 | constructor(type) {
73 | if (type === "localStorage" || type === "sessionStorage") {
74 | this._storage = window[type];
75 | } else if (type === "cookieStorage") {
76 | this._storage = new CookieStorage();
77 | } else {
78 | logger.error("unsupport storage type:", type);
79 | }
80 |
81 | // data source
82 | this._ds = [];
83 | // filterd data source
84 | this._fds = [];
85 | // filter keyword
86 | this._filter = "";
87 | // 下次加载的开始位置
88 | this._cursor = 0;
89 |
90 | this.refresh();
91 | }
92 |
93 | setFilter(filter) {
94 | const isChange = filter !== this._filter;
95 |
96 | if (typeof filter === "string" && filter) {
97 | this._filter = filter;
98 | } else {
99 | this._filter = "";
100 | }
101 |
102 | if (isChange) {
103 | this._refreshFilteredDataSource();
104 | }
105 | }
106 |
107 | refresh() {
108 | // 在 XStorage 和 cookie 之间增加了一层 CookieStorage
109 | // 当 cookie 发生变化后需要手动刷新 CookieStorage
110 | if (this._storage instanceof CookieStorage) {
111 | this._storage.refresh();
112 | }
113 |
114 | this._cursor = 0;
115 | this._refreshDataSource();
116 | }
117 |
118 | // 从指定位置开始,加载一页数据
119 | loadItems(startIndex, pageSize = XStorage.PAGE_SIZE) {
120 | return cloneDeep(this._fds.slice(startIndex, startIndex + pageSize));
121 | }
122 |
123 | // 从上一次加载结束位置开始,加载一页数据
124 | loadMoreItems() {
125 | logger.log("loadMoreItems");
126 | const items = this.loadItems(this._cursor);
127 | this._cursor += items.length;
128 | return items;
129 | }
130 |
131 | hasMore() {
132 | return this._cursor < this._fds.length;
133 | }
134 |
135 | get length() {
136 | return this._fds.length;
137 | }
138 |
139 | getItem(key) {
140 | return this._storage.getItem(key);
141 | }
142 |
143 | setItem(key, value) {
144 | this._storage.setItem(key, value);
145 | /**
146 | * 更新存在两种情况:
147 | * 1) 更新已存在项,这种情况不需要刷新数据源,只要更新数据源中指定项即可
148 | * 2) 新增项,这种情况必须刷新数据源,保证读出的数据源列表保持自然顺序
149 | *
150 | * 针对第 2 中情况进行示例说明,假设分页大小为 2 并且 localStorage 是倒序读取(storage 读取顺序是浏览器决定的,这里假设这种读取方式)
151 | * data source UI data action
152 | * [A, B, C, D] [D, C] 修改 D -> E
153 | * {E, A, B, C} [E, C] 加载下一页
154 | * {E, A, B, C} [E, C, A, E]
155 | * 由于修改后导致读取顺序变化,分页加载的数据不再准确
156 | */
157 | const item = this._ds.find(item => item.key === key);
158 | if (item) {
159 | // 更新已存在项
160 | item.value = value;
161 | const item2 = this._fds.find(item => item.key === key);
162 | if (item2) {
163 | item2.value = value;
164 | }
165 | } else {
166 | // 新增项
167 | this._refreshDataSource();
168 | }
169 | }
170 |
171 | removeItem(key) {
172 | this._storage.removeItem(key);
173 |
174 | /**
175 | * 为了提高性能并保留已读取出来的数据,不直接刷新数据源,而是从数据源中删除指定项
176 | * 删除不会影响已读取数据源中数据的排列顺序
177 | */
178 | const foundIndex = this._ds.findIndex(item => item.key === key);
179 | if (foundIndex !== -1) {
180 | this._ds.splice(foundIndex, 1);
181 | const foundIndex2 = this._fds.findIndex(item => item.key === key);
182 | if (foundIndex2 !== -1) {
183 | this._fds.splice(foundIndex2, 1);
184 | }
185 | // 当前游标前移 1 格
186 | this._cursor -= 1;
187 | }
188 | // logger.log('removeItem', this)
189 | }
190 |
191 | clear() {
192 | this._storage.clear();
193 | this._ds = [];
194 | this._fds = [];
195 | this._cursor = 0;
196 | }
197 |
198 | // 更新数据源
199 | _refreshDataSource() {
200 | logger.log("_refreshDataSource", this);
201 | let key, value;
202 | const storage = this._storage;
203 | const length = this._storage.length;
204 | this._ds = [];
205 | for (let j = 0; j < length; ++j) {
206 | key = storage.key(j);
207 | value = storage.getItem(key);
208 | this._ds.push({ key, value });
209 | }
210 | this._refreshFilteredDataSource();
211 | }
212 |
213 | // 更新过滤数据源
214 | _refreshFilteredDataSource() {
215 | logger.log("_refreshFilteredDataSource", this);
216 | const filter = this._filter;
217 | const ds = this._ds;
218 | this._fds = [];
219 | if (filter) {
220 | // 过滤出 key 或 value 中包含关键字的项
221 | this._fds = ds.filter(({ key, value }) => key.indexOf(filter) >= 0 || value.indexOf(filter) >= 0);
222 | } else {
223 | this._fds = ds;
224 | }
225 | }
226 | }
227 |
228 | window.CookieStorage = CookieStorage;
229 | window.XStorage = XStorage;
230 | export default XStorage;
231 |
--------------------------------------------------------------------------------
/public/test_plugin.js:
--------------------------------------------------------------------------------
1 | function plugin1(WebConsole) {
2 | return new WebConsole.Plugin({
3 | id: "plugin1",
4 | name: "插件1",
5 | component: function() {
6 | var plugin = this;
7 | var tag = plugin.toString();
8 | return {
9 | data: function() {
10 | return {
11 | text: "plugin1 panel",
12 | activeTab: "A"
13 | };
14 | },
15 | methods: {
16 | onWebConsoleReady: function(hostProxy) {
17 | this.hostProxy = hostProxy;
18 | console.log(tag, "onWebConsoleReady");
19 | },
20 | onWebConsoleShow: function(hostProxy) {
21 | console.log(tag, "onWebConsoleShow");
22 | },
23 | onWebConsoleHide: function(hostProxy) {
24 | console.log(tag, "onWebConsoleHide");
25 | },
26 | onWebConsoleTabChanged: function(hostProxy, newVal, oldVal) {
27 | console.log(tag, "onWebConsoleTabChanged", newVal);
28 | },
29 | onWebConsoleSettingsLoaded: function(hostProxy, settings) {
30 | console.log(tag, "onWebConsoleSettingsLoaded", settings);
31 | },
32 | onWebConsoleSettingsChanged: function(hostProxy, settings) {
33 | console.log(tag, "onWebConsoleSettingsChanged", settings);
34 | },
35 | hidePanel: function() {
36 | this.hostProxy.hidePanel();
37 | },
38 | printSettings: function() {
39 | console.log("settings:", this.hostProxy.getSettings());
40 | }
41 | },
42 | render: function(h) {
43 | var vm = this;
44 | return h(
45 | "div",
46 | {
47 | style: {
48 | display: "flex",
49 | "flex-direction": "column"
50 | }
51 | },
52 | [
53 | h(
54 | "div",
55 | {
56 | style: {
57 | flex: "1"
58 | }
59 | },
60 | [
61 | h(
62 | "button",
63 | {
64 | on: {
65 | click: this.hidePanel
66 | }
67 | },
68 | "hide panel"
69 | ),
70 | h(
71 | "button",
72 | {
73 | style: {
74 | "margin-left": "10px"
75 | },
76 | on: {
77 | click: this.printSettings
78 | }
79 | },
80 | "print settings"
81 | ),
82 | h(
83 | "div",
84 | {
85 | style: {
86 | "margin-top": "20px",
87 | display: "flex",
88 | "flex-direction": "row"
89 | }
90 | },
91 | ["setting", "close", "refresh", "ban", "edit", "save", "add", "cancel", "expand", "collapse"].map(
92 | name =>
93 | h("VIcon", {
94 | style: {
95 | "margin-left": "10px",
96 | width: "20px",
97 | height: "20px"
98 | },
99 | props: {
100 | name: name
101 | }
102 | })
103 | )
104 | )
105 | ]
106 | ),
107 | h("VJSONViewer", {
108 | props: {
109 | value: { a: 1, b: "2", c: { a: 1, b: "2" } }
110 | }
111 | }),
112 | h("VHighlightView", {
113 | props: {
114 | code: "var a = 1;\nvar b 2;\nconsole.log('a + b =', a + b)",
115 | language: "javascript"
116 | }
117 | }),
118 | h(
119 | "VTabBar",
120 | {
121 | props: {
122 | value: vm.activeTab,
123 | equalWidth: true
124 | },
125 | on: {
126 | input: function(id) {
127 | vm.activeTab = id;
128 | }
129 | }
130 | },
131 | [
132 | h("VTabBarItem", { props: { id: "A" } }, "Tab1"),
133 | h("VTabBarItem", { props: { id: "B" } }, "Tab2"),
134 | h("VTabBarItem", { props: { id: "C" } }, "Tab3"),
135 | h("VTabBarItem", { props: { id: "D" } }, "Tab4")
136 | ]
137 | ),
138 | h("v-foot-bar", {
139 | props: {
140 | buttons: [
141 | {
142 | text: "Hide",
143 | click: function() {
144 | vm.hostProxy.hidePanel();
145 | }
146 | }
147 | ]
148 | }
149 | }) // v-foot-bar
150 | ]
151 | ); // div
152 | }
153 | };
154 | },
155 | settings: [
156 | {
157 | type: "checkbox",
158 | name: "plugin0",
159 | value: false,
160 | desc: "test plugin0"
161 | }
162 | ]
163 | });
164 | }
165 |
166 | function plugin2(WebConsole) {
167 | return new WebConsole.Plugin({
168 | id: "plugin2",
169 | name: "插件2",
170 | component: {
171 | data: function() {
172 | return {
173 | text: "plugin2 panel"
174 | };
175 | },
176 | render: function(h) {
177 | return h("div", {}, this.text);
178 | }
179 | },
180 | settings: function() {
181 | return [
182 | {
183 | type: "checkbox",
184 | name: this.id,
185 | value: false,
186 | desc: "test " + this.id
187 | }
188 | ];
189 | }
190 | });
191 | }
192 |
193 | window.$plugins = [plugin1, plugin2];
194 |
--------------------------------------------------------------------------------
/src/components/VJSONViewer/JSONTextBlock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 | {{name}}
13 | :
14 |
15 |
16 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
31 |
32 |
206 |
207 |
260 |
--------------------------------------------------------------------------------
/src/panels/network/TabHeaders.vue:
--------------------------------------------------------------------------------
1 |
2 |
66 |
67 |
68 |
176 |
177 |
220 |
--------------------------------------------------------------------------------
/src/panels/element/ElementPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | Styles
21 | Computed
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
197 |
198 |
228 |
--------------------------------------------------------------------------------
/src/panels/console/TextInlineBlock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{value}}
6 |
7 | ƒ {{value.name}}()
8 |
9 |
10 |
11 | ƒ
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ({{value.length}})
22 | [
23 |
24 |
25 |
30 |
31 | ,
32 |
33 | , ...
34 | ]
35 |
36 | Array({{value.length}})
37 |
38 | Array({{value.length}})
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {
49 |
50 |
51 |
52 | {{propName}}:
53 |
58 | ,
59 |
60 | , ...
61 |
62 |
63 | ...
64 | }
65 |
66 |
67 | Object
68 |
69 |
70 |
71 |
72 | "
73 | {{value}}
74 | "
75 |
76 |
77 | {{value}}
78 |
79 |
80 |
81 | {{formattedValue}}
82 |
83 |
84 |
241 |
242 |
272 |
--------------------------------------------------------------------------------
/src/utils/style.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 处理 CSS 样式的工具方法
3 | */
4 | import { compare } from "specificity";
5 | import Logger from "./Logger";
6 |
7 | const logger = new Logger("[Style.js]");
8 |
9 | /**
10 | * 获取与指定元素匹配的所有样式规则
11 | *
12 | * ----- index.html
13 | *
20 | *
21 | *
22 | *
23 | * ----- stylesheet_import1.css
24 | * @import stylesheet_import2.css
25 | * #id {
26 | * color: blue;
27 | * }
28 | *
29 | * ----- stylesheet_import2.css
30 | * #id {
31 | * color: green;
32 | * }
33 | *
34 | * ----- stylesheet_link.css
35 | * #id {
36 | * color: yellow;
37 | * }
38 | *
39 | * Chrome DevTools 审查元素 div#id 的样式规则优先级从高到低为:
40 | * #id {color: yellow;} // from stylesheet_link.css
41 | * #id {color: red;} // from
42 | * #id {color: blue;} // from stylesheet_import1.css
43 | * #id {color: green;} // from stylesheet_import2.css
44 | *
45 | * 相同 specificity 的选择器按照出现的先后顺序层叠,后面的元素覆盖前面元素
46 | * 通过 @import 导入的元素视作原地展开后按出现顺序层叠。
47 | * document.styleSheets 返回的列表是按声明顺序排序,如果有 @import 则是递归嵌套
48 | *
49 | * @param {Element} el
50 | * @param {Array} styleSheets
51 | * @returns {Array} 匹配的规则列表,按出现先后顺序排列
52 | */
53 | function getMatchedCSSRules(el, styleSheets = [].slice.call(document.styleSheets)) {
54 | if (!(el instanceof Element)) {
55 | throw new Error("invalid params type");
56 | }
57 |
58 | const rules = [];
59 | // 深度优先遍历 样式表和 CSS 规则
60 | // stylesheet
61 | // import rule
62 | // stylesheet
63 | // style rule
64 | // style rule
65 | // stylesheet
66 | while (styleSheets.length > 0) {
67 | const styleSheet = styleSheets.shift();
68 | try {
69 | const cssRuleArr = [].slice.call(styleSheet.cssRules);
70 | while (cssRuleArr.length > 0) {
71 | const rule = cssRuleArr.shift();
72 | switch (rule.type) {
73 | case CSSRule.IMPORT_RULE:
74 | // 根据层叠规则,导入样式视作在当前样式表所有规则之前
75 | cssRuleArr.unshift(...getMatchedCSSRules(el, [rule.styleSheet]));
76 | break;
77 | case CSSRule.STYLE_RULE:
78 | if (el.matches(rule.selectorText)) {
79 | // 设置出现顺序,值越小表示越先出现
80 | rule.order = rules.length;
81 | rules.push(rule);
82 | // logger.log(rule.type, rule)
83 | }
84 | break;
85 | default:
86 | // TODO: 处理其他类型的 CSSRule
87 | logger.error("unknow CSSRule type:", rule.type);
88 | break;
89 | }
90 | }
91 | } catch (e) {
92 | logger.warn(e);
93 | continue;
94 | }
95 | }
96 | return rules;
97 | }
98 |
99 | /**
100 | * 根据特殊性排序,如果特殊性相同则按出现顺序排序,排在数组越前的特殊性越高
101 | * @param {CSSRule} a
102 | * @param {CSSRule} b
103 | * @returns {Number}
104 | */
105 | function compareCSSRule(a, b) {
106 | const selectorA = findMaxSpecificity(a.selectorText);
107 | const selectorB = findMaxSpecificity(b.selectorText);
108 | const c = compare(selectorA, selectorB);
109 | return c === 0 ? b.order - a.order : -c;
110 | }
111 |
112 | /**
113 | * 找出择器中特殊性最高的选择器
114 | *
115 | * 例如:
116 | * findMaxSpecificity('.a') // '.a'
117 | * findMaxSpecificity('.a, #b') // '#b'
118 | * findMaxSpecificity('.a, #b, #b.a') // '#b.a'
119 | */
120 | function findMaxSpecificity(selector) {
121 | let _selector;
122 | if (selector.indexOf(",") !== -1) {
123 | const selectorArr = selector.split(",");
124 | _selector = selectorArr[0];
125 | selectorArr.forEach(v => {
126 | if (compare(v, _selector) === 1) {
127 | _selector = v;
128 | }
129 | });
130 | } else {
131 | _selector = selector;
132 | }
133 | return _selector;
134 | }
135 |
136 | /**
137 | * 返回一个匹配 CSS 有效颜色值的 RegExp 对象
138 | */
139 | let _cachedColorRegExp = null;
140 | function getColorRegExp() {
141 | if (_cachedColorRegExp) {
142 | return _cachedColorRegExp;
143 | }
144 |
145 | const basicColor = [
146 | "black",
147 | "silver",
148 | "gray",
149 | "white",
150 | "maroon",
151 | "red",
152 | "purple",
153 | "fuchsia",
154 | "green",
155 | "lime",
156 | "olive",
157 | "yellow",
158 | "navy",
159 | "blue",
160 | "teal",
161 | "aqua"
162 | ].join("|");
163 | const extendColor = [
164 | "aliceblue",
165 | "antiquewhite",
166 | "aqua",
167 | "aquamarine",
168 | "azure",
169 | "beige",
170 | "bisque",
171 | "black",
172 | "blanchedalmond",
173 | "blue",
174 | "blueviolet",
175 | "brown",
176 | "burlywood",
177 | "cadetblue",
178 | "chartreuse",
179 | "chocolate",
180 | "coral",
181 | "cornflowerblue",
182 | "cornsilk",
183 | "crimson",
184 | "cyan",
185 | "darkblue",
186 | "darkcyan",
187 | "darkgoldenrod",
188 | "darkgray",
189 | "darkgreen",
190 | "darkgrey",
191 | "darkkhaki",
192 | "darkmagenta",
193 | "darkorange",
194 | "darkorchid",
195 | "darkred",
196 | "darksalmon",
197 | "darkseagreen",
198 | "darkslateblue",
199 | "darkslategray",
200 | "darkslategrey",
201 | "darkturquoise",
202 | "darkviolet",
203 | "deeppink",
204 | "deepskyblue",
205 | "dimgray",
206 | "dimgrey",
207 | "dodgerblue",
208 | "firebrick",
209 | "floralwhite",
210 | "forestgreen",
211 | "fuchsia",
212 | "gainsboro",
213 | "ghostwhite",
214 | "gold",
215 | "goldenrod",
216 | "gray",
217 | "green",
218 | "greenyellow",
219 | "grey",
220 | "honeydew",
221 | "hotpink",
222 | "indianred",
223 | "indigo",
224 | "ivory",
225 | "khaki",
226 | "lavender",
227 | "lavenderblush",
228 | "lawngreen",
229 | "lemonchiffon",
230 | "lightblue",
231 | "lightcoral",
232 | "lightcyan",
233 | "lightgoldenrodyellow",
234 | "lightgray",
235 | "lightgreen",
236 | "lightgrey",
237 | "lightpink",
238 | "lightsalmon",
239 | "lightseagreen",
240 | "lightskyblue",
241 | "lightslategray",
242 | "lightslategrey",
243 | "lightsteelblue",
244 | "lightyellow",
245 | "lime",
246 | "limegreen",
247 | "linen",
248 | "magenta",
249 | "maroon",
250 | "mediumaquamarine",
251 | "mediumblue",
252 | "mediumorchid",
253 | "mediumpurple",
254 | "mediumseagreen",
255 | "mediumslateblue",
256 | "mediumspringgreen",
257 | "mediumturquoise",
258 | "mediumvioletred",
259 | "midnightblue",
260 | "mintcream",
261 | "mistyrose",
262 | "moccasin",
263 | "navajowhite",
264 | "navy",
265 | "oldlace",
266 | "olive",
267 | "olivedrab",
268 | "orange",
269 | "orangered",
270 | "orchid",
271 | "palegoldenrod",
272 | "palegreen",
273 | "paleturquoise",
274 | "palevioletred",
275 | "papayawhip",
276 | "peachpuff",
277 | "peru",
278 | "pink",
279 | "plum",
280 | "powderblue",
281 | "purple",
282 | "red",
283 | "rosybrown",
284 | "royalblue",
285 | "saddlebrown",
286 | "salmon",
287 | "sandybrown",
288 | "seagreen",
289 | "seashell",
290 | "sienna",
291 | "silver",
292 | "skyblue",
293 | "slateblue",
294 | "slategray",
295 | "slategrey",
296 | "snow",
297 | "springgreen",
298 | "steelblue",
299 | "tan",
300 | "teal",
301 | "thistle",
302 | "tomato",
303 | "turquoise",
304 | "violet",
305 | "wheat",
306 | "white",
307 | "whitesmoke",
308 | "yellow",
309 | "yellowgreen"
310 | ].join("|");
311 | const rgbColor = "rgba?([^)]+)";
312 | const hslaColor = "hsla?([^)]+)";
313 | const hexColor = "#[0-9a-fA-F]{1,8}";
314 |
315 | const colorRegExp = new RegExp([hexColor, rgbColor, hslaColor, basicColor, extendColor].join("|"), "i");
316 | _cachedColorRegExp = colorRegExp;
317 | return colorRegExp;
318 | }
319 |
320 | export default {
321 | getMatchedCSSRules,
322 | compareCSSRule,
323 | getColorRegExp
324 | };
325 |
--------------------------------------------------------------------------------
/src/panels/element/NodeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
21 |
22 |
28 |
29 |
30 |
31 | ...
37 |
38 |
39 |
40 |
41 | {{el.textContent}}
47 |
48 |
49 |
55 | {{el.data}}
56 | "{{el.data}}"
57 |
58 |
66 |
72 | <!doctype html>
73 |
74 |
80 | <unknow({{el.nodeName}})>
81 |
82 |
83 |
84 |
85 |
218 |
219 |
276 |
--------------------------------------------------------------------------------
/src/panels/element/TabComputed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
{{name}}
19 |
20 |
21 | {{value}}
22 |
23 |
24 |
25 |
29 |
30 |
31 | {{rule.value}}
32 |
33 |
{{rule.selectorText}}
34 |
{{rule.href}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
177 |
178 |
179 |
296 |
--------------------------------------------------------------------------------
/public/test_console.js:
--------------------------------------------------------------------------------
1 | window.$console = (function() {
2 | function testLogLevel() {
3 | console.log("log");
4 | console.info("info");
5 | console.debug("debug");
6 | console.warn("warn");
7 | console.error("error");
8 | }
9 |
10 | function testLogParams() {
11 | console.log(100, true, undefined, null, { a: 100 });
12 | }
13 |
14 | // 测试log格式化输出
15 | function testLogFormat(value, format) {
16 | console.log("console.log(%" + format + ", %s)", JSON.stringify(value));
17 | console.log(format, value);
18 | }
19 |
20 | function testIntervalLog(interval = 1000) {
21 | console.log(Date.now());
22 | setTimeout(testIntervalLog.bind(null, arguments), interval);
23 | }
24 |
25 | function testException() {
26 | // 测试直接输出 Error 对象
27 | console.error(new ReferenceError("reference error"));
28 |
29 | // 测试未捕获的全局异常
30 | setTimeout(function() {
31 | throw new Error("This is a uncaught exception");
32 | });
33 |
34 | // 测试未处理的 rejected Promise
35 | Promise.reject("uncaught rejected promise");
36 | }
37 |
38 | // 打印对象
39 | function testObject() {
40 | let obj2 = {
41 | // 测试数值类型
42 | a: 1,
43 | a5: { a: 1, b: 2, c: { a: 1, b: 2 } },
44 | // 测试数组
45 | a3: [1, 2],
46 | a4: [1, 2, [1, 2, [1, 2, 3], 3], 3],
47 | // 测试属性值很长时的展示
48 | a1: "10101010101010101010101010101010101010101010101001010101",
49 | a2: 10101010101010101010110101010101010110101011010101011010101,
50 | // 测试字符串类型
51 | b: "b",
52 | // 测试转义字符展示
53 | b1: '"b1"',
54 | c: true,
55 | d: undefined,
56 | e: null,
57 | f: Symbol(),
58 | g: {
59 | a: 1,
60 | f1: () => {
61 | return 1;
62 | },
63 | f2: function() {
64 | return 2;
65 | },
66 | b: "b",
67 | c: {
68 | a: 1,
69 | b: "b"
70 | },
71 | d: undefined,
72 | e: null
73 | },
74 | [Symbol()]: 10,
75 | [Symbol("age")]: 28,
76 | f1: function() {},
77 | f2: () => {},
78 | f3: function say() {},
79 | Z: "Z",
80 | M: "m",
81 | z: "z",
82 | // 测试排序规则
83 | __a: "public __a",
84 | _a: "public _a",
85 | $a: "public $a"
86 | };
87 | Object.defineProperties(obj2, {
88 | // value
89 | h: {
90 | enumerable: true,
91 | configurable: true,
92 | value: 10,
93 | writable: false
94 | },
95 | // value
96 | i: {
97 | enumerable: false,
98 | configurable: false,
99 | value: 10,
100 | writable: true
101 | },
102 | // getter and setter
103 | j: {
104 | get: () => {
105 | return Math.random() + "";
106 | },
107 | set: v => {}
108 | },
109 | J: {
110 | get: () => {
111 | return "" + Math.random();
112 | },
113 | set: v => {}
114 | },
115 | // only getter
116 | k: {
117 | get: function() {
118 | return {
119 | a: 1,
120 | b: "b"
121 | };
122 | }
123 | },
124 | K: {
125 | get: function() {
126 | return [
127 | {
128 | a: 1,
129 | b: "b"
130 | }
131 | ];
132 | }
133 | },
134 | // only setter
135 | l: {
136 | set: function(v) {}
137 | },
138 | [Symbol("a")]: {
139 | value: "1",
140 | enumerable: false
141 | },
142 | // 测试属性排序规则
143 | __b: {
144 | value: "private __b",
145 | enumerable: false
146 | },
147 | _b: {
148 | value: "private _b",
149 | enumerable: false
150 | },
151 | $b: {
152 | value: "private $b",
153 | enumerable: false
154 | }
155 | });
156 | Object.defineProperties(obj2.g, {
157 | h: {
158 | enumerable: true,
159 | configurable: true,
160 | value: 10
161 | },
162 | i: {
163 | enumerable: false,
164 | configurable: false,
165 | value: 10
166 | },
167 | j: {
168 | get: () => {
169 | return Math.random();
170 | },
171 | set: v => {}
172 | },
173 | k: {
174 | get: function() {
175 | return Math.random();
176 | },
177 | set: function() {}
178 | }
179 | });
180 | Object.defineProperties(obj2.g.c, {
181 | c: {
182 | enumerable: false,
183 | configurable: false,
184 | value: 10
185 | },
186 | d: {
187 | get: function() {
188 | return Math.random();
189 | },
190 | set: function() {}
191 | }
192 | });
193 | console.log("对象全属性:", obj2);
194 | console.log("三种函数:", {
195 | f1: function() {},
196 | f2: () => {},
197 | f3: function say() {}
198 | });
199 | var arr = [];
200 | arr["10"] = "10";
201 | arr["29"] = "29";
202 | arr[" "] = " "; // 0x20
203 | arr[" "] = " ";
204 | arr[" a"] = " a";
205 | arr[" a"] = " a"; // 空白符处理
206 | arr["!"] = "!"; // 0x21
207 | arr["0"] = "0"; // 0x30
208 | arr["1"] = "1"; // 0x31
209 | arr["2"] = "2"; // 0x31
210 | arr[":"] = ":"; // 0x3A
211 | arr["A"] = "A"; // 0x41
212 | arr["B"] = "B"; // 0x42
213 | arr["["] = "["; // 0x5B
214 | arr["a"] = "a"; // 0x61
215 | arr["b"] = "b"; // 0x62
216 | arr["{"] = "{"; // 0x7B
217 | console.log("数组 key 排序:", arr);
218 |
219 | const thumbObj = {
220 | extra: `{"num_total":300,"num_left":157,"discount":"{\"create_time\":1531970051000,\"discount_data\":\"[{\\\"amount_min\\\":\\\"100\\\",\\\"amount_cut\\\":\\\"10\\\"},{\\\"amount_min\\\":\\\"200\\\",\\\"amount_cut\\\":\\\"25\\\"},{\\\"amount_min\\\":\\\"300\\\",\\\"amount_cut\\\":\\\"50\\\"}]\",\"discount_id\":3,\"discount_name\":\"满减-yanger\",\"discount_prod\":\"[\\\"WM-180101001\\\",\\\"WM-180101002\\\",\\\"WM-180101004\\\",\\\"WM-180101006\\\",\\\"WM-180101008\\\",\\\"WM-180525102\\\",\\\"WM-180101017\\\"]\",\"discount_provider\":\"0\",\"discount_status\":\"1\",\"discount_type\":\"1\",\"update_time\":1540458898000}"}`,
221 | isChange: false
222 | };
223 | const thumbObj2 = [
224 | `{"num_total":300,"num_left":157,"discount":"{\"create_time\":1531970051000,\"discount_data\":\"[{\\\"amount_min\\\":\\\"100\\\",\\\"amount_cut\\\":\\\"10\\\"},{\\\"amount_min\\\":\\\"200\\\",\\\"amount_cut\\\":\\\"25\\\"},{\\\"amount_min\\\":\\\"300\\\",\\\"amount_cut\\\":\\\"50\\\"}]\",\"discount_id\":3,\"discount_name\":\"满减-yanger\",\"discount_prod\":\"[\\\"WM-180101001\\\",\\\"WM-180101002\\\",\\\"WM-180101004\\\",\\\"WM-180101006\\\",\\\"WM-180101008\\\",\\\"WM-180525102\\\",\\\"WM-180101017\\\"]\",\"discount_provider\":\"0\",\"discount_status\":\"1\",\"discount_type\":\"1\",\"update_time\":1540458898000}"}`
225 | ];
226 | thumbObj2.push(thumbObj);
227 | console.log("测试对象缩略信息:", thumbObj, thumbObj2);
228 | }
229 |
230 | // 格式化输出
231 | function testFormat() {
232 | var numInt = 1;
233 | var numFloat = 1.23;
234 | var str = "hello";
235 | var obj = {
236 | a: 1,
237 | b: 2,
238 | c: {
239 | d: 3
240 | }
241 | };
242 | var arr = [1, 2, [3, obj]];
243 | // 测试占位符的支持类型
244 | testLogFormat(numInt, "%i");
245 | testLogFormat(numFloat, "%i");
246 | testLogFormat(numInt, "%d");
247 | testLogFormat(numFloat, "%d");
248 | testLogFormat(numInt, "%f");
249 | testLogFormat(numFloat, "%f");
250 | testLogFormat(obj, "%s");
251 | testLogFormat(obj, "%o");
252 | testLogFormat(arr, "%o");
253 | testLogFormat(obj, "%O");
254 | testLogFormat(arr, "%O");
255 | // 测试占位符缺少对应的参数
256 | console.log('console.log("-%s%i%d%f%o%O%c-")');
257 | console.log("-%s%i%d%f%o%O%c-");
258 | /* 测试占位符之间的空白符 */
259 | // 期望输出 "a1b2c 3"
260 | console.log("a%sb%sc", 1, 2, 3);
261 | // 测试参数多余占位符
262 | console.log('console.log("-%s-", 99, {a: "1"})');
263 | console.log("-%s-", 99, { a: 1 });
264 | // 测试占位符和参数的高亮
265 | console.log("console.log('%s-%s-%s-%s-%s-%s-%s', 100, true, null, undefined, '100', {}, [])");
266 | console.log("%s-%s-%s-%s-%s-%s-%s", 100, true, null, undefined, "100", {}, []);
267 | // 测试占位符 %c 的展示
268 | console.log(
269 | `%c111 222 %c 333 %c %c %O %o %i %f %s %%`,
270 | "color: white; background-color: rgba(0, 116, 217, 0.69); padding: 2px 5px; border-radius: 2px",
271 | "color: #0074D9",
272 | "",
273 | "color: white; background-color: rgba(255, 65, 54, 0.69); padding: 2px 5px; margin-right: 5px; border-radius: 2px",
274 | { a: 444 },
275 | { b: 555 },
276 | 666,
277 | 777,
278 | "888"
279 | );
280 | }
281 |
282 | return {
283 | testLogLevel: testLogLevel,
284 | testLogParams: testLogParams,
285 | testFormat: testFormat,
286 | testException: testException,
287 | testIntervalLog: testIntervalLog,
288 | testObject: testObject
289 | };
290 | })();
291 |
--------------------------------------------------------------------------------
/src/panels/console/ConsolePanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | All
6 | Log
7 | Info
8 | Warn
9 | Error
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
242 |
243 |
282 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {{PanelType.text(id)}}
35 |
36 | {{plugin.name}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
241 |
242 |
279 |
--------------------------------------------------------------------------------