351 lines
8.1 KiB
TypeScript
351 lines
8.1 KiB
TypeScript
import { html, render, TemplateResult } from 'lit-html'
|
|
|
|
export interface KvMessageOptions {
|
|
type?: 'success' | 'error' | 'loading'
|
|
message: string
|
|
duration?: number
|
|
closable?: boolean
|
|
position?: 'center' | 'right'
|
|
}
|
|
|
|
class KvMessage extends HTMLElement {
|
|
private options: KvMessageOptions
|
|
private timer: number | null = null
|
|
|
|
constructor() {
|
|
super()
|
|
this.options = {
|
|
type: 'success',
|
|
message: '',
|
|
duration: 2000,
|
|
closable: true
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render()
|
|
}
|
|
|
|
setOptions(options: KvMessageOptions) {
|
|
this.options = { ...this.options, ...options }
|
|
this.render()
|
|
}
|
|
|
|
private render() {
|
|
const { type, message, closable } = this.options
|
|
|
|
const getTypeIcon = () => {
|
|
switch (type) {
|
|
case 'success':
|
|
return '✓'
|
|
case 'error':
|
|
return '✕'
|
|
case 'loading':
|
|
return html`<div class="loading-spinner"></div>`
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
const template: TemplateResult = html`
|
|
<style>
|
|
:host {
|
|
display: block;
|
|
margin-bottom: 12px;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
.message-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 16px;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
background: white;
|
|
position: relative;
|
|
min-width: 300px;
|
|
max-width: 500px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.message-container.success {
|
|
border-left: 4px solid #52c41a;
|
|
}
|
|
|
|
.message-container.error {
|
|
border-left: 4px solid #ff4d4f;
|
|
}
|
|
|
|
.message-container.loading {
|
|
border-left: 4px solid #1890ff;
|
|
}
|
|
|
|
.message-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.success .message-icon {
|
|
color: #52c41a;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.error .message-icon {
|
|
color: #ff4d4f;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.loading .message-icon {
|
|
color: #1890ff;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid #f3f3f3;
|
|
border-top: 2px solid #1890ff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.message-content {
|
|
flex: 1;
|
|
color: #333;
|
|
}
|
|
|
|
.message-close {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: pointer;
|
|
color: #999;
|
|
background: none;
|
|
border: none;
|
|
font-size: 12px;
|
|
border-radius: 50%;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.message-close:hover {
|
|
color: #666;
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.removing {
|
|
animation: slideOut 0.3s ease-out forwards;
|
|
}
|
|
</style>
|
|
|
|
<div class="message-container ${type}">
|
|
<div class="message-icon">
|
|
${getTypeIcon()}
|
|
</div>
|
|
<div class="message-content">${message}</div>
|
|
${closable ? html`
|
|
<button class="message-close" @click=${() => this.remove()}>×</button>
|
|
` : ''}
|
|
</div>
|
|
`
|
|
|
|
render(template, this)
|
|
|
|
if (type !== 'loading' && this.options.duration && this.options.duration > 0) {
|
|
this.setTimer()
|
|
}
|
|
}
|
|
|
|
private setTimer() {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
}
|
|
|
|
this.timer = window.setTimeout(() => {
|
|
this.remove()
|
|
}, this.options.duration)
|
|
}
|
|
|
|
remove() {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
|
|
this.classList.add('removing')
|
|
|
|
setTimeout(() => {
|
|
if (this.parentNode) {
|
|
this.parentNode.removeChild(this)
|
|
}
|
|
}, 300)
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('kv-message', KvMessage)
|
|
|
|
export class KvMessageManager {
|
|
private static instance: KvMessageManager
|
|
private container: HTMLElement | null = null
|
|
private defaultPosition: 'center' | 'right' = 'center'
|
|
|
|
static getInstance(): KvMessageManager {
|
|
if (!KvMessageManager.instance) {
|
|
KvMessageManager.instance = new KvMessageManager()
|
|
}
|
|
return KvMessageManager.instance
|
|
}
|
|
|
|
setDefaultPosition(position: 'center' | 'right') {
|
|
this.defaultPosition = position
|
|
}
|
|
|
|
private getContainer(position?: 'center' | 'right'): HTMLElement {
|
|
const finalPosition = position || this.defaultPosition
|
|
|
|
if (!this.container) {
|
|
this.container = document.getElementById('messages')
|
|
if (!this.container) {
|
|
this.container = document.createElement('div')
|
|
this.container.id = 'messages'
|
|
|
|
if (finalPosition === 'center') {
|
|
this.container.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 9999;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
pointer-events: none;
|
|
`
|
|
} else {
|
|
this.container.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 9999;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
pointer-events: none;
|
|
`
|
|
}
|
|
|
|
document.body.appendChild(this.container)
|
|
}
|
|
}
|
|
return this.container
|
|
}
|
|
|
|
show(options: KvMessageOptions): KvMessage {
|
|
const container = this.getContainer(options.position)
|
|
|
|
const message = document.createElement('kv-message') as KvMessage
|
|
message.setOptions(options)
|
|
|
|
message.style.cssText = 'pointer-events: auto;'
|
|
|
|
container.appendChild(message)
|
|
|
|
return message
|
|
}
|
|
|
|
success(message: string, options?: { duration?: number; position?: 'center' | 'right'; closable?: boolean }): KvMessage {
|
|
return this.show({
|
|
type: 'success',
|
|
message,
|
|
duration: options?.duration || 2000,
|
|
position: options?.position,
|
|
closable: options?.closable
|
|
})
|
|
}
|
|
|
|
error(message: string, options?: { duration?: number; position?: 'center' | 'right'; closable?: boolean }): KvMessage {
|
|
return this.show({
|
|
type: 'error',
|
|
message,
|
|
duration: options?.duration || 3000,
|
|
position: options?.position,
|
|
closable: options?.closable
|
|
})
|
|
}
|
|
|
|
loading(message: string, options?: { position?: 'center' | 'right'; closable?: boolean }): KvMessage {
|
|
return this.show({
|
|
type: 'loading',
|
|
message,
|
|
duration: 0,
|
|
position: options?.position,
|
|
closable: options?.closable
|
|
})
|
|
}
|
|
|
|
remove(message: KvMessage) {
|
|
message.remove()
|
|
}
|
|
|
|
clear() {
|
|
const container = this.getContainer()
|
|
const messages = container.querySelectorAll('kv-message')
|
|
messages.forEach(message => {
|
|
(message as KvMessage).remove()
|
|
})
|
|
}
|
|
}
|
|
|
|
export const createMessage = () => KvMessageManager.getInstance()
|
|
|
|
// 将 createMessage 暴露到全局,以便 HTML 中的 JavaScript 可以使用
|
|
declare global {
|
|
interface Window {
|
|
createMessage: typeof createMessage
|
|
}
|
|
}
|
|
|
|
window.createMessage = createMessage |