mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-05 07:27:42 +08:00
feat: 日志Dialog
This commit is contained in:
Vendored
+14
@@ -9,5 +9,19 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@phosphor-icons/vue": "^2.2.1",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"naive-ui": "^2.42.0",
|
||||
|
||||
Generated
+13
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@phosphor-icons/vue':
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1(vue@3.5.17(typescript@5.6.3))
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2
|
||||
version: 2.6.0
|
||||
@@ -493,6 +496,12 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -2381,6 +2390,10 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
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': {}
|
||||
|
||||
'@quansync/fs@0.1.3':
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Context;
|
||||
use parking_lot::RwLock;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
@@ -387,3 +389,32 @@ pub async fn search(app: AppHandle, params: SearchParams) -> CommandResult<Searc
|
||||
};
|
||||
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,
|
||||
restore_download_tasks,
|
||||
search,
|
||||
get_logs_dir_size,
|
||||
show_path_in_file_manager,
|
||||
])
|
||||
.events(tauri_specta::collect_events![LogEvent, DownloadEvent]);
|
||||
|
||||
|
||||
+43
-174
@@ -1,180 +1,49 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue'
|
||||
import { commands } from './bindings.ts'
|
||||
<script setup lang="ts">
|
||||
import AppContent from './AppContent.vue'
|
||||
import { GlobalThemeOverrides } from 'naive-ui'
|
||||
|
||||
const greetMsg = ref('')
|
||||
const name = ref('')
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsg.value = await commands.greet(name.value)
|
||||
}
|
||||
|
||||
async function test() {
|
||||
const result = await commands.generateQrcode()
|
||||
console.log(result)
|
||||
if (result.status === 'ok') {
|
||||
const result2 = await commands.getQrcodeStatus(result.data.qrcode_key)
|
||||
console.log(result2)
|
||||
}
|
||||
}
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Test Component</h2>
|
||||
<p>This is a test component.</p>
|
||||
</div>
|
||||
)
|
||||
const themeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#0EA5E9',
|
||||
primaryColorHover: '#36BFF5',
|
||||
primaryColorPressed: '#027FC2',
|
||||
primaryColorSuppl: '#36BFF5',
|
||||
borderRadius: '4px',
|
||||
borderRadiusSmall: '3px',
|
||||
heightMedium: '32px',
|
||||
},
|
||||
Tabs: {
|
||||
tabGapSmallLine: '10px',
|
||||
tabPaddingSmallLine: '6px 8px',
|
||||
},
|
||||
Button: {
|
||||
paddingSmall: '0 8px',
|
||||
paddingMedium: '0 12px',
|
||||
},
|
||||
Radio: {
|
||||
buttonColorActive: '#0EA5E9',
|
||||
buttonTextColorActive: '#FFF',
|
||||
},
|
||||
Dropdown: {
|
||||
borderRadius: '5px',
|
||||
padding: '6px 2px',
|
||||
optionColorHover: '#0EA5E9',
|
||||
optionTextColorHover: '#FFF',
|
||||
optionHeightMedium: '28px',
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>Welcome to Tauri + Vue</h1>
|
||||
|
||||
<div class="row">
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" class="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<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>
|
||||
<n-config-provider :theme-overrides="themeOverrides">
|
||||
<n-dialog-provider>
|
||||
<n-modal-provider>
|
||||
<n-notification-provider placement="bottom-right" :max="3">
|
||||
<n-message-provider>
|
||||
<AppContent />
|
||||
</n-message-provider>
|
||||
</n-notification-provider>
|
||||
</n-modal-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</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;
|
||||
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 { ref } from 'vue'
|
||||
import { Config } from './bindings.ts'
|
||||
|
||||
export const useStore = defineStore('store', () => {
|
||||
return {}
|
||||
const config = ref<Config>()
|
||||
|
||||
return { config }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user