1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-15 10:14:44 +08:00
2024-10-18 08:55:38 +00:00

317 lines
8.3 KiB
Vue

<template>
<div v-loading="firstLoading">
<div v-if="defaultButton">
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail(false)">
{{ $t('commons.button.watch') }}
</el-checkbox>
<el-button class="ml-2.5" @click="onDownload" icon="Download" :disabled="logs.length === 0">
{{ $t('file.download') }}
</el-button>
<span v-if="$slots.button" class="ml-2.5">
<slot name="button"></slot>
</span>
</div>
<div class="log-container" ref="logContainer" @scroll="onScroll">
<div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
<div
v-for="(log, index) in visibleLogs"
:key="startIndex + index"
class="log-item"
:style="{ top: `${(startIndex + index) * logHeight}px` }"
>
<span>{{ log }}</span>
</div>
</div>
</div>
</template>
<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 {
id?: number;
type: string;
name?: string;
tail?: boolean;
}
const props = defineProps({
config: {
type: Object as () => LogProps | null,
default: () => ({
id: 0,
type: '',
name: '',
tail: false,
}),
},
style: {
type: String,
default: 'height: calc(100vh - 200px); width: 100%; min-height: 400px; overflow: auto;',
},
defaultButton: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: true,
},
hasContent: {
type: Boolean,
default: false,
},
});
const stopSignals = [
'docker-compose up failed!',
'docker-compose up successful!',
'image build failed!',
'image build successful!',
'image pull failed!',
'image pull successful!',
'image push failed!',
'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 maxPage = ref(0);
const minPage = ref(0);
let timer: NodeJS.Timer | null = null;
const logPath = ref('');
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);
const visibleLogs = computed(() => {
return logs.value.slice(startIndex.value, startIndex.value + visibleCount.value);
});
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 = () => {
loading.value = !loading.value;
emit('update:loading', loading.value);
};
const onDownload = async () => {
changeLoading();
downloadFile(logPath.value);
changeLoading();
};
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 = 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 () => {
tailLog.value = false;
clearInterval(Number(timer));
timer = null;
isLoading.value = false;
emit('update:isReading', false);
};
watch(
() => props.loading,
(newLoading) => {
loading.value = newLoading;
},
);
const init = async () => {
if (props.config.tail) {
tailLog.value = props.config.tail;
} else {
tailLog.value = false;
}
if (tailLog.value) {
changeTail(false);
}
readReq.latest = true;
await getContent(false);
};
onMounted(async () => {
firstLoading.value = true;
await init();
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = totalHeight.value;
containerHeight.value = logContainer.value.getBoundingClientRect().height;
}
});
});
defineExpose({ changeTail, onDownload, clearLog });
</script>
<style scoped>
.log-container {
height: calc(100vh - 405px);
overflow-y: auto;
overflow-x: auto;
position: relative;
background-color: #1e1e1e;
margin-top: 10px;
}
.log-spacer {
position: relative;
width: 100%;
}
.log-item {
position: absolute;
width: 100%;
padding: 5px;
color: #f5f5f5;
box-sizing: border-box;
white-space: nowrap;
}
.log-item span {
font-size: 14px;
font-weight: 300;
color: #cccccc;
}
</style>