|
|
|
@ -4,38 +4,38 @@
|
|
|
|
|
<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="data.content === ''"> |
|
|
|
|
<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="mt-2.5"> |
|
|
|
|
<highlightjs |
|
|
|
|
ref="editorRef" |
|
|
|
|
class="editor-main" |
|
|
|
|
language="JavaScript" |
|
|
|
|
:autodetect="false" |
|
|
|
|
:code="content" |
|
|
|
|
></highlightjs> |
|
|
|
|
<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 { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'; |
|
|
|
|
import { downloadFile } from '@/utils/util'; |
|
|
|
|
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 { |
|
|
|
|
id?: number; |
|
|
|
|
type: string; |
|
|
|
|
name?: string; |
|
|
|
|
tail?: boolean; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const props = defineProps({ |
|
|
|
|
config: { |
|
|
|
|
type: Object as () => LogProps | null, |
|
|
|
@ -63,23 +63,19 @@ const props = defineProps({
|
|
|
|
|
default: false, |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
const data = ref({ |
|
|
|
|
enable: false, |
|
|
|
|
content: '', |
|
|
|
|
path: '', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
let timer: NodeJS.Timer | null = null; |
|
|
|
|
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 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 loading = ref(props.loading); |
|
|
|
|
const readReq = reactive({ |
|
|
|
|
id: 0, |
|
|
|
|
type: '', |
|
|
|
@ -88,40 +84,77 @@ const readReq = reactive({
|
|
|
|
|
pageSize: 500, |
|
|
|
|
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( |
|
|
|
|
() => props.loading, |
|
|
|
|
(newLoading) => { |
|
|
|
|
loading.value = newLoading; |
|
|
|
|
}, |
|
|
|
|
); |
|
|
|
|
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 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 onDownload = async () => { |
|
|
|
|
changeLoading(); |
|
|
|
|
downloadFile(logPath.value); |
|
|
|
|
changeLoading(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
emit('update:isReading', true); |
|
|
|
|
readReq.id = props.config.id; |
|
|
|
|
readReq.type = props.config.type; |
|
|
|
|
readReq.name = props.config.name; |
|
|
|
@ -129,147 +162,103 @@ const getContent = (pre: boolean) => {
|
|
|
|
|
readReq.page = 1; |
|
|
|
|
} |
|
|
|
|
isLoading.value = true; |
|
|
|
|
ReadByLine(readReq).then((res) => { |
|
|
|
|
firstLoading.value = false; |
|
|
|
|
if (!end.value && res.data.end) { |
|
|
|
|
lastLogs.value = [...logs.value]; |
|
|
|
|
} |
|
|
|
|
emit('update:isReading', true); |
|
|
|
|
|
|
|
|
|
data.value = res.data; |
|
|
|
|
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++; |
|
|
|
|
} |
|
|
|
|
const res = await ReadByLine(readReq); |
|
|
|
|
logPath.value = res.data.path; |
|
|
|
|
firstLoading.value = false; |
|
|
|
|
|
|
|
|
|
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 (!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 (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]; |
|
|
|
|
} |
|
|
|
|
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 { |
|
|
|
|
if ((logs.value.length = 0)) { |
|
|
|
|
logs.value = newLogs; |
|
|
|
|
} else { |
|
|
|
|
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(() => { |
|
|
|
|
if (pre) { |
|
|
|
|
if (scrollerElement.value.scrollHeight > 2000) { |
|
|
|
|
scrollerElement.value.scrollTop = 2000; |
|
|
|
|
} |
|
|
|
|
logContainer.value.scrollTop = 2000; |
|
|
|
|
} else { |
|
|
|
|
scrollerElement.value.scrollTop = scrollerElement.value.scrollHeight; |
|
|
|
|
logContainer.value.scrollTop = totalHeight.value; |
|
|
|
|
containerHeight.value = logContainer.value.getBoundingClientRect().height; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
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--; |
|
|
|
|
minPage.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 { |
|
|
|
|
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 search = () => { |
|
|
|
|
throttledGetContent(false); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const changeTail = (fromOutSide: boolean) => { |
|
|
|
|
if (fromOutSide) { |
|
|
|
|
tailLog.value = !tailLog.value; |
|
|
|
|
} |
|
|
|
|
if (tailLog.value) { |
|
|
|
|
timer = setInterval(() => { |
|
|
|
|
search(); |
|
|
|
|
}, 1000 * 3); |
|
|
|
|
} else { |
|
|
|
|
onCloseLog(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const onDownload = async () => { |
|
|
|
|
changeLoading(); |
|
|
|
|
downloadFile(data.value.path); |
|
|
|
|
changeLoading(); |
|
|
|
|
isLoading.value = false; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const onCloseLog = async () => { |
|
|
|
|
emit('update:isReading', false); |
|
|
|
|
tailLog.value = false; |
|
|
|
|
clearInterval(Number(timer)); |
|
|
|
|
timer = null; |
|
|
|
|
isLoading.value = false; |
|
|
|
|
emit('update:isReading', false); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
function isScrolledToBottom(element: HTMLElement): boolean { |
|
|
|
|
return element.scrollTop + element.clientHeight + 1 >= element.scrollHeight; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function isScrolledToTop(element: HTMLElement): boolean { |
|
|
|
|
return element.scrollTop === 0; |
|
|
|
|
} |
|
|
|
|
watch( |
|
|
|
|
() => props.loading, |
|
|
|
|
(newLoading) => { |
|
|
|
|
loading.value = newLoading; |
|
|
|
|
}, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const init = () => { |
|
|
|
|
const init = async () => { |
|
|
|
|
if (props.config.tail) { |
|
|
|
|
tailLog.value = props.config.tail; |
|
|
|
|
} else { |
|
|
|
@ -279,58 +268,43 @@ const init = () => {
|
|
|
|
|
changeTail(false); |
|
|
|
|
} |
|
|
|
|
readReq.latest = true; |
|
|
|
|
search(); |
|
|
|
|
await getContent(false); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const clearLog = (): void => { |
|
|
|
|
content.value = ''; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const initCodemirror = () => { |
|
|
|
|
onMounted(async () => { |
|
|
|
|
firstLoading.value = true; |
|
|
|
|
await init(); |
|
|
|
|
nextTick(() => { |
|
|
|
|
if (editorRef.value) { |
|
|
|
|
scrollerElement.value = editorRef.value.$el as HTMLElement; |
|
|
|
|
scrollerElement.value.addEventListener('scroll', function () { |
|
|
|
|
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'; |
|
|
|
|
if (logContainer.value) { |
|
|
|
|
logContainer.value.scrollTop = totalHeight.value; |
|
|
|
|
containerHeight.value = logContainer.value.getBoundingClientRect().height; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
|
|
onCloseLog(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
initCodemirror(); |
|
|
|
|
init(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
defineExpose({ changeTail, onDownload, clearLog }); |
|
|
|
|
</script> |
|
|
|
|
<style lang="scss" scoped> |
|
|
|
|
.editor-main { |
|
|
|
|
height: calc(100vh - 480px); |
|
|
|
|
|
|
|
|
|
<style scoped> |
|
|
|
|
.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%; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.log-item { |
|
|
|
|
position: absolute; |
|
|
|
|
width: 100%; |
|
|
|
|
min-height: 600px; |
|
|
|
|
overflow: auto; |
|
|
|
|
padding: 5px; |
|
|
|
|
color: #f5f5f5; |
|
|
|
|
box-sizing: border-box; |
|
|
|
|
white-space: nowrap; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|