// 请求器组件 class API { constructor(options = {}) { // 接口基础地址 this.baseUrl = '' // 合并配置 Object.assign(this, options) } // 创建新的 XMLHttpRequest 实例 createXHR() { return new XMLHttpRequest() } // 设置请求头 setHeaders(xhr, headers = {}) { for (let key in headers) { xhr.setRequestHeader(key, headers[key]) } } // 格式化数据 formatData(data) { if (typeof data !== 'string') return data try { return JSON.parse(data) } catch (e) { return data } } // 中断请求 abort(xhr, callback = null) { if (xhr) { xhr.abort() callback && callback() } } // 处理响应 handleResponse(xhr, callback, success, fail, error) { try { const response = this.formatData(xhr.responseText) if (xhr.status >= 200 && xhr.status < 300) { callback && callback(response) success && success(response) } else { const err = new Error(`HTTP Error ${xhr.status}`) error && error(err) fail && fail(err) } } catch (e) { error && error(e) fail && fail(e) } } // get请求 get(options = {}) { const defaultOption = { url: '', headers: null, callback: null, success: null, fail: null, error: null, async: true, } const config = Object.assign({}, defaultOption, options) const { url, callback, success, fail, error, async, headers } = config const xhr = this.createXHR() xhr.open('GET', `${this.baseUrl}${url}`, async) if (headers) { this.setHeaders(xhr, headers) } xhr.addEventListener('error', err => { error && error(err) fail && fail(err) }) xhr.onreadystatechange = () => { if (xhr.readyState === 4) { this.handleResponse(xhr, callback, success, fail, error) } } xhr.send() return xhr } // post请求 post(options = {}) { const defaultOption = { url: '', data: null, headers: null, progress: null, callback: null, success: null, fail: null, error: null, async: true, } const config = Object.assign({}, defaultOption, options) const { url, data, progress, callback, success, error, fail, async, headers } = config const xhr = this.createXHR() xhr.open('POST', `${this.baseUrl}${url}`, async) if (headers) { this.setHeaders(xhr, headers) } if (progress) { xhr.upload.addEventListener('progress', evt => { progress(evt) }) } xhr.addEventListener('error', err => { error && error(err) fail && fail(err) }) xhr.onreadystatechange = () => { if (xhr.readyState === 4) { this.handleResponse(xhr, callback, success, fail, error) } } xhr.send(data) return xhr } } // UI组件 class UI { constructor(registry = null) { const that = this // 属性转换方法 const parseBoolean = value => { return value !== null && value !== 'false' } // 搜索框 this.Search = class Input extends HTMLElement { // 可用属性 static observedAttributes = ['value', 'placeholder', 'disabled', 'max', 'wrap'] static get observedAttributes() { return ['value', 'placeholder', 'disabled', 'max', 'wrap'] } // 构造函数 constructor() { super() this.attachShadow({ mode: 'open' }) // 输入框 this.input = null // 输入框值 this.value = '' // 输入框占位符 this.placeholder = '搜索...' // 输入框禁用 this.disabled = false // 输入框最大长度 this.max = Infinity // 是否换行 this.wrap = false } // 生命周期 connectedCallback() { this.init() this.bindEvent() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { if (this[name] === newValue) return this[name] = name === 'disabled' || name === 'wrap' ? parseBoolean(newValue) : newValue if (!this.input) return switch (name) { case 'disabled': const inputEl = this.shadowRoot.querySelector('.search-input') if (inputEl) { inputEl.toggleAttribute('disabled', this.disabled) inputEl.contentEditable = !this.disabled const clearBtn = this.shadowRoot.querySelector('.search-clear') if (clearBtn) { clearBtn.classList.toggle('visible') } } break case 'value': const searchInput = this.shadowRoot.querySelector('.search-input') if (searchInput) { searchInput.textContent = newValue } break } } // 初始化 init() { const shadow = this.shadowRoot shadow.innerHTML = ` ` this.input = shadow.querySelector('.search-input') this.input.textContent = '' this.value = '' // 初始清除按钮状态 this.updateClearButton() // 换行处理 if (this.wrap) { this.input.style.whiteSpace = 'pre-wrap' this.input.style.overflowY = 'auto' } else { this.input.style.whiteSpace = 'nowrap' this.input.style.overflowX = 'auto' } } // 清空输入框 updateClearButton() { const clearBtn = this.shadowRoot.querySelector('.search-clear') if (clearBtn) { clearBtn.classList.toggle('visible', this.value.length > 0) } } clear() { this.input.textContent = '' this.value = '' this.updateClearButton() this.dispatchEvent(new CustomEvent('change', { detail: this.value })) } // 绑定事件 bindEvent() { const clearBtn = this.shadowRoot.querySelector('.search-clear') const inputEl = this.input clearBtn.addEventListener('click', e => { e.preventDefault() this.clear() }) inputEl.addEventListener('input', e => { let newValue = inputEl.textContent.replace(/[\n]/g, '') if (newValue.length > this.max) { newValue = newValue.substring(0, this.max) inputEl.textContent = newValue } this.value = newValue this.updateClearButton() // 移动光标到末尾 const range = document.createRange() const sel = window.getSelection() range.selectNodeContents(inputEl) range.collapse(false) sel.removeAllRanges() sel.addRange(range) this.dispatchEvent(new CustomEvent('change', { detail: this.value })) }) inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault() this.dispatchEvent(new CustomEvent('enter', { detail: this.value })) } }) inputEl.addEventListener('blur', () => { this.dispatchEvent(new CustomEvent('blur')) }) inputEl.addEventListener('focus', () => { this.dispatchEvent(new CustomEvent('focus')) }) } } // 下拉框 this.Select = class Select extends HTMLElement { // 可用属性 static get observedAttributes() { return ['placeholder', 'list', 'value', 'current', 'disabled', 'focus'] } // 构造函数 constructor() { super() // 下拉框 this.select = null this.ele = null // 下拉框列表 this.list = '默认选项' this.dl = null this.options = [] // 下拉框当前下标 this.current = 0 // 下拉框值 this.value = '' // 下拉框占位符 this.placeholder = null // 下拉框禁用 this.disabled = false // 下拉框按钮 this.button = null // 下拉框焦点 this.focus = false this.selected = false } // 生命周期 connectedCallback() { this.init() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { this[name] = newValue if (name == 'disabled') { if (eval(this.disabled)) { this.ele.querySelector('.select-button').setAttribute('disabled', eval(this.disabled)) } else { this.ele.querySelector('.select-button').removeAttribute('disabled') } } else if (name == 'value') { if (this.dl) this.dl.innerHTML = '' this.addOption() if (this.button) this.button.innerText = this.placeholder || this.options[this.current].text } } // 重置 reset() { const { current, focus, options, placeholder, list } = this if (focus) { this.selected = false this.select.classList.remove('selected') } this.value = options[current].value this.button.innerText = placeholder || list.split(',')[current] } // 添加选项 addOption() { const { list, value } = this const options = list.split(',') const optionValues = value.split(',') if (this.dl) this.dl.innerHTML = '' // 添加选项 options.forEach((item, index) => { const dd = document.createElement('dd') dd.innerText = item dd.value = optionValues[index] dd.className = dd.part = 'select-option' if (index === Number(this.current)) { dd.classList.add('selected') } this.options.push({ text: item, value: optionValues[index] }) if (this.dl) this.dl.appendChild(dd) }) } // 初始化 init() { const { focus, disabled, placeholder, options, current } = this const shadow = this.attachShadow({ mode: 'open' }) this.select = document.createElement('div') this.select.className = this.select.part = 'select' this.dl = document.createElement('dl') const dl = this.dl dl.className = dl.part = 'select-list' dl.setAttribute('popover', '') this.button = document.createElement('button') const button = this.button button.disabled = disabled button.className = button.part = 'select-button' button.popoverTargetElement = dl const style = document.createElement('style') style.innerHTML = ` .select { position: relative; font-size: 1em; --background-color: #fff; --border-color: #ccc; --list-background-color: #fff; --hover-background-color: #dee6ff; --hover-text-color: #001eff; --selected-background-color: #dee6ff; --selected-text-color: #001eff; --border-radius: 5px; --min-width: 150px; min-width: var(--min-width); } .select.selected button{ background: var(--selected-background-color); color: var(--selected-text-color); border: 1px solid transparent; } .select-button { min-width: var(--min-width); width: 100%; padding: .5em 1em; border: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--background-color); cursor: pointer; font-size: 1em; } .select-button:disabled { background: #f5f5f5; cursor: not-allowed; opacity: .9; } .select-button::after { content: '▼'; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); } .select-list { position: absolute; min-width: var(--min-width); width: 100%; background: var(--list-background-color); border: none; border-radius: var(--border-radius); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); list-style: none; padding: 0; top: 0; left: 0; margin: 0; } .select-list dt,.select-list dd { padding: .5em 1em; margin: .2em; border-radius: calc(var(--border-radius) / 2); } .select-list dt { background: #f5f5f5; cursor: not-allowed; } .select-list dd:hover,.select-list dd.selected { background: var(--hover-background-color); cursor: pointer; color: var(--hover-text-color); } ` this.addOption() button.innerText = placeholder || options[current].text this.value = options[current].value this.select.appendChild(button) this.select.appendChild(dl) shadow.appendChild(style) shadow.appendChild(this.select) this.ele = shadow // 选项点击事件 dl.addEventListener('click', e => { if (e.target.tagName === 'DD') { const { innerText, value } = e.target button.innerText = innerText this.value = value if (focus) { this.selected = true this.select.classList.add('selected') } // 移除选中状态 dl.querySelectorAll('.select-option').forEach(item => { item.classList.remove('selected') }) e.target.classList.add('selected') dl.hidePopover() this.dispatchEvent(new CustomEvent('change', { detail: e.target })) } }) // 按钮点击事件 button.addEventListener('click', () => { const { left, top, height, width } = this.select.getBoundingClientRect() dl.style.top = `${top + height}px` dl.style.left = `${left}px` dl.style.width = `${width}px` }) } } // 加载中 this.Loading = class Loading extends HTMLElement { static observedAttributes = ['content', 'inline', 'hidden'] constructor() { super() this.loading = null // 加载中内容 this.content = '' // 是否内联 this.inline = false // 是否默认隐藏 this.hidden = true } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { if (name == 'content') { this[name] = newValue } else { this[name] = eval(newValue) } } //生命周期 connectedCallback() { this.init() !this.hidden && this.show() } // 初始化 init() { const shadow = this.attachShadow({ mode: 'open' }) const style = document.createElement('style') const loading = document.createElement('div') const mask = document.createElement('div') const content = document.createElement('div') mask.className = mask.part = 'loading' loading.className = loading.part = 'loader' content.className = content.part = 'loading-content' content.innerText = this.content style.innerHTML = ` .loading{ --background-color: rgba(0, 0, 0, 0.2); --text-color: #333; --shadow-color: rgba(0, 0, 0, 0.1); --icon-color: white; ${this.inline ? 'position: absolute;' : 'position: fixed;'} top: 0; left: 0; width: 100%; height: 100%; backdrop-filter: blur(5px); display: none; z-index: calc(Infinity + 1); font-size: 1rem; background: var(--background-color); } .loader { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 6em; height: 6em; filter: drop-shadow(0 0 10px var(--shadow-color)); display: flex; justify-content: center; align-items: center; flex-direction: column; } .loading svg { width: 100%; height: 100%; fill: var(--icon-color); } .loading-content { font-size: .9em; color: var(--text-color); white-space: nowrap; margin-top: 1em; font-weight: bold; } ` loading.innerHTML = ` ` loading.appendChild(content) shadow.appendChild(style) mask.appendChild(loading) shadow.appendChild(mask) this.loading = mask } // 显示loading show(fn = null) { this.hidden = false this.loading.style.display = 'block' this.loading.querySelector('.loading-content').innerText = this.content fn && fn() } // 隐藏loading hide(fn = null) { this.hidden = true this.loading.style.display = 'none' fn && fn() } } // TODO:泡泡列表 this.BubbleList = class BubbleList extends HTMLElement { // 可用属性 static observedAttributes = ['list', 'value', 'current', 'disabled', 'rows', 'size', 'selected'] static get observedAttributes() { return ['selected', 'current', 'disabled'] } constructor() { super() // 泡泡列表 this.bubbleList = null this.bubbles = [] // 泡泡列表属性 this.list = [] // 泡泡列表值 this.val = null // 泡泡列表当前下标 this.current = undefined // 泡泡列表当前值 this.selected = '' // 泡泡列表禁用 this.disabled = false // 泡泡列表行数 this.rows = null // 泡泡列表大小 this.size = 'auto' // 初始化 this.init() this.select() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { this.clear() switch (name) { // 列表 case 'list': if (newValue) { this.list = newValue.split(',') } break // 值 case 'value': if (newValue) { this.val = newValue.split(',') } break // 下标 case 'current': this.current = newValue this.bubbles.forEach((item, index) => { if (index === newValue) { item.classList.add('selected') item.part = 'selected' } else { item.classList.remove('selected') item.part = 'bubble' } }) break // 行数 case 'rows': this.rows = newValue if (this.rows) { const { rows } = this this.bubbleList.style.height = `calc(${Number(rows) * 2.5}em - var(--gap))` this.bubbleList.style.overflow = 'auto' } break default: this[name] = newValue break } if (this.val) { this.list.forEach((item, index) => { this.add(item, this.val[index]) }) } else { this.list.forEach(item => { this.add(item) }) } } // 初始化 init() { const shadow = this.attachShadow({ mode: 'open' }) this.bubbleList = document.createElement('div') this.bubbleList.className = this.bubbleList.part = 'bubble-list' const style = document.createElement('style') style.innerHTML = ` .bubble-list { --background-color: #3297f3; --text-color: #fff; --shadow-color: rgba(0, 0, 0, 0.1); --hover-background-color: #7983ff; --hover-text-color: #fff; --border: 1px solid transparent; --hover-border: 1px solid transparent; --scrollbar-width: 1px; --scrollbar-color: transparent; --gap:.5em; display: flex; flex-wrap: wrap; font-size: 1.2em; gap: var(--gap); user-select: none; } .bubble-list::-webkit-scrollbar { width: var(--scrollbar-width); height: var(--scrollbar-width); } .bubble-list::-webkit-scrollbar-thumb { background: var(--scrollbar-color); border-radius: 5px; } .bubble { padding: .2em 1.5em; border-radius: calc(infinity * 1px); background: var(--background-color); color: var(--text-color); font-size: 1em; box-shadow: 0 0 5px var(--shadow-color); cursor: pointer; transition: all .2s; border: var(--border); } .bubble.selected,.bubble:hover:not([disabled]) { background: var(--hover-background-color); box-shadow: 0 0 10px var(--shadow-color); color: var(--hover-text-color); border: var(--hover-border); } ` shadow.appendChild(style) shadow.appendChild(this.bubbleList) } // 添加泡泡 add(text = '', value = '') { const { current, disabled, size } = this const bubble = document.createElement('button') bubble.className = bubble.part = 'bubble' disabled && bubble.setAttribute('disabled', 'disabled') bubble.innerText = text bubble.value = value || text bubble.style.width = size if (size && size !== 'auto') { bubble.style.whiteSpace = 'nowrap' bubble.style.overflow = 'hidden' bubble.style.textOverflow = 'ellipsis' } this.bubbles.push(bubble) current && this.bubbles[current].classList.add('selected') this.bubbleList.appendChild(bubble) } // 清空泡泡 clear() { this.bubbleList.innerHTML = '' this.bubbles = [] } // 选中泡泡 select(fn = null) { const { bubbleList } = this bubbleList.addEventListener('click', e => { bubbleList.querySelectorAll('.bubble').forEach(item => { item.classList.remove('selected') item.part = 'bubble' }) if (e.target.className === 'bubble') { e.target.classList.add('selected') e.target.part = 'selected' const { innerText, value } = e.target this.selected = innerText this.value = value || innerText fn && fn(innerText, value) } this.dispatchEvent(new CustomEvent('change', { detail: this })) }) } } // 对话框 this.Dialog = class Dialog extends HTMLElement { // 可用属性 static get observedAttributes() { return ['subtitle', 'content', 'hidden'] } constructor() { super() this.dialog = null this.mask = null this.subtitle = '提示' this.content = '' this.confirmText = '确定' this.cancelText = '取消' this.confirm = null this.cancel = null this.hidden = true } // 生命周期 connectedCallback() { this.init() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { if (name == 'hidden') { this[name] = eval(newValue) } else { this[name] = newValue } if (this.dialog) { switch (name) { case 'subtitle': this.dialog.querySelector('.dialog-title').innerText = newValue break case 'content': this.dialog.querySelector('.dialog-text').innerText = newValue break } } } // 初始化 init() { const { subtitle, content, confirmText, cancelText, confirm, cancel, hidden } = this const shadow = this.attachShadow({ mode: 'open' }) const template = document.createElement('template') const style = document.createElement('style') const mask = document.createElement('div') const ctx = document.createElement('div') mask.className = mask.part = 'dialog-mask' ctx.className = ctx.part = 'dialog-content' this.dialog = document.createElement('div') this.dialog.className = this.dialog.part = 'dialog' template.content.appendChild(mask) ctx.innerHTML += ` ${subtitle}
${that.Icon.task}
${content}
` mask.appendChild(ctx) style.innerHTML = ` .dialog { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: calc(Infinity + 1); display: block; font-size: 1em; --background-color: #fff; --border-radius: 10px; --shadow-color: rgba(0, 0, 0, 0.1); --mask-color: rgba(0, 0, 0, 0.5); --show-icon: block; --min-width: 580px; --min-height: 320px; --btn-color: #fff; --btn-background-color: linear-gradient(to bottom, #7983ff, #3297f3); } .dialog-mask { position: fixed; width: 100%; height: 100%; background-color: var(--mask-color); display: flex; justify-content: center; align-items: center; } .dialog-content { padding: 20px; background:url(/static/v3/tipbg01.png) no-repeat,var(--background-color); background-size: cover; border-radius: var(--border-radius); overflow: hidden; box-shadow: 0 0 10px var(--shadow-color); text-align: center; min-width: var(--min-width); min-height: var(--min-height); color:black } .dialog-icon{ margin: 20px auto; display: var(--show-icon); } .dialog-title { font-size: 1.5em; margin-bottom: 10px; text-align: left; display: block; } .dialog-text { font-size: 1.2em; margin-bottom: 20px; } .dialog-btns { display: flex; justify-content: space-around; width: 80%; margin: 40px auto 0 auto; } .dialog-btn { padding: 5px 20px; margin: 0 10px; border: none; border-radius: 10px; background:var(--btn-background-color); cursor: pointer; color: var(--btn-color); font-size: 1.2em; } ` template.content.appendChild(style) this.dialog.style.display = hidden ? 'none' : 'block' this.dialog.appendChild(template.content.cloneNode(true)) shadow.appendChild(this.dialog) this.mask = this.dialog.querySelector('.dialog-mask') const confirmBtn = this.dialog.querySelector('.dialog-confirm') const cancelBtn = this.dialog.querySelector('.dialog-cancel') // 绑定事件 confirmBtn.addEventListener('click', () => { this.hide() confirm && confirm(this) }) cancelBtn.addEventListener('click', () => { this.hide() cancel && cancel(this) }) } // 显示对话框 show(detail = null) { this.dialog.style.display = 'block' this.hidden = false this.dispatchEvent(new CustomEvent('show', { detail })) } // 隐藏对话框 hide(detail = null) { this.dialog.style.display = 'none' this.hidden = true this.dispatchEvent(new CustomEvent('hide', { detail })) } // 设置对话框 set(options = {}) { Object.assign(this, options) } } // 模态框 this.Modal = class Modal extends HTMLElement { // 可用属性 static get observedAttributes() { return ['subtitle', 'content', 'hidden'] } constructor() { super() // 模态框 this.modal = null // 遮罩层 this.mask = null // 标题 this.subtitle = '标题' // 内容 this.content = '内容' // 动画定时器 this.timer = null // 是否隐藏 this.hidden = true } // 生命周期 connectedCallback() { this.init() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { if (name == 'hidden') { this[name] = eval(newValue) } else { this[name] = newValue } } // 初始化 init() { const { subtitle, content, hidden } = this const shadow = this.attachShadow({ mode: 'open' }) const template = document.createElement('template') const style = document.createElement('style') this.modal = document.createElement('div') this.modal.className = this.modal.part = 'modal' this.mask = document.createElement('div') this.mask.className = this.mask.part = 'modal-mask' this.mask.popoverTargetElement = this.modal template.innerHTML = ` ` style.innerHTML = ` .modal { display: block; font-size: 1rem; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; --background-color: #fff; --border-radius: 10px; --shadow-color: rgba(0, 0, 0, 0.1); --min-width: 200px; --min-height: 120px; --mask-color: rgba(0, 0, 0, 0.3); --mask-blur: 8px; transition: all .3s; } .modal-mask { position: absolute; width: 100%; height: 100%; background-color: var(--mask-color); backdrop-filter: blur(var(--mask-blur)); -moz-backdrop-filter: blur(var(--mask-blur)); -webkit-backdrop-filter: blur(var(--mask-blur)); } .modal-content { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 1em; background: var(--background-color); border-radius: var(--border-radius); box-shadow: 0 0 10px var(--shadow-color); text-align: center; min-width: var(--min-width); width: max-content; min-height: var(--min-height); box-sizing: border-box; } .modal-title { font-size: 1.5em; margin-block: 1vh; display: block; color: black; } .modal-text { font-size: 1.2em; } ` template.content.appendChild(style) this.modal.appendChild(this.mask) this.modal.style.display = hidden ? 'none' : 'block' this.modal.appendChild(template.content.cloneNode(true)) shadow.appendChild(this.modal) } // 动画 animate(action = 'open', params) { const { modal } = this this.timer && clearTimeout(this.timer) switch (action) { case 'open': modal.style.display = 'block' this.hidden = false modal.style.opacity = 0 this.timer = setTimeout(() => { modal.style.opacity = 1 modal.style.display = 'block' this.dispatchEvent(new CustomEvent('open', { detail: modal, params })) }, 100) break case 'close': modal.style.opacity = 0 this.timer = setTimeout(() => { modal.style.display = 'none' this.hidden = true this.dispatchEvent(new CustomEvent('close', { detail: modal, params })) }, 300) break } } // 显示模态框 open(params = null) { this.animate('open', params) } // 隐藏模态框 close(params = null) { this.animate('close', params) } } // TODO:选项卡 this.Tab = class Tab extends HTMLElement { // 可用属性 static get observedAttributes() { return ['list', 'current'] } constructor() { super() // 选项卡 this.tabs = null // 选项卡列表 this.list = null // 选项卡当前下标 this.current = 0 } // 生命周期 connectedCallback() { this.init() this.setTab() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { this['list'] = newValue.split('|') this['list'].map((item, index) => { this['list'][index] = item.split('>') }) } // 初始化 init() { const { list } = this const shadow = this.attachShadow({ mode: 'open' }) const template = document.createElement('template') const style = document.createElement('style') this.tabs = document.createElement('div') const tabList = document.createElement('dl') tabList.className = tabList.part = 'tab-list' this.tabs.className = this.tabs.part = 'tab-box' template.innerHTML = ` ` style.innerHTML = ` .tab-box { font-size: 1em; width: 100%; user-select: none; --highlight-color: #001EFF; --text-color: black; --background-color: transparent; --description-color: #424242; --gap: 1em; } .tab-list { width: 100%; display: flex; justify-content: center; gap: var(--gap); margin: 0; } .tab-item { padding: .6em 2em; background: var(--background-color); cursor: pointer; transition: all .3s; border-bottom: 3px solid transparent; text-align: center; display: flex; justify-content: center; align-items: center; flex-direction: column; color: var(--text-color); box-sizing: border-box; margin: 0; } .tab-item:hover,.tab-item.active { border-bottom: 3px solid var(--highlight-color); } .tab-item cite { font-size: .7em; color: var(--description-color); font-weight: 400; font-style: normal; margin-top: .4em; pointer-events: none; } ` list.forEach((item, index) => { const dd = document.createElement('dd') dd.className = dd.part = 'tab-item' dd.index = index if (index === this.current) { dd.classList.add('active') } // 判断是否有描述 if (item.length > 1) { const cite = document.createElement('cite') cite.className = cite.part = 'tab-description' cite.innerText = item[1] dd.innerText = item[0] dd.appendChild(cite) } else { dd.innerText = item } tabList.appendChild(dd) }) this.tabs.appendChild(tabList) shadow.appendChild(style) shadow.appendChild(this.tabs) shadow.appendChild(template.content.cloneNode(true)) } // 选项卡切换 setTab() { this.tabs.addEventListener('click', e => { if (e.target.tagName === 'DD') { this.tabs.querySelectorAll('.tab-item').forEach(item => { item.classList.remove('active') }) e.target.classList.add('active') this.dispatchEvent(new CustomEvent('change', { detail: e.target })) } }) } } // 提示条 this.Tip = class Tip extends HTMLElement { // 可用属性 static get observedAttributes() { return ['content', 'type', 'hidden', 'timeout'] } constructor() { super() // 提示条 this.Tip = null // 提示内容 this.content = '提示内容' // 提示类型 this.type = 'info' // 提示条隐藏 this.hidden = false // 提示条定时器 this.timer = null // 提示条显示时间 this.timeout = 3000 } // 生命周期 connectedCallback() { this.init() !this.hidden && this.show() } // 属性变化 attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'hidden': this[name] = newValue if (this[name]) { this.hide() } break case 'timeout': this[name] = Number(newValue) break case 'type': this.Tip.className = this.Tip.part = `tip ${newValue}` break case 'content': this[name] = newValue break } } // 初始化 init() { const { content, type, hidden } = this const shadow = this.attachShadow({ mode: 'open' }) const template = document.createElement('template') const style = document.createElement('style') this.Tip = document.createElement('div') this.Tip.className = this.Tip.part = `tip ${type}` const contentSlot = document.createElement('slot') const prefixSlot = document.createElement('slot') contentSlot.name = 'content' prefixSlot.name = 'prefix' const closeBtn = document.createElement('button') closeBtn.className = closeBtn.part = 'tip-close' closeBtn.innerHTML = that.Icon.clear contentSlot.innerHTML = `${content}` prefixSlot.innerHTML = `${that.Icon.task}` style.innerHTML = ` .tip { --icon-color: #333; --close-color: #333; --color: #78A2F5; --background-color: #E7F3FF; position: absolute; top: 1vh; left: 50%; transform: translate(-50%,-150%); padding: .5em .5em .5em 1em; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); border: 1px solid; border-radius: 5px; font-size: 16px; z-index: calc(Infinity + 1); transition: all .3s ease-out; display: none; align-items: center; width: auto; opacity: 0; box-sizing: border-box; } .tip-icon { width: 1.3em; height: 1.3em; flex-shrink: 0; margin-right: .5em; --icon-color:var(--color); } .tip-icon svg{ width: 100%; height: 100%; fill: var(--icon-color); } .tip-content { font-size: 1em; font-weight: bold; text-align: left; min-width: 100px; max-width: 400px; } .tip-close{ border: none; background: transparent; cursor: pointer; width: 2.2em; height: 2.2em; margin-left: .5em; padding: 0; flex-shrink: 0; } .tip-close svg { width: 100%; height: 100%; fill: var(--close-color); } .tip { background: var(--background-color); color: var(--color); } .tip.success { --color: #4CAF50; --background-color: #E8F5E9; } .tip.warn { --color: #ff9800; --background-color: #FFF3E0; } .tip.error { --color: #f44336; --background-color: #FFEBEE; } ` // 关闭提示条 closeBtn.onclick = () => { this.hide() } this.Tip.style.display = hidden ? 'none' : 'flex' template.content.appendChild(prefixSlot) template.content.appendChild(contentSlot) this.Tip.appendChild(template.content.cloneNode(true)) this.Tip.appendChild(closeBtn) shadow.appendChild(style) shadow.appendChild(this.Tip) } // 动画 animate(action = 'show') { const { Tip } = this switch (action) { case 'show': Tip.style.display = 'flex' setTimeout(() => { Tip.style.transform = 'translate(-50%, 0)' Tip.style.opacity = 1 }, 50) break case 'hide': Tip.style.transform = 'translate(-50%, -150%)' Tip.style.opacity = 0 Tip.addEventListener( 'transitionend', () => { Tip.style.display = 'none' }, { once: true } ) break } } // 显示提示条 show(msg = null) { const { timeout, Tip, content } = this Tip.querySelector('.tip-content').innerText = msg || content if (Tip.querySelector('.tip-content').innerText !== '') { this.animate('show') if (timeout && timeout > 0) { clearTimeout(this.timer) this.timer = setTimeout(() => { this.animate('hide') }, timeout) } } else { return false } } // 隐藏提示条 hide() { this.animate('hide') } } // 注册组件 if (registry) { registry.map(name => { const componentName = name.toLowerCase() switch (name.toLowerCase()) { case 'ui-search': this.registerComponent(componentName, this.Search) break case 'ui-select': this.registerComponent(componentName, this.Select) break case 'ui-loading': this.registerComponent(componentName, this.Loading) break case 'ui-bubble-list': this.registerComponent(componentName, this.BubbleList) break case 'ui-dialog': this.registerComponent(componentName, this.Dialog) break case 'ui-modal': this.registerComponent(componentName, this.Modal) break case 'ui-tab': this.registerComponent(componentName, this.Tab) break case 'ui-tip': this.registerComponent(componentName, this.Tip) break } }) } else { throw new Error('请传入注册组件名称') } } // 注册组件 registerComponent(name, component, extend = false) { // 如果是扩展元素 if (extend) { customElements.define(name, component, { extends: extend }) } else { customElements.define(name, component) } } // 组件图标 Icon = { // 清除&关闭 clear: ``, // 任务 task: ``, } }