feat: 实现图片预览功能
- 添加双击图片节点预览功能 - 添加右键菜单图片预览选项 - 实现图片预览模态框组件 - 添加图片URL智能处理(相对路径转绝对路径) - 实现图片预加载机制和超时保护 - 增强错误处理和调试信息 - 修复图片一直加载不出来的问题 - 添加美观的加载动画和错误提示
This commit is contained in:
parent
329d36bdd8
commit
6a7809a550
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -23,8 +23,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-3d4f89fc.js"></script>
|
<script type="module" crossorigin src="/assets/index-c91f543d.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index-2f08a1a5.css">
|
<link rel="stylesheet" href="/assets/index-1f5435d2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,34 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览模态框 -->
|
||||||
|
<div v-if="showImagePreview" class="image-preview-modal" @click="closeImagePreview">
|
||||||
|
<div class="image-preview-content" @click.stop>
|
||||||
|
<div class="image-preview-header">
|
||||||
|
<span class="image-preview-title">{{ imagePreviewTitle }}</span>
|
||||||
|
<button @click="closeImagePreview" class="image-preview-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="image-preview-body">
|
||||||
|
<div v-if="imagePreviewLoading" class="image-preview-loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>加载图片中...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="imagePreviewError" class="image-preview-error">
|
||||||
|
<p>❌ 图片加载失败</p>
|
||||||
|
<p>{{ imagePreviewError }}</p>
|
||||||
|
<button @click="retryLoadImage" class="retry-button">重试</button>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="imagePreviewUrl"
|
||||||
|
:alt="imagePreviewTitle"
|
||||||
|
class="preview-image"
|
||||||
|
@load="onImageLoad"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -213,6 +241,13 @@ const isDragging = ref(false);
|
||||||
const dragStartPos = ref({ x: 0, y: 0 });
|
const dragStartPos = ref({ x: 0, y: 0 });
|
||||||
const currentNodeId = ref(null);
|
const currentNodeId = ref(null);
|
||||||
|
|
||||||
|
// 图片预览相关状态
|
||||||
|
const showImagePreview = ref(false);
|
||||||
|
const imagePreviewUrl = ref('');
|
||||||
|
const imagePreviewTitle = ref('');
|
||||||
|
const imagePreviewLoading = ref(false);
|
||||||
|
const imagePreviewError = ref('');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 显示欢迎页面
|
// 显示欢迎页面
|
||||||
|
|
@ -238,6 +273,106 @@ const showMindMapPage = () => {
|
||||||
showWelcome.value = false;
|
showWelcome.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 图片预览相关方法
|
||||||
|
const openImagePreview = (imageUrl, altText = '') => {
|
||||||
|
console.log('🖼️ 打开图片预览:', { imageUrl, altText });
|
||||||
|
|
||||||
|
// 处理图片URL,确保是有效的URL
|
||||||
|
let processedUrl = imageUrl;
|
||||||
|
if (typeof imageUrl === 'string') {
|
||||||
|
// 如果是相对路径,转换为绝对路径
|
||||||
|
if (imageUrl.startsWith('/') || imageUrl.startsWith('./') || imageUrl.startsWith('../')) {
|
||||||
|
processedUrl = new URL(imageUrl, window.location.origin).href;
|
||||||
|
}
|
||||||
|
// 如果是base64图片,直接使用
|
||||||
|
else if (imageUrl.startsWith('data:image/')) {
|
||||||
|
processedUrl = imageUrl;
|
||||||
|
}
|
||||||
|
// 如果是完整URL,直接使用
|
||||||
|
else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||||
|
processedUrl = imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🖼️ 处理后的图片URL:', processedUrl);
|
||||||
|
|
||||||
|
showImagePreview.value = true;
|
||||||
|
imagePreviewTitle.value = altText || '图片预览';
|
||||||
|
imagePreviewLoading.value = true;
|
||||||
|
imagePreviewError.value = '';
|
||||||
|
|
||||||
|
// 启动超时机制
|
||||||
|
startImageLoadTimeout();
|
||||||
|
|
||||||
|
// 预加载图片,确保URL有效
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
console.log('✅ 图片预加载成功');
|
||||||
|
// 预加载成功后,直接设置图片URL并停止加载状态
|
||||||
|
imagePreviewUrl.value = processedUrl;
|
||||||
|
imagePreviewLoading.value = false;
|
||||||
|
clearImageLoadTimeout();
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
console.error('❌ 图片预加载失败:', processedUrl);
|
||||||
|
clearImageLoadTimeout();
|
||||||
|
imagePreviewLoading.value = false;
|
||||||
|
imagePreviewError.value = `图片加载失败: ${processedUrl}`;
|
||||||
|
};
|
||||||
|
img.src = processedUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeImagePreview = () => {
|
||||||
|
clearImageLoadTimeout();
|
||||||
|
showImagePreview.value = false;
|
||||||
|
imagePreviewUrl.value = '';
|
||||||
|
imagePreviewTitle.value = '';
|
||||||
|
imagePreviewLoading.value = false;
|
||||||
|
imagePreviewError.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageLoad = () => {
|
||||||
|
console.log('✅ 模态框图片加载成功');
|
||||||
|
// 图片已经在预加载阶段处理了状态,这里只是确认
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageError = (event) => {
|
||||||
|
console.error('❌ 模态框图片加载失败:', event);
|
||||||
|
console.error('❌ 失败的图片URL:', imagePreviewUrl.value);
|
||||||
|
// 这种情况应该很少发生,因为预加载已经验证了URL
|
||||||
|
imagePreviewLoading.value = false;
|
||||||
|
imagePreviewError.value = `图片显示失败: ${imagePreviewUrl.value}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const retryLoadImage = () => {
|
||||||
|
console.log('🔄 重试加载图片');
|
||||||
|
imagePreviewLoading.value = true;
|
||||||
|
imagePreviewError.value = '';
|
||||||
|
startImageLoadTimeout();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片加载超时处理
|
||||||
|
let imageLoadTimeout = null;
|
||||||
|
const startImageLoadTimeout = () => {
|
||||||
|
if (imageLoadTimeout) {
|
||||||
|
clearTimeout(imageLoadTimeout);
|
||||||
|
}
|
||||||
|
imageLoadTimeout = setTimeout(() => {
|
||||||
|
if (imagePreviewLoading.value) {
|
||||||
|
console.warn('⚠️ 图片加载超时');
|
||||||
|
imagePreviewLoading.value = false;
|
||||||
|
imagePreviewError.value = '图片加载超时,请检查网络连接或图片URL是否正确';
|
||||||
|
}
|
||||||
|
}, 10000); // 10秒超时
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearImageLoadTimeout = () => {
|
||||||
|
if (imageLoadTimeout) {
|
||||||
|
clearTimeout(imageLoadTimeout);
|
||||||
|
imageLoadTimeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载已有思维导图
|
// 加载已有思维导图
|
||||||
const loadExistingMindmap = async () => {
|
const loadExistingMindmap = async () => {
|
||||||
// 这里可以实现一个思维导图列表选择器
|
// 这里可以实现一个思维导图列表选择器
|
||||||
|
|
@ -484,6 +619,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
|
||||||
|
|
||||||
fixInitialRendering();
|
fixInitialRendering();
|
||||||
|
|
||||||
|
// 添加图片预览事件监听器
|
||||||
|
mindElixir.value.bus.addListener('showImagePreview', (imageUrl, altText) => {
|
||||||
|
console.log('🖼️ 收到图片预览事件:', { imageUrl, altText });
|
||||||
|
openImagePreview(imageUrl, altText);
|
||||||
|
});
|
||||||
|
|
||||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -565,6 +706,12 @@ const loadMindmapData = async (data, keepPosition = false, shouldCenterRoot = tr
|
||||||
|
|
||||||
fixDelayedRendering();
|
fixDelayedRendering();
|
||||||
|
|
||||||
|
// 添加图片预览事件监听器
|
||||||
|
mindElixir.value.bus.addListener('showImagePreview', (imageUrl, altText) => {
|
||||||
|
console.log('🖼️ 收到图片预览事件(延迟创建):', { imageUrl, altText });
|
||||||
|
openImagePreview(imageUrl, altText);
|
||||||
|
});
|
||||||
|
|
||||||
// 延迟执行后续操作
|
// 延迟执行后续操作
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
// Mind Elixir现在会自动使用markdown解析器渲染内容
|
||||||
|
|
@ -4576,5 +4723,149 @@ const updateMindMapRealtime = async (data, title) => {
|
||||||
margin-bottom: 8px !important;
|
margin-bottom: 8px !important;
|
||||||
object-fit: cover !important;
|
object-fit: cover !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 图片预览模态框样式 */
|
||||||
|
.image-preview-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-body {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 75vh;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-loading,
|
||||||
|
.image-preview-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #660874;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-error p {
|
||||||
|
margin: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #660874;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button:hover {
|
||||||
|
background: #7d0a8e;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,52 @@ export default function (mind: MindElixirInstance) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDblClick = (e: MouseEvent) => {
|
const handleDblClick = (e: MouseEvent) => {
|
||||||
if (!mind.editable) return
|
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
|
// 检查是否双击了图片
|
||||||
|
if (target.tagName === 'IMG') {
|
||||||
|
const img = target as HTMLImageElement
|
||||||
|
const imageUrl = img.src
|
||||||
|
const altText = img.alt || img.title || ''
|
||||||
|
|
||||||
|
console.log('🖼️ 双击图片节点,准备预览:', { imageUrl, altText })
|
||||||
|
|
||||||
|
// 触发图片预览事件
|
||||||
|
mind.bus.fire('showImagePreview', imageUrl, altText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否双击了包含图片的节点
|
||||||
if (isTopic(target)) {
|
if (isTopic(target)) {
|
||||||
|
const topic = target as Topic
|
||||||
|
|
||||||
|
// 检查节点是否有图片
|
||||||
|
if (topic.nodeObj?.image) {
|
||||||
|
const imageUrl = typeof topic.nodeObj.image === 'string' ? topic.nodeObj.image : topic.nodeObj.image.url
|
||||||
|
console.log('🖼️ 双击包含图片的节点,准备预览:', imageUrl)
|
||||||
|
mind.bus.fire('showImagePreview', imageUrl, topic.nodeObj.topic || '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查节点内容中是否包含图片
|
||||||
|
const imgInContent = topic.querySelector('img')
|
||||||
|
if (imgInContent) {
|
||||||
|
const imageUrl = imgInContent.src
|
||||||
|
const altText = imgInContent.alt || imgInContent.title || topic.nodeObj?.topic || ''
|
||||||
|
|
||||||
|
console.log('🖼️ 双击包含HTML图片的节点,准备预览:', { imageUrl, altText })
|
||||||
|
mind.bus.fire('showImagePreview', imageUrl, altText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有图片,则进入编辑模式
|
||||||
|
if (mind.editable) {
|
||||||
mind.beginEdit(target)
|
mind.beginEdit(target)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理其他双击事件
|
||||||
|
if (mind.editable) {
|
||||||
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
const trySvg = target.parentElement?.parentElement as unknown as SVGElement
|
||||||
if (trySvg.getAttribute('class') === 'topiclinks') {
|
if (trySvg.getAttribute('class') === 'topiclinks') {
|
||||||
mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
|
mind.editArrowLabel(target.parentElement as unknown as CustomSvg)
|
||||||
|
|
@ -58,6 +99,7 @@ export default function (mind: MindElixirInstance) {
|
||||||
mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
|
mind.editSummary(target.parentElement as unknown as SummarySvgGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let lastTap = 0
|
let lastTap = 0
|
||||||
const handleTouchDblClick = (e: PointerEvent) => {
|
const handleTouchDblClick = (e: PointerEvent) => {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
const link = createLi('cm-link', lang.link, '')
|
const link = createLi('cm-link', lang.link, '')
|
||||||
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
|
const linkBidirectional = createLi('cm-link-bidirectional', lang.linkBidirectional, '')
|
||||||
const summary = createLi('cm-summary', lang.summary, '')
|
const summary = createLi('cm-summary', lang.summary, '')
|
||||||
|
const imagePreview = createLi('cm-image-preview', '预览图片', '')
|
||||||
|
|
||||||
const menuUl = document.createElement('ul')
|
const menuUl = document.createElement('ul')
|
||||||
menuUl.className = 'menu-list'
|
menuUl.className = 'menu-list'
|
||||||
|
|
@ -93,6 +94,18 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
} else {
|
} else {
|
||||||
isRoot = false
|
isRoot = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查节点是否有图片,决定是否显示预览图片选项
|
||||||
|
const topic = target as Topic
|
||||||
|
const hasImage = topic.nodeObj?.image || topic.querySelector('img')
|
||||||
|
|
||||||
|
if (hasImage) {
|
||||||
|
imagePreview.style.display = 'block'
|
||||||
|
imagePreview.className = ''
|
||||||
|
} else {
|
||||||
|
imagePreview.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
focus.className = 'disabled'
|
focus.className = 'disabled'
|
||||||
up.className = 'disabled'
|
up.className = 'disabled'
|
||||||
|
|
@ -211,6 +224,28 @@ export default function (mind: MindElixirInstance, option: true | ContextMenuOpt
|
||||||
mind.createSummary()
|
mind.createSummary()
|
||||||
mind.unselectNodes(mind.currentNodes)
|
mind.unselectNodes(mind.currentNodes)
|
||||||
}
|
}
|
||||||
|
imagePreview.onclick = () => {
|
||||||
|
menuContainer.hidden = true
|
||||||
|
const target = mind.currentNode as Topic
|
||||||
|
if (target) {
|
||||||
|
// 检查节点是否有图片
|
||||||
|
if (target.nodeObj?.image) {
|
||||||
|
const imageUrl = typeof target.nodeObj.image === 'string' ? target.nodeObj.image : target.nodeObj.image.url
|
||||||
|
console.log('🖼️ 右键菜单预览图片:', imageUrl)
|
||||||
|
mind.bus.fire('showImagePreview', imageUrl, target.nodeObj.topic || '')
|
||||||
|
} else {
|
||||||
|
// 检查节点内容中是否包含图片
|
||||||
|
const imgInContent = target.querySelector('img')
|
||||||
|
if (imgInContent) {
|
||||||
|
const imageUrl = imgInContent.src
|
||||||
|
const altText = imgInContent.alt || imgInContent.title || target.nodeObj?.topic || ''
|
||||||
|
|
||||||
|
console.log('🖼️ 右键菜单预览HTML图片:', { imageUrl, altText })
|
||||||
|
mind.bus.fire('showImagePreview', imageUrl, altText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
// maybe useful?
|
// maybe useful?
|
||||||
add_child.onclick = null
|
add_child.onclick = null
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export type EventMap = {
|
||||||
*/
|
*/
|
||||||
updateArrowDelta: (arrow: Arrow) => void
|
updateArrowDelta: (arrow: Arrow) => void
|
||||||
showContextMenu: (e: MouseEvent) => void
|
showContextMenu: (e: MouseEvent) => void
|
||||||
|
showImagePreview: (imageUrl: string, altText?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {
|
export function createBus<T extends Record<string, (...args: any[]) => void> = EventMap>() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue