feat:完善批量修改文件名功能

This commit is contained in:
czhqwer
2025-04-09 23:45:57 +08:00
parent aaa64edd4b
commit 88bea91bcc
13 changed files with 595 additions and 288 deletions

View File

@@ -24,4 +24,10 @@ public class GlobalExceptionHandler {
log.error("业务异常: {}", e.getMessage(), e);
return Result.error(500, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("服务器异常: {}", e.getMessage(), e);
return Result.error(500, "服务器异常");
}
}

View File

@@ -1,6 +1,7 @@
package cn.czh.controller;
import cn.czh.base.Result;
import cn.czh.dto.req.RenameFile;
import cn.czh.service.ILocalFileService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@@ -74,6 +75,9 @@ public class LocalFileController {
return Result.success(localFileService.getFileTree(path, showFiles, showFolders, maxDepth));
}
/**
* 文件加密
*/
@PostMapping("/encrypt")
public Result<?> encryptFile(@RequestParam String filePath, @RequestParam String password) {
File file = new File(filePath);
@@ -86,6 +90,9 @@ public class LocalFileController {
return Result.success(localFileService.encryptFile(filePath, password));
}
/**
* 文件解密
*/
@PostMapping("/decrypt")
public Result<?> decryptFile(@RequestParam String filePath, @RequestParam String password) {
File file = new File(filePath);
@@ -97,4 +104,13 @@ public class LocalFileController {
}
return Result.success(localFileService.decryptFile(filePath, password));
}
/**
* 批量修改文件名
*/
@PostMapping("/batchRenameFile")
public Result<?> batchRenameFile(@RequestBody List<RenameFile> fileRenames) {
localFileService.batchRenameFile(fileRenames);
return Result.success();
}
}

View File

@@ -0,0 +1,12 @@
package cn.czh.dto.req;
import lombok.Data;
@Data
public class RenameFile {
private String absolutePath;
private String newName;
}

View File

@@ -2,6 +2,7 @@ package cn.czh.service;
import cn.czh.dto.FileNode;
import cn.czh.dto.IndexResult;
import cn.czh.dto.req.RenameFile;
import java.io.IOException;
import java.util.List;
@@ -19,4 +20,7 @@ public interface ILocalFileService {
String encryptFile(String filePath, String password);
String decryptFile(String filePath, String password);
void batchRenameFile(List<RenameFile> fileRenames);
}

View File

