review page, first
This commit is contained in:
@@ -3,6 +3,9 @@ use fs_extra::dir::CopyOptions;
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
// Define the configuration structure
|
// Define the configuration structure
|
||||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||||
@@ -11,6 +14,10 @@ pub struct AppConfig {
|
|||||||
pub template_dir: String,
|
pub template_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History Structure
|
||||||
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
|
pub struct HistoryMap(HashMap<String, Vec<String>>);
|
||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn greet(name: &str) -> String {
|
||||||
@@ -74,13 +81,178 @@ fn load_config(app_handle: tauri::AppHandle) -> Result<AppConfig, String> {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- History Commands ---
|
||||||
|
|
||||||
|
fn get_history_path(app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
||||||
|
let config_dir = app_handle.path().app_config_dir().map_err(|e| e.to_string())?;
|
||||||
|
if !config_dir.exists() {
|
||||||
|
fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(config_dir.join("history.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_history(app_handle: tauri::AppHandle) -> Result<HistoryMap, String> {
|
||||||
|
let path = get_history_path(&app_handle)?;
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(HistoryMap::default());
|
||||||
|
}
|
||||||
|
let json = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
|
let history: HistoryMap = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn save_history_item(app_handle: tauri::AppHandle, key: String, value: String) -> Result<HistoryMap, String> {
|
||||||
|
let path = get_history_path(&app_handle)?;
|
||||||
|
|
||||||
|
let mut history = if path.exists() {
|
||||||
|
let json = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
|
serde_json::from_str(&json).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
HistoryMap::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = history.0.entry(key).or_insert_with(Vec::new);
|
||||||
|
if !list.contains(&value) {
|
||||||
|
list.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&history).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, json).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn remove_history_item(app_handle: tauri::AppHandle, key: String, value: String) -> Result<HistoryMap, String> {
|
||||||
|
let path = get_history_path(&app_handle)?;
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(HistoryMap::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
|
let mut history: HistoryMap = serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(list) = history.0.get_mut(&key) {
|
||||||
|
list.retain(|x| x != &value);
|
||||||
|
if list.is_empty() {
|
||||||
|
history.0.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&history).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, json).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn check_dir_exists(path: String) -> bool {
|
||||||
|
Path::new(&path).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Renaming Logic ---
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Result<String, String> {
|
||||||
|
if files.is_empty() {
|
||||||
|
return Err("No files provided".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Group files by parent directory to ensure index uniqueness per directory
|
||||||
|
let mut files_by_dir: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
|
||||||
|
|
||||||
|
for file_str in files {
|
||||||
|
let path = std::path::PathBuf::from(file_str);
|
||||||
|
if path.exists() && path.is_file() {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
files_by_dir.entry(parent.to_path_buf()).or_default().push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut renamed_count = 0;
|
||||||
|
|
||||||
|
// 2. Process each directory
|
||||||
|
for (dir, file_list) in files_by_dir {
|
||||||
|
// Find existing indices
|
||||||
|
let mut occupied_indices = HashSet::new();
|
||||||
|
|
||||||
|
let read_dir = fs::read_dir(&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_file() {
|
||||||
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
// Check if file matches Pattern: Prefix + base_name + XX + .ext
|
||||||
|
// e.g., "AI-20231207-" + "Text" + "01" + ".mp4"
|
||||||
|
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()..];
|
||||||
|
// suffix should be digits (e.g., "01", "02")
|
||||||
|
if let Ok(index) = suffix.parse::<u32>() {
|
||||||
|
occupied_indices.insert(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename files in this directory
|
||||||
|
let mut current_index = 1;
|
||||||
|
for file_path in file_list {
|
||||||
|
// Find next available index
|
||||||
|
while occupied_indices.contains(¤t_index) {
|
||||||
|
current_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||||
|
let new_name = if ext.is_empty() {
|
||||||
|
format!("{}{}{:02}", prefix, base_name, current_index)
|
||||||
|
} else {
|
||||||
|
format!("{}{}{:02}.{}", prefix, base_name, current_index, ext)
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_path = dir.join(new_name);
|
||||||
|
|
||||||
|
// Should not happen due to index check, but safety first
|
||||||
|
if !new_path.exists() {
|
||||||
|
fs::rename(&file_path, &new_path).map_err(|e| e.to_string())?;
|
||||||
|
occupied_indices.insert(current_index);
|
||||||
|
renamed_count += 1;
|
||||||
|
} else {
|
||||||
|
// If it exists, skip to next index (rare race condition or manually named weirdly)
|
||||||
|
current_index += 1;
|
||||||
|
// Retry logic could go here, but for now let's just try next loop or fail this file
|
||||||
|
// For robustness, let's just skip this file to avoid overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("Successfully renamed {} files.", renamed_count))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![greet, copy_directory, save_config, load_config])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
greet,
|
||||||
|
copy_directory,
|
||||||
|
save_config,
|
||||||
|
load_config,
|
||||||
|
load_history,
|
||||||
|
save_history_item,
|
||||||
|
remove_history_item,
|
||||||
|
check_dir_exists,
|
||||||
|
rename_videos
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "review-videos",
|
"title": "review-videos",
|
||||||
"width": 1050,
|
"width": 1200,
|
||||||
"height": 840
|
"height": 840
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
344
src/App.vue
344
src/App.vue
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<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 { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { ElMessage } from 'element-plus';
|
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
|
// Navigation
|
||||||
const activeMenu = ref("preparation");
|
const activeMenu = ref("preparation");
|
||||||
@@ -12,7 +13,6 @@ const activeMenu = ref("preparation");
|
|||||||
const isDark = ref(false);
|
const isDark = ref(false);
|
||||||
|
|
||||||
const toggleTheme = (val: string | number | boolean) => {
|
const toggleTheme = (val: string | number | boolean) => {
|
||||||
// val comes from el-switch change event which is boolean | string | number
|
|
||||||
const isDarkMode = val === true;
|
const isDarkMode = val === true;
|
||||||
isDark.value = isDarkMode;
|
isDark.value = isDarkMode;
|
||||||
if (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 () => {
|
onMounted(async () => {
|
||||||
|
// Theme init
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
isDark.value = true;
|
isDark.value = true;
|
||||||
@@ -34,23 +53,35 @@ onMounted(async () => {
|
|||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config init
|
||||||
await loadConfig();
|
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
|
onUnmounted(() => {
|
||||||
const workingDir = ref("");
|
if (unlistenDrop) {
|
||||||
const templateDir = ref("");
|
unlistenDrop();
|
||||||
const selectedDate = ref<Date | null>(new Date());
|
}
|
||||||
const currentDir = ref("");
|
});
|
||||||
const videoNamePrefix = ref("");
|
|
||||||
|
|
||||||
// Configuration Types
|
// Config & Validation
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
working_dir: string;
|
working_dir: string;
|
||||||
template_dir: string;
|
template_dir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load/Save Config Logic
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const config = await invoke<AppConfig>('load_config');
|
const config = await invoke<AppConfig>('load_config');
|
||||||
@@ -74,16 +105,149 @@ const saveConfig = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watchers for Auto-Save
|
const validateCurrentDir = async () => {
|
||||||
watch(workingDir, () => {
|
if (!currentDir.value) {
|
||||||
saveConfig();
|
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, () => {
|
const reviewWarningMessage = computed(() => {
|
||||||
saveConfig();
|
if (!currentDir.value) return "请先设置当前目录";
|
||||||
|
if (!isCurrentDirValid.value) return "当前目录不存在,请检查";
|
||||||
|
if (!videoNamePrefix.value) return "文件前缀为空";
|
||||||
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
// Actions (Preparation)
|
||||||
const selectWorkingDir = async () => {
|
const selectWorkingDir = async () => {
|
||||||
const selected = await open({
|
const selected = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
@@ -133,33 +297,25 @@ const handleCopy = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ElMessage.success("目录拷贝并重命名成功!");
|
ElMessage.success("目录拷贝并重命名成功!");
|
||||||
|
|
||||||
// Update Section 2
|
|
||||||
currentDir.value = newPath;
|
currentDir.value = newPath;
|
||||||
// Auto-update prefix is handled by watcher
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(`错误: ${error}`);
|
ElMessage.error(`错误: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reactivity for Current Directory
|
// Reactivity for Current Directory Prefix
|
||||||
watch(currentDir, (newPath) => {
|
watch(currentDir, (newPath) => {
|
||||||
if (!newPath) return;
|
if (!newPath) return;
|
||||||
|
|
||||||
// Check if path ends with "ddmm视频"
|
|
||||||
// Robust split for Windows/Unix paths
|
|
||||||
const dirName = newPath.split(/[/\\]/).pop();
|
const dirName = newPath.split(/[/\\]/).pop();
|
||||||
|
|
||||||
if (!dirName) return;
|
if (!dirName) return;
|
||||||
|
|
||||||
const regex = /^(\d{2})(\d{2})视频$/; // matches mmdd视频
|
const regex = /^(\d{2})(\d{2})视频$/;
|
||||||
const match = dirName.match(regex);
|
const match = dirName.match(regex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const mm = match[1];
|
const mm = match[1];
|
||||||
const dd = match[2];
|
const dd = match[2];
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
// Format: AI-yyyymmdd-
|
|
||||||
videoNamePrefix.value = `AI-${year}${mm}${dd}-`;
|
videoNamePrefix.value = `AI-${year}${mm}${dd}-`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -201,6 +357,7 @@ watch(currentDir, (newPath) => {
|
|||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<el-main>
|
<el-main>
|
||||||
|
<!-- Preparation Page -->
|
||||||
<div v-if="activeMenu === 'preparation'">
|
<div v-if="activeMenu === 'preparation'">
|
||||||
<h2>准备工作</h2>
|
<h2>准备工作</h2>
|
||||||
|
|
||||||
@@ -270,9 +427,58 @@ watch(currentDir, (newPath) => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Review Page -->
|
||||||
<div v-else-if="activeMenu === 'review'">
|
<div v-else-if="activeMenu === 'review'">
|
||||||
<h2>审核</h2>
|
<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>
|
||||||
|
|
||||||
<div v-else-if="activeMenu === 'check'">
|
<div v-else-if="activeMenu === 'check'">
|
||||||
@@ -309,7 +515,85 @@ body {
|
|||||||
border-top: 1px solid var(--el-border-color);
|
border-top: 1px solid var(--el-border-color);
|
||||||
}
|
}
|
||||||
.box-card {
|
.box-card {
|
||||||
max-width: 800px;
|
/* max-width: 800px; */
|
||||||
margin-bottom: 20px;
|
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>
|
||||||
Reference in New Issue
Block a user