feat: 样本管理

This commit is contained in:
2025-10-13 16:50:21 +08:00
parent 4e9ebc76f7
commit 9eb4f52f0e
25 changed files with 3066 additions and 344 deletions

View 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);