// 请求器组件
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.value}
`
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: ``,
}
}