Compare commits
17 Commits
4fab0b813f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38f2f853d8 | ||
|
|
b914adba74 | ||
|
|
920c7fab9c | ||
|
|
b6b87e5800 | ||
|
|
268d7eed07 | ||
|
|
3285f5c015 | ||
|
|
2cb13d70f7 | ||
|
|
c0403e3b6e | ||
|
|
c1f153ee50 | ||
|
|
adc914694e | ||
|
|
05e714ef77 | ||
|
|
7fcc4d343b | ||
|
|
785931ef65 | ||
|
|
f42ee4ec9e | ||
|
|
ffd6916313 | ||
|
|
05ef56a69b | ||
|
|
930f3c40fa |
@@ -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.
|
Generated by Gemini CLI
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|||||||
BIN
app-icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + Vue + Typescript App</title>
|
<title>视频审核工具</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build && (cd src-tauri && cargo check)",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 895 B |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 766 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -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>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 517 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
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)]
|
||||||
@@ -18,12 +17,6 @@ pub struct AppConfig {
|
|||||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||||
pub struct HistoryMap(HashMap<String, Vec<String>>);
|
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]
|
#[tauri::command]
|
||||||
fn copy_directory(template_path: String, target_path: String, new_folder_name: String) -> Result<String, String> {
|
fn copy_directory(template_path: String, target_path: String, new_folder_name: String) -> Result<String, String> {
|
||||||
let template = Path::new(&template_path);
|
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);
|
let destination = target_parent.join(&new_folder_name);
|
||||||
|
|
||||||
if !template.exists() {
|
if !template.exists() {
|
||||||
return Err("Template directory does not exist".to_string());
|
return Err("模板目录不存在".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if destination.exists() {
|
// Remove the check for existing destination to allow merging/overwriting
|
||||||
return Err(format!("Destination directory already exists: {:?}", destination));
|
// 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
|
||||||
std::fs::create_dir_all(&destination).map_err(|e| e.to_string())?;
|
if !destination.exists() {
|
||||||
|
std::fs::create_dir_all(&destination).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut options = CopyOptions::new();
|
let mut options = CopyOptions::new();
|
||||||
options.content_only = true;
|
options.content_only = true;
|
||||||
|
options.overwrite = true; // Enable overwrite for existing files
|
||||||
|
|
||||||
fs_extra::dir::copy(template, &destination, &options)
|
fs_extra::dir::copy(template, &destination, &options)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -158,7 +155,7 @@ fn check_dir_exists(path: String) -> bool {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Result<String, String> {
|
fn rename_videos(files: Vec<String>, prefix: String, base_name: String) -> Result<String, String> {
|
||||||
if files.is_empty() {
|
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
|
// 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
|
// Rename files in this directory
|
||||||
let mut current_index = 1;
|
let mut current_index = 1;
|
||||||
for file_path in file_list {
|
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
|
// Find next available index
|
||||||
while occupied_indices.contains(¤t_index) {
|
while occupied_indices.contains(¤t_index) {
|
||||||
current_index += 1;
|
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(¤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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -243,7 +344,6 @@ pub fn run() {
|
|||||||
.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![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
|
||||||
copy_directory,
|
copy_directory,
|
||||||
save_config,
|
save_config,
|
||||||
load_config,
|
load_config,
|
||||||
@@ -251,8 +351,10 @@ pub fn run() {
|
|||||||
save_history_item,
|
save_history_item,
|
||||||
remove_history_item,
|
remove_history_item,
|
||||||
check_dir_exists,
|
check_dir_exists,
|
||||||
rename_videos
|
rename_videos,
|
||||||
|
delete_empty_dirs,
|
||||||
|
check_file_naming
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "review-videos",
|
"title": "视频审核工具",
|
||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 840
|
"height": 840
|
||||||
}
|
}
|
||||||
|
|||||||
360
src/App.vue
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<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 { 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, 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 { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
import appIcon from './assets/app-icon-96.png';
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
const activeMenu = ref("preparation");
|
const activeMenu = ref("preparation");
|
||||||
@@ -37,11 +38,37 @@ const importedFiles = ref<string[]>([]);
|
|||||||
const videoNameInput = ref("");
|
const videoNameInput = ref("");
|
||||||
const historyList = ref<string[]>([]);
|
const historyList = ref<string[]>([]);
|
||||||
const historyMap = ref<Record<string, 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
|
// Lifecycle
|
||||||
let unlistenDrop: UnlistenFn | null = null;
|
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 () => {
|
onMounted(async () => {
|
||||||
// Theme init
|
// Theme init
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
@@ -88,7 +115,7 @@ const loadConfig = async () => {
|
|||||||
if (config.working_dir) workingDir.value = config.working_dir;
|
if (config.working_dir) workingDir.value = config.working_dir;
|
||||||
if (config.template_dir) templateDir.value = config.template_dir;
|
if (config.template_dir) templateDir.value = config.template_dir;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load config:", e);
|
console.error("加载配置失败:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +128,7 @@ const saveConfig = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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');
|
const history = await invoke<Record<string, string[]>>('load_history');
|
||||||
historyMap.value = history || {};
|
historyMap.value = history || {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load history", e);
|
console.error("加载历史记录失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,35 +167,21 @@ const updateHistoryList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFileDrop = (paths: string[]) => {
|
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;
|
importedFiles.value = paths;
|
||||||
|
|
||||||
if (paths.length > 0 && currentDir.value) {
|
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];
|
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 separator = firstFile.includes('\\') ? '\\' : '/';
|
||||||
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
|
const lastSeparatorIndex = firstFile.lastIndexOf(separator);
|
||||||
const parentDir = firstFile.substring(0, lastSeparatorIndex);
|
const parentDir = firstFile.substring(0, lastSeparatorIndex);
|
||||||
|
|
||||||
if (parentDir.startsWith(currentDir.value)) {
|
if (parentDir.startsWith(currentDir.value)) {
|
||||||
let relative = parentDir.substring(currentDir.value.length);
|
let relative = parentDir.substring(currentDir.value.length);
|
||||||
// Remove leading separator if exists
|
|
||||||
if (relative.startsWith(separator)) {
|
if (relative.startsWith(separator)) {
|
||||||
relative = relative.substring(1);
|
relative = relative.substring(1);
|
||||||
}
|
}
|
||||||
currentHistoryKey.value = relative;
|
currentHistoryKey.value = relative;
|
||||||
} else {
|
} 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 = "";
|
currentHistoryKey.value = "";
|
||||||
}
|
}
|
||||||
updateHistoryList();
|
updateHistoryList();
|
||||||
@@ -199,7 +212,6 @@ const handleRename = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to history
|
|
||||||
if (currentHistoryKey.value) {
|
if (currentHistoryKey.value) {
|
||||||
try {
|
try {
|
||||||
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
const newMap = await invoke<Record<string, string[]>>('save_history_item', {
|
||||||
@@ -209,11 +221,10 @@ const handleRename = async () => {
|
|||||||
historyMap.value = newMap;
|
historyMap.value = newMap;
|
||||||
updateHistoryList();
|
updateHistoryList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("History save failed", e);
|
console.error("保存历史记录失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename Files
|
|
||||||
try {
|
try {
|
||||||
const msg = await invoke<string>('rename_videos', {
|
const msg = await invoke<string>('rename_videos', {
|
||||||
files: importedFiles.value,
|
files: importedFiles.value,
|
||||||
@@ -221,8 +232,6 @@ const handleRename = async () => {
|
|||||||
baseName: videoNameInput.value
|
baseName: videoNameInput.value
|
||||||
});
|
});
|
||||||
ElMessage.success(msg);
|
ElMessage.success(msg);
|
||||||
// Clear imported files after success? Or keep them?
|
|
||||||
// Usually clearing is better feedback that "job done".
|
|
||||||
importedFiles.value = [];
|
importedFiles.value = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error("重命名失败: " + e);
|
ElMessage.error("重命名失败: " + e);
|
||||||
@@ -233,6 +242,149 @@ const useHistoryItem = (val: string) => {
|
|||||||
videoNameInput.value = val;
|
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
|
// Computed
|
||||||
const isReviewDisabled = computed(() => {
|
const isReviewDisabled = computed(() => {
|
||||||
@@ -306,7 +458,7 @@ const handleCopy = async () => {
|
|||||||
// Reactivity for Current Directory Prefix
|
// Reactivity for Current Directory Prefix
|
||||||
watch(currentDir, (newPath) => {
|
watch(currentDir, (newPath) => {
|
||||||
if (!newPath) return;
|
if (!newPath) return;
|
||||||
const dirName = newPath.split(/[/\\]/).pop();
|
const dirName = newPath.split(/[\/\\]/).pop();
|
||||||
if (!dirName) return;
|
if (!dirName) return;
|
||||||
|
|
||||||
const regex = /^(\d{2})(\d{2})视频$/;
|
const regex = /^(\d{2})(\d{2})视频$/;
|
||||||
@@ -324,7 +476,10 @@ watch(currentDir, (newPath) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container class="layout-container" style="height: 100vh">
|
<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
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
class="el-menu-vertical-demo"
|
class="el-menu-vertical-demo"
|
||||||
@@ -356,10 +511,9 @@ watch(currentDir, (newPath) => {
|
|||||||
</div>
|
</div>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<el-main>
|
<el-main :class="{ 'review-active': activeMenu === 'review' }">
|
||||||
<!-- Preparation Page -->
|
<!-- Preparation Page -->
|
||||||
<div v-if="activeMenu === 'preparation'">
|
<div v-if="activeMenu === 'preparation'">
|
||||||
<h2>准备工作</h2>
|
|
||||||
|
|
||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -401,7 +555,7 @@ watch(currentDir, (newPath) => {
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-divider />
|
<!-- <el-divider /> -->
|
||||||
|
|
||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -428,8 +582,7 @@ watch(currentDir, (newPath) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Review Page -->
|
<!-- Review Page -->
|
||||||
<div v-else-if="activeMenu === 'review'">
|
<div v-else-if="activeMenu === 'review'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
|
||||||
<h2>审核</h2>
|
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="isReviewDisabled"
|
v-if="isReviewDisabled"
|
||||||
@@ -440,34 +593,35 @@ watch(currentDir, (newPath) => {
|
|||||||
style="margin-bottom: 20px"
|
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 -->
|
<!-- Drag Area -->
|
||||||
<div class="drag-area">
|
<div class="drag-area">
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
将视频文件拖拽到此处
|
将视频文件拖拽到此处
|
||||||
</div>
|
</div>
|
||||||
<div class="file-count" v-if="importedFiles.length > 0">
|
<div class="file-count-container">
|
||||||
已导入 {{ importedFiles.length }} 个文件
|
<div class="file-count" v-show="importedFiles.length > 0">
|
||||||
|
已导入 {{ importedFiles.length }} 个文件
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
|
|
||||||
<!-- Action Area -->
|
<!-- Action Area -->
|
||||||
<div class="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>
|
<template #prepend>{{ videoNamePrefix }}</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<el-button type="primary" @click="handleRename" :disabled="isReviewDisabled || importedFiles.length === 0">命名</el-button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- History List -->
|
<!-- 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>
|
<h4>历史记录 ({{ currentHistoryKey || '未关联目录' }})</h4>
|
||||||
<el-scrollbar max-height="300px">
|
<el-scrollbar style="flex: 1" class="history-scrollbar">
|
||||||
<ul class="history-list">
|
<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>
|
<span class="history-text">{{ item }}</span>
|
||||||
<el-icon class="delete-icon" @click.stop="deleteHistoryItem(item)"><Delete /></el-icon>
|
<el-icon class="delete-icon" @click.stop="deleteHistoryItem(item)"><Delete /></el-icon>
|
||||||
</li>
|
</li>
|
||||||
@@ -481,10 +635,38 @@ watch(currentDir, (newPath) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="activeMenu === 'check'">
|
<!-- Check Page -->
|
||||||
<h2>检查</h2>
|
<div v-else-if="activeMenu === 'check'" style="display: flex; flex-direction: column; height: 100%; overflow: hidden;">
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
@@ -507,6 +689,28 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
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 {
|
.aside-footer {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -530,23 +734,31 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: var(--el-transition-duration-fast);
|
transition: var(--el-transition-duration-fast);
|
||||||
padding: 40px 0;
|
padding: 20px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: var(--el-bg-color-overlay);
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.el-icon--upload {
|
.el-icon--upload {
|
||||||
font-size: 67px;
|
font-size: 48px;
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 10px;
|
||||||
line-height: 50px;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.el-upload__text {
|
.el-upload__text {
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
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 {
|
.file-count {
|
||||||
margin-top: 10px;
|
/* margin-top: 10px; /* Removed, container handles spacing */
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -556,6 +768,7 @@ body {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.history-area h4 {
|
.history-area h4 {
|
||||||
|
margin-top: 0;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
@@ -563,6 +776,8 @@ body {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
.history-scrollbar {
|
||||||
border: 1px solid var(--el-border-color);
|
border: 1px solid var(--el-border-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -596,4 +811,51 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--el-text-color-secondary);
|
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>
|
</style>
|
||||||
BIN
src/assets/app-icon-96.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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 |