@k3000/ce 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ce.mjs +1 -1
- package/console/components/div-test.mjs +11 -0
- package/console/components/g-button.mjs +79 -0
- package/console/components/g-cell.mjs +129 -0
- package/console/components/g-col.mjs +38 -0
- package/console/components/g-field.mjs +162 -0
- package/console/components/g-form.mjs +11 -0
- package/console/components/g-icon.mjs +82 -0
- package/console/components/g-image.mjs +127 -0
- package/console/components/g-popup.mjs +130 -0
- package/console/components/g-row.mjs +120 -0
- package/console/components/g-space.mjs +21 -0
- package/console/components/g-toast.mjs +116 -0
- package/console/components/layout-header.mjs +0 -179
- package/console/components/layout-menu.mjs +98 -1
- package/console/index.html +1 -0
- package/console/index2.html +46 -0
- package/console/pages/demo-button.mjs +116 -0
- package/console/pages/demo-cell.mjs +59 -0
- package/console/pages/demo-field.mjs +67 -0
- package/console/pages/demo-icon.mjs +200 -0
- package/console/pages/demo-image.mjs +113 -0
- package/console/pages/demo-layout.mjs +141 -0
- package/console/pages/demo-popup.mjs +158 -0
- package/console/pages/demo-space.mjs +51 -0
- package/console/pages/demo-toast.mjs +66 -0
- package/index.mjs +7 -0
- package/package.json +1 -1
- package/test.html +15 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useAttr, bind, useRef } from "../../ce.mjs";
|
|
2
|
+
|
|
3
|
+
const htmlStr = `
|
|
4
|
+
<div class="relative overflow-hidden inline-flex items-center justify-center {{wrapClass}} {{isLoading ? 'bg-white/20 dark:bg-gray-800/20 animate-pulse' : 'bg-white/10 dark:bg-gray-800/10'}} transition-colors" style="{{wrapStyle}}">
|
|
5
|
+
<!-- Image -->
|
|
6
|
+
<img ref="img" src="{{src}}" alt="{{alt}}" class="w-full h-full object-{{fit}} {{isLoaded ? 'opacity-100' : 'opacity-0'}} transition-opacity duration-300 {{imgClass}}" style="{{imgStyle}}" {{lazy ? 'loading="lazy"' : ''}} />
|
|
7
|
+
|
|
8
|
+
<!-- Loading State -->
|
|
9
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 dark:text-gray-500 {{isLoading ? '' : 'hidden'}} bg-white/40 dark:bg-gray-800/40 backdrop-blur-sm">
|
|
10
|
+
<slot name="loading">
|
|
11
|
+
<svg class="w-6 h-6 animate-spin text-blue-500/70" fill="none" viewBox="0 0 24 24">
|
|
12
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
13
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
14
|
+
</svg>
|
|
15
|
+
</slot>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Error State -->
|
|
19
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 dark:text-gray-500 {{isError ? '' : 'hidden'}} bg-white/40 dark:bg-gray-800/40 backdrop-blur-sm">
|
|
20
|
+
<slot name="error">
|
|
21
|
+
<g-icon name="view" class="w-6 h-6 mb-1 opacity-50"></g-icon>
|
|
22
|
+
<span class="text-[10px] sm:text-xs">加载失败</span>
|
|
23
|
+
</slot>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
export default class extends HTMLElement {
|
|
29
|
+
src = ''
|
|
30
|
+
alt = ''
|
|
31
|
+
fit = 'cover' // 'fill', 'contain', 'cover', 'none', 'scale-down'
|
|
32
|
+
position = '' // 'top', 'bottom', 'center', 'left', 'right', or string coords
|
|
33
|
+
lazy = false
|
|
34
|
+
wrapClass = ''
|
|
35
|
+
wrapStyle = ''
|
|
36
|
+
imgClass = ''
|
|
37
|
+
imgStyle = ''
|
|
38
|
+
width = ''
|
|
39
|
+
height = ''
|
|
40
|
+
|
|
41
|
+
isLoaded = false
|
|
42
|
+
isError = false
|
|
43
|
+
isLoading = true
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
super()
|
|
47
|
+
this.style.display = 'inline-block'
|
|
48
|
+
const attr = useAttr(this)
|
|
49
|
+
|
|
50
|
+
this.src = attr.src || ''
|
|
51
|
+
this.alt = attr.alt || ''
|
|
52
|
+
this.fit = attr.fit || 'cover'
|
|
53
|
+
this.position = attr.position || ''
|
|
54
|
+
this.lazy = attr.lazy !== undefined && attr.lazy !== 'false'
|
|
55
|
+
this.width = attr.width || ''
|
|
56
|
+
this.height = attr.height || ''
|
|
57
|
+
|
|
58
|
+
let baseClass = attr.class || ''
|
|
59
|
+
|
|
60
|
+
// If neither width/height nor explicit class dimensions are provided, default to a sensible size
|
|
61
|
+
if (!this.width && !this.height && !baseClass.includes('w-') && !baseClass.includes('h-')) {
|
|
62
|
+
baseClass += ' w-24 h-24 sm:w-32 sm:h-32'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.width) this.wrapStyle += `width: ${this.width}; `
|
|
66
|
+
if (this.height) this.wrapStyle += `height: ${this.height}; `
|
|
67
|
+
|
|
68
|
+
if (this.position) this.imgStyle += `object-position: ${this.position}; `
|
|
69
|
+
|
|
70
|
+
// Handling shapes
|
|
71
|
+
if (attr.round === 'true') {
|
|
72
|
+
baseClass += ' rounded-full'
|
|
73
|
+
} else if (attr.radius) {
|
|
74
|
+
baseClass += ` rounded-[${attr.radius}]`
|
|
75
|
+
} else if (!baseClass.includes('rounded')) {
|
|
76
|
+
baseClass += ' rounded-2xl' // Default glass rounded corner
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add glass border
|
|
80
|
+
this.wrapClass = baseClass + ' backdrop-blur-md border border-white/60 dark:border-white/10 shadow-[inner_0_1px_1px_rgba(255,255,255,0.8),0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-[inner_0_1px_1px_rgba(255,255,255,0.1),0_4px_12px_rgba(0,0,0,0.2)]'
|
|
81
|
+
|
|
82
|
+
// Pass some specific classes directly to img if needed based on shape constraints
|
|
83
|
+
if (baseClass.includes('rounded-full')) {
|
|
84
|
+
this.imgClass = 'rounded-full'
|
|
85
|
+
} else if (baseClass.includes('rounded-2xl')) {
|
|
86
|
+
this.imgClass = 'rounded-2xl'
|
|
87
|
+
} else if (attr.radius) {
|
|
88
|
+
this.imgClass = `rounded-[${attr.radius}]`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!this.src) {
|
|
92
|
+
this.isLoading = false
|
|
93
|
+
this.isError = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
ready() {
|
|
98
|
+
const nodes = bind(htmlStr).create(this)
|
|
99
|
+
this.appendChild(nodes)
|
|
100
|
+
|
|
101
|
+
const { img } = useRef(this)
|
|
102
|
+
|
|
103
|
+
if (this.src) {
|
|
104
|
+
// Check if image is already cached/loaded completely
|
|
105
|
+
if (img.complete && img.naturalHeight !== 0) {
|
|
106
|
+
this.setLoaded()
|
|
107
|
+
} else {
|
|
108
|
+
img.onload = () => this.setLoaded()
|
|
109
|
+
img.onerror = () => this.setError()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setLoaded() {
|
|
115
|
+
this.isLoading = false
|
|
116
|
+
this.isLoaded = true
|
|
117
|
+
this.isError = false
|
|
118
|
+
if (this.update) this.update()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setError() {
|
|
122
|
+
this.isLoading = false
|
|
123
|
+
this.isLoaded = false
|
|
124
|
+
this.isError = true
|
|
125
|
+
if (this.update) this.update()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useAttr, bind } from "../../ce.mjs";
|
|
2
|
+
import {useWatch} from "../../index.mjs";
|
|
3
|
+
|
|
4
|
+
let zIndex = 2000
|
|
5
|
+
|
|
6
|
+
export const innerHTML = `
|
|
7
|
+
<div class="fixed inset-0 flex {{wrapClass}} transition-all duration-300 {{visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}}" style="z-index: {{zIndex}};">
|
|
8
|
+
<!-- Overlay -->
|
|
9
|
+
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm {{overlay !== 'false' ? '' : 'hidden'}}" onclick="{{onOverlayClick}}"></div>
|
|
10
|
+
|
|
11
|
+
<!-- Content pane -->
|
|
12
|
+
<div class="relative bg-white/80 dark:bg-gray-800/80 backdrop-blur-2xl shadow-2xl transition-all duration-300 {{contentClass}} {{visible ? visibleClass : hiddenClass}}" onclick="event.stopPropagation()">
|
|
13
|
+
<slot></slot>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
export default class extends HTMLElement {
|
|
19
|
+
visible = false
|
|
20
|
+
position = 'center'
|
|
21
|
+
overlay = 'true'
|
|
22
|
+
closeOnClickOverlay = 'true'
|
|
23
|
+
round = 'false'
|
|
24
|
+
zIndex = 2000
|
|
25
|
+
|
|
26
|
+
wrapClass = 'items-center justify-center p-4'
|
|
27
|
+
contentClass = 'rounded-2xl max-w-full max-h-full overflow-y-auto'
|
|
28
|
+
visibleClass = 'opacity-100 scale-100 translate-y-0 translate-x-0'
|
|
29
|
+
hiddenClass = 'opacity-0 scale-95'
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super()
|
|
33
|
+
this.style.display = 'contents'
|
|
34
|
+
const attr = useAttr(this)
|
|
35
|
+
|
|
36
|
+
this.position = attr.position || 'center'
|
|
37
|
+
this.overlay = attr.overlay || 'true'
|
|
38
|
+
this.closeOnClickOverlay = attr['close-on-click-overlay'] || 'true'
|
|
39
|
+
this.round = attr.round || 'false'
|
|
40
|
+
|
|
41
|
+
this._initClasses()
|
|
42
|
+
|
|
43
|
+
const watch = useWatch(this)
|
|
44
|
+
|
|
45
|
+
watch({
|
|
46
|
+
status: (status, value) => {
|
|
47
|
+
|
|
48
|
+
if (status === value) return
|
|
49
|
+
|
|
50
|
+
if (status) {
|
|
51
|
+
|
|
52
|
+
this.open()
|
|
53
|
+
|
|
54
|
+
} else {
|
|
55
|
+
|
|
56
|
+
this.close()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ready() {
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_initClasses() {
|
|
66
|
+
const isRound = this.round === 'true'
|
|
67
|
+
switch (this.position) {
|
|
68
|
+
case 'bottom':
|
|
69
|
+
this.wrapClass = 'items-end justify-center'
|
|
70
|
+
this.contentClass = `w-full max-h-[90vh] overflow-y-auto ${isRound ? 'rounded-t-3xl' : ''}`
|
|
71
|
+
this.visibleClass = 'translate-y-0 opacity-100'
|
|
72
|
+
this.hiddenClass = 'translate-y-full opacity-0'
|
|
73
|
+
break
|
|
74
|
+
case 'top':
|
|
75
|
+
this.wrapClass = 'items-start justify-center'
|
|
76
|
+
this.contentClass = `w-full max-h-[90vh] overflow-y-auto ${isRound ? 'rounded-b-3xl' : ''}`
|
|
77
|
+
this.visibleClass = 'translate-y-0 opacity-100'
|
|
78
|
+
this.hiddenClass = '-translate-y-full opacity-0'
|
|
79
|
+
break
|
|
80
|
+
case 'left':
|
|
81
|
+
this.wrapClass = 'items-center justify-start'
|
|
82
|
+
this.contentClass = `h-full max-w-[90vw] overflow-y-auto ${isRound ? 'rounded-r-3xl' : ''}`
|
|
83
|
+
this.visibleClass = 'translate-x-0 opacity-100'
|
|
84
|
+
this.hiddenClass = '-translate-x-full opacity-0'
|
|
85
|
+
break
|
|
86
|
+
case 'right':
|
|
87
|
+
this.wrapClass = 'items-center justify-end'
|
|
88
|
+
this.contentClass = `h-full max-w-[90vw] overflow-y-auto ${isRound ? 'rounded-l-3xl' : ''}`
|
|
89
|
+
this.visibleClass = 'translate-x-0 opacity-100'
|
|
90
|
+
this.hiddenClass = 'translate-x-full opacity-0'
|
|
91
|
+
break
|
|
92
|
+
case 'center':
|
|
93
|
+
default:
|
|
94
|
+
this.wrapClass = 'items-center justify-center p-4' // safe area
|
|
95
|
+
this.contentClass = `max-w-[90vw] max-h-[90vh] overflow-y-auto ${isRound ? 'rounded-3xl' : ''}`
|
|
96
|
+
this.visibleClass = 'opacity-100 scale-100'
|
|
97
|
+
this.hiddenClass = 'opacity-0 scale-95'
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
open() {
|
|
103
|
+
this.zIndex = zIndex++
|
|
104
|
+
this.visible = true
|
|
105
|
+
if (this.update) this.update()
|
|
106
|
+
if (this.onopen) this.onopen()
|
|
107
|
+
|
|
108
|
+
// Prevent body scroll
|
|
109
|
+
document.body.style.overflow = 'hidden'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
close() {
|
|
113
|
+
this.visible = false
|
|
114
|
+
if (this.update) this.update()
|
|
115
|
+
if (this.onclose) this.onclose()
|
|
116
|
+
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
const hasOpenPopups = Array.from(document.querySelectorAll('g-popup')).some(p => p.visible)
|
|
119
|
+
if (!hasOpenPopups) {
|
|
120
|
+
document.body.style.overflow = ''
|
|
121
|
+
}
|
|
122
|
+
}, 300)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onOverlayClick() {
|
|
126
|
+
if (this.closeOnClickOverlay === 'true') {
|
|
127
|
+
this.close()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useAttr } from "../../ce.mjs";
|
|
2
|
+
|
|
3
|
+
export default class extends HTMLElement {
|
|
4
|
+
constructor() {
|
|
5
|
+
super()
|
|
6
|
+
this.style.display = 'flex'
|
|
7
|
+
this.style.flexWrap = 'wrap'
|
|
8
|
+
this.style.width = '100%'
|
|
9
|
+
this.style.boxSizing = 'border-box'
|
|
10
|
+
|
|
11
|
+
const attr = useAttr(this)
|
|
12
|
+
|
|
13
|
+
const justify = attr.justify || 'start'
|
|
14
|
+
const align = attr.align || 'top'
|
|
15
|
+
const gutter = attr.gutter || '0'
|
|
16
|
+
const classStr = attr.class || ''
|
|
17
|
+
|
|
18
|
+
// Map justify to flexbox
|
|
19
|
+
const justifyMap = {
|
|
20
|
+
'start': 'flex-start',
|
|
21
|
+
'end': 'flex-end',
|
|
22
|
+
'center': 'center',
|
|
23
|
+
'space-around': 'space-around',
|
|
24
|
+
'space-between': 'space-between'
|
|
25
|
+
}
|
|
26
|
+
this.style.justifyContent = justifyMap[justify] || 'flex-start'
|
|
27
|
+
|
|
28
|
+
// Map align to flexbox
|
|
29
|
+
const alignMap = {
|
|
30
|
+
'top': 'flex-start',
|
|
31
|
+
'middle': 'center',
|
|
32
|
+
'bottom': 'flex-end'
|
|
33
|
+
}
|
|
34
|
+
this.style.alignItems = alignMap[align] || 'flex-start'
|
|
35
|
+
|
|
36
|
+
// Handle gutter logic via context
|
|
37
|
+
// Gutter can be "20" or "[20, 30]"
|
|
38
|
+
let gutterX = 0
|
|
39
|
+
let gutterY = 0
|
|
40
|
+
|
|
41
|
+
if (gutter) {
|
|
42
|
+
if (gutter.startsWith('[')) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(gutter)
|
|
45
|
+
if (Array.isArray(parsed) && parsed.length >= 2) {
|
|
46
|
+
gutterX = Number(parsed[0])
|
|
47
|
+
gutterY = Number(parsed[1])
|
|
48
|
+
}
|
|
49
|
+
} catch (e) { }
|
|
50
|
+
} else {
|
|
51
|
+
gutterX = Number(gutter)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (gutterX || gutterY) {
|
|
56
|
+
const mx = gutterX ? `-${gutterX / 2}px` : '0'
|
|
57
|
+
const my = gutterY ? `-${gutterY / 2}px` : '0'
|
|
58
|
+
|
|
59
|
+
// Apply negative margins to row host
|
|
60
|
+
this.style.marginLeft = mx
|
|
61
|
+
this.style.marginRight = mx
|
|
62
|
+
this.style.marginTop = my
|
|
63
|
+
this.style.marginBottom = my
|
|
64
|
+
|
|
65
|
+
// Create a mutation observer to pass gutter context to children <g-col> dynamically or initially
|
|
66
|
+
this.xPadding = gutterX ? `${gutterX / 2}px` : '0'
|
|
67
|
+
this.yPadding = gutterY ? `${gutterY / 2}px` : '0'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (classStr) {
|
|
71
|
+
this.className = this.className ? this.className + ' ' + classStr : classStr
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Listen to children additions since standard Web Components don't easily do React-style context down effortlessly unless via MutationObserver or passing via DOM
|
|
75
|
+
this._setupObserver()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_setupObserver() {
|
|
79
|
+
this._observer = new MutationObserver((mutations) => {
|
|
80
|
+
if (!this.xPadding && !this.yPadding) return;
|
|
81
|
+
|
|
82
|
+
mutations.forEach((mutation) => {
|
|
83
|
+
if (mutation.type === 'childList') {
|
|
84
|
+
mutation.addedNodes.forEach(node => {
|
|
85
|
+
this._applyGutterToChild(node)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_applyGutterToChild(node) {
|
|
93
|
+
if (node.tagName === 'G-COL') {
|
|
94
|
+
if (this.xPadding) {
|
|
95
|
+
node.style.paddingLeft = this.xPadding
|
|
96
|
+
node.style.paddingRight = this.xPadding
|
|
97
|
+
}
|
|
98
|
+
if (this.yPadding) {
|
|
99
|
+
node.style.paddingTop = this.yPadding
|
|
100
|
+
node.style.paddingBottom = this.yPadding
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
connectedCallback() {
|
|
106
|
+
if (this._observer) {
|
|
107
|
+
this._observer.observe(this, { childList: true, subtree: false })
|
|
108
|
+
}
|
|
109
|
+
// Initially apply to existing children
|
|
110
|
+
if (this.xPadding || this.yPadding) {
|
|
111
|
+
Array.from(this.children).forEach(child => this._applyGutterToChild(child))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
disconnectedCallback() {
|
|
116
|
+
if (this._observer) {
|
|
117
|
+
this._observer.disconnect()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {useAttr} from "../../index.mjs";
|
|
2
|
+
|
|
3
|
+
export const shadowRoot = `
|
|
4
|
+
<style>
|
|
5
|
+
div {
|
|
6
|
+
display: flex;
|
|
7
|
+
gap: {{gap || '8px'}};
|
|
8
|
+
flex-direction: {{direction}}
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
<div>
|
|
12
|
+
<slot></slot>
|
|
13
|
+
</div>
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
export default class extends HTMLElement {
|
|
17
|
+
constructor() {
|
|
18
|
+
super()
|
|
19
|
+
Object.assign(this, useAttr(this))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useAttr } from "../../ce.mjs";
|
|
2
|
+
|
|
3
|
+
let zIndex = 5000
|
|
4
|
+
|
|
5
|
+
export const innerHTML = `
|
|
6
|
+
<div class="toast-container fixed top-[45%] left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-3 pointer-events-none" style="z-index: {{zIndex}};">
|
|
7
|
+
<slot></slot>
|
|
8
|
+
</div>
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
export default class extends HTMLElement {
|
|
12
|
+
constructor() {
|
|
13
|
+
super()
|
|
14
|
+
// this.style.display = 'contents'
|
|
15
|
+
this.zIndex = zIndex;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ready() {
|
|
19
|
+
// Expose to window globally
|
|
20
|
+
window.$toast = {
|
|
21
|
+
show: (msg, type = 'text', duration = 2000) => this.show(msg, type, duration),
|
|
22
|
+
success: (msg, duration = 2000) => this.show(msg, 'success', duration),
|
|
23
|
+
error: (msg, duration = 2000) => this.show(msg, 'error', duration),
|
|
24
|
+
warning: (msg, duration = 2000) => this.show(msg, 'warning', duration),
|
|
25
|
+
loading: (msg, duration = 0) => this.show(msg, 'loading', duration),
|
|
26
|
+
hide: () => this.hideAll()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
show(msg, type = 'text', duration = 2000) {
|
|
31
|
+
let container = this.querySelector('.toast-container') || this.firstElementChild;
|
|
32
|
+
if (!container) return;
|
|
33
|
+
|
|
34
|
+
const item = document.createElement('div');
|
|
35
|
+
|
|
36
|
+
let iconHtml = '';
|
|
37
|
+
if (type === 'success') {
|
|
38
|
+
iconHtml = '<svg class="w-5 h-5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>';
|
|
39
|
+
} else if (type === 'error') {
|
|
40
|
+
iconHtml = '<svg class="w-5 h-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>';
|
|
41
|
+
} else if (type === 'warning') {
|
|
42
|
+
iconHtml = '<svg class="w-5 h-5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>';
|
|
43
|
+
} else if (type === 'loading') {
|
|
44
|
+
iconHtml = `<svg class="w-5 h-5 text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
item.className = 'bg-gray-800/90 dark:bg-gray-200/90 text-white dark:text-gray-900 px-5 py-3 rounded-xl shadow-2xl backdrop-blur-md flex items-center gap-3 pointer-events-auto transition-all duration-300 transform scale-95 opacity-0 max-w-[80vw] !m-0 !mt-0 !mb-0 overflow-hidden';
|
|
48
|
+
|
|
49
|
+
// Starts with 0 height so the container shifts smoothly
|
|
50
|
+
item.style.maxHeight = '0px';
|
|
51
|
+
item.style.paddingTop = '0px';
|
|
52
|
+
item.style.paddingBottom = '0px';
|
|
53
|
+
item.style.opacity = '0';
|
|
54
|
+
|
|
55
|
+
item.innerHTML = `
|
|
56
|
+
${iconHtml}
|
|
57
|
+
<span class="text-sm font-medium whitespace-pre-wrap break-words inline-block">${msg}</span>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
container.appendChild(item);
|
|
61
|
+
|
|
62
|
+
// Animate in
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
item.classList.remove('scale-95');
|
|
66
|
+
item.classList.add('scale-100');
|
|
67
|
+
item.style.maxHeight = '150px'; // sufficiently large value
|
|
68
|
+
item.style.paddingTop = '';
|
|
69
|
+
item.style.paddingBottom = '';
|
|
70
|
+
item.style.opacity = '1';
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (duration > 0) {
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
this._removeItem(item);
|
|
77
|
+
}, duration);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return () => this._removeItem(item);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_removeItem(item) {
|
|
84
|
+
if (!item.parentNode) return;
|
|
85
|
+
item.classList.remove('scale-100');
|
|
86
|
+
item.classList.add('scale-75');
|
|
87
|
+
item.style.opacity = '0';
|
|
88
|
+
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (!item.parentNode) return;
|
|
91
|
+
|
|
92
|
+
// 固定当前高度以便开始过渡动画
|
|
93
|
+
item.style.height = item.offsetHeight + 'px';
|
|
94
|
+
item.style.overflow = 'hidden';
|
|
95
|
+
item.offsetHeight; // 强制重绘
|
|
96
|
+
|
|
97
|
+
// 收缩高度和边距
|
|
98
|
+
item.style.height = '0px';
|
|
99
|
+
item.style.paddingTop = '0px';
|
|
100
|
+
item.style.paddingBottom = '0px';
|
|
101
|
+
item.style.marginTop = '0px';
|
|
102
|
+
item.style.marginBottom = '0px';
|
|
103
|
+
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
if (item.parentNode) item.parentNode.removeChild(item);
|
|
106
|
+
}, 300);
|
|
107
|
+
}, 150);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
hideAll() {
|
|
111
|
+
const container = this.querySelector('.toast-container') || this.firstElementChild;
|
|
112
|
+
if (container) {
|
|
113
|
+
Array.from(container.children).forEach(item => this._removeItem(item));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|