From eaa6ae5030fcfc31db905ed24969995d06014af3 Mon Sep 17 00:00:00 2001 From: enncy <877526278@qq.com> Date: Thu, 10 Nov 2022 00:40:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=95=99=E7=A8=8B,=20=E9=87=8D=E8=BD=BD=20el=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0,=20=E4=BF=AE=E6=94=B9cors=E8=B7=A8=E5=9F=9F?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E4=BD=BF=E7=94=A8=20setTab,=20getTab=20?= =?UTF-8?q?=E8=BF=9B=E8=A1=8Ccors=E8=B7=A8=E5=9F=9F=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E5=88=86=E5=8C=BA.=20=E4=BC=98=E5=8C=96=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/assets/css/style.css | 640 ++++++++++---------- packages/core/assets/less/style.less | 103 ++-- packages/core/src/custom.d.ts | 0 packages/core/src/elements/config.ts | 15 +- packages/core/src/elements/message.ts | 4 +- packages/core/src/elements/model.ts | 5 +- packages/core/src/elements/script.panel.ts | 4 +- packages/core/src/interfaces/cors.ts | 149 ++--- packages/core/src/interfaces/project.ts | 1 + packages/core/src/interfaces/script.ts | 22 +- packages/core/src/projects/common.ts | 88 ++- packages/core/src/projects/cx.ts | 2 + packages/core/src/projects/init.ts | 61 +- packages/core/src/projects/zhs.ts | 3 +- packages/core/src/utils/common.ts | 105 ++-- packages/core/src/utils/dom.ts | 71 ++- packages/core/src/utils/start.ts | 9 +- packages/core/src/utils/tampermonkey.ts | 101 +++ packages/core/vite.config.ts | 5 +- packages/web/src/App.vue | 6 +- packages/web/src/pages/extensions/index.vue | 6 +- scripts/build-core.js | 19 +- 22 files changed, 814 insertions(+), 605 deletions(-) create mode 100644 packages/core/src/custom.d.ts create mode 100644 packages/core/src/utils/tampermonkey.ts diff --git a/packages/core/assets/css/style.css b/packages/core/assets/css/style.css index a07aa0e3..0dec9e6b 100644 --- a/packages/core/assets/css/style.css +++ b/packages/core/assets/css/style.css @@ -1,495 +1,507 @@ /** 默认字体 */ /** 输入框默认边距 */ .base-style-active-form-control { - border: 1px solid #ffffff00; + border: 1px solid #ffffff00; } .base-style-active-form-control:focus { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; } .base-style-active-form-control:focus:not([type='checkbox'], [type='radio']) { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; - background-color: white !important; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; + background-color: white !important; } .base-style-active-form-control:hover { - background-color: #ebeef4; + background-color: #ebeef4; } .base-style-input { - outline: none; - border: 1px solid #ffffff00; - padding: 2px 8px; - margin: 0px; - background-color: #eef2f7; - border-radius: 2px; - color: black; + outline: none; + border: 1px solid #ffffff00; + padding: 2px 8px; + margin: 0px; + background-color: #eef2f7; + border-radius: 2px; + color: black; } .base-style-input::placeholder { - color: #bababa; + color: #bababa; } .base-style-button { - appearance: none; - border: 1px solid #ffffff00; - background-color: #94bfff; - color: white; - border-radius: 4px; - cursor: pointer; + appearance: none; + border: 1px solid #ffffff00; + background-color: #94bfff; + color: white; + border-radius: 4px; + cursor: pointer; } .base-style-button:active { - box-shadow: 0px 0px 8px #0e8de2a5; + box-shadow: 0px 0px 8px #0e8de2a5; } container-element.close { - display: none; + display: none; } container-element.minimize { - min-width: unset; + min-width: unset; } container-element { - position: fixed; - top: 10%; - left: 10%; - z-index: 99; - text-align: left; - min-width: 300px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - color: #636363; - font: 12px/1.5 Menlo, Monaco, Consolas, 'Courier New', monospace; + position: fixed; + top: 10%; + left: 10%; + z-index: 99; + text-align: left; + min-width: 300px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #636363; + font: 12px/1.5 Menlo, Monaco, Consolas, 'Courier New', monospace; } .header { - cursor: move; - white-space: nowrap; - justify-content: end; - display: flex; - padding-right: 12px; - line-height: 32px; - height: 32px; - vertical-align: middle; - background: #fbfbfbee; - border-radius: 4px; - border: 1px solid #e6e6e6; - box-shadow: 0 0 24px -12px #3f3f3f; - margin-bottom: 4px; - user-select: none; + cursor: move; + white-space: nowrap; + justify-content: end; + display: flex; + padding-right: 12px; + line-height: 32px; + height: 32px; + vertical-align: middle; + background: #fbfbfbee; + border-radius: 4px; + border: 1px solid #e6e6e6; + box-shadow: 0 0 24px -12px #3f3f3f; + margin-bottom: 4px; + user-select: none; } .header > * { - margin-left: 12px; + margin-left: 12px; } .header > * { - text-align: center; - height: 18px; - line-height: 18px; - margin-top: calc((32px - 18px) / 2); -} -.header .close { - color: black; - width: 18px; - background: #ff00004f; - cursor: pointer; - border-radius: 100%; -} -.header .switch { - color: black; - width: 18px; - background: #ffa6003e; - cursor: pointer; - border-radius: 100%; -} -.header .panel-switch { - color: black; - width: 18px; - background: #14a00d50; - cursor: pointer; - border-radius: 100%; -} -.header .logo { - cursor: pointer; - border-radius: 100%; + height: 18px; + line-height: 18px; + margin-top: calc((32px - 18px) / 2); +} +.close, +.switch, +.panel-switch, +.logo { + color: black; + width: 18px; + cursor: pointer; + border-radius: 100%; + display: inline-block; + text-align: center; +} +.close { + background: #ff00004f; +} +.switch { + background: #ffa6003e; +} +.panel-switch { + background: #14a00d50; } .body { - padding: 8px; - width: auto; - height: 100%; - background-color: white; - box-shadow: 0 0 24px -12px #3f3f3f; - border: 1px solid #e6e6e6; - border-radius: 4px; - resize: both; - overflow: auto; + padding: 8px; + width: auto; + height: 100%; + background-color: white; + box-shadow: 0 0 24px -12px #3f3f3f; + border: 1px solid #e6e6e6; + border-radius: 4px; + resize: both; + overflow: auto; } scrip-panel-element + scrip-panel-element { - margin-top: 12px; + margin-top: 12px; } .card + .card { - margin-top: 12px; + margin-top: 12px; } .card { - border-radius: 3px; - padding: 4px; - width: -webkit-fill-available; + border-radius: 2px; + padding: 0px 4px; + width: -webkit-fill-available; } .notes { - background: #0099ff0e; - border-left: 4px solid #0099ff65; + background: #0099ff0e; + border-left: 4px solid #0099ff65; } .tooltip { - margin: 12px 0px 0px 12px; - padding: 4px; - color: black; - background: #f0f0f0; - box-shadow: 0px 0px 4px #949494; - position: fixed; - white-space: normal; - max-width: 200px; - height: auto; - border-radius: 2px; + margin: 12px 0px 0px 12px; + padding: 4px; + color: black; + background: #f0f0f0; + box-shadow: 0px 0px 4px #949494; + position: fixed; + white-space: normal; + max-width: 200px; + height: auto; + border-radius: 2px; } .configs { - display: table; - background: #e1e1e107; - border-left: 4px solid #cecece; + display: table; + background: #e1e1e107; } .configs .configs-body { - display: table-row-group; + display: table-row-group; } .configs .configs-body config-element + config-element label { - padding-top: 3px; + padding-top: 3px; } .configs .configs-body config-element + config-element .config-wrapper { - padding-top: 3px; + padding-top: 3px; } .configs .configs-body config-element { - width: 100%; - display: table-row; + width: 100%; + display: table-row; +} +.configs .configs-body config-element label { + font-size: 13px; + white-space: nowrap; + color: #4e5969; + display: table-cell; + padding-right: 12px; + text-align: left; + vertical-align: top; + margin-right: 12px; } .configs .configs-body config-element .config-wrapper { - display: table-cell; - /** check box 的样式 */ -} -.configs .configs-body config-element .config-wrapper label { - font-size: 13px; - white-space: nowrap; - color: #4e5969; - display: table-cell; - padding-right: 12px; - text-align: left; - vertical-align: top; + display: table-cell; + vertical-align: middle; + /** check box 的样式 */ } .configs .configs-body config-element .config-wrapper select { - outline: none; - border: none; - border: 1px solid #e4e4e4; - border-radius: 4px; - padding: 2px 8px; - height: 22px; - border: 1px solid #ffffff00; + outline: none; + border: none; + border: 1px solid #e4e4e4; + border-radius: 4px; + padding: 2px 8px; + height: 22px; + border: 1px solid #ffffff00; } .configs .configs-body config-element .config-wrapper select:focus { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; } .configs .configs-body config-element .config-wrapper select:focus:not([type='checkbox'], [type='radio']) { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; - background-color: white !important; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; + background-color: white !important; } .configs .configs-body config-element .config-wrapper select:hover { - background-color: #ebeef4; + background-color: #ebeef4; } .configs .configs-body config-element .config-wrapper textarea { - padding: 2px 8px; - outline: none; - border: none; - border: 1px solid #ffffff00; + padding: 2px 8px; + outline: none; + border: none; + border: 1px solid #ffffff00; } .configs .configs-body config-element .config-wrapper textarea:focus { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; } .configs .configs-body config-element .config-wrapper textarea:focus:not([type='checkbox'], [type='radio']) { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; - background-color: white !important; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; + background-color: white !important; } .configs .configs-body config-element .config-wrapper textarea:hover { - background-color: #ebeef4; + background-color: #ebeef4; } .configs .configs-body config-element .config-wrapper input:not([type='button']) { - outline: none; - padding: 2px 8px; - margin: 0px; - background-color: #eef2f7; - border-radius: 2px; - color: black; - border: 1px solid #ffffff00; + outline: none; + padding: 2px 8px; + margin: 0px; + background-color: #eef2f7; + border-radius: 2px; + color: black; + border: 1px solid #ffffff00; } .configs .configs-body config-element .config-wrapper input:not([type='button'])::placeholder { - color: #bababa; + color: #bababa; } .configs .configs-body config-element .config-wrapper input:not([type='button']):focus { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; -} -.configs .configs-body config-element .config-wrapper input:not([type='button']):focus:not([type='checkbox'], [type='radio']) { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; - background-color: white !important; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; +} +.configs + .configs-body + config-element + .config-wrapper + input:not([type='button']):focus:not([type='checkbox'], [type='radio']) { + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; + background-color: white !important; } .configs .configs-body config-element .config-wrapper input:not([type='button']):hover { - background-color: #ebeef4; + background-color: #ebeef4; } .configs .configs-body config-element .config-wrapper input[type='button'] { - appearance: none; - border: 1px solid #ffffff00; - background-color: #94bfff; - color: white; - border-radius: 4px; - cursor: pointer; + appearance: none; + border: 1px solid #ffffff00; + background-color: #94bfff; + color: white; + border-radius: 4px; + cursor: pointer; } .configs .configs-body config-element .config-wrapper input[type='button']:active { - box-shadow: 0px 0px 8px #0e8de2a5; + box-shadow: 0px 0px 8px #0e8de2a5; } .configs .configs-body config-element .config-wrapper input[type='checkbox'] { - appearance: none; - width: fit-content; - min-width: 32px; - height: 16px; - border-radius: 100px !important; - display: flex; - align-items: center; - padding: 2px 4px; - transition: all 0.2s ease-in-out; - width: auto; + appearance: none; + width: fit-content; + min-width: 32px; + height: 16px; + border-radius: 100px; + display: flex; + align-items: center; + padding: 2px 4px; + transition: all 0.2s ease-in-out; + width: auto; } .configs .configs-body config-element .config-wrapper input[type='checkbox']:checked { - background: #1890ff !important; + background: #1890ff; +} +.configs .configs-body config-element .config-wrapper input[type='checkbox']:disabled { + background-color: #f7f7f78b; } .configs .configs-body config-element .config-wrapper input[type='checkbox']:checked::before { - transform: translate(100%, 0px); + transform: translate(100%, 0px); } .configs .configs-body config-element .config-wrapper input[type='checkbox']::before { - background-color: #fff; - border-radius: 9px; - box-shadow: 0 2px 4px #00230b33; - width: 12px; - height: 12px; - content: ''; + background-color: #fff; + border-radius: 9px; + box-shadow: 0 2px 4px #00230b33; + width: 12px; + height: 12px; + content: ''; } .configs .configs-body config-element .config-wrapper input:not([type='checkbox'], [type='radio']), .configs .configs-body config-element .config-wrapper textarea, .configs .configs-body config-element .config-wrapper select { - width: -webkit-fill-available; + width: -webkit-fill-available; } .configs .configs-body config-element .config-wrapper input[type='checkbox'], .configs .configs-body config-element .config-wrapper input[type='radio'], .configs .configs-body config-element .config-wrapper input[type='range'] { - accent-color: #0e8ee2; + accent-color: #0e8ee2; } .configs .configs-body config-element .config-wrapper > *:not(.tooltip) { - background-color: #eef2f7; - border-radius: 2px; - color: black; + background-color: #eef2f7; + border-radius: 2px; + color: black; + float: right; } .configs .configs-body config-element .config-wrapper > *:disabled { - cursor: not-allowed; - background-color: #f7f7f78b; + cursor: not-allowed; + background-color: #f7f7f78b; } .message-container { - margin-bottom: 4px; - position: absolute; - bottom: 100%; - left: 50%; - width: 100%; - transform: translate(-50%, 0px); + margin-bottom: 4px; + position: absolute; + bottom: 100%; + left: 50%; + width: 100%; + transform: translate(-50%, 0px); } .message-container message-element { - display: block; - border-radius: 4px; - margin-bottom: 4px; - padding: 4px 12px; - font-size: 12px; + display: block; + border-radius: 4px; + margin-bottom: 4px; + padding: 4px 12px; + font-size: 12px; } .message-container message-element .message-text { - letter-spacing: 1px; - font-weight: bold; + letter-spacing: 1px; + font-weight: bold; } .message-container message-element .message-closer { - width: 18px; - cursor: pointer; - background-color: white; - color: #a8a8a8; - border-radius: 100%; - float: right; - text-align: center; - height: 18px; - line-height: 18px; + width: 18px; + cursor: pointer; + background-color: white; + color: #a8a8a8; + border-radius: 100%; + float: right; + text-align: center; + height: 18px; + line-height: 18px; } .message-container message-element.error { - background-color: #ff000699; - color: #fff; - border: 1px solid #f36c70; + background-color: #ff000699; + color: #fff; + border: 1px solid #f36c70; } .message-container message-element.info { - background-color: #2196f3a3; - color: white; - border: 1px solid #1890ff; + background-color: #2196f3a3; + color: white; + border: 1px solid #1890ff; } .message-container message-element.success { - background-color: #84d346d1; - color: #fff; - border: 1px solid #6fd91d; + background-color: #84d346d1; + color: #fff; + border: 1px solid #6fd91d; } .message-container message-element.warn { - background-color: #fbbf30e6; - color: #725206; - border: 1px solid #ffc107; + background-color: #fbbf30e6; + color: #725206; + border: 1px solid #ffc107; } model-element { - position: absolute; - top: 50%; - left: 50%; - background-color: white; - border-radius: 4px; - box-shadow: 0px 0px 24px -12px black; - border: 1px solid #929292; - height: fit-content; - transform: translate(-50%, -50%); - padding: 12px 18px 18px 18px; - font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; - z-index: 99999; + position: absolute; + top: 50%; + left: 50%; + background-color: white; + border-radius: 4px; + box-shadow: 0px 0px 24px -12px black; + border: 1px solid #929292; + height: fit-content; + transform: translate(-50%, -50%); + padding: 12px 18px 18px 18px; + font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; + z-index: 99999; } model-element .model-profile { - zoom: 0.8; - color: #969696; - user-select: none; - margin-bottom: 4px; + zoom: 0.8; + color: #969696; + user-select: none; + margin-bottom: 4px; } model-element .model-title { - font-size: 18px; - font-weight: bold; - user-select: none; + font-size: 18px; + font-weight: bold; + user-select: none; } model-element .model-body { - margin: 12px 0px; + margin: 12px 0px; } model-element .model-footer { - display: flex; - white-space: nowrap; - justify-content: end; + display: flex; + white-space: nowrap; + justify-content: end; } model-element .model-footer * + * { - margin-left: 12px; + margin-left: 12px; } model-element .model-input { - outline: none; - padding: 2px 8px; - margin: 0px; - background-color: #eef2f7; - border-radius: 2px; - color: black; - border: 1px solid #ffffff00; - width: -webkit-fill-available; + outline: none; + padding: 2px 8px; + margin: 0px; + background-color: #eef2f7; + border-radius: 2px; + color: black; + border: 1px solid #ffffff00; + width: -webkit-fill-available; } model-element .model-input::placeholder { - color: #bababa; + color: #bababa; } model-element .model-input:focus { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; } model-element .model-input:focus:not([type='checkbox'], [type='radio']) { - border: 1px solid #0e8de290; - box-shadow: 0px 0px 4px #0e8de252; - background-color: white !important; + border: 1px solid #0e8de290; + box-shadow: 0px 0px 4px #0e8de252; + background-color: white !important; } model-element .model-input:hover { - background-color: #ebeef4; + background-color: #ebeef4; } model-element .model-cancel-button { - appearance: none; - border: 1px solid #ffffff00; - background-color: #94bfff; - color: white; - border-radius: 4px; - cursor: pointer; - min-width: 60px; - color: gray; - background-color: white; - border: 1px solid #dcdcdc; + appearance: none; + border: 1px solid #ffffff00; + background-color: #94bfff; + color: white; + border-radius: 4px; + cursor: pointer; + min-width: 60px; + color: gray; + background-color: white; + border: 1px solid #dcdcdc; } model-element .model-cancel-button:active { - box-shadow: 0px 0px 8px #0e8de2a5; + box-shadow: 0px 0px 8px #0e8de2a5; } model-element .model-confirm-button { - appearance: none; - border: 1px solid #ffffff00; - background-color: #94bfff; - color: white; - border-radius: 4px; - cursor: pointer; - min-width: 60px; + appearance: none; + border: 1px solid #ffffff00; + background-color: #94bfff; + color: white; + border-radius: 4px; + cursor: pointer; + min-width: 60px; } model-element .model-confirm-button:active { - box-shadow: 0px 0px 8px #0e8de2a5; + box-shadow: 0px 0px 8px #0e8de2a5; } model-element.alert .model-input, model-element.alert .model-cancel-button { - display: none; + display: none; } model-element.alert .model-confirm-button { - margin: 0; + margin: 0; } model-element.prompt .model-input, model-element.prompt .model-cancel-button, model-element.prompt .model-confirm-button { - display: block; + display: block; } model-element.confirm .model-input { - display: none; + display: none; } .model-wrapper { - width: 100%; - height: 100%; - z-index: 9999; - position: fixed; - top: 0px; - left: 0px; - z-index: 9999999; - background-color: rgba(0, 0, 0, 0.265); + width: 100%; + height: 100%; + z-index: 9999; + position: fixed; + top: 0px; + left: 0px; + z-index: 9999999; + background-color: rgba(0, 0, 0, 0.265); + color: #636363; + font: 12px/1.5 Menlo, Monaco, Consolas, 'Courier New', monospace; } .pointer { - cursor: pointer; + cursor: pointer; } .project-selector { - background: #ffffff00; - border: none; - font-size: inherit; - color: inherit; + background: #ffffff00; + border: none; + font-size: inherit; + color: inherit; } .project-selector.expand-all { - display: none; + display: none; } .separator { - display: flex; - align-items: center; - text-align: center; - padding: 4px 0px; + display: flex; + align-items: center; + text-align: center; + padding: 4px 0px; } .separator::before, .separator::after { - content: ''; - flex: 1; - border-bottom: 1px solid #63636346; + content: ''; + flex: 1; + border-bottom: 1px solid #63636346; } .separator:not(:empty)::before { - margin-right: 0.25em; + margin-right: 0.25em; } .separator:not(:empty)::after { - margin-left: 0.25em; + margin-left: 0.25em; } .minimize .body, .minimize .footer, .minimize .project-selector, .minimize .panel-switch { - display: none; + display: none; +} +.user-guide { + max-width: 400px; + padding-left: 16px; +} +.user-guide > li { + padding: 4px 0px; } diff --git a/packages/core/assets/less/style.less b/packages/core/assets/less/style.less index 68c2253f..8dca8630 100644 --- a/packages/core/assets/less/style.less +++ b/packages/core/assets/less/style.less @@ -70,10 +70,10 @@ container-element { font: 12px/1.5 @base-font-family; } -.header { - @base-height: 32px; - @size: 18px; +@base-height: 32px; +@size: 18px; +.header { cursor: move; white-space: nowrap; justify-content: end; @@ -93,40 +93,34 @@ container-element { margin-left: 12px; } & > * { - text-align: center; height: @size; line-height: @size; margin-top: calc((@base-height - @size) / 2); } +} - .close { - color: black; - width: @size; - background: #ff00004f; - cursor: pointer; - border-radius: 100%; - } +.close, +.switch, +.panel-switch, +.logo { + color: black; + width: @size; + cursor: pointer; + border-radius: 100%; + display: inline-block; + text-align: center; +} - .switch { - color: black; - width: @size; - background: #ffa6003e; - cursor: pointer; - border-radius: 100%; - } +.close { + background: #ff00004f; +} - .panel-switch { - color: black; - width: @size; - background: #14a00d50; - cursor: pointer; - border-radius: 100%; - } +.switch { + background: #ffa6003e; +} - .logo { - cursor: pointer; - border-radius: 100%; - } +.panel-switch { + background: #14a00d50; } .body { @@ -146,17 +140,13 @@ scrip-panel-element + scrip-panel-element { margin-top: 12px; } -.footer { -} - .card + .card { margin-top: 12px; } .card { - border-radius: 3px; - padding: 4px; - + border-radius: 2px; + padding: 0px 4px; width: -webkit-fill-available; } @@ -180,9 +170,7 @@ scrip-panel-element + scrip-panel-element { .configs { display: table; - background: #e1e1e107; - border-left: 4px solid #cecece; .configs-body { display: table-row-group; @@ -200,18 +188,20 @@ scrip-panel-element + scrip-panel-element { width: 100%; display: table-row; - .config-wrapper { + label { + font-size: 13px; + white-space: nowrap; + color: #4e5969; display: table-cell; + padding-right: 12px; + text-align: left; + vertical-align: top; + margin-right: 12px; + } - label { - font-size: 13px; - white-space: nowrap; - color: #4e5969; - display: table-cell; - padding-right: 12px; - text-align: left; - vertical-align: top; - } + .config-wrapper { + display: table-cell; + vertical-align: middle; select { outline: none; @@ -246,7 +236,7 @@ scrip-panel-element + scrip-panel-element { width: fit-content; min-width: 32px; height: 16px; - border-radius: 100px !important; + border-radius: 100px; display: flex; align-items: center; padding: 2px 4px; @@ -254,7 +244,10 @@ scrip-panel-element + scrip-panel-element { width: auto; &:checked { - background: #1890ff !important; + background: #1890ff; + } + &:disabled { + background-color: #f7f7f78b; } &:checked::before { @@ -288,6 +281,7 @@ scrip-panel-element + scrip-panel-element { background-color: @form-control-bg; border-radius: 2px; color: black; + float: right; } .config-wrapper > *:disabled { cursor: not-allowed; @@ -452,6 +446,8 @@ model-element.confirm { left: 0px; z-index: 9999999; background-color: rgba(0, 0, 0, 0.265); + color: #636363; + font: 12px/1.5 @base-font-family; } .pointer { @@ -498,3 +494,12 @@ model-element.confirm { display: none; } } + +.user-guide { + max-width: 400px; + padding-left: 16px; + + & > li { + padding: 4px 0px; + } +} diff --git a/packages/core/src/custom.d.ts b/packages/core/src/custom.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/core/src/elements/config.ts b/packages/core/src/elements/config.ts index eca7b574..814a2c78 100644 --- a/packages/core/src/elements/config.ts +++ b/packages/core/src/elements/config.ts @@ -1,5 +1,5 @@ -import { getValue, setValue } from '../utils/common'; import { el, tooltip } from '../utils/dom'; +import { getValue, setValue } from '../utils/tampermonkey'; import { ConfigTagMap } from './configs/interface'; import { IElement } from './interface'; @@ -24,8 +24,15 @@ export class ConfigElement extends IElem this.provider = el('input'); if (['checkbox', 'radio'].some((t) => t === this.attrs?.type)) { this.provider.checked = getValue(this.key); + const provider = this.provider; + provider.onchange = () => { + setValue(this.key, provider.checked); + }; } else { this.provider.value = getValue(this.key); + this.provider.onchange = () => { + setValue(this.key, this.provider.value); + }; } }; switch (this.tag) { @@ -58,12 +65,6 @@ export class ConfigElement extends IElem } } - // 储存值 - this.provider.onchange = () => { - // eslint-disable-next-line no-undef - setValue(this.key, this.provider.value); - }; - // 处理跨域 if (this.sync) { // eslint-disable-next-line no-undef diff --git a/packages/core/src/elements/message.ts b/packages/core/src/elements/message.ts index eaee8708..6dbabcc7 100644 --- a/packages/core/src/elements/message.ts +++ b/packages/core/src/elements/message.ts @@ -14,7 +14,9 @@ export class MessageElement extends IElement { connectedCallback() { this.classList.add(this.type); - this.contentContainer.append(this.content); + this.contentContainer.append( + typeof this.content === 'string' ? el('div', { innerHTML: this.content }) : this.content + ); this.duration = Math.max(this.duration, 0); this.append(this.contentContainer); diff --git a/packages/core/src/elements/model.ts b/packages/core/src/elements/model.ts index f7927a2c..b3ff35e9 100644 --- a/packages/core/src/elements/model.ts +++ b/packages/core/src/elements/model.ts @@ -1,9 +1,10 @@ import { el } from '../utils/dom'; +import { getInfos } from '../utils/tampermonkey'; import { IElement } from './interface'; export class ModelElement extends IElement { modelProfile: HTMLDivElement = el('div', { - innerText: '弹窗来自: OCS-' + process.env.__VERSION__, + innerText: '弹窗来自: OCS-' + getInfos().script.version, className: 'model-profile' }); @@ -26,7 +27,7 @@ export class ModelElement extends IElement { connectedCallback() { this.classList.add(this.type); this.modalTitle.innerText = this.title; - this.modalBody.append(this.content); + this.modalBody.append(typeof this.content === 'string' ? el('div', { innerHTML: this.content }) : this.content); this.cancelButton.innerText = this.cancelButtonText || '取消'; this.confirmButton.innerText = this.confirmButtonText || '确定'; this.modalInput.placeholder = this.placeholder || ''; diff --git a/packages/core/src/elements/script.panel.ts b/packages/core/src/elements/script.panel.ts index 8a1962f9..431b6f00 100644 --- a/packages/core/src/elements/script.panel.ts +++ b/packages/core/src/elements/script.panel.ts @@ -16,8 +16,8 @@ export class ScriptPanelElement extends IElement { this.replaceChildren(); this.separator.innerText = this.name || ''; this.append(this.separator); - this.notesContainer.childElementCount && this.append(this.notesContainer); - this.configsBody.childElementCount && this.append(this.configsContainer); + this.append(this.notesContainer); + this.append(this.configsContainer); this.append(this.body); } } diff --git a/packages/core/src/interfaces/cors.ts b/packages/core/src/interfaces/cors.ts index f42439c8..0501686b 100644 --- a/packages/core/src/interfaces/cors.ts +++ b/packages/core/src/interfaces/cors.ts @@ -1,11 +1,13 @@ +import { uuid } from '../utils/common'; import { - listValues, deleteValue, setValue, - getValue, addConfigChangeListener, - removeConfigChangeListener -} from '../utils/common'; + removeConfigChangeListener, + getValue, + getTab, + listValues +} from '../utils/tampermonkey'; /** * 跨域脚本事件通讯 @@ -13,23 +15,6 @@ import { export class CorsEventEmitter { eventMap: Map = new Map(); - init() { - // 删除全部未处理的模态框临时变量 - listValues().forEach((key) => { - if (/_temp_.event.[0-9a-z]{32}.(state|return|arguments)/.test(key)) { - deleteValue(key); - } - }); - } - - private uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } - private eventKey(name: string) { return 'cors.events.' + name; } @@ -57,27 +42,29 @@ export class CorsEventEmitter { * @param callback 事件回调,可以接收返回值 */ emit(name: string, args: any[], callback: (returnValue: any, remote: boolean) => void): void { - const id = this.uuid().replace(/-/g, ''); - const key = this.eventKey(name); + getTab(({ tabId }) => { + const id = uuid().replace(/-/g, ''); + const key = tabId + '.' + this.eventKey(name); + + /** 状态, 0 等待交互 , 1 确定 , 2 取消 , 后面紧跟着模态框中获取到的值,如果模态框类型是 prompt 则有值,否则为空字符串 */ + setValue(this.keyOfState(id), 0); + /** 模态框所需参数 */ + setValue(this.keyOfArguments(id), args); + + const listenerId = addConfigChangeListener(this.keyOfState(id), (pre, curr, remote) => { + // 移除此监听器 + removeConfigChangeListener(listenerId); + // 执行回调 + callback(getValue(this.keyOfReturn(id)), remote); + // 移除冗余的本地临时存储变量 + deleteValue(this.keyOfState(id)); + deleteValue(this.keyOfReturn(id)); + deleteValue(this.keyOfArguments(id)); + }); - /** 状态, 0 等待交互 , 1 确定 , 2 取消 , 后面紧跟着模态框中获取到的值,如果模态框类型是 prompt 则有值,否则为空字符串 */ - setValue(this.keyOfState(id), 0); - /** 模态框所需参数 */ - setValue(this.keyOfArguments(id), args); - - const listenerId = addConfigChangeListener(this.keyOfState(id), (pre, curr, remote) => { - // 移除此监听器 - removeConfigChangeListener(listenerId); - // 执行回调 - callback(getValue(this.keyOfReturn(id)), remote); - // 移除冗余的本地临时存储变量 - deleteValue(this.keyOfState(id)); - deleteValue(this.keyOfReturn(id)); - deleteValue(this.keyOfArguments(id)); + /** 添加 id 到监听队列 */ + setValue(key, (getValue(key) ? String(getValue(key)).split(',') : []).concat(id).join(',')); }); - - /** 添加 id 到监听队列 */ - setValue(key, (getValue(key) ? String(getValue(key)).split(',') : []).concat(id).join(',')); } /** @@ -86,38 +73,44 @@ export class CorsEventEmitter { * @param handler 处理器,可以通过处理器返回任意值作为另外一端的回调值 * @returns */ - on(name: string, handler: (args: any[]) => any): number { - const key = this.eventKey(name); - const originId = this.eventMap.get(key); - if (originId) { - return originId; - } else { - // 清空未处理的事件 - setValue(key, ''); - // 添加 models 监听队列 - const id = addConfigChangeListener(key, async (pre, curr, remote) => { - if (remote) { - const list = String(curr).split(','); - // 处理队列 - const id = list.pop(); - - if (id) { - // 设置返回参数 - setValue(this.keyOfReturn(id), await handler(getValue(this.keyOfArguments(id)))); - - // 更新队列 - setTimeout(() => { - // 这里改变参数,可以触发另一端的监听 - setValue(this.keyOfState(id), 1); - // 完成监听,删除id - setValue(key, list.join(',')); - }, 100); - } + on(name: string, handler: (args: any[]) => any) { + return new Promise((resolve) => { + getTab(({ tabId }) => { + const key = tabId + '.' + this.eventKey(name); + const originId = this.eventMap.get(key); + + if (originId) { + resolve(originId); + } else { + // 清空未处理的事件 + setValue(key, ''); + // 添加 models 监听队列 + const id = addConfigChangeListener(key, async (pre, curr, remote) => { + if (remote) { + const list = String(curr).split(','); + // 处理队列 + const id = list.pop(); + + if (id) { + // 设置返回参数 + setValue(this.keyOfReturn(id), await handler(getValue(this.keyOfArguments(id)))); + + // 更新队列 + setTimeout(() => { + // 这里改变参数,可以触发另一端的监听 + setValue(this.keyOfState(id), 1); + // 完成监听,删除id + setValue(key, list.join(',')); + }, 100); + } + } + }); + this.eventMap.set(key, id); + + resolve(id); } }); - this.eventMap.set(key, id); - return id; - } + }); } off(name: string) { @@ -130,6 +123,22 @@ export class CorsEventEmitter { } } +if (typeof GM_listValues !== 'undefined') { + // 离开页面后 + window.onload = () => { + // 删除全部未处理的模态框临时变量,以及监听队列 + listValues().forEach((key) => { + if (/_temp_.event.[0-9a-z]{32}.(state|return|arguments)/.test(key)) { + deleteValue(key); + } + + if (/[0-9a-z]{32}.cors.events.model/.test(key)) { + deleteValue(key); + } + }); + }; +} + /** * 全局跨域对象 */ diff --git a/packages/core/src/interfaces/project.ts b/packages/core/src/interfaces/project.ts index c65e1a7a..dd1b1ed1 100644 --- a/packages/core/src/interfaces/project.ts +++ b/packages/core/src/interfaces/project.ts @@ -2,6 +2,7 @@ import { Script } from './script'; export interface Project { name: string; + level?: number; domains: string[]; scripts: Script[]; } diff --git a/packages/core/src/interfaces/script.ts b/packages/core/src/interfaces/script.ts index df064d60..d592374d 100644 --- a/packages/core/src/interfaces/script.ts +++ b/packages/core/src/interfaces/script.ts @@ -1,6 +1,7 @@ import { HeaderElement } from '../elements/header'; import { ScriptPanelElement } from '../elements/script.panel'; -import { getValue, namespaceKey, setValue } from '../utils/common'; +import { namespaceKey } from '../utils/common'; +import { getValue, setValue } from '../utils/tampermonkey'; import { Config } from './config'; export type ScriptConfigsProvider> = T | { (): T }; @@ -8,6 +9,7 @@ export type ScriptConfigsProvider> = T | { (): export interface ScriptOptions> { name: string; url: (string | RegExp)[]; + level?: number; namespace?: string; notes?: string[]; configs?: ScriptConfigsProvider; @@ -39,17 +41,21 @@ export type ScriptConfigs = { export class Script extends BaseScript { /** 名字 */ name: string; + projectName?: string; /** 匹配路径 */ url: (string | RegExp)[]; /** 唯一命名空间,用于避免 config 重名 */ namespace?: string; /** 后台脚本(不提供管理页面) */ hideInPanel?: boolean; + level?: number; /** 通过 configs 映射并经过解析后的配置对象 */ cfg: Record & { notes?: string } = {} as any; /** 经过初始化页面脚本注入的页面元素,如果初始化脚本未运行,则此元素为空 */ panel?: ScriptPanelElement; + /** 操作面板头部元素 */ header?: HeaderElement; + listenerId?: number; /** 未经处理的 configs 原对象 */ private _configs?: ScriptConfigsProvider; /** 存储已经处理过的 configs 对象,避免重复调用方法 */ @@ -116,9 +122,17 @@ export class Script extends BaseScript } onConfigChange(key: keyof T, handler: (pre: any, curr: any, remote: boolean) => any) { + if (this.listenerId) { + // eslint-disable-next-line no-undef + GM_removeValueChangeListener(this.listenerId); + } + // eslint-disable-next-line no-undef - GM_addValueChangeListener(namespaceKey(this.namespace, key.toString()), (_, pre, curr, remote) => { - handler(pre, curr, remote); - }); + this.listenerId = GM_addValueChangeListener( + namespaceKey(this.namespace, key.toString()), + (_, pre, curr, remote) => { + handler(pre, curr, remote); + } + ); } } diff --git a/packages/core/src/projects/common.ts b/packages/core/src/projects/common.ts index 89a1bd04..736d4c4b 100644 --- a/packages/core/src/projects/common.ts +++ b/packages/core/src/projects/common.ts @@ -1,37 +1,91 @@ import { getDefinedProjects } from '.'; import { Project } from '../interfaces/project'; import { Script } from '../interfaces/script'; -import { getAllRawConfigs, addConfigChangeListener } from '../utils/common'; +import { getAllRawConfigs } from '../utils/common'; import cloneDeep from 'lodash/cloneDeep'; import { $model } from './init'; +import { el } from '../utils/dom'; +import { addConfigChangeListener } from '../utils/tampermonkey'; export const CommonProject: Project = { name: '通用', domains: [], scripts: [ new Script({ - name: '新手教程', + name: '使用教程', url: [/.*/], namespace: 'common.guide', configs: { notes: { - defaultValue: ` - 11 - ` + defaultValue: '' }, - showGuide: { defaultValue: true, label: '显示新手教程', attrs: { type: 'checkbox' } } + showGuide: { defaultValue: true, label: '显示使用教程', attrs: { type: 'checkbox' } } }, - onactive() { - if (self === top && this.cfg.showGuide) { - $model('alert', { - title: '新手教程', - content: this.panel?.notesContainer || '', - confirmButtonText: '我已阅读', - onConfirm: () => { - this.cfg.showGuide = false; - console.log(this.cfg); - } - }); + + oncomplete() { + if (self === top) { + const projectSelector = this.header?.projectSelector?.cloneNode(true) as HTMLSelectElement; + projectSelector.style.border = '1px solid gray'; + const expandSwitcher = this.header?.expandSwitcher?.cloneNode(true) as HTMLDivElement; + const visualSwitcher = this.header?.visualSwitcher?.cloneNode(true) as HTMLDivElement; + const closeButton = this.header?.closeButton?.cloneNode(true) as HTMLDivElement; + + const guide = el('div', [ + el('ul', { className: 'user-guide' }, [ + el('li', [ + 'OCS会根据当前的页面自动选择脚本进行运行,如果没有达到您预期的效果,则代表当前页面并没有脚本运行。', + '以下是全部支持的网课以及包含的脚本:' + ]), + el( + 'ul', + getDefinedProjects().map((project) => { + return el('li', [ + el('div', project.name), + el( + 'ul', + project.scripts.map((script) => + el( + 'li', + { + title: [ + '隐藏操作页面:\t' + (script.hideInPanel ? '是' : '否'), + '在以下页面中运行:\t' + script.url.join(',') + ].join('\n') + }, + script.name + ) + ) + ) + ]); + }) + ), + el('li', '最后温馨提示:请将浏览器页面保持最大化,或者缩小窗口,不能最小化,可能导致脚本卡死!') + ]), + el('hr'), + el('ul', { className: 'user-guide' }, [ + el('li', '以下是窗口顶部菜单栏的解析:'), + el('li', [ + projectSelector || '', + ' ', + '菜单栏的选择框,可选择脚本操作页面,部分脚本会提供操作页面(包含脚本设置和脚本提示)。' + ]), + el('li', [expandSwitcher || '', ' ', '可以 展开/收缩 脚本操作页面。']), + el('li', [visualSwitcher || '', ' ', '可以 最小化/展开 窗口。']), + el('li', [closeButton || '', ' ', closeButton?.title || '']) + ]) + ]); + this.cfg.notes = guide.outerHTML; + + if (this.cfg.showGuide) { + $model('confirm', { + title: '使用教程', + content: this.panel?.notesContainer.outerHTML || '', + confirmButtonText: '我已阅读,不再提示', + onConfirm: () => { + this.cfg.showGuide = false; + } + }); + } } } }), diff --git a/packages/core/src/projects/cx.ts b/packages/core/src/projects/cx.ts index c4628fa5..a6de7af9 100644 --- a/packages/core/src/projects/cx.ts +++ b/packages/core/src/projects/cx.ts @@ -5,6 +5,7 @@ import { $model } from './init'; export const CXProject: Project = { name: '超星学习通', + level: 99, domains: ['chaoxing.com'], scripts: [ new Script({ @@ -41,6 +42,7 @@ export const CXProject: Project = { new Script({ name: '课程学习', namespace: 'cx.study', + level: 99, url: [/\/mycourse\/studentstudy/], configs: { diff --git a/packages/core/src/projects/init.ts b/packages/core/src/projects/init.ts index 4bb78f01..f06323ed 100644 --- a/packages/core/src/projects/init.ts +++ b/packages/core/src/projects/init.ts @@ -6,10 +6,11 @@ import { Config } from '../interfaces/config'; import { cors } from '../interfaces/cors'; import { Project } from '../interfaces/project'; import { Script } from '../interfaces/script'; -import { getMatchedScripts, getValue, namespaceKey } from '../utils/common'; +import { getMatchedScripts, namespaceKey } from '../utils/common'; import { el, enableElementDraggable, tooltip } from '../utils/dom'; import { StartConfig } from '../utils/start'; import { humpToTarget } from '../utils/string'; +import { getInfos, getValue } from '../utils/tampermonkey'; export type ModelAttrs = Pick< ModelElement, @@ -53,17 +54,17 @@ const InitPanelScript = new Script({ /** 图标 */ container.header.logo = tooltip( el('img', { - src: 'https://cdn.ocsjs.com/logo.png', + src: getInfos().script.icon || '', width: 18, className: 'logo', title: '官方教程', onclick: () => { - window.open('https://docs.ocsjs.com', '_blank'); + window.open(getInfos().script.homepage || '', '_blank'); } }) ); /** 版本简介 */ - container.header.profile = el('div', { className: 'profile' }, 'OCS-' + (process.env.__VERSION__ || '0')); + container.header.profile = el('div', { className: 'profile' }, 'OCS-' + (getInfos().script.version || '0')); /** 面板切换器 */ const projectSelector = tooltip( @@ -71,7 +72,7 @@ const InitPanelScript = new Script({ 'select', { className: ['project-selector', this.cfg.expandAll ? 'expand-all' : ''].join(' '), - title: '选择脚本管理页面,当全部展开时,显示全部管理页面。', + title: '点击选择脚本操作页面,部分脚本会提供操作页面(包含脚本设置和脚本提示)。', onchange: () => { this.cfg.currentPanelName = projectSelector.value; // 替换元素 @@ -79,8 +80,10 @@ const InitPanelScript = new Script({ } }, (select) => { - for (const project of projects) { - const scripts = getMatchedScripts([project], getValue('_urls_') || []).filter((s) => !s.hideInPanel); + for (const project of projects.sort(({ level: a = 0 }, { level: b = 0 }) => b - a)) { + const scripts = getMatchedScripts([project], getValue('_urls_') || []) + .filter((s) => !s.hideInPanel) + .sort(({ level: a = 0 }, { level: b = 0 }) => b - a); for (const script of scripts) { select.append( el('option', { @@ -122,11 +125,11 @@ const InitPanelScript = new Script({ const visualSwitcher = tooltip( el('div', { className: 'switch ', - title: isMinimize() ? '展开窗口' : '最小化窗口', + title: isMinimize() ? '点击展开窗口' : '点击最小化窗口', innerText: isMinimize() ? '□' : '-', onclick: () => { this.cfg.visual = isMinimize() ? 'normal' : 'minimize'; - visualSwitcher.title = isMinimize() ? '展开窗口' : '最小化窗口'; + visualSwitcher.title = isMinimize() ? '点击展开窗口' : '点击最小化窗口'; visualSwitcher.innerText = isMinimize() ? '□' : '-'; } }) @@ -138,7 +141,7 @@ const InitPanelScript = new Script({ el('div', { className: 'close ', innerText: 'x', - title: '关闭窗口(不会影响脚本运行,连续点击三次页面可以重新唤出)', + title: '点击关闭窗口(不会影响脚本运行,连续点击三次页面任意位置可以重新唤出窗口)', onclick: () => (this.cfg.visual = 'close') }) ); @@ -152,11 +155,12 @@ const InitPanelScript = new Script({ // 监听提示内容改变 script.onConfigChange('notes', (pre, curr) => { - scriptPanel.notesContainer.replaceChildren(...createNotes(script)); + scriptPanel.notesContainer.innerHTML = script.cfg.notes || ''; }); // 注入 panel 对象 , 脚本可修改 panel 对象进行面板的内容自定义 script.panel = scriptPanel; - scriptPanel.notesContainer.replaceChildren(...createNotes(script)); + + scriptPanel.notesContainer.innerHTML = script.cfg.notes || ''; scriptPanel.configsBody.append(...createConfigs(script.namespace, script.configs || {})); scriptPanel.configsContainer.append(scriptPanel.configsBody); @@ -166,32 +170,33 @@ const InitPanelScript = new Script({ /** 创建内容 */ const createBody = () => { const scriptContainers = []; + const allScript = []; for (const project of projects) { const scripts = getMatchedScripts([project], getValue('_urls_') || [location.href]).filter( (s) => !s.hideInPanel ); + allScript.push(...scripts); const initPanelAndScript = (script: Script) => { const panel = createScriptPanel(project.name, script); + script.projectName = project.name; script.panel = panel; script.header = container.header; return panel; }; - - // 如果全部展开 - if (this.cfg.expandAll) { - for (const script of scripts) { - scriptContainers.push(initPanelAndScript(script)); - } - } else { - const script = scripts.find((s) => project.name + '-' + s.name === this.cfg.currentPanelName); - if (script) { - return [initPanelAndScript(script)]; - } + for (const script of scripts) { + scriptContainers.push(initPanelAndScript(script)); } } + if (!this.cfg.expandAll) { + const index = allScript.findIndex((s) => s.projectName + '-' + s.name === this.cfg.currentPanelName); + if (index !== -1) { + return [scriptContainers[index]]; + } + } + // 如果全部展开 return scriptContainers; }; @@ -275,15 +280,6 @@ const InitPanelScript = new Script({ return elements; }; - /** 创建内容板块 */ - const createNotes = (script: Script) => { - const notes: HTMLDivElement[] = []; - - for (const note of script.cfg.notes?.split('\n') || []) { - notes.push(el('div', {}, note)); - } - return notes; - }; /** 初始化模态框系统 */ const initModelSystem = () => { @@ -293,6 +289,7 @@ const InitPanelScript = new Script({ const attrs = _attrs as ModelAttrs; attrs.onCancel = () => resolve(''); attrs.onConfirm = resolve; + $model(type, attrs); }); }); diff --git a/packages/core/src/projects/zhs.ts b/packages/core/src/projects/zhs.ts index ce0d7d39..9783c3a0 100644 --- a/packages/core/src/projects/zhs.ts +++ b/packages/core/src/projects/zhs.ts @@ -3,12 +3,13 @@ import { Script } from '../interfaces/script'; export const ZHSProject: Project = { name: '知道智慧树', + level: 99, domains: ['zhihuishu.com'], scripts: [ new Script({ name: '课程学习', url: [/.*/], - + level: 99, namespace: 'zhs.study', configs: { notes: { diff --git a/packages/core/src/utils/common.ts b/packages/core/src/utils/common.ts index c7eb7817..74f520bd 100644 --- a/packages/core/src/utils/common.ts +++ b/packages/core/src/utils/common.ts @@ -1,26 +1,42 @@ import { Config } from '../interfaces/config'; import { Project } from '../interfaces/project'; import { Script } from '../interfaces/script'; +import { getValue, setValue } from './tampermonkey'; /** - * 通过key获取存储的值 - * @param key 键 - * @param defaultValue 默认值 + * 构造 config 配置对象, 可进行响应式存储 + * @param script * @returns */ -export function getValue(key: string, defaultValue?: any) { - // eslint-disable-next-line no-undef - return GM_getValue(key, defaultValue); -} +export function createConfigProxy(script: Script) { + const proxy = new Proxy(script.cfg, { + set(target, propertyKey, value) { + const key = namespaceKey(script.namespace, propertyKey); + setValue(key, value); + return Reflect.set(target, propertyKey, value); + }, + get(target, propertyKey) { + const value = getValue(namespaceKey(script.namespace, propertyKey)); + Reflect.set(target, propertyKey, value); + return value; + } + }); -export function deleteValue(key: string) { - // eslint-disable-next-line no-undef - GM_deleteValue(key); -} + // 设置默认值 + for (const key in script.configs) { + if (Object.prototype.hasOwnProperty.call(script.configs, key)) { + const element = Reflect.get(script.configs, key); + const value = getValue(namespaceKey(script.namespace, key)); + Reflect.set(proxy, key, value === '' ? element.defaultValue : value); + } + } + + if (script.namespace) { + // 重置特殊的 notes 对象 + proxy.notes = script.configs?.notes?.defaultValue; + } -export function listValues() { - // eslint-disable-next-line no-undef - return GM_listValues(); + return proxy; } /** @@ -44,29 +60,6 @@ export function getAllRawConfigs(scripts: Script[]): Record { return object; } -/** - * 存储值 - * @param key 键 - * @param value 值 - * @returns - */ -export function setValue(key: string, value: any) { - // eslint-disable-next-line no-undef - GM_setValue(key, typeof value === 'undefined' ? '' : value); -} - -export function addConfigChangeListener(key: string, handler: (pre: any, curr: any, remote: boolean) => any) { - // eslint-disable-next-line no-undef - return GM_addValueChangeListener(key, (_, pre, curr, remote) => { - handler(pre, curr, remote); - }); -} - -export function removeConfigChangeListener(listenerId: number) { - // eslint-disable-next-line no-undef - GM_removeValueChangeListener(listenerId); -} - /** * 获取匹配到的程序 * @param projects 程序列表 @@ -94,38 +87,10 @@ export function namespaceKey(namespace: string | undefined, key: any) { return namespace ? namespace + '.' + key.toString() : key.toString(); } -/** - * 构造 config 配置对象, 可进行响应式存储 - * @param script - * @returns - */ -export function createConfigProxy(script: Script) { - const proxy = new Proxy(script.cfg, { - set(target, propertyKey, value) { - const key = namespaceKey(script.namespace, propertyKey); - setValue(key, value); - return Reflect.set(target, propertyKey, value); - }, - get(target, propertyKey) { - const value = getValue(namespaceKey(script.namespace, propertyKey)); - Reflect.set(target, propertyKey, value); - return value; - } +export function uuid() { + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); }); - - // 设置默认值 - for (const key in script.configs) { - if (Object.prototype.hasOwnProperty.call(script.configs, key)) { - const element = Reflect.get(script.configs, key); - const value = getValue(namespaceKey(script.namespace, key)); - Reflect.set(proxy, key, value === '' ? element.defaultValue : value); - } - } - - if (script.namespace) { - // 重置特殊的 notes 对象 - proxy.notes = script.configs?.notes?.defaultValue; - } - - return proxy; } diff --git a/packages/core/src/utils/dom.ts b/packages/core/src/utils/dom.ts index fbd331cd..2f8baf29 100644 --- a/packages/core/src/utils/dom.ts +++ b/packages/core/src/utils/dom.ts @@ -2,48 +2,79 @@ import { CustomElementTagMap } from '../elements/interface'; export type AllElementTagMaps = HTMLElementTagNameMap & CustomElementTagMap; +export type AllElementTagKeys = keyof AllElementTagMaps; +/** 子元素 */ +export type ElementChildren = (string | Node)[] | string; +/** 元素数学 */ +export type ElementAttrs = Partial; +/** 元素处理回调 */ +export type ElementHandler = ( + this: AllElementTagMaps[K], + el: AllElementTagMaps[K] +) => void; /** * 创建元素,效果等同于 document.createElement(tagName, options) * @param tagName 标签名 - * @param attrs 元素属性 - * @param arg 可以是一个子元素列表,或者一个创建元素成功后的回调函数 + * @param attrsOrChildren 元素属性,或者子元素列表,或者字符串 + * @param childrenOrHandler 子元素列表,或者元素生成的回调函数 */ -export function el( +export function el(tagName: K, children?: ElementChildren): AllElementTagMaps[K]; +export function el( tagName: K, attrs?: Partial ): AllElementTagMaps[K]; - -export function el( +export function el( + tagName: K, + attrsOrChildren?: ElementAttrs | ElementChildren +): AllElementTagMaps[K]; +export function el( tagName: K, attrs?: Partial, children?: (string | HTMLElement)[] | string ): AllElementTagMaps[K]; -export function el( +export function el( tagName: K, attrs?: Partial, - handler?: { (el: AllElementTagMaps[K]): void } + handler?: ElementHandler ): AllElementTagMaps[K]; -export function el( +export function el( tagName: K, attrs?: Partial, - arg?: { (el: AllElementTagMaps[K]): void } | (string | HTMLElement)[] | string + childrenOrHandler?: ElementChildren | ElementHandler +): AllElementTagMaps[K]; +export function el( + tagName: K, + attrsOrChildren?: ElementAttrs | ElementChildren, + childrenOrHandler?: ElementChildren | ElementHandler ): AllElementTagMaps[K] { const element: AllElementTagMaps[K] = document.createElement(tagName) as any; - /** 设置属性 */ - for (const key in attrs) { - if (Object.prototype.hasOwnProperty.call(attrs, key)) { - const value = attrs[key]; - Reflect.set(element, key, value); + if (attrsOrChildren) { + if (Array.isArray(attrsOrChildren)) { + element.append(...attrsOrChildren); + } else if (typeof attrsOrChildren === 'string') { + element.append(attrsOrChildren); + } else { + const attrs = attrsOrChildren; + /** 设置属性 */ + for (const key in attrs) { + if (Object.prototype.hasOwnProperty.call(attrs, key)) { + const value = attrs[key]; + Reflect.set(element, key, value); + } + } } } - if (typeof arg === 'function') { - arg?.(element); - } else if (typeof arg === 'object') { - element.append(...arg); - } else if (typeof arg === 'string') { - element.append(arg); + if (childrenOrHandler) { + if (typeof childrenOrHandler === 'function') { + childrenOrHandler.call(element, element); + } else if (Array.isArray(childrenOrHandler)) { + element.append(...childrenOrHandler); + } else if (typeof childrenOrHandler === 'string') { + element.append(childrenOrHandler); + } } + return element; } diff --git a/packages/core/src/utils/start.ts b/packages/core/src/utils/start.ts index 75c0c891..fc328c0f 100644 --- a/packages/core/src/utils/start.ts +++ b/packages/core/src/utils/start.ts @@ -1,7 +1,7 @@ -import { cors } from '../interfaces/cors'; import { Project } from '../interfaces/project'; -import { getMatchedScripts, createConfigProxy } from './common'; +import { createConfigProxy, getMatchedScripts, uuid } from './common'; +import { setTab } from './tampermonkey'; /** * 启动配置 @@ -16,6 +16,9 @@ export interface StartConfig { * @param cfg 启动配置 */ export function start(cfg: StartConfig) { + // 添加当前标签 id + setTab({ tabId: uuid() }); + /** 为对象添加响应式特性,在设置值的时候同步到本地存储中 */ cfg.projects = cfg.projects.map((p) => { p.scripts = p.scripts.map((s) => { @@ -46,6 +49,4 @@ export function start(cfg: StartConfig) { script.onbeforeunload?.(cfg); }); }; - - cors.init(); } diff --git a/packages/core/src/utils/tampermonkey.ts b/packages/core/src/utils/tampermonkey.ts new file mode 100644 index 00000000..f5ab7422 --- /dev/null +++ b/packages/core/src/utils/tampermonkey.ts @@ -0,0 +1,101 @@ +/** + * 油猴封装库 + * + * 在对本地持久化存储时可以使用 getValue , setValue 等方法 + * 在对当前标签页中的临时变量,可以使用 getTab , setTab 的方法 + * + * 例如设置信息则使用本地持久化存储 + * 而对于消息推送,弹窗通知等临时,但是需要跨域的变量,可以使用标签页去存储临时信息 + */ + +/** + * 通过key获取存储的值 + * @param key 键 + * @param defaultValue 默认值 + * @returns + */ +export function getValue(key: string, defaultValue?: any) { + // eslint-disable-next-line no-undef + return GM_getValue(key, defaultValue); +} + +export function deleteValue(key: string) { + // eslint-disable-next-line no-undef + GM_deleteValue(key); +} + +export function listValues() { + // eslint-disable-next-line no-undef + return GM_listValues(); +} + +/** + * 存储值 + * @param key 键 + * @param value 值 + * @returns + */ +export function setValue(key: string, value: any) { + // eslint-disable-next-line no-undef + GM_setValue(key, typeof value === 'undefined' ? '' : value); +} + +export function addConfigChangeListener(key: string, handler: (pre: any, curr: any, remote: boolean) => any) { + // eslint-disable-next-line no-undef + return GM_addValueChangeListener(key, (_, pre, curr, remote) => { + handler(pre, curr, remote); + }); +} + +export function removeConfigChangeListener(listenerId: number) { + // eslint-disable-next-line no-undef + GM_removeValueChangeListener(listenerId); +} + +export function getTab(callback: (obj: any) => void) { + // eslint-disable-next-line no-undef + GM_getTab(callback); +} + +export function setTab(value: object) { + // eslint-disable-next-line no-undef + GM_saveTab(value); +} + +export function getInfos() { + // eslint-disable-next-line no-undef + return GM_info; +} + +/** + * 发送系统通知 + * @param content 内容 + * @param options 选项 + */ +export function notification( + content: string, + options?: { + /** 通知点击时 */ + onclick?: () => void; + /** 通知关闭时 */ + ondone?: () => void; + /** 通知是否重要 */ + important?: boolean; + /** 显示时间,默认为0 */ + timeout?: number; + } +) { + const { onclick, ondone, important, timeout } = options || {}; + const { icon, name } = getInfos().script; + // eslint-disable-next-line no-undef + GM_notification({ + title: name, + text: content, + image: icon || '', + highlight: important, + onclick, + ondone, + silent: true, + timeout + }); +} diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 9bff5999..b793f0a4 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -1,7 +1,7 @@ import { visualizer } from 'rollup-plugin-visualizer'; import { defineConfig } from 'vite'; import banner from 'vite-plugin-banner'; -import { author, description, homepage, license, name, version } from '../../package.json'; +import { author, description, homepage, license, name } from '../../package.json'; const bannerContent = ` /*! @@ -31,9 +31,6 @@ export default defineConfig({ formats: ['umd'] } }, - define: { - 'process.env.__VERSION__': JSON.stringify(version) - }, plugins: [visualizer(), banner(bannerContent)] }); diff --git a/packages/web/src/App.vue b/packages/web/src/App.vue index f37ace9d..53813c4c 100644 --- a/packages/web/src/App.vue +++ b/packages/web/src/App.vue @@ -101,7 +101,11 @@ onMounted(() => { 'OCS更新程序', `更新中: ${(chunkLength / 1024 / 1024).toFixed(2)}MB/${(totalLength / 1024 / 1024).toFixed(2)}MB`, 'updater', - { type: 'info', duration: 5, close: false } + { + type: 'info', + duration: 5, + close: false + } ); } }); diff --git a/packages/web/src/pages/extensions/index.vue b/packages/web/src/pages/extensions/index.vue index 3010b412..a5c4ce3d 100644 --- a/packages/web/src/pages/extensions/index.vue +++ b/packages/web/src/pages/extensions/index.vue @@ -177,7 +177,11 @@ function installListener(name: string, channel: string, rate: number, chunkLengt '拓展下载', `${name} 下载中: ${(chunkLength / 1024 / 1024).toFixed(2)}MB/${(totalLength / 1024 / 1024).toFixed(2)}MB`, 'download-extensions-' + name, - { type: 'info', duration: 5, close: false } + { + type: 'info', + duration: 5, + close: false + } ); } } diff --git a/scripts/build-core.js b/scripts/build-core.js index 605b9c5f..e4e77c92 100644 --- a/scripts/build-core.js +++ b/scripts/build-core.js @@ -36,23 +36,30 @@ async function createUserJs(cb) { metadata: { name: 'OCS 网课助手', version: version, - description: `OCS 网课助手,支持 ${ocs.definedProjects.map( - (s) => s.name - )},等网课的视频学习,自动跳转,及部分的作业,考试功能。`, + description: `OCS(online-course-script) 网课助手,专注于帮助大学生从网课中释放出来。让自己的时间把握在自己的手中,拥有人性化的操作页面,流畅的步骤提示,支持 ${ocs + .getDefinedProjects() + .map((s) => s.name)},等网课的学习,作业。具体的功能请查看官网的功能列表 https://docs.ocsjs.com 。`, author: 'enncy', license: 'MIT', namespace: 'https://enncy.cn', homepage: 'https://docs.ocsjs.com', source: 'https://github.com/ocsjs/ocsjs', - icon: 'https://cdn.ocsjs.com/logo.ico', + icon: 'https://cdn.ocsjs.com/logo.png', connect: ['enncy.cn', 'icodef.com', 'ocsjs.com', 'localhost'], - match: ocs.definedProjects.map((p) => p.domains.map((d) => `*://*.${d}/*`)).flat(), + match: ocs + .getDefinedProjects() + .map((p) => p.domains.map((d) => `*://*.${d}/*`)) + .flat(), grant: [ - 'unsafeWindow', + 'GM_info', + 'GM_getTab', + 'GM_saveTab', 'GM_setValue', 'GM_getValue', + 'unsafeWindow', 'GM_listValues', 'GM_deleteValue', + 'GM_notification', 'GM_xmlhttpRequest', 'GM_getResourceText', 'GM_addValueChangeListener',