mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
feat: 日志Dialog
This commit is contained in:
Vendored
+14
@@ -9,5 +9,19 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDialog: typeof import('naive-ui')['NDialog']
|
||||||
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||||
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
|
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"naive-ui": "^2.42.0",
|
"naive-ui": "^2.42.0",
|
||||||
|
|||||||
Generated
+13
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@phosphor-icons/vue':
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1(vue@3.5.17(typescript@5.6.3))
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
@@ -493,6 +496,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
'@phosphor-icons/vue@2.2.1':
|
||||||
|
resolution: {integrity: sha512-3RNg1utc2Z5RwPKWFkW3eXI/0BfQAwXgtFxPUPeSzi55jGYUq16b+UqcgbKLazWFlwg5R92OCLKjDiJjeiJcnA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: '>=3.2.39'
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -2381,6 +2390,10 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.19.1
|
fastq: 1.19.1
|
||||||
|
|
||||||
|
'@phosphor-icons/vue@2.2.1(vue@3.5.17(typescript@5.6.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.17(typescript@5.6.3)
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@quansync/fs@0.1.3':
|
'@quansync/fs@0.1.3':
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
use anyhow::Context;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
@@ -387,3 +389,32 @@ pub async fn search(app: AppHandle, params: SearchParams) -> CommandResult<Searc
|
|||||||
};
|
};
|
||||||
Ok(search_result)
|
Ok(search_result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_logs_dir_size(app: AppHandle) -> CommandResult<u64> {
|
||||||
|
let logs_dir = logger::logs_dir(&app)
|
||||||
|
.context("获取日志目录失败")
|
||||||
|
.map_err(|err| CommandError::from("获取日志目录大小失败", err))?;
|
||||||
|
let logs_dir_size = std::fs::read_dir(&logs_dir)
|
||||||
|
.context(format!("读取日志目录`{}`失败", logs_dir.display()))
|
||||||
|
.map_err(|err| CommandError::from("获取日志目录大小失败", err))?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter_map(|entry| entry.metadata().ok())
|
||||||
|
.map(|metadata| metadata.len())
|
||||||
|
.sum::<u64>();
|
||||||
|
tracing::debug!("获取日志目录大小成功");
|
||||||
|
Ok(logs_dir_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn show_path_in_file_manager(app: AppHandle, path: &str) -> CommandResult<()> {
|
||||||
|
app.opener()
|
||||||
|
.reveal_item_in_dir(path)
|
||||||
|
.context(format!("在文件管理器中打开`{path}`失败"))
|
||||||
|
.map_err(|err| CommandError::from("在文件管理器中打开失败", err))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ pub fn run() {
|
|||||||
restart_download_tasks,
|
restart_download_tasks,
|
||||||
restore_download_tasks,
|
restore_download_tasks,
|
||||||
search,
|
search,
|
||||||
|
get_logs_dir_size,
|
||||||
|
show_path_in_file_manager,
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![LogEvent, DownloadEvent]);
|
.events(tauri_specta::collect_events![LogEvent, DownloadEvent]);
|
||||||
|
|
||||||
|
|||||||
+43
-174
@@ -1,180 +1,49 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import AppContent from './AppContent.vue'
|
||||||
import { commands } from './bindings.ts'
|
import { GlobalThemeOverrides } from 'naive-ui'
|
||||||
|
|
||||||
const greetMsg = ref('')
|
const themeOverrides: GlobalThemeOverrides = {
|
||||||
const name = ref('')
|
common: {
|
||||||
|
primaryColor: '#0EA5E9',
|
||||||
async function greet() {
|
primaryColorHover: '#36BFF5',
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
primaryColorPressed: '#027FC2',
|
||||||
greetMsg.value = await commands.greet(name.value)
|
primaryColorSuppl: '#36BFF5',
|
||||||
}
|
borderRadius: '4px',
|
||||||
|
borderRadiusSmall: '3px',
|
||||||
async function test() {
|
heightMedium: '32px',
|
||||||
const result = await commands.generateQrcode()
|
},
|
||||||
console.log(result)
|
Tabs: {
|
||||||
if (result.status === 'ok') {
|
tabGapSmallLine: '10px',
|
||||||
const result2 = await commands.getQrcodeStatus(result.data.qrcode_key)
|
tabPaddingSmallLine: '6px 8px',
|
||||||
console.log(result2)
|
},
|
||||||
}
|
Button: {
|
||||||
}
|
paddingSmall: '0 8px',
|
||||||
|
paddingMedium: '0 12px',
|
||||||
function TestComponent() {
|
},
|
||||||
return (
|
Radio: {
|
||||||
<div>
|
buttonColorActive: '#0EA5E9',
|
||||||
<h2>Test Component</h2>
|
buttonTextColorActive: '#FFF',
|
||||||
<p>This is a test component.</p>
|
},
|
||||||
</div>
|
Dropdown: {
|
||||||
)
|
borderRadius: '5px',
|
||||||
|
padding: '6px 2px',
|
||||||
|
optionColorHover: '#0EA5E9',
|
||||||
|
optionTextColorHover: '#FFF',
|
||||||
|
optionHeightMedium: '28px',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="container">
|
<n-config-provider :theme-overrides="themeOverrides">
|
||||||
<h1>Welcome to Tauri + Vue</h1>
|
<n-dialog-provider>
|
||||||
|
<n-modal-provider>
|
||||||
<div class="row">
|
<n-notification-provider placement="bottom-right" :max="3">
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
<n-message-provider>
|
||||||
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
|
<AppContent />
|
||||||
</a>
|
</n-message-provider>
|
||||||
<a href="https://tauri.app" target="_blank">
|
</n-notification-provider>
|
||||||
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
</n-modal-provider>
|
||||||
</a>
|
</n-dialog-provider>
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
</n-config-provider>
|
||||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p>Click on the Tauri, Vite, and Vue logos to learn more.</p>
|
|
||||||
|
|
||||||
<form class="row" @submit.prevent="greet">
|
|
||||||
<input id="greet-input" v-model="name" placeholder="Enter a name..." />
|
|
||||||
<button type="submit">Greet</button>
|
|
||||||
</form>
|
|
||||||
<p class="text-red">{{ greetMsg }}</p>
|
|
||||||
<n-button @click="test">测试</n-button>
|
|
||||||
<TestComponent />
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.logo.vite:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #747bff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.vue:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #249b73);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 0;
|
|
||||||
padding-top: 10vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: 0.75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.tauri:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #24c8db);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
color: #0f0f0f;
|
|
||||||
background-color: #ffffff;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border-color: #396cd8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
border-color: #396cd8;
|
|
||||||
background-color: #e8e8e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#greet-input {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
color: #f6f6f6;
|
|
||||||
background-color: #2f2f2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #24c8db;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button {
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #0f0f0f98;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
background-color: #0f0f0f69;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useStore } from './store.ts'
|
||||||
|
import LogDialog from './dialogs/LogDialog.vue'
|
||||||
|
import { PhClockCounterClockwise } from '@phosphor-icons/vue'
|
||||||
|
import { commands } from './bindings.ts'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const logDialogShowing = ref<boolean>(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 屏蔽浏览器右键菜单
|
||||||
|
document.oncontextmenu = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
store.config = await commands.getConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen flex flex-col">
|
||||||
|
<div v-if="store.config !== undefined" class="h-full w-full flex overflow-hidden select-none">
|
||||||
|
<div class="flex flex-col box-border p-1 border-r-solid border-r-1 border-r-[#DADADA] bg-[#F9F9F9] flex-shrink-0">
|
||||||
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
|
日志
|
||||||
|
<template #trigger>
|
||||||
|
<n-button text class="py-1 px-2" @click="logDialogShowing = true">
|
||||||
|
<n-icon size="28">
|
||||||
|
<PhClockCounterClockwise />
|
||||||
|
</n-icon>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LogDialog v-model:showing="logDialogShowing" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:global(.n-notification-main__header) {
|
||||||
|
@apply break-words;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.n-tabs-pane-wrapper) {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.selection-area) {
|
||||||
|
@apply bg-[rgba(46,115,252,0.5)];
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-badge-sup) {
|
||||||
|
@apply pointer-events-none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -161,6 +161,22 @@ async search(params: SearchParams) : Promise<Result<SearchResult, CommandError>>
|
|||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async getLogsDirSize() : Promise<Result<number, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_logs_dir_size") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showPathInFileManager(path: string) : Promise<Result<null, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("show_path_in_file_manager", { path }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { LogEvent, LogLevel, events, commands } from '../bindings.ts'
|
||||||
|
import { useNotification } from 'naive-ui'
|
||||||
|
import { onMounted, ref, watch, computed } from 'vue'
|
||||||
|
import { appDataDir } from '@tauri-apps/api/path'
|
||||||
|
import { path } from '@tauri-apps/api'
|
||||||
|
import { useStore } from '../store.ts'
|
||||||
|
import { darkTheme } from 'naive-ui'
|
||||||
|
|
||||||
|
type LogRecord = LogEvent & { id: number; formatedLog: string }
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const notification = useNotification()
|
||||||
|
|
||||||
|
const showing = defineModel<boolean>('showing', { required: true })
|
||||||
|
|
||||||
|
let nextLogRecordId = 1
|
||||||
|
|
||||||
|
const logRecords = ref<LogRecord[]>([])
|
||||||
|
const searchText = ref<string>('')
|
||||||
|
const selectedLevel = ref<LogLevel>('INFO')
|
||||||
|
const logsDirSize = ref<number>(0)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const result = await commands.getLogsDirSize()
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查日志目录大小
|
||||||
|
if (result.data > 50 * 1024 * 1024) {
|
||||||
|
notification.warning({
|
||||||
|
title: '日志目录大小超过50MB,请及时清理日志文件',
|
||||||
|
description: () => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
点击左下角的 <span class="bg-gray-2 px-1">日志</span> 按钮
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
里边有 <span class="bg-gray-2 px-1">打开日志目录</span> 按钮
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
你也可以在里边取消勾选 <span class="bg-gray-2 px-1">输出文件日志</span>
|
||||||
|
</div>
|
||||||
|
<div>这样将不再产生文件日志</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatedLogsDirSize = computed<string>(() => {
|
||||||
|
const units = ['B', 'KB', 'MB']
|
||||||
|
let size = logsDirSize.value
|
||||||
|
let unitIndex = 0
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < 2) {
|
||||||
|
size /= 1024
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留两位小数
|
||||||
|
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||||
|
})
|
||||||
|
const filteredLogs = computed<LogRecord[]>(() => {
|
||||||
|
return logRecords.value.filter(({ level, formatedLog }) => {
|
||||||
|
// 定义日志等级的优先级顺序
|
||||||
|
const logLevelPriority = {
|
||||||
|
TRACE: 0,
|
||||||
|
DEBUG: 1,
|
||||||
|
INFO: 2,
|
||||||
|
WARN: 3,
|
||||||
|
ERROR: 4,
|
||||||
|
}
|
||||||
|
// 首先按日志等级筛选
|
||||||
|
if (logLevelPriority[level] < logLevelPriority[selectedLevel.value]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 然后按搜索文本筛选
|
||||||
|
if (searchText.value === '') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatedLog.toLowerCase().includes(searchText.value.toLowerCase())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(showing, async () => {
|
||||||
|
if (showing.value) {
|
||||||
|
const result = await commands.getLogsDirSize()
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logsDirSize.value = result.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await events.logEvent.listen(async ({ payload: logEvent }) => {
|
||||||
|
const logRecord: LogRecord = {
|
||||||
|
...logEvent,
|
||||||
|
id: nextLogRecordId++,
|
||||||
|
formatedLog: formatLogEvent(logEvent),
|
||||||
|
}
|
||||||
|
logRecords.value.push(logRecord)
|
||||||
|
|
||||||
|
const { level, fields } = logEvent
|
||||||
|
if (level === 'ERROR') {
|
||||||
|
notification.error({
|
||||||
|
title: fields['err_title'] as string,
|
||||||
|
description: fields['message'] as string,
|
||||||
|
duration: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatLogEvent(logEvent: LogEvent): string {
|
||||||
|
const { timestamp, level, fields, target, filename, line_number } = logEvent
|
||||||
|
const fields_str = Object.entries(fields)
|
||||||
|
.sort(([key1], [key2]) => key1.localeCompare(key2))
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join(' ')
|
||||||
|
return `${timestamp} ${level} ${target}: ${filename}:${line_number} ${fields_str}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLevelStyles(level: LogLevel) {
|
||||||
|
switch (level) {
|
||||||
|
case 'TRACE':
|
||||||
|
return 'text-gray-400'
|
||||||
|
case 'DEBUG':
|
||||||
|
return 'text-green-400'
|
||||||
|
case 'INFO':
|
||||||
|
return 'text-blue-400'
|
||||||
|
case 'WARN':
|
||||||
|
return 'text-yellow-400'
|
||||||
|
case 'ERROR':
|
||||||
|
return 'text-red-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logLevelOptions = [
|
||||||
|
{ value: 'TRACE', label: 'TRACE' },
|
||||||
|
{ value: 'DEBUG', label: 'DEBUG' },
|
||||||
|
{ value: 'INFO', label: 'INFO' },
|
||||||
|
{ value: 'WARN', label: 'WARN' },
|
||||||
|
{ value: 'ERROR', label: 'ERROR' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function clearLogRecords() {
|
||||||
|
logRecords.value = []
|
||||||
|
nextLogRecordId = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showLogsDirInFileManager() {
|
||||||
|
const logsDir = await path.join(await appDataDir(), '日志')
|
||||||
|
const result = await commands.showPathInFileManager(logsDir)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal v-model:show="showing" v-if="store.config !== undefined">
|
||||||
|
<n-dialog
|
||||||
|
:showIcon="false"
|
||||||
|
:title="`日志目录总大小:${formatedLogsDirSize}`"
|
||||||
|
@close="showing = false"
|
||||||
|
style="width: 95%">
|
||||||
|
<div class="mb-2 flex flex-wrap gap-2">
|
||||||
|
<n-input-group class="w-100">
|
||||||
|
<n-input size="small" v-model:value="searchText" placeholder="搜素日志..." clearable />
|
||||||
|
<n-select size="small" v-model:value="selectedLevel" :options="logLevelOptions" style="width: 120px" />
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 ml-auto items-center">
|
||||||
|
<n-button size="small" @click="showLogsDirInFileManager">打开日志目录</n-button>
|
||||||
|
<n-checkbox v-model:checked="store.config.enable_file_logger">输出文件日志</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-config-provider :theme="darkTheme" :theme-overrides="{ Scrollbar: { width: '8px' } }">
|
||||||
|
<n-virtual-list
|
||||||
|
class="h-[calc(100vh-300px)] overflow-hidden bg-gray-900"
|
||||||
|
:item-size="42"
|
||||||
|
item-resizable
|
||||||
|
:hoverable="false"
|
||||||
|
:items="filteredLogs"
|
||||||
|
:scrollbar-props="{ trigger: 'none' }">
|
||||||
|
<template #default="{ item: { level, formatedLog } }: { item: LogRecord }">
|
||||||
|
<div :class="['py-1 px-3 hover:bg-white/10 whitespace-pre-wrap mr-4', getLevelStyles(level)]">
|
||||||
|
{{ formatedLog }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-virtual-list>
|
||||||
|
</n-config-provider>
|
||||||
|
<div class="pt-1 flex">
|
||||||
|
<n-button ghost class="ml-auto" size="small" type="error" @click="clearLogRecords">清空日志浏览器</n-button>
|
||||||
|
</div>
|
||||||
|
</n-dialog>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
+5
-1
@@ -1,5 +1,9 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Config } from './bindings.ts'
|
||||||
|
|
||||||
export const useStore = defineStore('store', () => {
|
export const useStore = defineStore('store', () => {
|
||||||
return {}
|
const config = ref<Config>()
|
||||||
|
|
||||||
|
return { config }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user