@@ -3,9 +3,11 @@ package cn.czh.service.impl;
import cn.czh.base.BusinessException;
import cn.czh.dto.FileNode;
import cn.czh.dto.IndexResult;
import cn.czh.dto.req.RenameFile;
import cn.czh.service.ILocalFileService;
import cn.czh.utils.FileSearchUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
@@ -250,6 +252,65 @@ public class LocalFileServiceImpl implements ILocalFileService {
}
}
@Override
public void batchRenameFile(List<RenameFile> fileRenames) {
if (fileRenames == null || fileRenames.isEmpty()) {
throw new BusinessException("文件重命名列表为空");
}
List<File> successfullyRenamed = Lists.newArrayList();
List<String> originalPaths = Lists.newArrayList();
try {
for (RenameFile fileRename : fileRenames) {
File oldFile = new File(fileRename.getAbsolutePath());
if (!oldFile.exists()) {
throw new BusinessException("文件未找到: " + fileRename.getAbsolutePath());
}
String parentPath = oldFile.getParent();
File newFile = new File(parentPath + File.separator + fileRename.getNewName());
// 执行重命名
if (oldFile.renameTo(newFile)) {
successfullyRenamed.add(newFile);
originalPaths.add(fileRename.getAbsolutePath());
} else {
throw new BusinessException("文件重命名失败: " + fileRename.getAbsolutePath());
}
}
} catch (Exception e) {
rollbackRenamedFiles(successfullyRenamed, originalPaths);
throw e;
}
}
/**
* 回滚已重命名的文件
* @param renamedFiles 已重命名的文件列表
* @param originalPaths 原始文件路径列表
*/
private void rollbackRenamedFiles(List<File> renamedFiles, List<String> originalPaths) {
if (renamedFiles.isEmpty()) {
return;
}
for (int i = 0; i < renamedFiles.size(); i++) {
File renamedFile = renamedFiles.get(i);
File originalFile = new File(originalPaths.get(i));
try {
if (renamedFile.exists()) {
if (!renamedFile.renameTo(originalFile)) {
log.error("回滚失败: {}", renamedFile.getAbsolutePath());
}
}
} catch (Exception ex) {
log.error("回滚失败: {}", ex.getMessage(), ex);
}
}
}
private SecretKey generateKey(String password, byte[] salt) throws Exception {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

View File

@@ -5,16 +5,33 @@
<el-container>
<el-main>
<!-- 文件列表 -->
<!-- 重命名选项 -->
<el-card class="mt-4">
<el-tabs v-model="activeTab">
<el-tab-pane label="插入/替换" name="insert">
<InsertTab :rules="insertRules" @updateRules="updateInsertRules" />
</el-tab-pane>
<el-tab-pane label="删除" name="delete">
<DeleteTab :rules="deleteRules" @updateRules="updateDeleteRules" />
</el-tab-pane>
<el-tab-pane label="编号" name="numbering">
<NumberingTab :rules="numberingRules" @updateRules="updateNumberingRules" />
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 文件列表与预览 -->
<el-card v-show="files.length > 0" class="mt-4">
<div slot="header" class="flex justify-between items-center">
<span>已选择的文件</span>
<span>文件重命名预览</span>
<el-button type="danger" size="small" @click="clearFiles" style="margin-left: 10px;">
<i class="el-icon-delete mr-1"></i>全部清除
</el-button>
</div>
<el-table :data="files" max-height="250">
<el-table-column prop="name" label="文件名" width="400"></el-table-column>
<el-table :data="previewData" max-height="400">
<el-table-column prop="original" label="文件名" width="300"></el-table-column>
<el-table-column label="→" width="50" align="center"></el-table-column>
<el-table-column prop="new" label="新文件名" width="300"></el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button type="text" size="small" @click="removeFile(scope.$index)">
@@ -25,58 +42,12 @@
</el-table>
</el-card>
<!-- 重命名选项 -->
<el-card class="mt-4">
<el-tabs v-model="activeTab">
<el-tab-pane label="插入/替换" name="insert">
<InsertTab />
</el-tab-pane>
<el-tab-pane label="删除" name="delete">
<DeleteTab />
</el-tab-pane>
<el-tab-pane label="编号" name="numbering">
<NumberingTab />
</el-tab-pane>
<el-tab-pane label="清理" name="cleanup">
<CleanupTab />
</el-tab-pane>
<el-tab-pane label="高级" name="advanced">
<AdvancedTab />
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 操作按钮组 -->
<div class="action-buttons mt-4">
<el-button size="medium" @click="reset">重置</el-button>
<el-button size="medium" type="info" @click="showPreview">预览</el-button>
<el-button size="medium" type="primary" @click="rename">重命名</el-button>
<el-button size="medium" @click="reset" :disabled="files.length === 0">重置</el-button>
<el-button size="medium" type="primary" @click="rename" :disabled="files.length === 0">重命名</el-button>
</div>
<!-- 预览对话框 -->
<el-dialog
title="重命名预览"
:visible.sync="previewDialogVisible"
width="70%"
:modal="false"
:append-to-body="true"
:before-close="handlePreviewClose">
<div class="preview-area">
<div v-if="files.length === 0" class="text-center text-gray-500 py-8">
<i class="el-icon-document text-3xl mb-2"></i>
<p>未选择文件或预览不可用</p>
</div>
<el-table v-else :data="previewData" max-height="400">
<el-table-column prop="original" label="原文件名" width="300"></el-table-column>
<el-table-column label="→" width="50" align="center"></el-table-column>
<el-table-column prop="new" label="新文件名" width="300"></el-table-column>
</el-table>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="previewDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="rename">确认重命名</el-button>
</span>
</el-dialog>
</el-main>
</el-container>
</div>
@@ -87,17 +58,14 @@ import SelectDir from '@/components/SelectDir/SelectDir.vue'
import InsertTab from './BatchRename/InsertTab.vue'
import DeleteTab from './BatchRename/DeleteTab.vue'
import NumberingTab from './BatchRename/NumberingTab.vue'
import CleanupTab from './BatchRename/CleanupTab.vue'
import AdvancedTab from './BatchRename/AdvancedTab.vue'
import { batchRenameFile } from '@/utils/api'
export default {
components: {
SelectDir,
InsertTab,
DeleteTab,
NumberingTab,
CleanupTab,
AdvancedTab
NumberingTab
},
data() {
return {
@@ -105,33 +73,212 @@ export default {
activeTab: 'insert',
isDragging: false,
filePaths: '',
previewDialogVisible: false
// Insert/Replace tab rules
insertRules: {
position: 'start',
text: '',
searchText: '',
replaceText: '',
replaceOption: 'all',
regexPattern: '',
regexReplace: '',
caseOption: ''
},
// Delete tab rules
deleteRules: {
deleteText: '',
deleteOption: 'all',
startPos: 0,
endPos: 0,
keepExt: true,
deleteAllNames: false,
removeAllExt: false,
specificExt: ''
},
// Numbering tab rules
numberingRules: {
startNum: 1,
step: 1,
repeat: 1,
digits: 1,
numberPosition: 'start',
position: 1,
padZero: true,
padChar: '0',
format: '{n}'
}
}
},
computed: {
previewData() {
return this.files.map(file => ({
original: file.name,
new: file.name // 这里应实现实际的重命名逻辑
}))
return this.files.map((file, index) => {
let newName = file.name
let ext = newName.includes('.') ? newName.substring(newName.lastIndexOf('.')) : ''
let nameWithoutExt = newName.includes('.') ? newName.substring(0, newName.lastIndexOf('.')) : newName
let num, paddedNum, formattedNum, pos
// Apply rules based on active tab
switch (this.activeTab) {
case 'insert':
// Insert/Replace rules
if (this.insertRules.text) {
if (this.insertRules.position === 'start') {
nameWithoutExt = this.insertRules.text + nameWithoutExt
} else if (this.insertRules.position === 'end') {
nameWithoutExt = nameWithoutExt + this.insertRules.text
}
}
if (this.insertRules.searchText && this.insertRules.replaceText) {
if (this.insertRules.replaceOption === 'all') {
const regex = new RegExp(this.insertRules.searchText, 'g')
nameWithoutExt = nameWithoutExt.replace(regex, this.insertRules.replaceText)
} else if (this.insertRules.replaceOption === 'first') {
const index = nameWithoutExt.indexOf(this.insertRules.searchText)
if (index !== -1) {
nameWithoutExt = nameWithoutExt.substring(0, index) +
this.insertRules.replaceText +
nameWithoutExt.substring(index + this.insertRules.searchText.length)
}
} else if (this.insertRules.replaceOption === 'last') {
const lastIndex = nameWithoutExt.lastIndexOf(this.insertRules.searchText)
if (lastIndex !== -1) {
nameWithoutExt = nameWithoutExt.substring(0, lastIndex) +
this.insertRules.replaceText +
nameWithoutExt.substring(lastIndex + this.insertRules.searchText.length)
}
}
}
if (this.insertRules.regexPattern && this.insertRules.regexReplace) {
try {
const regex = new RegExp(this.insertRules.regexPattern)
nameWithoutExt = nameWithoutExt.replace(regex, this.insertRules.regexReplace)
} catch (e) {
console.error('Invalid regex pattern:', e)
}
}
if (this.insertRules.caseOption) {
switch (this.insertRules.caseOption) {
case 'uppercase':
nameWithoutExt = nameWithoutExt.toUpperCase()
break
case 'lowercase':
nameWithoutExt = nameWithoutExt.toLowerCase()
break
case 'titlecase':
nameWithoutExt = nameWithoutExt.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join(' ')
break
}
}
break
case 'delete':
// Delete rules
if (this.deleteRules.deleteText && nameWithoutExt) {
if (this.deleteRules.deleteOption === 'all') {
const regex = new RegExp(this.deleteRules.deleteText, 'g')
nameWithoutExt = nameWithoutExt.replace(regex, '')
} else if (this.deleteRules.deleteOption === 'first') {
const index = nameWithoutExt.indexOf(this.deleteRules.deleteText)
if (index !== -1) {
nameWithoutExt = nameWithoutExt.substring(0, index) +
nameWithoutExt.substring(index + this.deleteRules.deleteText.length)
}
} else if (this.deleteRules.deleteOption === 'last') {
const lastIndex = nameWithoutExt.lastIndexOf(this.deleteRules.deleteText)
if (lastIndex !== -1) {
nameWithoutExt = nameWithoutExt.substring(0, lastIndex) +
nameWithoutExt.substring(lastIndex + this.deleteRules.deleteText.length)
}
}
}
if (this.deleteRules.startPos && this.deleteRules.endPos) {
const start = Math.max(0, this.deleteRules.startPos - 1)
const end = Math.min(nameWithoutExt.length, this.deleteRules.endPos)
if (start < end) {
nameWithoutExt = nameWithoutExt.substring(0, start) + nameWithoutExt.substring(end)
}
}
if (this.deleteRules.deleteAllNames) {
nameWithoutExt = ''
}
if (this.deleteRules.removeAllExt) {
ext = ''
} else if (this.deleteRules.specificExt && ext.toLowerCase() === this.deleteRules.specificExt.toLowerCase()) {
ext = ''
}
break
case 'numbering':
// Numbering rules
num = this.numberingRules.startNum + (index * this.numberingRules.step)
paddedNum = this.numberingRules.padZero
? String(num).padStart(this.numberingRules.digits, this.numberingRules.padChar)
: String(num)
formattedNum = this.numberingRules.format
.replace('{n}', paddedNum)
.replace('{name}', nameWithoutExt)
.replace('{ext}', ext)
switch (this.numberingRules.numberPosition) {
case 'start':
nameWithoutExt = paddedNum + nameWithoutExt
break
case 'end':
nameWithoutExt = nameWithoutExt + paddedNum
break
case 'position':
pos = Math.min(this.numberingRules.position - 1, nameWithoutExt.length)
nameWithoutExt = nameWithoutExt.substring(0, pos) + paddedNum + nameWithoutExt.substring(pos)
break
case 'replace':
nameWithoutExt = formattedNum
break
}
break
}
// Combine name and extension
newName = nameWithoutExt + (this.deleteRules.keepExt ? ext : '')
return {
original: file.name,
new: newName,
path: file.path
}
})
}
},
methods: {
handlePathChange(newPaths) {
this.filePaths = newPaths
// Handle both string and array inputs
const pathArray = Array.isArray(newPaths) ? newPaths : [newPaths]
const newFiles = pathArray.map(path => ({
name: path.split('\\').pop(), // Get the last part of the path as filename
path: path // Store the full path
}))
// Filter out duplicates before adding
const uniqueNewFiles = newFiles.filter(newFile =>
!this.files.some(existingFile => existingFile.path === newFile.path)
)
this.files.push(...uniqueNewFiles)
// Get the paths of currently selected files
const currentPaths = new Set(pathArray)
// Remove files that are no longer selected
this.files = this.files.filter(file => currentPaths.has(file.path))
// Add new files that weren't in the list before
const newFiles = pathArray
.filter(path => !this.files.some(file => file.path === path))
.map(path => ({
name: path.split('\\').pop(), // Get the last part of the path as filename
path: path // Store the full path
}))
this.files.push(...newFiles)
},
handleDrop(e) {
this.unhighlight()
@@ -172,21 +319,35 @@ export default {
this.$message.warning('请选择要重命名的文件')
return
}
this.$message.success(`将重命名 ${this.files.length} 个文件(此演示为模拟操作)`)
},
showPreview() {
if (this.files.length === 0) {
this.$message.warning('请选择要重命名的文件')
return
}
this.previewDialogVisible = true
},
handlePreviewClose(done) {
this.$confirm('确认关闭预览?')
.then(() => {
done()
// Prepare the data for the API call using the preview data
const fileRenames = this.previewData.map(file => ({
absolutePath: file.path,
newName: file.new
}))
// Call the backend API
batchRenameFile(fileRenames)
.then(response => {
console.log(response)
this.$message.success('文件重命名成功')
// Clear both the file list and SelectDir component
this.clearFiles()
this.filePaths = '' // Clear the SelectDir component
})
.catch(() => {})
.catch(error => {
console.error('重命名失败:', error)
this.$message.error('文件重命名失败: ' + (error.message || '未知错误'))
})
},
updateInsertRules(rules) {
this.insertRules = { ...this.insertRules, ...rules }
},
updateDeleteRules(rules) {
this.deleteRules = { ...this.deleteRules, ...rules }
},
updateNumberingRules(rules) {
this.numberingRules = { ...this.numberingRules, ...rules }
}
}
}

View File

@@ -1,84 +0,0 @@
<template>
<div class="grid grid-cols-2 gap-4">
<div>
<el-form label-width="80px">
<el-form-item label="随机字符串">
<div class="grid grid-cols-2 gap-2">
<el-form-item label="长度" label-width="40px">
<el-input-number v-model="randomLength" :min="1" :max="32" size="small"></el-input-number>
</el-form-item>
<el-form-item label="类型" label-width="40px">
<el-select v-model="randomType" size="small">
<el-option label="字母数字混合" value="alphanum"></el-option>
<el-option label="仅字母" value="alpha"></el-option>
<el-option label="仅数字" value="num"></el-option>
<el-option label="自定义字符" value="custom"></el-option>
</el-select>
</el-form-item>
</div>
<div class="mt-2">
<el-checkbox v-model="useUpper">大写</el-checkbox>
<el-checkbox v-model="useLower" class="ml-4">小写</el-checkbox>
</div>
</el-form-item>
<el-form-item label="日期/时间">
<div class="flex gap-2">
<el-select v-model="dateType" class="flex-1">
<el-option label="创建日期" value="created"></el-option>
<el-option label="修改日期" value="modified"></el-option>
<el-option label="当前日期" value="current"></el-option>
</el-select>
<el-input v-model="dateFormat" class="flex-1" placeholder="YYYY-MM-DD"></el-input>
</div>
<div class="text-xs text-gray-500 mt-1">
格式YYYYMMDDHHmmss
</div>
</el-form-item>
</el-form>
</div>
<div>
<el-form label-width="80px">
<el-form-item label="文件元数据">
<div class="flex gap-2">
<el-select v-model="metadataType" class="flex-1">
<el-option label="文件大小" value="size"></el-option>
<el-option label="MD5 哈希" value="md5"></el-option>
<el-option label="SHA-1 哈希" value="sha1"></el-option>
<el-option label="尺寸(图片)" value="dimensions"></el-option>
<el-option label="时长(媒体)" value="duration"></el-option>
</el-select>
<el-select v-model="metadataPosition" class="flex-1">
<el-option label="在开头" value="start"></el-option>
<el-option label="在结尾" value="end"></el-option>
<el-option label="替换整个名称" value="replace"></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item label="JS 规则">
<el-input type="textarea" v-model="jsRule" :rows="4" placeholder="function rename(name, index) { return name.toUpperCase(); }"></el-input>
<div class="text-xs text-gray-500 mt-1">
可用变量name文件名ext扩展名index文件编号
</div>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
randomLength: 8,
randomType: 'alphanum',
useUpper: false,
useLower: true,
dateType: 'created',
dateFormat: 'YYYY-MM-DD',
metadataType: 'size',
metadataPosition: 'start',
jsRule: ''
}
}
}
</script>

View File

@@ -1,67 +0,0 @@
<template>
<div class="grid grid-cols-2 gap-4">
<div>
<el-form label-width="80px">
<el-form-item label="移除内容">
<div class="flex flex-wrap gap-2">
<el-button size="small" @click="removeParentheses">移除括号 () 中的文本</el-button>
<el-button size="small" @click="removeBrackets">移除方括号 [] 中的文本</el-button>
<el-button size="small" @click="removeBraces">移除大括号 {} 中的文本</el-button>
<el-button size="small" @click="removeAllBrackets">移除所有括号</el-button>
</div>
</el-form-item>
<el-form-item label="修剪选项">
<div class="flex flex-wrap gap-2">
<el-button size="small" @click="trimSpaces">修剪空格</el-button>
<el-button size="small" @click="trimUnderscores">修剪下划线</el-button>
<el-button size="small" @click="trimDots">修剪点号</el-button>
<el-button size="small" @click="trimAll">修剪所有</el-button>
</div>
</el-form-item>
</el-form>
</div>
<div>
<el-form label-width="80px">
<el-form-item label="替换字符">
<div class="flex flex-wrap gap-2">
<el-button size="small" @click="spaceToUnderscore">空格转为下划线</el-button>
<el-button size="small" @click="underscoreToSpace">下划线转为空格</el-button>
<el-button size="small" @click="spaceToDot">空格转为点号</el-button>
<el-button size="small" @click="dotToSpace">点号转为空格</el-button>
</div>
</el-form-item>
<el-form-item label="重复字符">
<div class="flex flex-wrap gap-2">
<el-button size="small" @click="removeDupSpaces">移除重复空格</el-button>
<el-button size="small" @click="removeDupUnderscores">移除重复下划线</el-button>
<el-button size="small" @click="removeDupDots">移除重复点号</el-button>
<el-button size="small" @click="removeAllDup">移除所有重复</el-button>
</div>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
methods: {
removeParentheses() { this.$message.info('移除括号内容(模拟操作)') },
removeBrackets() { this.$message.info('移除方括号内容(模拟操作)') },
removeBraces() { this.$message.info('移除大括号内容(模拟操作)') },
removeAllBrackets() { this.$message.info('移除所有括号(模拟操作)') },
trimSpaces() { this.$message.info('修剪空格(模拟操作)') },
trimUnderscores() { this.$message.info('修剪下划线(模拟操作)') },
trimDots() { this.$message.info('修剪点号(模拟操作)') },
trimAll() { this.$message.info('修剪所有(模拟操作)') },
spaceToUnderscore() { this.$message.info('空格转为下划线(模拟操作)') },
underscoreToSpace() { this.$message.info('下划线转为空格(模拟操作)') },
spaceToDot() { this.$message.info('空格转为点号(模拟操作)') },
dotToSpace() { this.$message.info('点号转为空格(模拟操作)') },
removeDupSpaces() { this.$message.info('移除重复空格(模拟操作)') },
removeDupUnderscores() { this.$message.info('移除重复下划线(模拟操作)') },
removeDupDots() { this.$message.info('移除重复点号(模拟操作)') },
removeAllDup() { this.$message.info('移除所有重复(模拟操作)') }
}
}
</script>

