review page, first

This commit is contained in:
Julian Freeman
2025-12-07 19:41:25 -04:00
parent b9a41342a5
commit 4fab0b813f
3 changed files with 489 additions and 33 deletions

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import { ref, watch, onMounted, computed, onUnmounted } 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 } from '@element-plus/icons-vue';
import { Document, VideoPlay, CircleCheck, Moon, Sunny, Delete, UploadFilled } from '@element-plus/icons-vue';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
// Navigation
const activeMenu = ref("preparation");
@@ -12,7 +13,6 @@ const activeMenu = ref("preparation");
const isDark = ref(false);
const toggleTheme = (val: string | number | boolean) => {
// val comes from el-switch change event which is boolean | string | number
const isDarkMode = val === true;
isDark.value = isDarkMode;
if (isDarkMode) {
@@ -24,7 +24,26 @@ const toggleTheme = (val: string | number | boolean) => {
}
};
// 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(""); // Derived from dropped files relative to currentDir
// Lifecycle
let unlistenDrop: UnlistenFn | null = null;
onMounted(async () => {
// Theme init
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true;
@@ -34,23 +53,35 @@ onMounted(async () => {
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);
}
});
});
// Preparation Data
const workingDir = ref("");
const templateDir = ref("");
const selectedDate = ref<Date | null>(new Date());
const currentDir = ref("");
const videoNamePrefix = ref("");
onUnmounted(() => {
if (unlistenDrop) {
unlistenDrop();
}
});
// Configuration Types
// Config & Validation
interface AppConfig {
working_dir: string;
template_dir: string;
}
// Load/Save Config Logic
const loadConfig = async () => {
try {
const config = await invoke<AppConfig>('load_config');
@@ -74,16 +105,149 @@ const saveConfig = async () => {
}
}
// Watchers for Auto-Save
watch(workingDir, () => {
saveConfig();
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[]) => {
// 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();
}
}
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;
}
// Save to history
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);
}
}
// Rename Files
try {
const msg = await invoke<string>('rename_videos', {
files: importedFiles.value,
prefix: videoNamePrefix.value,
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);
}
}
const useHistoryItem = (val: string) => {
videoNameInput.value = val;
}
// Computed
const isReviewDisabled = computed(() => {
return !isCurrentDirValid.value || !videoNamePrefix.value;
});
watch(templateDir, () => {
saveConfig();
const reviewWarningMessage = computed(() => {
if (!currentDir.value) return "请先设置当前目录";
if (!isCurrentDirValid.value) return "当前目录不存在,请检查";
if (!videoNamePrefix.value) return "文件前缀为空";
return "";
});
// Actions
// Actions (Preparation)
const selectWorkingDir = async () => {
const selected = await open({
directory: true,
@@ -133,33 +297,25 @@ const handleCopy = async () => {
});
ElMessage.success("目录拷贝并重命名成功!");
// Update Section 2
currentDir.value = newPath;
// Auto-update prefix is handled by watcher
} catch (error) {
ElMessage.error(`错误: ${error}`);
}
};
// Reactivity for Current Directory
// Reactivity for Current Directory Prefix
watch(currentDir, (newPath) => {
if (!newPath) return;
// Check if path ends with "ddmm视频"
// Robust split for Windows/Unix paths
const dirName = newPath.split(/[/\\]/).pop();
if (!dirName) return;
const regex = /^(\d{2})(\d{2})视频$/; // matches mmdd视频
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();
// Format: AI-yyyymmdd-
videoNamePrefix.value = `AI-${year}${mm}${dd}-`;
}
});
@@ -201,6 +357,7 @@ watch(currentDir, (newPath) => {
</el-aside>
<el-main>
<!-- Preparation Page -->
<div v-if="activeMenu === 'preparation'">
<h2>准备工作</h2>
@@ -270,9 +427,58 @@ watch(currentDir, (newPath) => {
</div>
<!-- Review Page -->
<div v-else-if="activeMenu === 'review'">
<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 }">
<!-- 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" v-if="importedFiles.length > 0">
已导入 {{ importedFiles.length }} 个文件
</div>
</div>
<el-divider />
<!-- 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>
<div v-else-if="activeMenu === 'check'">
@@ -309,7 +515,85 @@ body {
border-top: 1px solid var(--el-border-color);
}
.box-card {
max-width: 800px;
/* max-width: 800px; */
margin-bottom: 20px;
}
</style>
/* 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: 40px 0;
text-align: center;
background-color: var(--el-bg-color-overlay);
}
.el-icon--upload {
font-size: 67px;
color: var(--el-text-color-placeholder);
margin-bottom: 16px;
line-height: 50px;
}
.el-upload__text {
color: var(--el-text-color-regular);
font-size: 14px;
text-align: center;
}
.file-count {
margin-top: 10px;
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);
}
</style>