mirror of https://github.com/1Panel-dev/1Panel
parent
c47075beeb
commit
d15bd1d6b4
|
@ -683,3 +683,27 @@ func (b *BaseApi) Keys(c *gin.Context) {
|
|||
res.Keys = keys
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Read file by Line
|
||||
// @Description 按行读取文件
|
||||
// @Param request body request.FileReadByLineReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /files/read [post]
|
||||
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
|
||||
var req request.FileReadByLineReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
lines, end, err := files.ReadFileByLine(req.Path, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
}
|
||||
res := response.FileLineContent{
|
||||
End: end,
|
||||
}
|
||||
res.Path = req.Path
|
||||
res.Content = strings.Join(lines, "\n")
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
|
|
@ -109,6 +109,12 @@ type FileRoleUpdate struct {
|
|||
Sub bool `json:"sub" validate:"required"`
|
||||
}
|
||||
|
||||
type FileReadByLineReq struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Page int `json:"page" validate:"required"`
|
||||
PageSize int `json:"pageSize" validate:"required"`
|
||||
}
|
||||
|
||||
type FileExistReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Dir string `json:"dir" validate:"required"`
|
||||
|
|
|
@ -33,6 +33,12 @@ type FileWgetRes struct {
|
|||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileLineContent struct {
|
||||
Content string `json:"content"`
|
||||
End bool `json:"end"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type FileExist struct {
|
||||
Exist bool `json:"exist"`
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
|
|||
fileRouter.POST("/size", baseApi.Size)
|
||||
fileRouter.GET("/ws", baseApi.Ws)
|
||||
fileRouter.GET("/keys", baseApi.Keys)
|
||||
fileRouter.POST("/read", baseApi.ReadFileByLine)
|
||||
|
||||
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
|
||||
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by swaggo/swag. DO NOT EDIT.
|
||||
|
||||
// Package docs GENERATED BY SWAG; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
@ -5666,6 +5666,36 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/files/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按行读取文件",
|
||||
"tags": [
|
||||
"File"
|
||||
],
|
||||
"summary": "Read file by Line",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/request.FileReadByLineReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/files/recycle/clear": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -16955,6 +16985,25 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"request.FileReadByLineReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"page",
|
||||
"pageSize",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.FileRename": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -5659,6 +5659,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/files/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "按行读取文件",
|
||||
"tags": [
|
||||
"File"
|
||||
],
|
||||
"summary": "Read file by Line",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/request.FileReadByLineReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/files/recycle/clear": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -16948,6 +16978,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"request.FileReadByLineReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"page",
|
||||
"pageSize",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"request.FileRename": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
@ -2998,6 +2998,19 @@ definitions:
|
|||
required:
|
||||
- path
|
||||
type: object
|
||||
request.FileReadByLineReq:
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
pageSize:
|
||||
type: integer
|
||||
path:
|
||||
type: string
|
||||
required:
|
||||
- page
|
||||
- pageSize
|
||||
- path
|
||||
type: object
|
||||
request.FileRename:
|
||||
properties:
|
||||
newName:
|
||||
|
@ -7955,6 +7968,24 @@ paths:
|
|||
formatEN: Change owner [paths] => [user]/[group]
|
||||
formatZH: 修改用户/组 [paths] => [user]/[group]
|
||||
paramKeys: []
|
||||
/files/read:
|
||||
post:
|
||||
description: 按行读取文件
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.FileReadByLineReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Read file by Line
|
||||
tags:
|
||||
- File
|
||||
/files/recycle/clear:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
@ -164,6 +164,12 @@ export namespace File {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export interface FileReadByLine {
|
||||
path: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface Favorite extends CommonModel {
|
||||
path: string;
|
||||
isDir: boolean;
|
||||
|
|
|
@ -109,6 +109,10 @@ export const AddFavorite = (path: string) => {
|
|||
return http.post<any>('files/favorite', { path: path });
|
||||
};
|
||||
|
||||
export const ReadByLine = (req: File.FileReadByLine) => {
|
||||
return http.post<any>('files/read', req);
|
||||
};
|
||||
|
||||
export const RemoveFavorite = (id: number) => {
|
||||
return http.post<any>('files/favorite/del', { id: id });
|
||||
};
|
||||
|
|
|
@ -39,6 +39,11 @@
|
|||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('website.log')" prop="">
|
||||
<template #default="{ row }">
|
||||
<el-button @click="openLog(row)" link type="primary">{{ $t('website.check') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
|
@ -61,6 +66,7 @@
|
|||
|
||||
<CreateRuntime ref="createRef" @close="search" />
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<Log ref="logRef" @close="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -74,6 +80,7 @@ import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
|||
import Status from '@/components/status/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import RouterMenu from '../index.vue';
|
||||
import Log from '@/views/website/runtime/php/log/index.vue';
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'runtime-page-size',
|
||||
|
@ -89,6 +96,7 @@ let req = reactive<Runtime.RuntimeReq>({
|
|||
});
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
const opRef = ref();
|
||||
const logRef = ref();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
|
@ -133,6 +141,10 @@ const openDetail = (row: Runtime.Runtime) => {
|
|||
createRef.value.acceptParams({ type: row.type, mode: 'edit', id: row.id, appID: row.appID });
|
||||
};
|
||||
|
||||
const openLog = (row: Runtime.RuntimeDTO) => {
|
||||
logRef.value.acceptParams({ path: row.path + '/' + 'build.log' });
|
||||
};
|
||||
|
||||
const openDelete = async (row: Runtime.Runtime) => {
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.msg.deleteTitle'),
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<el-drawer v-model="open" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('website.log')" :back="handleClose" />
|
||||
</template>
|
||||
<div>
|
||||
<div class="mt-2.5">
|
||||
<el-checkbox border v-model="tailLog" class="float-left" @change="changeTail">
|
||||
{{ $t('commons.button.watch') }}
|
||||
</el-checkbox>
|
||||
<el-button class="ml-5" @click="onDownload" icon="Download" :disabled="data.content === ''">
|
||||
{{ $t('file.download') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<Codemirror
|
||||
ref="logContainer"
|
||||
style="height: calc(100vh - 200px); width: 100%; min-height: 400px"
|
||||
:autofocus="true"
|
||||
:placeholder="$t('website.noLog')"
|
||||
:indent-with-tab="true"
|
||||
:tabSize="4"
|
||||
:lineWrapping="true"
|
||||
:matchBrackets="true"
|
||||
theme="cobalt"
|
||||
:styleActiveLine="true"
|
||||
:extensions="extensions"
|
||||
v-model="content"
|
||||
:disabled="true"
|
||||
@ready="handleReady"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Codemirror } from 'vue-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { nextTick, onUnmounted, reactive, ref, shallowRef } from 'vue';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import { ReadByLine } from '@/api/modules/files';
|
||||
|
||||
const extensions = [javascript(), oneDark];
|
||||
|
||||
interface LogProps {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const data = ref({
|
||||
enable: false,
|
||||
content: '',
|
||||
path: '',
|
||||
});
|
||||
const tailLog = ref(false);
|
||||
let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const view = shallowRef();
|
||||
const editorContainer = ref<HTMLDivElement | null>(null);
|
||||
const handleReady = (payload) => {
|
||||
view.value = payload.view;
|
||||
editorContainer.value = payload.container;
|
||||
};
|
||||
const content = ref('');
|
||||
const end = ref(false);
|
||||
const lastContent = ref('');
|
||||
const open = ref(false);
|
||||
const logContainer = ref();
|
||||
|
||||
const readReq = reactive({
|
||||
path: '',
|
||||
page: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
const em = defineEmits(['close']);
|
||||
|
||||
const handleClose = (search: boolean) => {
|
||||
open.value = false;
|
||||
em('close', search);
|
||||
};
|
||||
|
||||
const getContent = () => {
|
||||
if (!end.value) {
|
||||
readReq.page += 1;
|
||||
}
|
||||
ReadByLine(readReq).then((res) => {
|
||||
if (!end.value && res.data.end) {
|
||||
lastContent.value = content.value;
|
||||
}
|
||||
data.value = res.data;
|
||||
if (res.data.content != '') {
|
||||
if (end.value) {
|
||||
content.value = lastContent.value + '\n' + res.data.content;
|
||||
} else {
|
||||
if (content.value == '') {
|
||||
content.value = res.data.content;
|
||||
} else {
|
||||
content.value = content.value + '\n' + res.data.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
end.value = res.data.end;
|
||||
nextTick(() => {
|
||||
const state = view.value.state;
|
||||
view.value.dispatch({
|
||||
selection: { anchor: state.doc.length, head: state.doc.length },
|
||||
});
|
||||
view.value.focus();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const changeTail = () => {
|
||||
if (tailLog.value) {
|
||||
timer = setInterval(() => {
|
||||
getContent();
|
||||
}, 1000 * 1);
|
||||
} else {
|
||||
onCloseLog();
|
||||
}
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
downloadFile(data.value.path);
|
||||
};
|
||||
|
||||
const onCloseLog = async () => {
|
||||
tailLog.value = false;
|
||||
clearInterval(Number(timer));
|
||||
timer = null;
|
||||
};
|
||||
|
||||
function isScrolledToBottom(element: HTMLElement): boolean {
|
||||
return element.scrollTop + element.clientHeight === element.scrollHeight;
|
||||
}
|
||||
|
||||
const acceptParams = (props: LogProps) => {
|
||||
readReq.path = props.path;
|
||||
open.value = true;
|
||||
tailLog.value = false;
|
||||
|
||||
getContent();
|
||||
nextTick(() => {
|
||||
let editorElement = editorContainer.value.querySelector('.cm-editor');
|
||||
let scrollerElement = editorElement.querySelector('.cm-scroller') as HTMLElement;
|
||||
if (scrollerElement) {
|
||||
scrollerElement.addEventListener('scroll', function () {
|
||||
if (isScrolledToBottom(scrollerElement)) {
|
||||
getContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
onCloseLog();
|
||||
});
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
Loading…
Reference in New Issue