重构:优化记录格式并修复核心功能
- 修改记录存储格式为 [4B len][8B offset][4B CRC][16B UUID][data] - 修复 TopicProcessor 中 WaitGroup 使用错误导致 handler 不执行的问题 - 修复写入保护逻辑,避免 dirtyOffset=-1 时误判为写入中 - 添加统计信息定期持久化功能 - 改进 UTF-8 字符截断处理,防止 CJK 字符乱码 - 优化 Web UI:显示人类可读的文件大小,支持点击外部关闭弹窗 - 重构示例代码,添加 webui 和 webui_integration 示例 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
568
ui/index.html
Normal file
568
ui/index.html
Normal file
@@ -0,0 +1,568 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Seqlog Dashboard</title>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
[v-cloak] { display: none; }
|
||||
.log-entry {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.log-data {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.log-data:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* Dialog 样式 */
|
||||
dialog {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
padding: 0;
|
||||
max-width: 56rem;
|
||||
width: 90vw;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
dialog .dialog-container {
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
dialog .dialog-content {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
dialog:not([open]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div id="app" v-cloak>
|
||||
<!-- 头部 -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Seqlog Dashboard</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-500">实时日志查询系统</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="col-span-12 lg:col-span-3">
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Topics</h2>
|
||||
|
||||
<div v-if="topics.length === 0" class="text-gray-500 text-sm text-center py-4">
|
||||
暂无 Topic
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="topicInfo in topics"
|
||||
:key="topicInfo.topic"
|
||||
@click="selectTopic(topicInfo.topic)"
|
||||
:class="[
|
||||
'w-full text-left px-4 py-2 rounded-md transition-colors',
|
||||
selectedTopic === topicInfo.topic
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-50 text-gray-700 hover:bg-gray-100'
|
||||
]">
|
||||
{{ topicInfo.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div v-if="stats" class="bg-white rounded-lg shadow p-4 mt-4">
|
||||
<h3 class="text-sm font-semibold text-gray-900 mb-3">统计信息</h3>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">写入次数:</span>
|
||||
<span class="font-medium">{{ stats.write_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">写入大小:</span>
|
||||
<span class="font-medium">{{ formatBytes(stats.write_bytes) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">已处理:</span>
|
||||
<span class="font-medium">{{ stats.processed_count }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">处理大小:</span>
|
||||
<span class="font-medium">{{ formatBytes(stats.processed_bytes) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">错误次数:</span>
|
||||
<span class="font-medium text-red-600">{{ stats.error_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="col-span-12 lg:col-span-9">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<!-- 控制栏 -->
|
||||
<div class="border-b px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button
|
||||
@click="loadLogs"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm font-medium">
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
@click="queryFirst"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors text-sm">
|
||||
最早
|
||||
</button>
|
||||
<button
|
||||
@click="queryLast"
|
||||
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors text-sm">
|
||||
最新
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-sm text-gray-600">显示:</label>
|
||||
<input
|
||||
v-model.number="queryCount"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
class="w-20 px-2 py-1 border rounded text-sm">
|
||||
<span class="text-sm text-gray-600">条</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTopic" class="text-sm text-gray-500">
|
||||
Topic: <span class="font-medium text-gray-900">{{ selectedTopic }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<div class="p-4">
|
||||
<div v-if="!selectedTopic" class="text-center py-12 text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p class="mt-2">请选择一个 Topic 开始查看日志</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading" class="text-center py-12">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p class="mt-2 text-gray-500">加载中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="logs.length === 0" class="text-center py-12 text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
||||
</svg>
|
||||
<p class="mt-2">暂无日志记录</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2 max-h-[600px] overflow-y-auto" ref="logContainer" @scroll="handleScroll">
|
||||
<div
|
||||
v-for="log in logs"
|
||||
:key="log.index"
|
||||
:class="[
|
||||
'log-entry p-3 rounded-md border-l-4 transition-colors',
|
||||
getLogClass(log.status)
|
||||
]">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs font-mono text-gray-500">#{{ log.index }}</span>
|
||||
<span :class="['px-2 py-0.5 rounded-full text-xs font-medium', getStatusClass(log.status)]">
|
||||
{{ getStatusText(log.status) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">{{ formatBytes(log.dataSize || (log.data ? log.data.length : 0)) }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400 font-mono">{{ formatUUID(log.uuid) }}</span>
|
||||
</div>
|
||||
<div class="log-data text-sm text-gray-800 break-all whitespace-pre-wrap" @click="showLogDetail(log)">
|
||||
{{ log.dataPreview || log.data }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情对话框 -->
|
||||
<dialog ref="logDialog">
|
||||
<div class="dialog-container">
|
||||
<template v-if="selectedLog">
|
||||
<!-- 对话框头部 -->
|
||||
<div class="flex items-center justify-between p-4 border-b flex-shrink-0">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900">日志详情</h3>
|
||||
<span class="text-sm text-gray-500">#{{ selectedLog.index }}</span>
|
||||
</div>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 对话框内容 -->
|
||||
<div class="dialog-content p-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-4 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-600">UUID:</span>
|
||||
<span class="font-mono text-gray-900">{{ selectedLog.uuid }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-600">状态:</span>
|
||||
<span :class="['px-2 py-0.5 rounded-full text-xs font-medium', getStatusClass(selectedLog.status)]">
|
||||
{{ getStatusText(selectedLog.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-600">大小:</span>
|
||||
<span class="font-medium text-gray-900">{{ formatBytes(selectedLog.dataSize || (selectedLog.data ? selectedLog.data.length : 0)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">日志内容</label>
|
||||
<pre class="bg-gray-50 p-4 rounded-md text-sm font-mono text-gray-800 whitespace-pre-wrap break-all border border-gray-200">{{ selectedLog.data }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话框底部 -->
|
||||
<div class="p-4 border-t flex justify-end flex-shrink-0">
|
||||
<button @click="closeModal" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
topics: [],
|
||||
selectedTopic: null,
|
||||
logs: [],
|
||||
stats: null,
|
||||
loading: false,
|
||||
queryCount: 20,
|
||||
selectedLog: null,
|
||||
loadingOlder: false, // 正在加载更早的记录
|
||||
loadingNewer: false, // 正在加载更新的记录
|
||||
scrollThreshold: 50, // 触发加载的阈值(像素)
|
||||
loadingDetail: false // 正在加载详情
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadTopics() {
|
||||
try {
|
||||
const response = await fetch('/api/topics');
|
||||
this.topics = await response.json();
|
||||
} catch (error) {
|
||||
console.error('加载 topics 失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async selectTopic(topic) {
|
||||
this.selectedTopic = topic;
|
||||
await this.loadStats();
|
||||
await this.loadLogs();
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
if (!this.selectedTopic) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stats?topic=${this.selectedTopic}`);
|
||||
this.stats = await response.json();
|
||||
} catch (error) {
|
||||
console.error('加载统计信息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadLogs() {
|
||||
if (!this.selectedTopic) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs?topic=${this.selectedTopic}&count=${this.queryCount}`
|
||||
);
|
||||
const data = await response.json();
|
||||
this.logs = data.records || [];
|
||||
if (data.stats) {
|
||||
this.stats = data.stats;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志失败:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async queryFirst() {
|
||||
if (!this.selectedTopic) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs/first?topic=${this.selectedTopic}&count=${this.queryCount}`
|
||||
);
|
||||
const data = await response.json();
|
||||
this.logs = data.records || [];
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async queryLast() {
|
||||
if (!this.selectedTopic) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs/last?topic=${this.selectedTopic}&count=${this.queryCount}`
|
||||
);
|
||||
const data = await response.json();
|
||||
this.logs = data.records || [];
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getLogClass(status) {
|
||||
const classes = {
|
||||
'bg-gray-50 border-gray-300': status === 'pending',
|
||||
'bg-yellow-50 border-yellow-400': status === 'processing',
|
||||
'bg-green-50 border-green-400': status === 'processed'
|
||||
};
|
||||
return classes[status] || 'bg-gray-50 border-gray-300';
|
||||
},
|
||||
|
||||
getStatusClass(status) {
|
||||
const classes = {
|
||||
'bg-gray-200 text-gray-700': status === 'pending',
|
||||
'bg-yellow-200 text-yellow-800': status === 'processing',
|
||||
'bg-green-200 text-green-800': status === 'processed'
|
||||
};
|
||||
return classes[status] || 'bg-gray-200 text-gray-700';
|
||||
},
|
||||
|
||||
getStatusText(status) {
|
||||
const texts = {
|
||||
'pending': '待处理',
|
||||
'processing': '处理中',
|
||||
'processed': '已处理'
|
||||
};
|
||||
return texts[status] || '未知';
|
||||
},
|
||||
|
||||
formatUUID(uuid) {
|
||||
// 只显示前 8 位
|
||||
return uuid ? uuid.substring(0, 8) : '';
|
||||
},
|
||||
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
async showLogDetail(log) {
|
||||
// 如果已经有完整数据,直接显示
|
||||
if (log.data && !log.dataPreview) {
|
||||
this.selectedLog = log;
|
||||
this.$refs.logDialog.showModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则先显示弹窗,然后加载完整数据
|
||||
this.selectedLog = { ...log, data: '加载中...' };
|
||||
this.$refs.logDialog.showModal();
|
||||
this.loadingDetail = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs/record?topic=${this.selectedTopic}&index=${log.index}`
|
||||
);
|
||||
const fullRecord = await response.json();
|
||||
// 更新 selectedLog 为完整数据
|
||||
this.selectedLog = fullRecord;
|
||||
} catch (error) {
|
||||
console.error('加载日志详情失败:', error);
|
||||
this.selectedLog.data = '加载失败';
|
||||
} finally {
|
||||
this.loadingDetail = false;
|
||||
}
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.$refs.logDialog.close();
|
||||
this.selectedLog = null;
|
||||
},
|
||||
|
||||
// 处理滚动事件
|
||||
handleScroll(event) {
|
||||
const container = event.target;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
// 触顶:加载更早的记录
|
||||
if (scrollTop < this.scrollThreshold && !this.loadingOlder && this.logs.length > 0) {
|
||||
this.loadOlder();
|
||||
}
|
||||
|
||||
// 触底:加载更新的记录
|
||||
if (scrollHeight - scrollTop - clientHeight < this.scrollThreshold && !this.loadingNewer && this.logs.length > 0) {
|
||||
this.loadNewer();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更早的记录(向前)
|
||||
async loadOlder() {
|
||||
if (!this.selectedTopic || this.logs.length === 0 || this.loadingOlder) return;
|
||||
|
||||
// 获取当前最小索引
|
||||
const minIndex = Math.min(...this.logs.map(log => log.index));
|
||||
if (minIndex === 0) return; // 已经到达最早
|
||||
|
||||
this.loadingOlder = true;
|
||||
const container = this.$refs.logContainer;
|
||||
const oldScrollHeight = container.scrollHeight;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs/older?topic=${this.selectedTopic}&refIndex=${minIndex}&count=${this.queryCount}`
|
||||
);
|
||||
const data = await response.json();
|
||||
const newRecords = data.records || [];
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
// 将新记录添加到数组开头
|
||||
this.logs = [...newRecords, ...this.logs];
|
||||
|
||||
// 保持滚动位置
|
||||
this.$nextTick(() => {
|
||||
const newScrollHeight = container.scrollHeight;
|
||||
container.scrollTop = newScrollHeight - oldScrollHeight;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载更早记录失败:', error);
|
||||
} finally {
|
||||
this.loadingOlder = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更新的记录(向后)
|
||||
async loadNewer() {
|
||||
if (!this.selectedTopic || this.logs.length === 0 || this.loadingNewer) return;
|
||||
|
||||
// 获取当前最大索引
|
||||
const maxIndex = Math.max(...this.logs.map(log => log.index));
|
||||
|
||||
this.loadingNewer = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/logs/newer?topic=${this.selectedTopic}&refIndex=${maxIndex}&count=${this.queryCount}`
|
||||
);
|
||||
const data = await response.json();
|
||||
const newRecords = data.records || [];
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
// 将新记录添加到数组末尾
|
||||
this.logs = [...this.logs, ...newRecords];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载更新记录失败:', error);
|
||||
} finally {
|
||||
this.loadingNewer = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTopics();
|
||||
|
||||
// 每 3 秒自动刷新统计信息
|
||||
setInterval(() => {
|
||||
if (this.selectedTopic) {
|
||||
this.loadStats();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// 监听 dialog 关闭事件(ESC 键)
|
||||
this.$refs.logDialog.addEventListener('close', () => {
|
||||
this.selectedLog = null;
|
||||
});
|
||||
|
||||
// 点击弹窗外部关闭弹窗
|
||||
this.$refs.logDialog.addEventListener('click', (event) => {
|
||||
const rect = this.$refs.logDialog.getBoundingClientRect();
|
||||
const isInDialog = (
|
||||
rect.top <= event.clientY &&
|
||||
event.clientY <= rect.top + rect.height &&
|
||||
rect.left <= event.clientX &&
|
||||
event.clientX <= rect.left + rect.width
|
||||
);
|
||||
if (!isInDialog) {
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user