Files
pure-component/index.js
袁涛 ef7df25139 修复 下拉框属性设置问题;
修复 对话框属性设置问题;
修复 模态框属性设置问题;
2025-08-13 15:12:46 +08:00

1634 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 请求器组件
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 = `
<style>
.search {
--background-color: #fff;
--border-color: #ccc;
--placeholder-color: var(--border-color);
--clear-color: var(--border-color);
--clear-hover-color: #333;
--border-radius: 5px;
--padding: .5em 1em;
--min-width: 150px;
--gap: .5em;
--scrollbar-color: var(--border-color);
display: flex;
align-items: center;
font-size: 1em;
position: relative;
background: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding);
box-sizing: border-box;
width: 100%;
min-width: var(--min-width);
gap: var(--gap);
cursor: text;
}
.search-input {
width: 100%;
font-size: 1em;
outline: none;
transition: all .2s;
resize: none;
min-height: 1em;
overflow: hidden;
}
.search-input::-webkit-scrollbar {
width: 0;
height: .1em;
}
.search-input::-webkit-scrollbar-thumb {
background: var(--scrollbar-color);
}
.search-input:empty::before {
content: attr(placeholder);
color: var(--placeholder-color);
}
.search-input:focus::before {
display: none;
}
.search:focus-within {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.search-clear {
cursor: pointer;
color: var(--clear-color);
width: 20px;
height: 20px;
background: none;
border: none;
opacity: 0;
transition: all .3s;
position: relative;
flex-shrink: 0;
pointer-events: none;
}
.search-clear.visible {
opacity: 1;
pointer-events: auto;
}
.search-clear svg {
fill: var(--clear-color);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
width: 100%;
height: 100%;
}
</style>
<div class="search" part="search">
<slot name="prefix"></slot>
<div class="search-input" part="search-input"
placeholder="${this.placeholder}"
${this.disabled ? 'disabled' : ''}
contenteditable="${!this.disabled}">
${this.value}
</div>
<button class="search-clear" part="search-clear">
${that.Icon.clear}
</button>
<slot name="append"></slot>
</div>
`
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 = `
<slot name="icon">
<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>
</slot>
`
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 += `
<span class="dialog-title" part="dialog-title"><slot name="title">${subtitle}</slot></span>
<div class="dialog-icon" part="dialog-icon"><slot name="icon">${that.Icon.task}</slot></div>
<div class="dialog-text" part="dialog-text"><slot name="content">${content}</slot></div>
<div class="dialog-btns" part="dialog-btns">
<slot name="button">
<button class="dialog-btn dialog-confirm">${confirmText}</button>
<button class="dialog-btn dialog-cancel">${cancelText}</button>
</slot>
</div>
<slot name="close"><i></i></slot>
`
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 = `
<div class="modal-content" part="modal-content">
<slot name="title"><span part="modal-title" class="modal-title">${subtitle}</span></slot>
<slot name="content"><div part="modal-text" class="modal-text">${content}</div></slot>
</div>
`
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 = `
<slot name='content'></slot>
`
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 = `<span class="tip-content" part="tip-content">${content}</span>`
prefixSlot.innerHTML = `<i class="tip-icon" part="tip-icon">${that.Icon.task}</i>`
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: `<svg t="1713249570364" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2336"><path d="M673.28 331.968a32 32 0 0 0-45.312 0L512 448 395.904 331.968a32 32 0 0 0-45.248 0l-18.752 18.752a32 32 0 0 0 0 45.248L448 512 331.904 628.032a32 32 0 0 0 0 45.248l18.752 18.752a32 32 0 0 0 45.248 0L512 576l116.032 116.032a32 32 0 0 0 45.312 0l18.688-18.752a32 32 0 0 0 0-45.248L576 512l116.032-116.032a32 32 0 0 0 0-45.248l-18.688-18.752z" p-id="2337"></path></svg>`,
// 任务
task: `<svg t="1714280326335" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2382" width="200" height="200"><path d="M831.825474 63.940169H191.939717C121.2479 63.940169 63.940169 121.2479 63.940169 191.939717v639.885757C63.940169 902.517291 121.2479 959.825022 191.939717 959.825022h639.885757c70.691817 0 127.999548-57.307731 127.999548-127.999548V191.939717C959.825022 121.2479 902.517291 63.940169 831.825474 63.940169zM895.884854 831.998871A63.835408 63.835408 0 0 1 831.912173 895.884854H192.087827c-17.112123 0-33.270563-6.574639-45.372232-18.67631S127.880338 849.110994 127.880338 831.998871V192.001129A64.236389 64.236389 0 0 1 192.087827 127.880338h639.824346A64.037705 64.037705 0 0 1 895.884854 192.001129v639.997742z" p-id="2383"></path><path d="M791.998335 351.851551h-255.999097a31.970084 31.970084 0 0 0 0 63.940169h255.999097a31.970084 31.970084 0 0 0 0-63.940169zM791.998335 607.973471h-255.999097a31.970084 31.970084 0 0 0 0 63.940169h255.999097a31.970084 31.970084 0 0 0 0-63.940169zM344.001722 527.997686c-61.855792 0-111.985607 50.144265-111.985607 111.985606s50.144265 111.985607 111.985607 111.985607 111.985607-50.144265 111.985606-111.985607-50.129815-111.985607-111.985606-111.985606z m33.982213 145.982269a48.045438 48.045438 0 1 1 14.088511-33.982213 47.745605 47.745605 0 0 1-14.088511 33.985826zM417.395643 297.394035L311.999125 402.78694 270.6078 361.392003a31.970084 31.970084 0 1 0-45.213286 45.213285l63.997968 64.001581a31.970084 31.970084 0 0 0 45.213286 0l127.999548-127.999549a31.970084 31.970084 0 0 0-45.209673-45.213285z" p-id="2384"></path></svg>`,
}
}