mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-31 14:08:06 +08:00
pref: 优化网站日志读取 (#6710)
Refs https://github.com/1Panel-dev/1Panel/issues/6642
This commit is contained in:
parent
c86947b69c
commit
02b7744995
@ -4,38 +4,38 @@
|
|||||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
|
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
|
||||||
{{ $t('commons.button.watch') }}
|
{{ $t('commons.button.watch') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="logs.length === 0">
|
||||||
{{ $t('file.download') }}
|
{{ $t('file.download') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<span v-if="$slots.button" class="ml-2.5">
|
<span v-if="$slots.button" class="ml-2.5">
|
||||||
<slot name="button"></slot>
|
<slot name="button"></slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2.5">
|
<div class="log-container" ref="logContainer" @scroll="onScroll">
|
||||||
<highlightjs
|
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
|
||||||
ref="editorRef"
|
<div
|
||||||
class="editor-main"
|
v-for="(log, index) in visibleLogs"
|
||||||
language="JavaScript"
|
:key="startIndex + index"
|
||||||
:autodetect="false"
|
class="log-item"
|
||||||
:code="content"
|
:style="{ top: `${(startIndex + index) * logHeight}px` }"
|
||||||
></highlightjs>
|
>
|
||||||
|
<span>{{ log }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
||||||
import { downloadFile } from '@/utils/util';
|
|
||||||
import { ReadByLine } from '@/api/modules/files';
|
|
||||||
import { watch } from 'vue';
|
|
||||||
|
|
||||||
const editorRef = ref();
|
<script lang="ts" setup>
|
||||||
|
import { ReadByLine } from '@/api/modules/files';
|
||||||
|
import { ref, computed, onMounted, watch, nextTick, reactive } from 'vue';
|
||||||
|
import { downloadFile } from '@/utils/util';
|
||||||
|
|
||||||
interface LogProps {
|
interface LogProps {
|
||||||
id?: number;
|
id?: number;
|
||||||
type: string;
|
type: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
tail?: boolean;
|
tail?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
config: {
|
config: {
|
||||||
type: Object as () => LogProps | null,
|
type: Object as () => LogProps | null,
|
||||||
@ -63,47 +63,6 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const data = ref({
|
|
||||||
enable: false,
|
|
||||||
content: '',
|
|
||||||
path: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
const tailLog = ref(false);
|
|
||||||
const content = ref('');
|
|
||||||
const end = ref(false);
|
|
||||||
const scrollerElement = ref<HTMLElement | null>(null);
|
|
||||||
const minPage = ref(1);
|
|
||||||
const maxPage = ref(1);
|
|
||||||
const logs = ref([]);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const firstLoading = ref(true);
|
|
||||||
|
|
||||||
const readReq = reactive({
|
|
||||||
id: 0,
|
|
||||||
type: '',
|
|
||||||
name: '',
|
|
||||||
page: 1,
|
|
||||||
pageSize: 500,
|
|
||||||
latest: false,
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
|
|
||||||
|
|
||||||
const loading = ref(props.loading);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.loading,
|
|
||||||
(newLoading) => {
|
|
||||||
loading.value = newLoading;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeLoading = () => {
|
|
||||||
loading.value = !loading.value;
|
|
||||||
emit('update:loading', loading.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopSignals = [
|
const stopSignals = [
|
||||||
'docker-compose up failed!',
|
'docker-compose up failed!',
|
||||||
'docker-compose up successful!',
|
'docker-compose up successful!',
|
||||||
@ -114,124 +73,63 @@ const stopSignals = [
|
|||||||
'image push failed!',
|
'image push failed!',
|
||||||
'image push successful!',
|
'image push successful!',
|
||||||
];
|
];
|
||||||
|
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
|
||||||
|
const tailLog = ref(false);
|
||||||
|
const loading = ref(props.loading);
|
||||||
|
const readReq = reactive({
|
||||||
|
id: 0,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 500,
|
||||||
|
latest: false,
|
||||||
|
});
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const end = ref(false);
|
||||||
const lastLogs = ref([]);
|
const lastLogs = ref([]);
|
||||||
|
const maxPage = ref(0);
|
||||||
|
const minPage = ref(0);
|
||||||
|
let timer: NodeJS.Timer | null = null;
|
||||||
|
const logPath = ref('');
|
||||||
|
|
||||||
const getContent = (pre: boolean) => {
|
const firstLoading = ref(false);
|
||||||
if (isLoading.value) {
|
const logs = ref<string[]>([]);
|
||||||
return;
|
const logContainer = ref<HTMLElement | null>(null);
|
||||||
}
|
const logHeight = 20;
|
||||||
emit('update:isReading', true);
|
const logCount = ref(0);
|
||||||
readReq.id = props.config.id;
|
const totalHeight = computed(() => logHeight * logCount.value);
|
||||||
readReq.type = props.config.type;
|
const containerHeight = ref(500);
|
||||||
readReq.name = props.config.name;
|
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight)); // 计算可见日志条数(容器高度 / 日志高度)
|
||||||
if (readReq.page < 1) {
|
const startIndex = ref(0);
|
||||||
readReq.page = 1;
|
|
||||||
}
|
|
||||||
isLoading.value = true;
|
|
||||||
ReadByLine(readReq).then((res) => {
|
|
||||||
firstLoading.value = false;
|
|
||||||
if (!end.value && res.data.end) {
|
|
||||||
lastLogs.value = [...logs.value];
|
|
||||||
}
|
|
||||||
|
|
||||||
data.value = res.data;
|
const visibleLogs = computed(() => {
|
||||||
if (res.data.lines && res.data.lines.length > 0) {
|
return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
|
||||||
res.data.lines = res.data.lines.map((line) =>
|
});
|
||||||
line.replace(/\\u(\w{4})/g, function (match, grp) {
|
|
||||||
return String.fromCharCode(parseInt(grp, 16));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const newLogs = res.data.lines;
|
|
||||||
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
|
|
||||||
readReq.page++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const onScroll = () => {
|
||||||
readReq.type == 'php' &&
|
if (logContainer.value) {
|
||||||
logs.value.length > 0 &&
|
const scrollTop = logContainer.value.scrollTop;
|
||||||
newLogs.length > 0 &&
|
if (scrollTop == 0) {
|
||||||
newLogs[newLogs.length - 1] === logs.value[logs.value.length - 1]
|
readReq.page = minPage.value - 1;
|
||||||
) {
|
if (readReq.page < 1) {
|
||||||
isLoading.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
minPage.value = readReq.page;
|
||||||
if (stopSignals.some((signal) => newLogs[newLogs.length - 1].endsWith(signal))) {
|
getContent(true);
|
||||||
onCloseLog();
|
|
||||||
}
|
|
||||||
if (end.value) {
|
|
||||||
if ((logs.value.length = 0)) {
|
|
||||||
logs.value = newLogs;
|
|
||||||
} else {
|
|
||||||
logs.value = pre ? [...newLogs, ...lastLogs.value] : [...lastLogs.value, ...newLogs];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((logs.value.length = 0)) {
|
|
||||||
logs.value = newLogs;
|
|
||||||
} else {
|
|
||||||
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
startIndex.value = Math.floor(scrollTop / logHeight);
|
||||||
end.value = res.data.end;
|
}
|
||||||
content.value = logs.value.join('\n');
|
|
||||||
|
|
||||||
emit('update:hasContent', content.value !== '');
|
|
||||||
nextTick(() => {
|
|
||||||
if (pre) {
|
|
||||||
if (scrollerElement.value.scrollHeight > 2000) {
|
|
||||||
scrollerElement.value.scrollTop = 2000;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (readReq.latest) {
|
|
||||||
readReq.page = res.data.total;
|
|
||||||
readReq.latest = false;
|
|
||||||
maxPage.value = res.data.total;
|
|
||||||
minPage.value = res.data.total;
|
|
||||||
}
|
|
||||||
if (logs.value && logs.value.length > 1000) {
|
|
||||||
logs.value.splice(0, readReq.pageSize);
|
|
||||||
if (minPage.value > 1) {
|
|
||||||
minPage.value--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function throttle<T extends (...args: any[]) => any>(func: T, limit: number): (...args: Parameters<T>) => void {
|
const changeLoading = () => {
|
||||||
let inThrottle: boolean;
|
loading.value = !loading.value;
|
||||||
let lastFunc: ReturnType<typeof setTimeout>;
|
emit('update:loading', loading.value);
|
||||||
let lastRan: number;
|
};
|
||||||
return function (this: any, ...args: Parameters<T>) {
|
|
||||||
if (!inThrottle) {
|
|
||||||
func.apply(this, args);
|
|
||||||
lastRan = Date.now();
|
|
||||||
inThrottle = true;
|
|
||||||
setTimeout(() => (inThrottle = false), limit);
|
|
||||||
} else {
|
|
||||||
clearTimeout(lastFunc);
|
|
||||||
lastFunc = setTimeout(() => {
|
|
||||||
if (Date.now() - lastRan >= limit) {
|
|
||||||
func.apply(this, args);
|
|
||||||
lastRan = Date.now();
|
|
||||||
}
|
|
||||||
}, limit - (Date.now() - lastRan));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const throttledGetContent = throttle(getContent, 3000);
|
const onDownload = async () => {
|
||||||
|
changeLoading();
|
||||||
const search = () => {
|
downloadFile(logPath.value);
|
||||||
throttledGetContent(false);
|
changeLoading();
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeTail = (fromOutSide: boolean) => {
|
const changeTail = (fromOutSide: boolean) => {
|
||||||
@ -240,36 +138,127 @@ const changeTail = (fromOutSide: boolean) => {
|
|||||||
}
|
}
|
||||||
if (tailLog.value) {
|
if (tailLog.value) {
|
||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
search();
|
getContent(false);
|
||||||
}, 1000 * 3);
|
}, 1000 * 3);
|
||||||
} else {
|
} else {
|
||||||
onCloseLog();
|
onCloseLog();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownload = async () => {
|
const clearLog = (): void => {
|
||||||
changeLoading();
|
logs.value = [];
|
||||||
downloadFile(data.value.path);
|
readReq.page = 1;
|
||||||
changeLoading();
|
lastLogs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContent = async (pre: boolean) => {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
readReq.id = props.config.id;
|
||||||
|
readReq.type = props.config.type;
|
||||||
|
readReq.name = props.config.name;
|
||||||
|
if (readReq.page < 1) {
|
||||||
|
readReq.page = 1;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
emit('update:isReading', true);
|
||||||
|
|
||||||
|
const res = await ReadByLine(readReq);
|
||||||
|
logPath.value = res.data.path;
|
||||||
|
firstLoading.value = false;
|
||||||
|
|
||||||
|
if (!end.value && res.data.end) {
|
||||||
|
lastLogs.value = [...logs.value];
|
||||||
|
}
|
||||||
|
if (res.data.lines && res.data.lines.length > 0) {
|
||||||
|
res.data.lines = res.data.lines.map((line) =>
|
||||||
|
line.replace(/\\u(\w{4})/g, function (match, grp) {
|
||||||
|
return String.fromCharCode(parseInt(grp, 16));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const newLogs = res.data.lines;
|
||||||
|
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
|
||||||
|
readReq.page++;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
readReq.type == 'php' &&
|
||||||
|
logs.value.length > 0 &&
|
||||||
|
newLogs.length > 0 &&
|
||||||
|
newLogs[newLogs.length - 1] === logs.value[logs.value.length - 1]
|
||||||
|
) {
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopSignals.some((signal) => newLogs[newLogs.length - 1].endsWith(signal))) {
|
||||||
|
onCloseLog();
|
||||||
|
}
|
||||||
|
if (end.value) {
|
||||||
|
if ((logs.value.length = 0)) {
|
||||||
|
logs.value = newLogs;
|
||||||
|
} else {
|
||||||
|
logs.value = pre ? [...newLogs, ...lastLogs.value] : [...lastLogs.value, ...newLogs];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((logs.value.length = 0)) {
|
||||||
|
logs.value = newLogs;
|
||||||
|
} else {
|
||||||
|
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (pre) {
|
||||||
|
logContainer.value.scrollTop = 2000;
|
||||||
|
} else {
|
||||||
|
logContainer.value.scrollTop = totalHeight.value;
|
||||||
|
containerHeight.value = logContainer.value.getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logCount.value = logs.value.length;
|
||||||
|
end.value = res.data.end;
|
||||||
|
emit('update:hasContent', logs.value.length > 0);
|
||||||
|
if (readReq.latest) {
|
||||||
|
readReq.page = res.data.total;
|
||||||
|
readReq.latest = false;
|
||||||
|
maxPage.value = res.data.total;
|
||||||
|
minPage.value = res.data.total;
|
||||||
|
}
|
||||||
|
if (logs.value && logs.value.length > 3000) {
|
||||||
|
if (pre) {
|
||||||
|
logs.value.splice(logs.value.length - readReq.pageSize, readReq.pageSize);
|
||||||
|
if (maxPage.value > 1) {
|
||||||
|
maxPage.value--;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logs.value.splice(0, readReq.pageSize);
|
||||||
|
if (minPage.value > 1) {
|
||||||
|
minPage.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloseLog = async () => {
|
const onCloseLog = async () => {
|
||||||
emit('update:isReading', false);
|
|
||||||
tailLog.value = false;
|
tailLog.value = false;
|
||||||
clearInterval(Number(timer));
|
clearInterval(Number(timer));
|
||||||
timer = null;
|
timer = null;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
emit('update:isReading', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
watch(
|
||||||
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight;
|
() => props.loading,
|
||||||
}
|
(newLoading) => {
|
||||||
|
loading.value = newLoading;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function isScrolledToTop(element: HTMLElement): boolean {
|
const init = async () => {
|
||||||
return element.scrollTop === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
if (props.config.tail) {
|
if (props.config.tail) {
|
||||||
tailLog.value = props.config.tail;
|
tailLog.value = props.config.tail;
|
||||||
} else {
|
} else {
|
||||||
@ -279,58 +268,43 @@ const init = () => {
|
|||||||
changeTail(false);
|
changeTail(false);
|
||||||
}
|
}
|
||||||
readReq.latest = true;
|
readReq.latest = true;
|
||||||
search();
|
await getContent(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearLog = (): void => {
|
onMounted(async () => {
|
||||||
content.value = '';
|
firstLoading.value = true;
|
||||||
};
|
await init();
|
||||||
|
|
||||||
const initCodemirror = () => {
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (editorRef.value) {
|
if (logContainer.value) {
|
||||||
scrollerElement.value = editorRef.value.$el as HTMLElement;
|
logContainer.value.scrollTop = totalHeight.value;
|
||||||
scrollerElement.value.addEventListener('scroll', function () {
|
containerHeight.value = logContainer.value.getBoundingClientRect().height;
|
||||||
if (tailLog.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isScrolledToBottom(scrollerElement.value)) {
|
|
||||||
if (maxPage.value > 1) {
|
|
||||||
readReq.page = maxPage.value;
|
|
||||||
}
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
if (isScrolledToTop(scrollerElement.value)) {
|
|
||||||
readReq.page = minPage.value - 1;
|
|
||||||
if (readReq.page < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
minPage.value = readReq.page;
|
|
||||||
throttledGetContent(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
|
|
||||||
hljsDom.style['min-height'] = '500px';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
onCloseLog();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initCodemirror();
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ changeTail, onDownload, clearLog });
|
defineExpose({ changeTail, onDownload, clearLog });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
.editor-main {
|
<style scoped>
|
||||||
height: calc(100vh - 480px);
|
.log-container {
|
||||||
|
height: calc(100vh - 405px);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: #121212;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-spacer {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 600px;
|
}
|
||||||
overflow: auto;
|
|
||||||
|
.log-item {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
color: #f5f5f5;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user