Compare commits

..

17 Commits

Author SHA1 Message Date
Julian Freeman
38f2f853d8 fix review ui 2025-12-07 22:39:12 -04:00
Julian Freeman
b914adba74 fix review margin 2025-12-07 22:24:07 -04:00
Julian Freeman
920c7fab9c fix review ui 2025-12-07 22:17:17 -04:00
Julian Freeman
b6b87e5800 change lang to zh 2025-12-07 21:57:42 -04:00
Julian Freeman
268d7eed07 readme and remove template 2025-12-07 21:44:43 -04:00
Julian Freeman
3285f5c015 fix sidebar ui 2025-12-07 21:42:28 -04:00
Julian Freeman
2cb13d70f7 change sidebar ui and icon 2025-12-07 21:31:56 -04:00
Julian Freeman
c0403e3b6e check page, change log font family 2025-12-07 21:09:15 -04:00
Julian Freeman
c1f153ee50 check page, fix output format 2025-12-07 21:06:00 -04:00
Julian Freeman
adc914694e prepare page, fix ui and logic 2025-12-07 20:54:53 -04:00
Julian Freeman
05e714ef77 check page, keep scrollbar pos 2025-12-07 20:47:23 -04:00
Julian Freeman
7fcc4d343b check page, fix ui 2025-12-07 20:41:38 -04:00
Julian Freeman
785931ef65 check page, fix some 2025-12-07 20:33:32 -04:00
Julian Freeman
f42ee4ec9e check page 2025-12-07 20:27:00 -04:00
Julian Freeman
ffd6916313 review page, adjust drag ui 2025-12-07 20:00:35 -04:00
Julian Freeman
05ef56a69b review page, fix rename bug 2025-12-07 19:56:26 -04:00
Julian Freeman
930f3c40fa review page, fix ui 2025-12-07 19:50:33 -04:00
63 changed files with 446 additions and 86 deletions

View File

@@ -1,7 +1,3 @@
# Tauri + Vue + TypeScript
# Review Videos
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
Generated by Gemini CLI

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,10 +1,9 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + Typescript App</title>
<title>视频审核工具</title>
</head>
<body>

View File

