Compare commits
10 Commits
4fab0b813f
...
c0403e3b6e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0403e3b6e | ||
|
|
c1f153ee50 | ||
|
|
adc914694e | ||
|
|
05e714ef77 | ||
|
|
7fcc4d343b | ||
|
|
785931ef65 | ||
|
|
f42ee4ec9e | ||
|
|
ffd6916313 | ||
|
|
05ef56a69b | ||
|
|
930f3c40fa |
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vue-tsc --noEmit && vite build && (cd src-tauri && cargo check)",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
|
||||
|
||||
// Define the configuration structure
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
@@ -34,15 +34,19 @@ fn copy_directory(template_path: String, target_path: String, new_folder_name: S
|
||||
return Err("Template directory does not exist".to_string());
|
||||
}
|
||||
|
||||
if destination.exists() {
|
||||
return Err(format!("Destination directory already exists: {:?}", destination));
|
||||
}
|
||||
// Remove the check for existing destination to allow merging/overwriting
|
||||
// if destination.exists() {
|
||||
// return Err(format!("Destination directory already exists: {:?}", destination));
|
||||
// }
|
||||
|
||||
// Create the destination directory first
|
||||
std::fs::create_dir_all(&destination).map_err(|e| e.to_string())?;
|
||||
// Create the destination directory first if it doesn't exist
|
||||
if !destination.exists() {
|
||||
std::fs::create_dir_all(&destination).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut options = CopyOptions::new();
|
||||
options.content_only = true;
|
||||
options.overwrite = true; // Enable overwrite for existing files
|
||||
|
||||
fs_extra::dir::copy(template, &destination, &options)
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -205,6 +209,20 @@ fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Resul
|
||||
// Rename files in this directory
|
||||
let mut current_index = 1;
|
||||
for file_path in file_list {
|
||||
// Check if the file already matches the desired format
|
||||
if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
|
||||
let name_without_ext = Path::new(file_name).file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||
let prefix_base = format!("{}{}", prefix, base_name);
|
||||
|
||||
if name_without_ext.starts_with(&prefix_base) {
|
||||
let suffix = &name_without_ext[prefix_base.len()..];
|
||||
// If it ends with 2 digits, it's already named correctly
|
||||
if suffix.len() == 2 && suffix.chars().all(|c| c.is_digit(10)) {
|
||||
continue; // Skip this file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find next available index
|
||||
while occupied_indices.contains(¤t_index) {
|
||||
current_index += 1;
|
||||
@@ -236,6 +254,96 @@ fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Resul
|
||||
Ok(format!("Successfully renamed {} files.", renamed_count))
|
||||
}
|
||||
|
||||
// --- Check Logic ---
|
||||
|
||||
fn remove_empty_dirs_recursive(path: &Path, deleted_list: &mut Vec<String>) -> std::io::Result<()> {
|
||||
if path.is_dir() {
|
||||
let read_dir = fs::read_dir(path)?;
|
||||
for entry in read_dir {
|
||||
let entry = entry?;
|
||||
let child_path = entry.path();
|
||||
if child_path.is_dir() {
|
||||
remove_empty_dirs_recursive(&child_path, deleted_list)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-check if empty after processing children
|
||||
// Use read_dir again to check if it's empty now
|
||||
let mut is_empty = true;
|
||||
let read_dir_check = fs::read_dir(path)?;
|
||||
for _ in read_dir_check {
|
||||
is_empty = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if is_empty {
|
||||
fs::remove_dir(path)?;
|
||||
deleted_list.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn delete_empty_dirs(path: String) -> Result<Vec<String>, String> {
|
||||
let root_path = Path::new(&path);
|
||||
if !root_path.exists() || !root_path.is_dir() {
|
||||
return Err("Path is not a valid directory".to_string());
|
||||
}
|
||||
|
||||
let mut deleted = Vec::new();
|
||||
|
||||
// We cannot delete the root path itself even if empty, per logic usually expected in tools.
|
||||
// So we iterate children and call recursive function.
|
||||
let read_dir = fs::read_dir(root_path).map_err(|e| e.to_string())?;
|
||||
|
||||
for entry in read_dir {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let child_path = entry.path();
|
||||
if child_path.is_dir() {
|
||||
remove_empty_dirs_recursive(&child_path, &mut deleted).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn check_file_naming(path: String, prefix: String) -> Result<Vec<String>, String> {
|
||||
let root_path = Path::new(&path);
|
||||
if !root_path.exists() || !root_path.is_dir() {
|
||||
return Err("Path is not a valid directory".to_string());
|
||||
}
|
||||
|
||||
let mut mismatches = Vec::new();
|
||||
let mut stack = vec![root_path.to_path_buf()];
|
||||
|
||||
while let Some(current_dir) = stack.pop() {
|
||||
let read_dir = fs::read_dir(¤t_dir).map_err(|e| e.to_string())?;
|
||||
|
||||
for entry in read_dir {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
// Check against prefix
|
||||
// NOTE: Should we check hidden files? Assuming ignoring hidden files for now if starting with dot?
|
||||
// Or just check everything. Let's check everything visible.
|
||||
if !name.starts_with(".") {
|
||||
if !name.starts_with(&prefix) {
|
||||
mismatches.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mismatches)
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
@@ -251,7 +359,9 @@ pub fn run() {
|
||||
save_history_item,
|
||||
remove_history_item,
|
||||
check_dir_exists,
|
||||
rename_videos
|
||||
rename_videos,
|
||||
delete_empty_dirs,
|
||||
check_file_naming
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
290
src/App.vue
290
src/App.vue
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed, onUnmounted } from "vue";
|
||||
import { ref, watch, onMounted, computed, onUnmounted, nextTick } from "vue";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Document, VideoPlay, CircleCheck, Moon, Sunny, Delete, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { Document, VideoPlay, CircleCheck, Moon, Sunny, Delete, UploadFilled, Refresh } from '@element-plus/icons-vue';
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
|
||||
// Navigation
|
||||
@@ -37,11 +37,30 @@ const importedFiles = ref<string[]>([]);
|
||||
const videoNameInput = ref("");
|
||||
const historyList = ref<string[]>([]);
|
||||
const historyMap = ref<Record<string, string[]>>({});
|
||||
const currentHistoryKey = ref(""); // Derived from dropped files relative to currentDir
|
||||
const currentHistoryKey = ref("");
|
||||
|
||||
// Check Data
|
||||
const checkLogs = ref<string[]>([]);
|
||||
const logContainerRef = ref<HTMLElement | null>(null);
|
||||
const checkPageScrollTop = ref(0);
|
||||
|
||||
// Lifecycle
|
||||
let unlistenDrop: UnlistenFn | null = null;
|
||||
|
||||
const handleLogScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
checkPageScrollTop.value = target.scrollTop;
|
||||
}
|
||||
|
||||
watch(activeMenu, async (newVal) => {
|
||||
if (newVal === 'check') {
|
||||
await nextTick();
|
||||
if (logContainerRef.value) {
|
||||
logContainerRef.value.scrollTop = checkPageScrollTop.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
// Theme init
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
@@ -140,35 +159,21 @@ const updateHistoryList = () => {
|
||||
}
|
||||
|
||||
const handleFileDrop = (paths: string[]) => {
|
||||
// Filter for common video extensions if needed, or just accept all
|
||||
// For now, accept all but assume they are videos
|
||||
importedFiles.value = paths;
|
||||
|
||||
if (paths.length > 0 && currentDir.value) {
|
||||
// Calculate Key: Path relative to Current Dir
|
||||
// Logic: Take first file's parent dir. Remove currentDir prefix.
|
||||
// Example: Current=C:\A, File=C:\A\B\vid.mp4. Parent=C:\A\B. Key=B.
|
||||
|
||||
// Normalize paths for comparison (remove trailing slashes, handle win/unix separators)
|
||||
// Simple string manipulation for now.
|
||||
const firstFile = paths[0];
|
||||
// We need the parent directory of the file
|
||||
// Since we don't have node 'path' module easily, we do simple string parsing
|
||||
const separator = firstFile.includes('\\') ? '\\' : '/';
|
||||
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
|
||||
const parentDir = firstFile.substring(0, lastSeparatorIndex);
|
||||
|
||||
if (parentDir.startsWith(currentDir.value)) {
|
||||
let relative = parentDir.substring(currentDir.value.length);
|
||||
// Remove leading separator if exists
|
||||
if (relative.startsWith(separator)) {
|
||||
relative = relative.substring(1);
|
||||
}
|
||||
currentHistoryKey.value = relative;
|
||||
} else {
|
||||
// File is outside current dir?
|
||||
// Requirement says: "video file path minus current directory prefix"
|
||||
// If outside, maybe use full path or just handle gracefully
|
||||
currentHistoryKey.value = "";
|
||||
}
|
||||
updateHistoryList();
|
||||
@@ -199,7 +204,6 @@ const handleRename = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to history
|
||||
if (currentHistoryKey.value) {
|
||||
try {
|
||||
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
||||
@@ -213,7 +217,6 @@ const handleRename = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Rename Files
|
||||
try {
|
||||
const msg = await invoke<string>('rename_videos', {
|
||||
files: importedFiles.value,
|
||||
@@ -221,8 +224,6 @@ const handleRename = async () => {
|
||||
baseName: videoNameInput.value
|
||||
});
|
||||
ElMessage.success(msg);
|
||||
// Clear imported files after success? Or keep them?
|
||||
// Usually clearing is better feedback that "job done".
|
||||
importedFiles.value = [];
|
||||
} catch (e) {
|
||||
ElMessage.error("重命名失败: " + e);
|
||||
@@ -233,6 +234,149 @@ const useHistoryItem = (val: string) => {
|
||||
videoNameInput.value = val;
|
||||
}
|
||||
|
||||
// Check Logic
|
||||
const appendLog = (msg: string) => {
|
||||
checkLogs.value.push(msg);
|
||||
nextTick(() => {
|
||||
if (logContainerRef.value) {
|
||||
logContainerRef.value.scrollTop = logContainerRef.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const appendLogs = (msgs: string[]) => {
|
||||
checkLogs.value.push(...msgs);
|
||||
nextTick(() => {
|
||||
if (logContainerRef.value) {
|
||||
logContainerRef.value.scrollTop = logContainerRef.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleDeleteEmptyDirs = async () => {
|
||||
appendLog("\n" + "=".repeat(20));
|
||||
appendLog("正在扫描并删除空目录...");
|
||||
try {
|
||||
const deleted = await invoke<string[]>('delete_empty_dirs', { path: currentDir.value });
|
||||
if (deleted.length === 0) {
|
||||
appendLog("未发现空目录。");
|
||||
} else {
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < deleted.length; i += batchSize) {
|
||||
const batch = deleted.slice(i, i + batchSize).map(p => `已删除: ${p}`);
|
||||
appendLogs(batch);
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Small delay for rendering
|
||||
}
|
||||
appendLog(`删除完毕,共删除 ${deleted.length} 个空目录。`);
|
||||
}
|
||||
} catch (e) {
|
||||
appendLog(`错误: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format paths as tree structure
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
isFile: boolean;
|
||||
children: Map<string, TreeNode>;
|
||||
}
|
||||
|
||||
const formatTreeStructure = (paths: string[], rootPath: string): string[] => {
|
||||
if (paths.length === 0) return [];
|
||||
|
||||
const root: TreeNode = { name: '', isFile: false, children: new Map() };
|
||||
|
||||
// Build the tree data structure
|
||||
paths.forEach(fullPath => {
|
||||
let relPath = fullPath;
|
||||
if (rootPath && fullPath.startsWith(rootPath)) {
|
||||
relPath = fullPath.substring(rootPath.length);
|
||||
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
|
||||
relPath = relPath.substring(1);
|
||||
}
|
||||
}
|
||||
relPath = relPath.replace(/\\/g, '/'); // Normalize to forward slashes
|
||||
|
||||
const parts = relPath.split('/').filter(p => p.length > 0);
|
||||
|
||||
let currentNode = root;
|
||||
parts.forEach((part, i) => {
|
||||
if (!currentNode.children.has(part)) {
|
||||
currentNode.children.set(part, { name: part, isFile: false, children: new Map() });
|
||||
}
|
||||
currentNode = currentNode.children.get(part)!;
|
||||
if (i === parts.length - 1) { // Mark last part as file
|
||||
currentNode.isFile = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const output: string[] = [];
|
||||
|
||||
// Recursive function to print the tree
|
||||
const printTreeRecursive = (node: TreeNode, prefix: string, isLastChildArr: boolean[]) => {
|
||||
const sortedChildrenNames = Array.from(node.children.keys()).sort();
|
||||
|
||||
sortedChildrenNames.forEach((childName, index) => {
|
||||
const childNode = node.children.get(childName)!;
|
||||
const isLastChildOfCurrentNode = (index === sortedChildrenNames.length - 1);
|
||||
|
||||
let currentLine = prefix;
|
||||
for (let i = 0; i < isLastChildArr.length; i++) {
|
||||
currentLine += isLastChildArr[i] ? " " : "│ ";
|
||||
}
|
||||
|
||||
currentLine += isLastChildOfCurrentNode ? "└── " : "├── ";
|
||||
|
||||
output.push(`${currentLine}${childNode.name}`);
|
||||
|
||||
// Recurse for children
|
||||
// Only recurse if the childNode is a directory (has children itself)
|
||||
// or if it's a file but represents a path segment that also needs further processing.
|
||||
// In our case, `isFile` marks only the actual file at the end of path.
|
||||
// If it's a directory (i.e., has children or might have children in other paths), we recurse.
|
||||
if (childNode.children.size > 0) {
|
||||
printTreeRecursive(childNode, prefix, [...isLastChildArr, isLastChildOfCurrentNode]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Print the root node first
|
||||
output.push(`${rootPath || '.'}`);
|
||||
// Then print its children
|
||||
printTreeRecursive(root, "", []);
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const handleCheckNaming = async () => {
|
||||
appendLog("\n" + "=".repeat(20));
|
||||
appendLog(`正在检查文件命名,前缀要求: ${videoNamePrefix.value} ...`);
|
||||
try {
|
||||
const mismatches = await invoke<string[]>('check_file_naming', {
|
||||
path: currentDir.value,
|
||||
prefix: videoNamePrefix.value
|
||||
});
|
||||
|
||||
if (mismatches.length === 0) {
|
||||
appendLog("检查完毕,所有文件均符合命名规范。");
|
||||
} else {
|
||||
appendLog(`发现 ${mismatches.length} 个文件不符合规范:`);
|
||||
|
||||
// Format logs as tree
|
||||
const formattedLogs = formatTreeStructure(mismatches, currentDir.value);
|
||||
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < formattedLogs.length; i += batchSize) {
|
||||
const batch = formattedLogs.slice(i, i + batchSize);
|
||||
appendLogs(batch);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
appendLog(`错误: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Computed
|
||||
const isReviewDisabled = computed(() => {
|
||||
@@ -306,7 +450,7 @@ const handleCopy = async () => {
|
||||
// Reactivity for Current Directory Prefix
|
||||
watch(currentDir, (newPath) => {
|
||||
if (!newPath) return;
|
||||
const dirName = newPath.split(/[/\\]/).pop();
|
||||
const dirName = newPath.split(/[\/\\]/).pop();
|
||||
if (!dirName) return;
|
||||
|
||||
const regex = /^(\d{2})(\d{2})视频$/;
|
||||
@@ -401,7 +545,7 @@ watch(currentDir, (newPath) => {
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-divider />
|
||||
<!-- <el-divider /> -->
|
||||
|
||||
<el-card class="box-card" shadow="hover">
|
||||
<template #header>
|
||||
@@ -447,13 +591,13 @@ watch(currentDir, (newPath) => {
|
||||
<div class="el-upload__text">
|
||||
将视频文件拖拽到此处
|
||||
</div>
|
||||
<div class="file-count" v-if="importedFiles.length > 0">
|
||||
已导入 {{ importedFiles.length }} 个文件
|
||||
<div class="file-count-container">
|
||||
<div class="file-count" v-show="importedFiles.length > 0">
|
||||
已导入 {{ importedFiles.length }} 个文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- Action Area -->
|
||||
<div class="action-area">
|
||||
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
|
||||
@@ -481,10 +625,39 @@ watch(currentDir, (newPath) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeMenu === 'check'">
|
||||
<!-- Check Page -->
|
||||
<div v-else-if="activeMenu === 'check'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
|
||||
<h2>检查</h2>
|
||||
<el-empty description="功能待定" />
|
||||
|
||||
<el-alert
|
||||
v-if="isReviewDisabled"
|
||||
:title="reviewWarningMessage"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
/>
|
||||
|
||||
<div :class="{ 'disabled-area': isReviewDisabled }" style="display: flex; flex-direction: column; flex-grow: 1; overflow: hidden;">
|
||||
<div class="check-actions">
|
||||
<el-button type="danger" :icon="Delete" @click="handleDeleteEmptyDirs">删除空目录</el-button>
|
||||
<el-button type="primary" :icon="Refresh" @click="handleCheckNaming">检查命名</el-button>
|
||||
</div>
|
||||
|
||||
<div class="log-container">
|
||||
<div class="log-header">日志输出</div>
|
||||
<div class="log-content" ref="logContainerRef" @scroll="handleLogScroll">
|
||||
<div v-for="(log, index) in checkLogs" :key="index" class="log-item">
|
||||
{{ log }}
|
||||
</div>
|
||||
<div v-if="checkLogs.length === 0" class="log-empty">
|
||||
暂无日志
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
@@ -530,23 +703,31 @@ body {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
padding: 40px 0;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.el-icon--upload {
|
||||
font-size: 67px;
|
||||
font-size: 48px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 16px;
|
||||
line-height: 50px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
.el-upload__text {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.file-count-container {
|
||||
min-height: 24px; /* Adjust height as needed to fit the text */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px; /* Add some space from the text above */
|
||||
}
|
||||
.file-count {
|
||||
margin-top: 10px;
|
||||
/* margin-top: 10px; /* Removed, container handles spacing */
|
||||
color: var(--el-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -596,4 +777,45 @@ body {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* Check Page Styles */
|
||||
.check-actions {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.log-container {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1; /* Make it fill available height */
|
||||
overflow: hidden; /* Prevent container from expanding beyond parent */
|
||||
}
|
||||
.log-header {
|
||||
padding: 10px 15px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
.log-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; /* Changed to monospaced font stack */
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.log-item {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.log-empty {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user