您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通用 UI 组件和工具函数库
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/539247/1611171/%E9%80%9A%E7%94%A8%E7%BB%84%E4%BB%B6%E5%BA%93.js
// ==UserScript== // @name 通用组件库 // @namespace https://greasyfork.org/zh-CN/users/1296281 // @version 1.4.1 // @license GPL-3.0 // @description 通用 UI 组件和工具函数库 // @author ShineByPupil // @match * // @grant none // ==/UserScript== (function () { "use strict"; const colors = { primary: "#4C6EF5", success: "#67c23a", info: "#909399", warning: "#e6a23c", danger: "#f56c6c", }; const defaultColors = []; const lightColors = []; const darkColors = []; const mixColor = (color1, color2, percent) => { // 去掉井号并转换为 0~255 的整数 const c1 = color1.replace(/^#/, ""); const c2 = color2.replace(/^#/, ""); const r1 = parseInt(c1.substr(0, 2), 16); const g1 = parseInt(c1.substr(2, 2), 16); const b1 = parseInt(c1.substr(4, 2), 16); const r2 = parseInt(c2.substr(0, 2), 16); const g2 = parseInt(c2.substr(2, 2), 16); const b2 = parseInt(c2.substr(4, 2), 16); // 百分比转 0~1 const t = Math.min(Math.max(percent, 0), 100) / 100; // 插值计算 const r = Math.round(r1 + (r2 - r1) * t); const g = Math.round(g1 + (g2 - g1) * t); const b = Math.round(b1 + (b2 - b1) * t); // 转回两位十六进制,不足两位补零 const toHex = (x) => x.toString(16).padStart(2, "0"); return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }; for (let key in colors) { const color = colors[key]; defaultColors.push(`--${key}-color: ${color};`); for (let i = 1; i <= 9; i++) { const p = i * 10; lightColors.push( `--${key}-color-light-${i}: ${mixColor(color, "#ffffff", p)};`, ); darkColors.push( `--${key}-color-light-${i}: ${mixColor(color, "#141414", p)};`, ); } } const commonCssTemplate = document.createElement("template"); commonCssTemplate.innerHTML = ` <style> /* 明亮模式 */ :host { /* 主题色 */ ${defaultColors.join("\n")} /* 明亮渐变色 */ ${lightColors.join("\n")} --border-color: #dcdfe6; --border-color-hover: #C0C4CC; --bg-color: #FFFFFF; --text-color: #333333; --placeholder-color: #a8abb2; } :host { font-family: Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", 微软雅黑, Arial, sans-serif; } :host([disabled]) * { cursor: not-allowed; } /* 夜间模式 */ :host-context(:is(.ex, .dark, [data-theme="dark"])) { /* 夜间渐变色 */ ${darkColors.join("\n")} --border-color: #4C4D4F; --border-color-hover: #6C6E72; --bg-color: #141414; --text-color: #CFD3DC; --placeholder-color: #8D9095; } </style> `; class Input extends HTMLElement { input = null; constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = `<input type="text" part="input" />`; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { display: inline-flex; height: 32px; color: var(--text-color); border-color: var(--border-color); border-radius: 4px; background-color: var(--bg-color); } :host(:not([disabled]):hover) { border-color: var(--border-color-hover); } :host(:not([disabled]):focus) { border-color: var(--primary-color); border-inline-end-width: 1px; } input::placeholder { color: var(--placeholder-color); } /* 禁用 */ :host([disabled]) { background-color: #f5f7fa; } :host([disabled]):host-context(:is(.ex, .dark, [data-theme="dark"])) { background-color: #262727; } input { width: -webkit-fill-available; height: inherit; color: currentColor; outline: none; box-sizing: border-box; padding: 4px 11px; border-width: 1px; border-style: solid; border-color: inherit; border-radius: inherit; background-color: inherit; vertical-align: top; transition: all 0.3s; text-align: inherit; } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append( htmlTemplate.content, commonCssTemplate.content.cloneNode(true), cssTemplate.content, ); this.input = this.shadowRoot.querySelector("input"); } connectedCallback() { this.input.addEventListener("input", (e) => { e.stopPropagation(); this.value = e.target.value; this.dispatchEvent(new CustomEvent("input", { detail: this.value })); }); Object.values(this.attributes).forEach((attr) => { if (!/^on/.test(attr.name)) { this.input.setAttribute(attr.name, attr.value); } }); const mo = new MutationObserver((mutationsList) => { for (const m of mutationsList) { if (m.type === "attributes") { const val = this.getAttribute(m.attributeName); if (val === null) { this.input.removeAttribute(m.attributeName); } else { this.input.setAttribute(m.attributeName, val); } } } }); mo.observe(this, { attributes: true }); } get value() { return this.input.value; } set value(val) { this.input.value = val; } } // todo class Option extends HTMLElement { constructor() { super(); } } // todo class Select extends HTMLElement { constructor() { super(); } } class Button extends HTMLElement { static observedAttributes = ["type", "circle", "disabled", "ripple"]; constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = ` <button part="button"> <slot></slot> </button> `; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { --text-color-hover: var(--primary-color); --bg-color: var(--bg-color); --bg-color-hover: var(--primary-color-light-9); --bg-color-disabled: #FFFFFF; --border-color-hover: var(--primary-color); --border-color-disabled: var(--border-color); --border-radius: 5px; } :host-context(:is(.ex, .dark, [data-theme="dark"])) { --bg-color-disabled: transparent; --border-color-hover: var(--primary-color); } /* 禁用 */ :host([disabled]) { --text-color: #a8abb2; --border-color: #dcdfe6; } :host([disabled]):host-context(:is(.ex, .dark, [data-theme="dark"])) { --text-color: rgba(255, 255, 255, .5); --border-color: #414243; } :host([disabled]) button { background-color: var(--bg-color-disabled); border-color: var(--border-color-disabled); } /* 圆形 */ :host([circle]) { border-radius: 50%; --border-radius: 50%; aspect-ratio: 1 / 1; } :host([circle]) button { padding: 8px; } ${Object.keys(colors) .map((type) => { return ` :host([type='${type}']) { --text-color: #FFFFFF; --text-color-hover: #FFFFFF; --bg-color: var(--${type}-color); --bg-color-hover: var(--${type}-color-light-3); --bg-color-disabled: var(--${type}-color-light-5); --border-color: var(--${type}-color); --border-color-hover: var(--${type}-color-light-3); --border-color-disabled: var(--${type}-color-light-5); } `; }) .join("\n")} :host { position: relative; display: inline-flex; box-sizing: border-box; height: 32px; overflow: hidden; color: var(--text-color); background-color: var(--bg-color); border-radius: var(--border-radius); border-color: var(--border-color); } :host([disabled]) { border-color: var(--border-color-disabled); } :host(:not([disabled]):hover) { color: var(--text-color-hover); background-color: var(--bg-color-hover); border-color: var(--border-color-hover); transition: all 0.3s; } button { width: inherit; height: inherit; padding: 8px 15px; display: inline-flex; align-items: center; justify-content: center; gap: 4px; font-family: inherit; color: currentColor; background: inherit; border-width: 1px; border-style: solid; border-color: inherit; border-radius: inherit; outline: none; cursor: pointer; } /* 波纹元素 */ .ripple { position: absolute; border-radius: 50%; transform: scale(0); background-color: rgba(255, 255, 255, 0.6); animation: ripple-animation 600ms linear; pointer-events: none; } /* 波纹动画关键帧 */ @keyframes ripple-animation { to { transform: scale(4); opacity: 0; } } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append( htmlTemplate.content, commonCssTemplate.content.cloneNode(true), cssTemplate.content, ); } connectedCallback() { if (this.hasAttribute("ripple")) { this.addEventListener("click", function (e) { // 计算点击位置相对于按钮的位置 const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; // 创建波纹元素 const ripple = document.createElement("span"); ripple.classList.add("ripple"); ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; // 将波纹添加到按钮,并在动画结束后移除 this.shadowRoot.append(ripple); ripple.addEventListener("animationend", () => { ripple.remove(); }); }); } } } class Switch extends HTMLElement { // 事件来源类型: user | broadcast #currentChangeSource = "user"; static get observedAttributes() { return ["checked", "disabled", "@change"]; } constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = ` <div class="track"> <div class="thumb"></div> </div>`; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { --bg-color: #ccc; --cursor: pointer; } :host { display: inline-block; aspect-ratio: 2/1; height: 20px; } :host([checked]) { --bg-color: ${colors.primary}; } :host([checked]) .thumb { transform: translateX(calc(100% + 4px)); } :host([disabled]) { --cursor: not-allowed; } .track { width: 100%; height: 100%; background: var(--bg-color); border-radius: 14px; position: relative; transition: background .3s; cursor: var(--cursor); outline: none; } .thumb { aspect-ratio: 1/1; height: calc(100% - 4px); background: #fff; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: transform .3s; } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append(htmlTemplate.content, cssTemplate.content); } connectedCallback() { const track = this.shadowRoot.querySelector(".track"); track.addEventListener("click", () => this.toggle()); } attributeChangedCallback(name, oldValue, newValue) { if (name === "checked" && oldValue !== newValue) { const oldChecked = oldValue !== null; const newChecked = newValue !== null; this.dispatchEvent( new CustomEvent("change", { detail: { value: newChecked, oldValue: oldChecked, source: this.#currentChangeSource, }, }), ); this.#currentChangeSource = "user"; } } get checked() { return this.hasAttribute("checked"); } set checked(val) { val ? this.setAttribute("checked", "") : this.removeAttribute("checked"); } get disabled() { return this.hasAttribute("disabled"); } set disabled(val) { val ? this.setAttribute("disabled", "") : this.removeAttribute("disabled"); } toggle() { if (!this.disabled) this.checked = !this.checked; } // 静默更新方法(不触发事件) updateFromBroadcast(value) { this.#currentChangeSource = "broadcast"; this.checked = value; } } class Message extends HTMLElement { static #instance = null; static observedAttributes = ["type"]; constructor() { super(); this.type = this.getAttribute("type"); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = ` <div class="message-box"> <mx-icon class="icon"></mx-icon> <span class="message"></span> </div> `; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> ${Object.keys(colors) .map((type) => { return ` :host([type='${type}']) { --text-color: var(--${type}-color); --bg-color: var(--${type}-color-light-7); --border-color: var(--${type}-color-light-4); } `; }) .join("\n")} .message-box { max-width: 300px; font-size: 14px; display: none; align-items: center; gap: 8px; position: fixed; top: 20px; left: 50%; transform: translate(-50%, 20px); opacity: 0; background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); padding: 10px 15px; border-radius: 5px; z-index: 100; } .message-box.show { transform: translate(-50%, 0); opacity: 1; transition: transform 0.3s ease, opacity 0.3s ease; } .message-box.hide { transform: translate(-50%, -20px); opacity: 0; transition: transform 0.6s ease, opacity 0.6s ease; } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append( htmlTemplate.content, commonCssTemplate.content.cloneNode(true), cssTemplate.content, ); this.box = this.shadowRoot.querySelector(".message-box"); this.icon = this.shadowRoot.querySelector(".icon"); this.message = this.shadowRoot.querySelector(".message"); } connectedCallback() { this.box.addEventListener("transitionend", (e) => { if (this.box.classList.contains("hide")) { this.box.style.display = "none"; this.box.classList.remove("hide"); } }); this.message.addEventListener("click", (e) => { navigator.clipboard.writeText(e.target.textContent); }); } attributeChangedCallback(attrName, oldVal, newVal) { if (attrName === "type") { const map = { primary: "info", success: "success", info: "info", warning: "warning", danger: "close", }; const iconType = map[newVal]; this.icon.setAttribute("type", iconType); } } static get instance() { if (!this.#instance) { const el = document.createElement(getComponentName(this)); document.documentElement.appendChild(el); this.#instance = el; } return this.#instance; } #show(message, type = "info", duration) { const calcDuration = (message) => { // 最小 2 秒, 最大 5 秒, 基础 0.5 秒, 每个字符 50 ms const [min, max, base, perChar] = [2000, 5000, 500, 50]; const lengthTime = message.length * perChar; return Math.min(max, Math.max(min, base + lengthTime)); }; this.setAttribute("type", type); this.message.textContent = message; // 设置信息 this.message.title = message; this.box.style.display = "flex"; requestAnimationFrame(() => { requestAnimationFrame(() => { this.box.classList.add("show"); }); }); clearTimeout(this._hideTimer); this._hideTimer = setTimeout( () => { this.box.classList.remove("show"); this.box.classList.add("hide"); }, duration || calcDuration(message), ); } primary(message, duration) { this.#show(message, "primary", duration); } info(message, duration) { this.#show(message, "info", duration); } success(message, duration) { this.#show(message, "success", duration); } error(message, duration) { this.#show(message, "danger", duration); } warning(message, duration) { this.#show(message, "warning", duration); } } class Dialog extends HTMLElement { visible = false; #confirmBtn = null; #cancelBtn = null; #closeBtn = null; static get observedAttributes() { return ["cancel-text", "confirm-text"]; } constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = ` <main> <header> <slot name="header"></slot> <button class="close">✕</button> </header> <article> <slot></slot> </article> <footer> <slot name="footer"> <slot name="button-before"></slot> <mx-button class="cancel">取消</mx-button> <slot name="button-center"></slot> <mx-button class="confirm" type="primary">确认</mx-button> <slot name="button-after"></slot> </slot> </footer> </main> <div class="mask"></div> `; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { display: none; } main { min-width: 500px; padding: 16px; position: fixed; left: 50%; top: calc(20vh); transform: translateX(-50%); z-index: 3001; border-radius: 4px; background-color: var(--bg-color); color: var(--text-color); box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, .04), 0px 8px 20px rgba(0, 0, 0, .08); } header { padding-bottom: 16px; font-size: 18px; } article { min-width: 500px; } footer { display: flex; justify-content: flex-end; gap: 12px; padding-top: 16px; } .close { font-size: 16px; aspect-ratio: 1/1; padding: 0; position: fixed; top: 16px; right: 16px; background-color: inherit; border: 0; } .close:hover { color: #F56C6C; } .mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 3000; background: rgba(0, 0, 0, 0.5); } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append( htmlTemplate.content, commonCssTemplate.content.cloneNode(true), cssTemplate.content, ); this.#confirmBtn = this.shadowRoot.querySelector(".confirm"); this.#cancelBtn = this.shadowRoot.querySelector(".cancel"); this.#closeBtn = this.shadowRoot.querySelector(".close"); } connectedCallback() { // 按钮文字 { const cancelText = this.getAttribute("cancel-text") || "取消"; const confirmText = this.getAttribute("confirm-text") || "确认"; this.#cancelBtn.textContent = cancelText; this.#confirmBtn.textContent = confirmText; } // 事件初始化 { // 提交按钮 this.#confirmBtn?.addEventListener("click", (e) => { this.visible = false; this.style.display = "none"; this.dispatchEvent(new CustomEvent("confirm")); }); const cancel = () => { this.visible = false; this.style.display = "none"; this.dispatchEvent(new CustomEvent("cancel")); }; // 关闭按钮 this.#cancelBtn?.addEventListener("click", cancel); this.#closeBtn?.addEventListener("click", cancel); // ESC 键盘事件 document.addEventListener("keydown", (e) => { if (e.key === "Escape" && this.visible) { cancel(); } }); } } attributeChangedCallback(name, oldValue, newValue) { if (name === "visible" && oldValue !== newValue) { this.style.display = newValue !== null ? "block" : "none"; } } open() { this.visible = true; this.style.display = "block"; this.dispatchEvent(new CustomEvent("open")); } } class Icon extends HTMLElement { #paths = { info: "M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64m67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344M590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.992 12.992 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z", success: "M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z", warning: "M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 192a58.432 58.432 0 0 0-58.24 63.744l23.36 256.384a35.072 35.072 0 0 0 69.76 0l23.296-256.384A58.432 58.432 0 0 0 512 256m0 512a51.2 51.2 0 1 0 0-102.4 51.2 51.2 0 0 0 0 102.4", close: "M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z", closeBold: "M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z", setting: "M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357.12 357.12 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a351.616 351.616 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357.12 357.12 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294.113 294.113 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293.12 293.12 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294.113 294.113 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288.282 288.282 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293.12 293.12 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a287.616 287.616 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256", search: "m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704", refresh: "M771.776 794.88A384 384 0 0 1 128 512h64a320 320 0 0 0 555.712 216.448H654.72a32 32 0 1 1 0-64h149.056a32 32 0 0 1 32 32v148.928a32 32 0 1 1-64 0v-50.56zM276.288 295.616h92.992a32 32 0 0 1 0 64H220.16a32 32 0 0 1-32-32V178.56a32 32 0 0 1 64 0v50.56A384 384 0 0 1 896.128 512h-64a320 320 0 0 0-555.776-216.384z", }; static observedAttributes = ["type"]; constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = `<svg viewBox="0 0 1024 1024"><path d=""></path></svg>`; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { display: inline-block; width: 1em; height: 1em; color: currentColor; } svg { width: 100%; height: 100%; fill: currentColor; } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append( htmlTemplate.content, commonCssTemplate.content.cloneNode(true), cssTemplate.content, ); this.path = this.shadowRoot.querySelector("path"); } connectedCallback() {} attributeChangedCallback(attributeName, oldValue, newValue) { if (attributeName === "type") { this.toggle(); } } toggle() { if (this.hasAttribute("type")) { this.type = this.getAttribute("type"); if (this.type in this.#paths) { this.path.setAttribute("d", this.#paths[this.type]); } else { console.warn("出现未知的 icon 类型", this); } } } } class Badge extends HTMLElement { constructor() { super(); const htmlTemplate = document.createElement("template"); htmlTemplate.innerHTML = `<slot></slot><sup></sup>`; const cssTemplate = document.createElement("template"); cssTemplate.innerHTML = ` <style> :host { position: relative; } sup { position: absolute; top: 0; right: 0; transform: translate(50%, -50%); background-color: #f56c6c; border-radius: 10px; padding: 0 4px; color: #FFFFFF; } </style> `; this.attachShadow({ mode: "open" }); this.shadowRoot.append(htmlTemplate.content, cssTemplate.content); this.sup = this.shadowRoot.querySelector("sup"); } set value(val) { this.sup.textContent = val; } get value() { return this.sup.textContent; } } // todo class MessageBox {} class ConcurrencyManager { #activeCount = 0; #queue = []; #max; constructor(max = 5) { this.#max = max; } enqueue(fn) { return new Promise((resolve, reject) => { this.#queue.push({ fn, resolve, reject }); this.#next(); }); } setConcurrency(n) { this.#max = n; this.#next(); } #next() { if (this.#activeCount >= this.#max) return; const job = this.#queue.shift(); if (!job) return; this.#activeCount++; job .fn() .then(job.resolve, job.reject) .finally(() => { this.#activeCount--; this.#next(); }); } } const getComponentName = (component) => `mx-${component.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}`; // 注册组件 [Input, Select, Button, Option, Switch, Message, Dialog, Icon, Badge].forEach( (n) => { const name = getComponentName(n); if (!customElements.get(name)) { customElements.define(name, n); } else { console.error(`${name} 组件已注册`); } }, ); Object.assign(window, { MxMessage: Message.instance, MxMgr: new ConcurrencyManager(), }); })();