@@ -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"
},

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 895 B

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -5,7 +5,6 @@ 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)]
@@ -18,12 +17,6 @@ pub struct AppConfig {
#[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/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn copy_directory(template_path: String, target_path: String, new_folder_name: String) -> Result<String, String> {
let template = Path::new(&template_path);
@@ -31,18 +24,22 @@ fn copy_directory(template_path: String, target_path: String, new_folder_name: S
let destination = target_parent.join(&new_folder_name);
if !template.exists() {
return Err("Template directory does not exist".to_string());
return Err("模板目录不存在".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
// 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())?;
@@ -158,7 +155,7 @@ fn check_dir_exists(path: String) -> bool {
#[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());
return Err("未提供文件".to_string());
}
// 1. Group files by parent directory to ensure index uniqueness per directory
@@ -205,6 +202,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(&current_index) {
current_index += 1;
@@ -233,7 +244,97 @@ fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Resul
}
}
Ok(format!("Successfully renamed {} files.", renamed_count))
Ok(format!("成功重命名 {} 个文件。", 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("路径不是有效的目录".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("路径不是有效的目录".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(&current_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)
}
@@ -243,7 +344,6 @@ pub fn run() {
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![
greet,
copy_directory,
save_config,
load_config,
@@ -251,7 +351,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");

View File

@@ -12,7 +12,7 @@
"app": {
"windows": [
{
"title": "review-videos",
"title": "视频审核工具",
"width": 1200,
"height": 840
}

View File

@@ -1,10 +1,11 @@
<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';
import appIcon from './assets/app-icon-96.png';
// Navigation
const activeMenu = ref("preparation");
@@ -37,11 +38,37 @@ 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("");
const filteredHistoryList = computed(() => {
if (!videoNameInput.value) {
return historyList.value;
}
return historyList.value.filter(item => item.toLowerCase().includes(videoNameInput.value.toLowerCase()));
});
// 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');
@@ -88,7 +115,7 @@ const loadConfig = async () => {
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);
console.error("加载配置失败:", e);
}
}
@@ -101,7 +128,7 @@ const saveConfig = async () => {
}
});
} catch (e) {
console.error("Failed to save config:", e);
console.error("保存配置失败:", e);
}
}
@@ -127,7 +154,7 @@ const loadHistory = async () => {
const history = await invoke<Record<string, string[]>>('load_history');
historyMap.value = history || {};
} catch (e) {
console.error("Failed to load history", e);
console.error("加载历史记录失败", e);
}
}
@@ -140,35 +167,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 +212,6 @@ const handleRename = async () => {
return;
}
// Save to history
if (currentHistoryKey.value) {
try {
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
@@ -209,11 +221,10 @@ const handleRename = async () => {
historyMap.value = newMap;
updateHistoryList();
} catch (e) {
console.error("History save failed", e);
console.error("保存历史记录失败", e);
}
}
// Rename Files
try {
const msg = await invoke<string>('rename_videos', {
files: importedFiles.value,
@@ -221,8 +232,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 +242,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 +458,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})视频$/;
@@ -324,7 +476,10 @@ watch(currentDir, (newPath) => {
<template>
<el-container class="layout-container" style="height: 100vh">
<el-aside width="200px" class="main-aside">
<el-aside width="160px" class="main-aside">
<div class="aside-header">
<img :src="appIcon" alt="App Icon" class="app-icon" />
</div>
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
@@ -356,10 +511,9 @@ watch(currentDir, (newPath) => {
</div>
</el-aside>
<el-main>
<el-main :class="{ 'review-active': activeMenu === 'review' }">
<!-- Preparation Page -->
<div v-if="activeMenu === 'preparation'">
<h2>准备工作</h2>
<el-card class="box-card" shadow="hover">
<template #header>
@@ -401,7 +555,7 @@ watch(currentDir, (newPath) => {
</el-form>
</el-card>
<el-divider />
<!-- <el-divider /> -->
<el-card class="box-card" shadow="hover">
<template #header>
@@ -428,8 +582,7 @@ watch(currentDir, (newPath) => {
</div>
<!-- Review Page -->
<div v-else-if="activeMenu === 'review'">
<h2>审核</h2>
<div v-else-if="activeMenu === 'review'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
<el-alert
v-if="isReviewDisabled"
@@ -440,34 +593,35 @@ watch(currentDir, (newPath) => {
style="margin-bottom: 20px"
/>
<div :class="{ 'disabled-area': isReviewDisabled }">
<div :class="{ 'disabled-area': isReviewDisabled }" style="display: flex; flex-direction: column; flex: 1; overflow: hidden;">
<!-- 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">
<div class="file-count-container">
<div class="file-count" v-show="importedFiles.length > 0">
已导入 {{ importedFiles.length }} 个文件
</div>
</div>
<el-divider />
</div>
<!-- Action Area -->
<div class="action-area">
<el-input v-model="videoNameInput" placeholder="输入视频名称 (例如: 文本)" class="name-input">
<el-input v-model="videoNameInput" placeholder="视频名称" class="name-input" @keyup.enter="handleRename">
<template #prepend>{{ videoNamePrefix }}</template>
</el-input>
<el-button type="primary" @click="handleRename" :disabled="isReviewDisabled || importedFiles.length === 0">命名</el-button>
<el-button @click="videoNameInput = ''" class="clear-button-margin-fix">清空</el-button>
</div>
<!-- History List -->
<div class="history-area" v-if="historyList.length > 0">
<div class="history-area" v-if="historyList.length > 0" style="display: flex; flex-direction: column; flex: 1; overflow: hidden; margin-bottom: 0px;">
<h4>历史记录 ({{ currentHistoryKey || '未关联目录' }})</h4>
<el-scrollbar max-height="300px">
<el-scrollbar style="flex: 1" class="history-scrollbar">
<ul class="history-list">
<li v-for="item in historyList" :key="item" @click="useHistoryItem(item)">
<li v-for="item in filteredHistoryList" :key="item" @click="useHistoryItem(item)">
<span class="history-text">{{ item }}</span>
<el-icon class="delete-icon" @click.stop="deleteHistoryItem(item)"><Delete /></el-icon>
</li>
@@ -481,10 +635,38 @@ watch(currentDir, (newPath) => {
</div>
</div>
<div v-else-if="activeMenu === 'check'">
<h2>检查</h2>
<el-empty description="功能待定" />
<!-- Check Page -->
<div v-else-if="activeMenu === 'check'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
<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>
@@ -507,6 +689,28 @@ body {
flex-direction: column;
transition: background-color 0.3s, border-color 0.3s;
}
.aside-header {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.app-icon {
width: 64px;
height: 64px;
}
.el-menu-vertical-demo .el-menu-item {
font-size: 20px; /* Increase text size */
height: 80px; /* Adjust height to accommodate larger font/icons and maintain spacing */
display: flex; /* Make it a flex container */
justify-content: center;
padding-right: 50px;
}
.el-menu-vertical-demo .el-menu-item .el-icon {
font-size: 20px; /* Increase icon size */
margin-right: 12px;
}
.aside-footer {
padding: 16px;
display: flex;
@@ -530,23 +734,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;
}
@@ -556,6 +768,7 @@ body {
margin-bottom: 20px;
}
.history-area h4 {
margin-top: 0;
margin-bottom: 10px;
color: var(--el-text-color-primary);
}
@@ -563,6 +776,8 @@ body {
list-style: none;
padding: 0;
margin: 0;
}
.history-scrollbar {
border: 1px solid var(--el-border-color);
border-radius: 4px;
}
@@ -596,4 +811,51 @@ 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;
}
.el-main.review-active {
padding: 20px;
}
.action-area .clear-button-margin-fix {
margin-left: 0; /* Adjust this value as needed to fine-tune the gap */
}
</style>

BIN
src/assets/app-icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B