// RenameRule.tsx - 修复占位符替换顺序 import { DownOutlined, ExclamationCircleOutlined, QuestionCircleOutlined, UpOutlined, } from '@ant-design/icons'; import { Alert, Button, Card, Input, InputNumber, Select, Space, Tooltip, Typography, } from 'antd'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import './index.less'; const { Text } = Typography; const { Option } = Select; export interface FileItem { id: number; originalName: string; } export interface RenameRule { pattern: string; startNumber: number; numberDirection: 'up' | 'down'; matchPattern: string; useMatch: boolean; numberPadding: number; } export interface RenameResult extends FileItem { newName: string; isChanged: boolean; isMatched: boolean; } interface RenameRuleProps { files: FileItem[]; onPreview?: (results: RenameResult[]) => void; onRename?: (results: RenameResult[]) => void; loading?: boolean; className?: string; } // 占位符配置 // const PLACEHOLDERS = [ // { key: "$n", desc: "数字序号(不补零)", example: "1, 2, 3..." }, // { key: "$nn", desc: "数字序号(2位补零)", example: "01, 02, 03..." }, // { key: "$nnn", desc: "数字序号(3位补零)", example: "001, 002, 003..." }, // { key: "$nnnn", desc: "数字序号(4位补零)", example: "0001, 0002, 0003..." }, // { key: "$name", desc: "原文件名", example: "photo1" }, // { key: "$ext", desc: "扩展名(不含点)", example: "jpg" }, // { key: "$EXT", desc: "扩展名(大写)", example: "JPG" }, // ]; const RenameRuleComponent: React.FC = ({ files, onPreview, className, }) => { const [rule, setRule] = useState({ pattern: '', startNumber: 1, numberDirection: 'up', matchPattern: '', useMatch: false, numberPadding: 2, }); const [matchError, setMatchError] = useState(''); // 初始化时自动设置模式 useEffect(() => { console.log(files); if (files.length > 0 && !rule.pattern) { setRule((prev) => ({ ...prev, pattern: '$name$nn' })); } }, [files, rule.pattern]); // 安全的正则表达式匹配 const isFileMatched = useCallback( (file: FileItem, pattern: string): boolean => { if (!pattern.trim()) return true; try { const regex = new RegExp(pattern, 'i'); const fullFileName = file.originalName; return ( regex.test(file.originalName) || // regex.test(file.extension) || // regex.test(file.extension?.replace(".", "")) || regex.test(fullFileName) ); } catch (error) { console.warn('正则表达式错误:', error); setMatchError( `正则表达式错误: ${ error instanceof Error ? error.message : '未知错误' }`, ); return false; } }, [], ); // 修复的占位符替换函数 const replacePlaceholders = useCallback( (pattern: string, file: FileItem, fileIndex: number) => { let result = pattern; // 计算序号 let number: number; if (rule.numberDirection === 'up') { number = rule.startNumber + fileIndex; } else { number = rule.startNumber - fileIndex; } // 先用临时标记替换非数字占位符 const tempMarkers = { name: '___TEMP_NAME___', ext: '___TEMP_EXT___', EXT: '___TEMP_EXT_UPPER___', }; // 第一步:替换非数字占位符为临时标记 result = result.replace(/\$name/g, tempMarkers.name); result = result.replace(/\$EXT/g, tempMarkers.EXT); result = result.replace(/\$ext/g, tempMarkers.ext); // 第二步:替换数字占位符(按长度从长到短,避免冲突) result = result.replace(/\$nnnn/g, number.toString().padStart(4, '0')); result = result.replace(/\$nnn/g, number.toString().padStart(3, '0')); result = result.replace(/\$nn/g, number.toString().padStart(2, '0')); result = result.replace(/\$n/g, number.toString()); // 第三步:将临时标记替换为实际值 result = result.replace( new RegExp(tempMarkers.name, 'g'), file.originalName, ); // result = result.replace( // new RegExp(tempMarkers.ext, "g"), // // file.extension.replace(".", "") // ); // result = result.replace( // new RegExp(tempMarkers.EXT, "g"), // file.extension.replace(".", "").toUpperCase() // ); return result; }, [rule.startNumber, rule.numberDirection], ); // 使用 useMemo 缓存计算结果 const previewResults = useMemo(() => { setMatchError(''); const matchedFiles: FileItem[] = []; const allResults: RenameResult[] = []; files.forEach((file) => { const isMatched = rule.useMatch ? isFileMatched(file, rule.matchPattern) : true; if (isMatched) { matchedFiles.push(file); } }); files.forEach((file) => { const isMatched = rule.useMatch ? isFileMatched(file, rule.matchPattern) : true; if (!isMatched || !rule.pattern) { allResults.push({ ...file, newName: file.originalName, isChanged: false, isMatched, }); return; } const matchedIndex = matchedFiles.findIndex((f) => f.id === file.id); const newName = replacePlaceholders(rule.pattern, file, matchedIndex); const isChanged = newName !== file.originalName; allResults.push({ ...file, newName, isChanged, isMatched, }); }); return allResults; }, [files, rule, isFileMatched, replacePlaceholders]); // 调用预览回调 useEffect(() => { if (onPreview) { onPreview(previewResults); } }, [previewResults]); // // 重置规则 // const handleReset = useCallback(() => { // setRule({ // pattern: "$name$nn", // startNumber: 1, // numberDirection: "up", // matchPattern: "", // useMatch: false, // numberPadding: 2, // }); // setMatchError(""); // }, []); // 规则更新处理函数 const updateRule = useCallback((updates: Partial) => { setRule((prev) => ({ ...prev, ...updates })); if (updates.matchPattern !== undefined) { setMatchError(''); } }, []); // // 插入占位符 // const insertPlaceholder = useCallback( // (placeholder: string) => { // const input = document.querySelector( // ".pattern-input" // ) as HTMLInputElement; // if (input) { // const start = input.selectionStart || 0; // const end = input.selectionEnd || 0; // const currentValue = rule.pattern; // const newValue = // currentValue.slice(0, start) + placeholder + currentValue.slice(end); // updateRule({ pattern: newValue }); // setTimeout(() => { // input.focus(); // input.setSelectionRange( // start + placeholder.length, // start + placeholder.length // ); // }, 0); // } // }, // [rule.pattern, updateRule] // ); // // 递增起始编号 // const incrementStartNumber = useCallback(() => { // updateRule({ startNumber: rule.startNumber + 1 }); // }, [rule.startNumber, updateRule]); // // 递减起始编号 // const decrementStartNumber = useCallback(() => { // updateRule({ startNumber: Math.max(0, rule.startNumber - 1) }); // }, [rule.startNumber, updateRule]); // 快速设置常用模式 const setQuickPattern = useCallback( (pattern: string) => { updateRule({ pattern }); }, [updateRule], ); return (
预览: {previewResults.map((item) => (

{`${item.newName}`}

))}
{/* 错误提示 */} {matchError && ( } closable onClose={() => setMatchError('')} /> )} 快速模式: {/* 命名模式 */} updateRule({ pattern: e.target.value })} suffix={ } /> updateRule({ matchPattern: e.target.value })} /> 起始编号: updateRule({ startNumber: value || 1 })} style={{ width: '60%', textAlign: 'center' }} controls={true} />
); }; export default React.memo(RenameRuleComponent);