399 lines
11 KiB
TypeScript
399 lines
11 KiB
TypeScript
// 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<RenameRuleProps> = ({
|
|
files,
|
|
onPreview,
|
|
className,
|
|
}) => {
|
|
const [rule, setRule] = useState<RenameRule>({
|
|
pattern: '',
|
|
startNumber: 1,
|
|
numberDirection: 'up',
|
|
matchPattern: '',
|
|
useMatch: false,
|
|
numberPadding: 2,
|
|
});
|
|
|
|
const [matchError, setMatchError] = useState<string>('');
|
|
|
|
// 初始化时自动设置模式
|
|
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<RenameRule>) => {
|
|
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 (
|
|
<Card className={`rename-rule-card ${className || ''}`}>
|
|
<div className="rename-rule-card-preview">
|
|
<div className="left">
|
|
<Text strong style={{ marginBottom: 8, display: 'block' }}>
|
|
预览:
|
|
</Text>
|
|
{previewResults.map((item) => (
|
|
<p key={item.id}>{`${item.newName}`}</p>
|
|
))}
|
|
</div>
|
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
|
{/* 错误提示 */}
|
|
{matchError && (
|
|
<Alert
|
|
message="匹配模式错误"
|
|
description={matchError}
|
|
type="error"
|
|
icon={<ExclamationCircleOutlined />}
|
|
closable
|
|
onClose={() => setMatchError('')}
|
|
/>
|
|
)}
|
|
|
|
<Space wrap>
|
|
<Text strong>快速模式:</Text>
|
|
<Button
|
|
size="small"
|
|
onClick={() => setQuickPattern('$name$nn')}
|
|
type={rule.pattern === '$name$nn' ? 'primary' : 'default'}
|
|
>
|
|
原名+序号
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
onClick={() => setQuickPattern('$nn_$name')}
|
|
type={rule.pattern === '$nn_$name' ? 'primary' : 'default'}
|
|
>
|
|
序号+原名
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
onClick={() => setQuickPattern('新文件$nnn')}
|
|
type={rule.pattern === '新文件$nnn' ? 'primary' : 'default'}
|
|
>
|
|
新文件+序号
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
onClick={() => setQuickPattern('$name_副本$nn')}
|
|
type={rule.pattern === '$name_副本$nn' ? 'primary' : 'default'}
|
|
>
|
|
原名+副本
|
|
</Button>
|
|
</Space>
|
|
|
|
{/* 命名模式 */}
|
|
|
|
<Space>
|
|
<Input
|
|
className="pattern-input"
|
|
placeholder="例如: $name$nn 或 新文件$nnn"
|
|
value={rule.pattern}
|
|
allowClear
|
|
onChange={(e) => updateRule({ pattern: e.target.value })}
|
|
suffix={
|
|
<Tooltip title="使用占位符创建命名规则">
|
|
<QuestionCircleOutlined style={{ color: '#bfbfbf' }} />
|
|
</Tooltip>
|
|
}
|
|
/>
|
|
</Space>
|
|
<Input
|
|
placeholder="例如: photo (匹配包含photo的文件) 或 \.jpg$ (匹配jpg文件)"
|
|
value={rule.matchPattern}
|
|
allowClear
|
|
onChange={(e) => updateRule({ matchPattern: e.target.value })}
|
|
/>
|
|
<Space>
|
|
<Text strong>起始编号:</Text>
|
|
<InputNumber
|
|
min={0}
|
|
max={9999}
|
|
value={rule.startNumber}
|
|
onChange={(value) => updateRule({ startNumber: value || 1 })}
|
|
style={{ width: '60%', textAlign: 'center' }}
|
|
controls={true}
|
|
/>
|
|
<Select
|
|
value={rule.numberDirection}
|
|
onChange={(value) => updateRule({ numberDirection: value })}
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Option value="up">
|
|
<Space>
|
|
<UpOutlined />
|
|
递增
|
|
</Space>
|
|
</Option>
|
|
<Option value="down">
|
|
<Space>
|
|
<DownOutlined />
|
|
递减
|
|
</Space>
|
|
</Option>
|
|
</Select>
|
|
</Space>
|
|
</Space>
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default React.memo(RenameRuleComponent);
|