754 lines
22 KiB
Vue
754 lines
22 KiB
Vue
<script setup lang="ts">
|
|
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, Refresh } from '@element-plus/icons-vue';
|
|
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
|
|
|
// Navigation
|
|
const activeMenu = ref("preparation");
|
|
|
|
// Theme
|
|
const isDark = ref(false);
|
|
|
|
const toggleTheme = (val: string | number | boolean) => {
|
|
const isDarkMode = val === true;
|
|
isDark.value = isDarkMode;
|
|
if (isDarkMode) {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
};
|
|
|
|
// Data
|
|
const workingDir = ref("");
|
|
const templateDir = ref("");
|
|
const selectedDate = ref<Date | null>(new Date());
|
|
const currentDir = ref("");
|
|
const videoNamePrefix = ref("");
|
|
const isCurrentDirValid = ref(false);
|
|
|
|
// Review Data
|
|
const importedFiles = ref<string[]>([]);
|
|
const videoNameInput = ref("");
|
|
const historyList = ref<string[]>([]);
|
|
const historyMap = ref<Record<string, string[]>>({});
|
|
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');
|
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
isDark.value = true;
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
isDark.value = false;
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
|
|
// Config init
|
|
await loadConfig();
|
|
|
|
// Validate initial dir
|
|
validateCurrentDir();
|
|
|
|
// Load History
|
|
loadHistory();
|
|
|
|
// Listen for file drops
|
|
unlistenDrop = await listen<{ paths: string[] }>('tauri://drag-drop', (event) => {
|
|
if (activeMenu.value === 'review' && !isReviewDisabled.value) {
|
|
handleFileDrop(event.payload.paths);
|
|
}
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (unlistenDrop) {
|
|
unlistenDrop();
|
|
}
|
|
});
|
|
|
|
// Config & Validation
|
|
interface AppConfig {
|
|
working_dir: string;
|
|
template_dir: string;
|
|
}
|
|
|
|
const loadConfig = async () => {
|
|
try {
|
|
const config = await invoke<AppConfig>('load_config');
|
|
if (config.working_dir) workingDir.value = config.working_dir;
|
|
if (config.template_dir) templateDir.value = config.template_dir;
|
|
} catch (e) {
|
|
console.error("Failed to load config:", e);
|
|
}
|
|
}
|
|
|
|
const saveConfig = async () => {
|
|
try {
|
|
await invoke('save_config', {
|
|
config: {
|
|
working_dir: workingDir.value,
|
|
template_dir: templateDir.value
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to save config:", e);
|
|
}
|
|
}
|
|
|
|
const validateCurrentDir = async () => {
|
|
if (!currentDir.value) {
|
|
isCurrentDirValid.value = false;
|
|
return;
|
|
}
|
|
try {
|
|
const exists = await invoke<boolean>('check_dir_exists', { path: currentDir.value });
|
|
isCurrentDirValid.value = exists;
|
|
} catch (e) {
|
|
isCurrentDirValid.value = false;
|
|
}
|
|
}
|
|
|
|
watch([workingDir, templateDir], saveConfig);
|
|
watch(currentDir, validateCurrentDir);
|
|
|
|
// History Logic
|
|
const loadHistory = async () => {
|
|
try {
|
|
const history = await invoke<Record<string, string[]>>('load_history');
|
|
historyMap.value = history || {};
|
|
} catch (e) {
|
|
console.error("Failed to load history", e);
|
|
}
|
|
}
|
|
|
|
const updateHistoryList = () => {
|
|
if (!currentHistoryKey.value) {
|
|
historyList.value = [];
|
|
return;
|
|
}
|
|
historyList.value = historyMap.value[currentHistoryKey.value] || [];
|
|
}
|
|
|
|
const handleFileDrop = (paths: string[]) => {
|
|
importedFiles.value = paths;
|
|
|
|
if (paths.length > 0 && currentDir.value) {
|
|
const firstFile = paths[0];
|
|
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);
|
|
if (relative.startsWith(separator)) {
|
|
relative = relative.substring(1);
|
|
}
|
|
currentHistoryKey.value = relative;
|
|
} else {
|
|
currentHistoryKey.value = "";
|
|
}
|
|
updateHistoryList();
|
|
}
|
|
}
|
|
|
|
const deleteHistoryItem = async (val: string) => {
|
|
if (!currentHistoryKey.value) return;
|
|
try {
|
|
const newMap = await invoke<Record<string, string[]>>('remove_history_item', {
|
|
key: currentHistoryKey.value,
|
|
value: val
|
|
});
|
|
historyMap.value = newMap;
|
|
updateHistoryList();
|
|
} catch (e) {
|
|
ElMessage.error("删除失败: " + e);
|
|
}
|
|
}
|
|
|
|
const handleRename = async () => {
|
|
if (!videoNameInput.value) {
|
|
ElMessage.warning("请输入名称");
|
|
return;
|
|
}
|
|
if (importedFiles.value.length === 0) {
|
|
ElMessage.warning("请先拖入视频文件");
|
|
return;
|
|
}
|
|
|
|
if (currentHistoryKey.value) {
|
|
try {
|
|
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
|
key: currentHistoryKey.value,
|
|
value: videoNameInput.value
|
|
});
|
|
historyMap.value = newMap;
|
|
updateHistoryList();
|
|
} catch (e) {
|
|
console.error("History save failed", e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const msg = await invoke<string>('rename_videos', {
|
|
files: importedFiles.value,
|
|
prefix: videoNamePrefix.value,
|
|
baseName: videoNameInput.value
|
|
});
|
|
ElMessage.success(msg);
|
|
importedFiles.value = [];
|
|
} catch (e) {
|
|
ElMessage.error("重命名失败: " + e);
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
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} 个文件不符合规范:`);
|
|
mismatches.sort();
|
|
|
|
// Format logs first
|
|
const formattedLogs = mismatches.map(path => {
|
|
let displayPath = path;
|
|
if (path.startsWith(currentDir.value)) {
|
|
displayPath = "." + path.substring(currentDir.value.length);
|
|
}
|
|
return displayPath;
|
|
});
|
|
|
|
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(() => {
|
|
return !isCurrentDirValid.value || !videoNamePrefix.value;
|
|
});
|
|
|
|
const reviewWarningMessage = computed(() => {
|
|
if (!currentDir.value) return "请先设置当前目录";
|
|
if (!isCurrentDirValid.value) return "当前目录不存在,请检查";
|
|
if (!videoNamePrefix.value) return "文件前缀为空";
|
|
return "";
|
|
});
|
|
|
|
|
|
// Actions (Preparation)
|
|
const selectWorkingDir = async () => {
|
|
const selected = await open({
|
|
directory: true,
|
|
multiple: false,
|
|
});
|
|
if (selected) {
|
|
workingDir.value = selected as string;
|
|
}
|
|
};
|
|
|
|
const selectTemplateDir = async () => {
|
|
const selected = await open({
|
|
directory: true,
|
|
multiple: false,
|
|
});
|
|
if (selected) {
|
|
templateDir.value = selected as string;
|
|
}
|
|
};
|
|
|
|
const selectCurrentDir = async () => {
|
|
const selected = await open({
|
|
directory: true,
|
|
multiple: false,
|
|
});
|
|
if (selected) {
|
|
currentDir.value = selected as string;
|
|
}
|
|
};
|
|
|
|
const handleCopy = async () => {
|
|
if (!workingDir.value || !templateDir.value || !selectedDate.value) {
|
|
ElMessage.error("请填写完整信息(工作目录、模板目录、日期)");
|
|
return;
|
|
}
|
|
|
|
const date = selectedDate.value;
|
|
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(date.getDate()).padStart(2, '0');
|
|
const folderName = `${mm}${dd}视频`;
|
|
|
|
try {
|
|
const newPath = await invoke<string>("copy_directory", {
|
|
templatePath: templateDir.value,
|
|
targetPath: workingDir.value,
|
|
newFolderName: folderName,
|
|
});
|
|
|
|
ElMessage.success("目录拷贝并重命名成功!");
|
|
currentDir.value = newPath;
|
|
} catch (error) {
|
|
ElMessage.error(`错误: ${error}`);
|
|
}
|
|
};
|
|
|
|
// Reactivity for Current Directory Prefix
|
|
watch(currentDir, (newPath) => {
|
|
if (!newPath) return;
|
|
const dirName = newPath.split(/[\/\\]/).pop();
|
|
if (!dirName) return;
|
|
|
|
const regex = /^(\d{2})(\d{2})视频$/;
|
|
const match = dirName.match(regex);
|
|
|
|
if (match) {
|
|
const mm = match[1];
|
|
const dd = match[2];
|
|
const year = new Date().getFullYear();
|
|
videoNamePrefix.value = `AI-${year}${mm}${dd}-`;
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<el-container class="layout-container" style="height: 100vh">
|
|
<el-aside width="200px" class="main-aside">
|
|
<el-menu
|
|
:default-active="activeMenu"
|
|
class="el-menu-vertical-demo"
|
|
@select="(index: string) => activeMenu = index"
|
|
style="border-right: none; flex: 1;"
|
|
>
|
|
<el-menu-item index="preparation">
|
|
<el-icon><Document /></el-icon>
|
|
<span>准备</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="review">
|
|
<el-icon><VideoPlay /></el-icon>
|
|
<span>审核</span>
|
|
</el-menu-item>
|
|
<el-menu-item index="check">
|
|
<el-icon><CircleCheck /></el-icon>
|
|
<span>检查</span>
|
|
</el-menu-item>
|
|
</el-menu>
|
|
|
|
<div class="aside-footer">
|
|
<el-switch
|
|
v-model="isDark"
|
|
inline-prompt
|
|
:active-icon="Moon"
|
|
:inactive-icon="Sunny"
|
|
@change="toggleTheme"
|
|
/>
|
|
</div>
|
|
</el-aside>
|
|
|
|
<el-main>
|
|
<!-- Preparation Page -->
|
|
<div v-if="activeMenu === 'preparation'">
|
|
<h2>准备工作</h2>
|
|
|
|
<el-card class="box-card" shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>新建任务</span>
|
|
</div>
|
|
</template>
|
|
|
|
<el-form label-position="top">
|
|
<el-form-item label="工作目录">
|
|
<el-input v-model="workingDir" placeholder="请选择工作目录" readonly>
|
|
<template #append>
|
|
<el-button @click="selectWorkingDir">选择</el-button>
|
|
</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="分类模板">
|
|
<el-input v-model="templateDir" placeholder="请选择分类模板" readonly>
|
|
<template #append>
|
|
<el-button @click="selectTemplateDir">选择</el-button>
|
|
</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="日期">
|
|
<el-date-picker
|
|
v-model="selectedDate"
|
|
type="date"
|
|
placeholder="选择日期"
|
|
format="YYYY-MM-DD"
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item>
|
|
<el-button type="primary" @click="handleCopy" style="width: 100%">拷贝并重命名</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-card>
|
|
|
|
<!-- <el-divider /> -->
|
|
|
|
<el-card class="box-card" shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>当前任务</span>
|
|
</div>
|
|
</template>
|
|
|
|
<el-form label-position="top">
|
|
<el-form-item label="当前目录">
|
|
<el-input v-model="currentDir" placeholder="请选择或生成目录" readonly>
|
|
<template #append>
|
|
<el-button @click="selectCurrentDir">选择</el-button>
|
|
</template>
|
|
</el-input>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="文件前缀">
|
|
<el-input v-model="videoNamePrefix" placeholder="AI-yyyymmdd-" />
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-card>
|
|
|
|
</div>
|
|
|
|
<!-- Review Page -->
|
|
<div v-else-if="activeMenu === 'review'">
|
|
<h2>审核</h2>
|
|
|
|
<el-alert
|
|
v-if="isReviewDisabled"
|
|
:title="reviewWarningMessage"
|
|
type="warning"
|
|
show-icon
|
|
:closable="false"
|
|
style="margin-bottom: 20px"
|
|
/>
|
|
|
|
<div :class="{ 'disabled-area': isReviewDisabled }">
|
|
<!-- Drag Area -->
|
|
<div class="drag-area">
|
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
|
<div class="el-upload__text">
|
|
将视频文件拖拽到此处
|
|
</div>
|
|
<div class="file-count-container">
|
|
<div class="file-count" v-show="importedFiles.length > 0">
|
|
已导入 {{ importedFiles.length }} 个文件
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Area -->
|
|
<div class="action-area">
|
|
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
|
|
<template #prepend>{{ videoNamePrefix }}</template>
|
|
</el-input>
|
|
<el-button type="primary" @click="handleRename" :disabled="isReviewDisabled || importedFiles.length === 0">命名</el-button>
|
|
</div>
|
|
|
|
<!-- History List -->
|
|
<div class="history-area" v-if="historyList.length > 0">
|
|
<h4>历史记录 ({{ currentHistoryKey || '未关联目录' }})</h4>
|
|
<el-scrollbar max-height="300px">
|
|
<ul class="history-list">
|
|
<li v-for="item in historyList" :key="item" @click="useHistoryItem(item)">
|
|
<span class="history-text">{{ item }}</span>
|
|
<el-icon class="delete-icon" @click.stop="deleteHistoryItem(item)"><Delete /></el-icon>
|
|
</li>
|
|
</ul>
|
|
</el-scrollbar>
|
|
</div>
|
|
<div v-else-if="currentHistoryKey" class="no-history">
|
|
<span class="text-secondary">该目录下暂无历史记录</span>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Check Page -->
|
|
<div v-else-if="activeMenu === 'check'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
|
|
<h2>检查</h2>
|
|
|
|
<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>
|
|
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
|
background-color: var(--el-bg-color-page);
|
|
color: var(--el-text-color-primary);
|
|
transition: background-color 0.3s, color 0.3s;
|
|
}
|
|
.layout-container {
|
|
height: 100vh;
|
|
}
|
|
.main-aside {
|
|
background-color: var(--el-bg-color);
|
|
border-right: 1px solid var(--el-border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
transition: background-color 0.3s, border-color 0.3s;
|
|
}
|
|
.aside-footer {
|
|
padding: 16px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-top: 1px solid var(--el-border-color);
|
|
}
|
|
.box-card {
|
|
/* max-width: 800px; */
|
|
margin-bottom: 20px;
|
|
}
|
|
/* Review Page Styles */
|
|
.disabled-area {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.drag-area {
|
|
border: 2px dashed var(--el-border-color);
|
|
border-radius: 6px;
|
|
cursor: default;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: var(--el-transition-duration-fast);
|
|
padding: 20px 0;
|
|
text-align: center;
|
|
background-color: var(--el-bg-color-overlay);
|
|
margin-bottom: 20px;
|
|
}
|
|
.el-icon--upload {
|
|
font-size: 48px;
|
|
color: var(--el-text-color-placeholder);
|
|
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; /* Removed, container handles spacing */
|
|
color: var(--el-color-primary);
|
|
font-weight: bold;
|
|
}
|
|
.action-area {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.history-area h4 {
|
|
margin-bottom: 10px;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
.history-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
border: 1px solid var(--el-border-color);
|
|
border-radius: 4px;
|
|
}
|
|
.history-list li {
|
|
padding: 10px 15px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
background-color: var(--el-bg-color);
|
|
}
|
|
.history-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.history-list li:hover {
|
|
background-color: var(--el-fill-color-light);
|
|
}
|
|
.history-text {
|
|
flex: 1;
|
|
}
|
|
.delete-icon {
|
|
color: var(--el-text-color-secondary);
|
|
transition: color 0.2s;
|
|
}
|
|
.delete-icon:hover {
|
|
color: var(--el-color-danger);
|
|
}
|
|
.no-history {
|
|
padding: 20px;
|
|
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: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; /* Matched body font */
|
|
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>
|