feat: 样本管理
This commit is contained in:
398
src/components/RenameRule/index.tsx
Normal file
398
src/components/RenameRule/index.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user