View File

@@ -2,7 +2,7 @@
<div class="grid grid-cols-2 gap-4">
<div>
<el-form label-width="80px">
<el-form-item label="文本除">
<el-form-item label="文本除">
<div class="flex gap-2">
<el-input v-model="deleteText" placeholder="要删除的文本" class="flex-1"></el-input>
<el-select v-model="deleteOption" class="flex-1">
@@ -12,11 +12,11 @@
</el-select>
</div>
</el-form-item>
<el-form-item label="按位置删除">
<el-form-item label="索引移除">
<div class="flex gap-2 items-center">
<el-input-number v-model="startPos" :min="1" size="small" placeholder="起始"></el-input-number>
<el-input-number v-model="startPos" :min="0" size="small" placeholder="起始"></el-input-number>
<span></span>
<el-input-number v-model="endPos" :min="1" size="small" placeholder="结束"></el-input-number>
<el-input-number v-model="endPos" :min="0" size="small" placeholder="结束"></el-input-number>
<span>字符</span>
</div>
</el-form-item>
@@ -24,11 +24,11 @@
</div>
<div>
<el-form label-width="80px">
<el-form-item label="删除文件名">
<el-form-item label="清空名称">
<el-checkbox v-model="keepExt">保留扩展名</el-checkbox>
<el-button type="danger" size="small" class="ml-2" @click="deleteAllNames">删除所有名称</el-button>
</el-form-item>
<el-form-item label="除扩展">
<el-form-item label="除扩展">
<div class="flex gap-2">
<el-button size="small" @click="removeAllExt">移除所有扩展名</el-button>
<el-input v-model="specificExt" placeholder=".ext" style="width: 100px;"></el-input>
@@ -42,29 +42,61 @@
<script>
export default {
props: {
rules: {
type: Object,
required: true
}
},
data() {
return {
deleteText: '',
deleteOption: 'all',
startPos: 1,
endPos: 1,
keepExt: true,
specificExt: ''
deleteText: this.rules.deleteText || '',
deleteOption: this.rules.deleteOption || 'all',
startPos: this.rules.startPos || 0,
endPos: this.rules.endPos || 0,
keepExt: this.rules.keepExt !== undefined ? this.rules.keepExt : true,
specificExt: this.rules.specificExt || ''
}
},
watch: {
deleteText() { this.emitRules() },
deleteOption() { this.emitRules() },
startPos() { this.emitRules() },
endPos() { this.emitRules() },
keepExt() { this.emitRules() },
specificExt() { this.emitRules() }
},
methods: {
deleteAllNames() {
this.$message.info('删除所有名称(模拟操作)')
this.$emit('updateRules', { ...this.getRules(), deleteAllNames: true })
this.$message.success('已删除所有文件名')
},
removeAllExt() {
this.$message.info('移除所有扩展名(模拟操作)')
this.$emit('updateRules', { ...this.getRules(), removeAllExt: true })
this.$message.success('已移除所有扩展名')
},
removeSpecificExt() {
if (!this.specificExt) {
this.$message.warning('请输入要移除的扩展名')
return
}
this.$message.info(`移除扩展名 ${this.specificExt}(模拟操作)`)
this.emitRules()
this.$message.success(`已移除扩展名 ${this.specificExt}`)
},
getRules() {
return {
deleteText: this.deleteText,
deleteOption: this.deleteOption,
startPos: this.startPos,
endPos: this.endPos,
keepExt: this.keepExt,
deleteAllNames: false,
removeAllExt: false,
specificExt: this.specificExt
}
},
emitRules() {
this.$emit('updateRules', this.getRules())
}
}
}

