pref: 优化网站日志读取 (#6710)

Refs https://github.com/1Panel-dev/1Panel/issues/6642
pull/6719/head
zhengkunwang 2 months ago committed by GitHub
parent c86947b69c
commit 02b7744995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
import { downloadFile } from '@/utils/util';
import { ReadByLine } from '@/api/modules/files'; import { ReadByLine } from '@/api/modules/files';
import { watch } from 'vue'; import { ref, computed, onMounted, watch, nextTick, reactive } from 'vue';
import { downloadFile } from '@/utils/util';
const editorRef = ref();
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,23 +63,19 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const data = ref({ const stopSignals = [
enable: false, 'docker-compose up failed!',
content: '', 'docker-compose up successful!',
path: '', 'image build failed!',
}); 'image build successful!',
'image pull failed!',
let timer: NodeJS.Timer | null = null; 'image pull successful!',
'image push failed!',
'image push successful!',
];
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']);
const tailLog = ref(false); const tailLog = ref(false);
const content = ref(''); const loading = ref(props.loading);
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({ const readReq = reactive({
id: 0, id: 0,
type: '', type: '',
@ -88,40 +84,77 @@ const readReq = reactive({
pageSize: 500, pageSize: 500,
latest: false, latest: false,
}); });
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading']); const isLoading = ref(false);
const end = ref(false);
const lastLogs = ref([]);
const maxPage = ref(0);
const minPage = ref(0);
let timer: NodeJS.Timer | null = null;
const logPath = ref('');
const loading = ref(props.loading); const firstLoading = ref(false);
const logs = ref<string[]>([]);
const logContainer = ref<HTMLElement | null>(null);
const logHeight = 20;
const logCount = ref(0);
const totalHeight = computed(() => logHeight * logCount.value);
const containerHeight = ref(500);
const visibleCount = computed(() => Math.ceil(containerHeight.value / logHeight)); // /
const startIndex = ref(0);
watch( const visibleLogs = computed(() => {
() => props.loading, return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
(newLoading) => { });
loading.value = newLoading;
}, const onScroll = () => {
); if (logContainer.value) {
const scrollTop = logContainer.value.scrollTop;
if (scrollTop == 0) {
readReq.page = minPage.value - 1;
if (readReq.page < 1) {
return;
}
minPage.value = readReq.page;
getContent(true);
}
startIndex.value = Math.floor(scrollTop / logHeight);
}
};
const changeLoading = () => { const changeLoading = () => {
loading.value = !loading.value; loading.value = !loading.value;
emit('update:loading', loading.value); emit('update:loading', loading.value);
}; };
const stopSignals = [ const onDownload = async () => {
'docker-compose up failed!', changeLoading();
'docker-compose up successful!', downloadFile(logPath.value);
'image build failed!', changeLoading();
'image build successful!', };
'image pull failed!',
'image pull successful!',
'image push failed!',
'image push successful!',
];
const lastLogs = ref([]); const changeTail = (fromOutSide: boolean) => {
if (fromOutSide) {
tailLog.value = !tailLog.value;
}
if (tailLog.value) {
timer = setInterval(() => {
getContent(false);
}, 1000 * 3);
} else {
onCloseLog();
}
};
const clearLog = (): void => {
logs.value = [];
readReq.page = 1;
lastLogs.value = [];
};
const getContent = (pre: boolean) => { const getContent = async (pre: boolean) => {
if (isLoading.value) { if (isLoading.value) {
return; return;
} }
emit('update:isReading', true);
readReq.id = props.config.id; readReq.id = props.config.id;
readReq.type = props.config.type; readReq.type = props.config.type;
readReq.name = props.config.name; readReq.name = props.config.name;
@ -129,13 +162,15 @@ const getContent = (pre: boolean) => {
readReq.page = 1; readReq.page = 1;
} }
isLoading.value = true; isLoading.value = true;
ReadByLine(readReq).then((res) => { emit('update:isReading', true);
const res = await ReadByLine(readReq);
logPath.value = res.data.path;
firstLoading.value = false; firstLoading.value = false;
if (!end.value && res.data.end) { if (!end.value && res.data.end) {
lastLogs.value = [...logs.value]; lastLogs.value = [...logs.value];
} }
data.value = res.data;
if (res.data.lines && res.data.lines.length > 0) { if (res.data.lines && res.data.lines.length > 0) {
res.data.lines = res.data.lines.map((line) => res.data.lines = res.data.lines.map((line) =>
line.replace(/\\u(\w{4})/g, function (match, grp) { line.replace(/\\u(\w{4})/g, function (match, grp) {
@ -146,7 +181,6 @@ const getContent = (pre: boolean) => {
if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) { if (newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
readReq.page++; readReq.page++;
} }
if ( if (
readReq.type == 'php' && readReq.type == 'php' &&
logs.value.length > 0 && logs.value.length > 0 &&
@ -173,103 +207,58 @@ const getContent = (pre: boolean) => {
logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs]; logs.value = pre ? [...newLogs, ...logs.value] : [...logs.value, ...newLogs];
} }
} }
}
end.value = res.data.end;
content.value = logs.value.join('\n');
emit('update:hasContent', content.value !== '');
nextTick(() => { nextTick(() => {
if (pre) { if (pre) {
if (scrollerElement.value.scrollHeight > 2000) { logContainer.value.scrollTop = 2000;
scrollerElement.value.scrollTop = 2000;
}
} else { } else {
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight; 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) { if (readReq.latest) {
readReq.page = res.data.total; readReq.page = res.data.total;
readReq.latest = false; readReq.latest = false;
maxPage.value = res.data.total; maxPage.value = res.data.total;
minPage.value = res.data.total; minPage.value = res.data.total;
} }
if (logs.value && logs.value.length > 1000) { if (logs.value && logs.value.length > 3000) {
logs.value.splice(0, readReq.pageSize); if (pre) {
if (minPage.value > 1) { logs.value.splice(logs.value.length - readReq.pageSize, readReq.pageSize);
minPage.value--; if (maxPage.value > 1) {
} maxPage.value--;
} }
isLoading.value = false;
});
};
function throttle<T extends (...args: any[]) => any>(func: T, limit: number): (...args: Parameters<T>) => void {
let inThrottle: boolean;
let lastFunc: ReturnType<typeof setTimeout>;
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 { } else {
clearTimeout(lastFunc); logs.value.splice(0, readReq.pageSize);
lastFunc = setTimeout(() => { if (minPage.value > 1) {
if (Date.now() - lastRan >= limit) { minPage.value++;
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
} }
const throttledGetContent = throttle(getContent, 3000);
const search = () => {
throttledGetContent(false);
};
const changeTail = (fromOutSide: boolean) => {
if (fromOutSide) {
tailLog.value = !tailLog.value;
} }
if (tailLog.value) {
timer = setInterval(() => {
search();
}, 1000 * 3);
} else {
onCloseLog();
} }
}; isLoading.value = false;
const onDownload = async () => {
changeLoading();
downloadFile(data.value.path);
changeLoading();
}; };
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 { },
return element.scrollTop === 0; );
}
const init = () => { const init = async () => {
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 => {
content.value = '';
}; };
const initCodemirror = () => { onMounted(async () => {
firstLoading.value = true;
await init();
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';
}
}); });
}; defineExpose({ changeTail, onDownload, clearLog });
</script>
onUnmounted(() => { <style scoped>
onCloseLog(); .log-container {
}); height: calc(100vh - 405px);
overflow-y: auto;
overflow-x: auto;
position: relative;
background-color: #121212;
margin-top: 10px;
}
onMounted(() => { .log-spacer {
initCodemirror(); position: relative;
init(); width: 100%;
}); }
defineExpose({ changeTail, onDownload, clearLog }); .log-item {
</script> position: absolute;
<style lang="scss" scoped>
.editor-main {
height: calc(100vh - 480px);
width: 100%; width: 100%;
min-height: 600px; padding: 5px;
overflow: auto; color: #f5f5f5;
box-sizing: border-box;
white-space: nowrap;
} }
</style> </style>

Loading…
Cancel
Save