View File

@@ -3,19 +3,21 @@
<div>
<el-form label-width="80px">
<el-form-item label="插入文本">
<div class="flex gap-2">
<el-select v-model="insertPosition" class="flex-1">
<div class="insert-form-row">
<el-select v-model="insertPosition">
<el-option label="在开头" value="start"></el-option>
<el-option label="在结尾" value="end"></el-option>
<el-option label="在指定位置" value="position"></el-option>
</el-select>
<el-input v-model="insertText" placeholder="要插入的文本" class="flex-1"></el-input>
<el-input v-model="insertText" placeholder="要插入的文本"></el-input>
</div>
</el-form-item>
<el-form-item label="替换文本">
<el-input v-model="searchText" placeholder="查找" class="mb-2"></el-input>
<el-input v-model="replaceText" placeholder="替换为" class="mb-2"></el-input>
<el-radio-group v-model="replaceOption">
<div class="replace-form-row">
<el-input v-model="searchText" placeholder="查找"></el-input>
<el-input v-model="replaceText" placeholder="替换为"></el-input>
</div>
<el-radio-group v-model="replaceOption" class="mt-2">
<el-radio label="all">全部替换</el-radio>
<el-radio label="first">仅首次</el-radio>
<el-radio label="last">仅最后</el-radio>
@@ -25,18 +27,20 @@
</div>
<div>
<el-form label-width="80px">
<el-form-item label="正则表达">
<el-input v-model="regexPattern" placeholder="正则模式" class="mb-2"></el-input>
<el-input v-model="regexReplace" placeholder="替换为 ($1, $2...)" class="mb-2"></el-input>
<div class="text-xs text-gray-500">
<el-form-item label="正则表达">
<div class="regex-form-row">
<el-input v-model="regexPattern" placeholder="正则模式"></el-input>
<el-input v-model="regexReplace" placeholder="替换为 ($1, $2...)"></el-input>
</div>
<div class="text-xs text-gray-500 mt-2">
使用 $1, $2 等引用捕获组例如"(.*)\.(.*)" "$2.$1" 可交换点前后的部分
</div>
</el-form-item>
<el-form-item label="大小写选项">
<el-form-item label="大小写">
<el-button-group>
<el-button size="small">全大写</el-button>
<el-button size="small">全小写</el-button>
<el-button size="small">标题格式</el-button>
<el-button size="small" @click="handleCaseChange('uppercase')" :type="caseOption === 'uppercase' ? 'primary' : ''">全大写</el-button>
<el-button size="small" @click="handleCaseChange('lowercase')" :type="caseOption === 'lowercase' ? 'primary' : ''">全小写</el-button>
<el-button size="small" @click="handleCaseChange('titlecase')" :type="caseOption === 'titlecase' ? 'primary' : ''">标题格式</el-button>
</el-button-group>
</el-form-item>
</el-form>
@@ -46,16 +50,119 @@
<script>
export default {
props: {
rules: {
type: Object,
required: true
}
},
data() {
return {
insertPosition: 'start',
insertText: '',
searchText: '',
replaceText: '',
replaceOption: 'all',
regexPattern: '',
regexReplace: ''
insertPosition: this.rules.position || 'start',
insertText: this.rules.text || '',
searchText: this.rules.searchText || '',
replaceText: this.rules.replaceText || '',
replaceOption: this.rules.replaceOption || 'all',
regexPattern: this.rules.regexPattern || '',
regexReplace: this.rules.regexReplace || '',
caseOption: this.rules.caseOption || ''
}
},
watch: {
insertPosition: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
insertText: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
searchText: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
replaceText: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
replaceOption: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
regexPattern: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
regexReplace: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
},
caseOption: {
handler(newVal) {
this.emitRulesUpdate(newVal)
}
}
},
methods: {
handleCaseChange(option) {
this.caseOption = this.caseOption === option ? '' : option
this.emitRulesUpdate()
},
emitRulesUpdate() {
this.$emit('updateRules', {
position: this.insertPosition,
text: this.insertText,
searchText: this.searchText,
replaceText: this.replaceText,
replaceOption: this.replaceOption,
regexPattern: this.regexPattern,
regexReplace: this.regexReplace,
caseOption: this.caseOption
})
}
}
}
</script>
</script>
<style scoped>
.insert-form-row {
display: flex;
align-items: center;
gap: 8px;
}
.insert-form-row :deep(.el-select) {
width: 120px;
}
.insert-form-row :deep(.el-input) {
flex: 1;
}
.replace-form-row {
display: flex;
align-items: center;
gap: 8px;
}
.replace-form-row :deep(.el-input) {
flex: 1;
}
.regex-form-row {
display: flex;
align-items: center;
gap: 8px;
}
.regex-form-row :deep(.el-input) {
flex: 1;
}
</style>

View File

@@ -4,13 +4,13 @@
<el-form label-width="80px">
<el-form-item label="编号选项">
<div class="grid grid-cols-2 gap-2">
<el-form-item label="起始数字" label-width="60px">
<el-form-item label="起始" label-width="60px">
<el-input-number v-model="startNum" :min="0" size="small"></el-input-number>
</el-form-item>
<el-form-item label="步长" label-width="60px">
<el-input-number v-model="step" :min="1" size="small"></el-input-number>
</el-form-item>
<el-form-item label="重复次数" label-width="60px">
<el-form-item label="重复" label-width="60px">
<el-input-number v-model="repeat" :min="1" size="small"></el-input-number>
</el-form-item>
<el-form-item label="位数" label-width="60px">
@@ -43,7 +43,7 @@
<el-button type="primary" size="small" @click="previewNumber">预览</el-button>
</div>
<div class="text-xs text-gray-500 mt-1">
使用 {n} 表示数字{name} 表示原文件名{ext} 表示扩展名
使用 {n} 表示数字{name} 表示原文件名{ext} 表示扩展名替换整个名称时可用
</div>
</el-form-item>
</el-form>
@@ -53,22 +53,67 @@
<script>
export default {
props: {
rules: {
type: Object,
required: true
}
},
data() {
return {
startNum: 1,
step: 1,
repeat: 1,
digits: 1,
numberPosition: 'start',
position: 1,
padZero: true,
padChar: '0',
format: '{n}'
startNum: this.rules.startNum || 1,
step: this.rules.step || 1,
repeat: this.rules.repeat || 1,
digits: this.rules.digits || 1,
numberPosition: this.rules.numberPosition || 'start',
position: this.rules.position || 1,
padZero: this.rules.padZero !== undefined ? this.rules.padZero : true,
padChar: this.rules.padChar || '0',
format: this.rules.format || '{n}'
}
},
watch: {
startNum() { this.emitRules() },
step() { this.emitRules() },
repeat() { this.emitRules() },
digits() { this.emitRules() },
numberPosition() { this.emitRules() },
position() { this.emitRules() },
padZero() { this.emitRules() },
padChar() { this.emitRules() },
format() { this.emitRules() }
},
methods: {
previewNumber() {
this.$message.info('预览数字格式(模拟操作)')
let result = []
for (let i = 0; i < this.repeat; i++) {
const num = this.startNum + (i * this.step)
const paddedNum = this.padZero
? String(num).padStart(this.digits, this.padChar)
: String(num)
const preview = this.format
.replace('{n}', paddedNum)
.replace('{name}', 'example')
.replace('{ext}', '.txt')
result.push(preview)
}
this.$message.success(`预览: ${result.join(', ')}`)
},
emitRules() {
this.$emit('updateRules', {
startNum: this.startNum,
step: this.step,
repeat: this.repeat,
digits: this.digits,
numberPosition: this.numberPosition,
position: this.position,
padZero: this.padZero,
padChar: this.padChar,
format: this.format
})
}
}
}

View File

@@ -10,6 +10,10 @@ const routes = [
path: '/',
name: 'home',
component: MainPage
},
{
path: '/LabelStudio',
name: 'LabelStudio'
}
]

View File

@@ -351,6 +351,15 @@ const decryptFile = (filePath, password) => {
return http.post("/localFile/decrypt", formData);
}
/**
* 批量修改文件名
* @param {array} fileRenames
* @returns
*/
const batchRenameFile = (fileRenames) => {
return http.post("/localFile/batchRenameFile", fileRenames);
}
export {
getUploadProgress,
createMultipartUpload,
@@ -378,5 +387,6 @@ export {
getFileTree,
encryptFile,
decryptFile,
batchRenameFile,
httpExtra
};