feat: 添加手动输入和文件上传模式切换功能,支持密钥搜索和高亮显示

This commit is contained in:
CaIon
2025-07-17 19:53:33 +08:00
parent 6f81f2d143
commit a5da09dfb9

View File

@@ -26,6 +26,7 @@ import {
Form, Form,
Row, Row,
Col, Col,
Highlight,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers'; import { getChannelModels, copy, getChannelIcon, getModelCategories } from '../../helpers';
import { import {
@@ -122,6 +123,8 @@ const EditChannel = (props) => {
const [vertexFileList, setVertexFileList] = useState([]); const [vertexFileList, setVertexFileList] = useState([]);
const vertexErroredNames = useRef(new Set()); // 避免重复报错 const vertexErroredNames = useRef(new Set()); // 避免重复报错
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false); const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
const [channelSearchValue, setChannelSearchValue] = useState('');
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
const getInitValues = () => ({ ...originInputs }); const getInitValues = () => ({ ...originInputs });
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
if (formApiRef.current) { if (formApiRef.current) {
@@ -190,6 +193,9 @@ const EditChannel = (props) => {
setInputs((inputs) => ({ ...inputs, models: localModels })); setInputs((inputs) => ({ ...inputs, models: localModels }));
} }
setBasicModels(localModels); setBasicModels(localModels);
// 重置手动输入模式状态
setUseManualInput(false);
} }
//setAutoBan //setAutoBan
}; };
@@ -418,6 +424,8 @@ const EditChannel = (props) => {
} else { } else {
formApiRef.current?.setValues(getInitValues()); formApiRef.current?.setValues(getInitValues());
} }
// 重置手动输入模式状态
setUseManualInput(false);
} else { } else {
formApiRef.current?.reset(); formApiRef.current?.reset();
} }
@@ -468,41 +476,60 @@ const EditChannel = (props) => {
let localInputs = { ...formValues }; let localInputs = { ...formValues };
if (localInputs.type === 41) { if (localInputs.type === 41) {
let keys = vertexKeys; if (useManualInput) {
// 手动输入模式
// 若当前未选择文件,尝试从已上传文件列表解析(异步读取) if (localInputs.key && localInputs.key.trim() !== '') {
if (keys.length === 0 && vertexFileList.length > 0) { try {
try { // 验证 JSON 格式
const parsed = await Promise.all( const parsedKey = JSON.parse(localInputs.key);
vertexFileList.map(async (item) => { // 确保是有效的密钥格式
const fileObj = item.fileInstance; localInputs.key = JSON.stringify(parsedKey);
if (!fileObj) return null; } catch (err) {
const txt = await fileObj.text(); showError(t('密钥格式无效,请输入有效的 JSON 格式密钥'));
return JSON.parse(txt); return;
}) }
); } else if (!isEdit) {
keys = parsed.filter(Boolean); showInfo(t('请输入密钥!'));
} catch (err) {
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
return; return;
} }
}
// 创建模式必须上传密钥;编辑模式可选
if (keys.length === 0) {
if (!isEdit) {
showInfo(t('请上传密钥文件!'));
return;
} else {
// 编辑模式且未上传新密钥,不修改 key
delete localInputs.key;
}
} else { } else {
// 有新密钥,则覆盖 // 文件上传模式
if (batch) { let keys = vertexKeys;
localInputs.key = JSON.stringify(keys);
// 若当前未选择文件,尝试从已上传文件列表解析(异步读取)
if (keys.length === 0 && vertexFileList.length > 0) {
try {
const parsed = await Promise.all(
vertexFileList.map(async (item) => {
const fileObj = item.fileInstance;
if (!fileObj) return null;
const txt = await fileObj.text();
return JSON.parse(txt);
})
);
keys = parsed.filter(Boolean);
} catch (err) {
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
return;
}
}
// 创建模式必须上传密钥;编辑模式可选
if (keys.length === 0) {
if (!isEdit) {
showInfo(t('请上传密钥文件!'));
return;
} else {
// 编辑模式且未上传新密钥,不修改 key
delete localInputs.key;
}
} else { } else {
localInputs.key = JSON.stringify(keys[0]); // 有新密钥,则覆盖
if (batch) {
localInputs.key = JSON.stringify(keys);
} else {
localInputs.key = JSON.stringify(keys[0]);
}
} }
} }
} }
@@ -646,23 +673,33 @@ const EditChannel = (props) => {
if (!checked) { if (!checked) {
setMultiToSingle(false); setMultiToSingle(false);
setMultiKeyMode('random'); setMultiKeyMode('random');
} else {
// 批量模式下禁用手动输入,并清空手动输入的内容
setUseManualInput(false);
if (inputs.type === 41) {
// 清空手动输入的密钥内容
if (formApiRef.current) {
formApiRef.current.setValue('key', '');
}
handleInputChange('key', '');
}
} }
}} }}
>{t('批量创建')}</Checkbox> >{t('批量创建')}</Checkbox>
{batch && ( {/*{batch && (*/}
<Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => { {/* <Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {*/}
setMultiToSingle(prev => !prev); {/* setMultiToSingle(prev => !prev);*/}
setInputs(prev => { {/* setInputs(prev => {*/}
const newInputs = { ...prev }; {/* const newInputs = { ...prev };*/}
if (!multiToSingle) { {/* if (!multiToSingle) {*/}
newInputs.multi_key_mode = multiKeyMode; {/* newInputs.multi_key_mode = multiKeyMode;*/}
} else { {/* } else {*/}
delete newInputs.multi_key_mode; {/* delete newInputs.multi_key_mode;*/}
} {/* }*/}
return newInputs; {/* return newInputs;*/}
}); {/* });*/}
}}>{t('密钥聚合模式')}</Checkbox> {/* }}>{t('密钥聚合模式')}</Checkbox>*/}
)} {/*)}*/}
</Space> </Space>
) : null; ) : null;
@@ -670,16 +707,68 @@ const EditChannel = (props) => {
() => () =>
CHANNEL_OPTIONS.map((opt) => ({ CHANNEL_OPTIONS.map((opt) => ({
...opt, ...opt,
label: ( // 保持 label 为纯文本以支持搜索
<span className="flex items-center gap-2"> label: opt.label,
{getChannelIcon(opt.value)}
{opt.label}
</span>
),
})), })),
[], [],
); );
const renderChannelOption = (renderProps) => {
const {
disabled,
selected,
label,
value,
focused,
className,
style,
onMouseEnter,
onClick,
...rest
} = renderProps;
const searchWords = channelSearchValue ? [channelSearchValue] : [];
// 构建样式类名
const optionClassName = [
'flex items-center gap-3 px-3 py-2 transition-all duration-200 rounded-lg mx-2 my-1',
focused && 'bg-blue-50 shadow-sm',
selected && 'bg-blue-100 text-blue-700 shadow-lg ring-2 ring-blue-200 ring-opacity-50',
disabled && 'opacity-50 cursor-not-allowed',
!disabled && 'hover:bg-gray-50 hover:shadow-md cursor-pointer',
className
].filter(Boolean).join(' ');
return (
<div
style={style}
className={optionClassName}
onClick={() => !disabled && onClick()}
onMouseEnter={e => onMouseEnter()}
>
<div className="flex items-center gap-3 w-full">
<div className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
{getChannelIcon(value)}
</div>
<div className="flex-1 min-w-0">
<Highlight
sourceString={label}
searchWords={searchWords}
className="text-sm font-medium truncate"
/>
</div>
{selected && (
<div className="flex-shrink-0 text-blue-600">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
</svg>
</div>
)}
</div>
</div>
);
};
return ( return (
<> <>
<SideSheet <SideSheet
@@ -749,6 +838,8 @@ const EditChannel = (props) => {
style={{ width: '100%' }} style={{ width: '100%' }}
filter filter
searchPosition='dropdown' searchPosition='dropdown'
onSearch={(value) => setChannelSearchValue(value)}
renderOptionItem={renderChannelOption}
onChange={(value) => handleInputChange('type', value)} onChange={(value) => handleInputChange('type', value)}
/> />
@@ -797,22 +888,91 @@ const EditChannel = (props) => {
) : ( ) : (
<> <>
{inputs.type === 41 ? ( {inputs.type === 41 ? (
<Form.Upload <>
field='vertex_files' {!batch && (
label={t('密钥文件 (.json)')} <div className="flex items-center justify-between mb-3">
accept='.json' <Text className="text-sm font-medium">{t('密钥输入方式')}</Text>
draggable <Space>
dragIcon={<IconBolt />} <Button
dragMainText={t('点击上传文件或拖拽文件到这里')} size="small"
dragSubText={t('仅支持 JSON 文件')} type={!useManualInput ? 'primary' : 'tertiary'}
style={{ marginTop: 10 }} onClick={() => {
uploadTrigger='custom' setUseManualInput(false);
beforeUpload={() => false} // 切换到文件上传模式时清空手动输入的密钥
onChange={handleVertexUploadChange} if (formApiRef.current) {
fileList={vertexFileList} formApiRef.current.setValue('key', '');
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]} }
extraText={batchExtra} handleInputChange('key', '');
/> }}
>
{t('文件上传')}
</Button>
<Button
size="small"
type={useManualInput ? 'primary' : 'tertiary'}
onClick={() => {
setUseManualInput(true);
// 切换到手动输入模式时清空文件上传相关状态
setVertexKeys([]);
setVertexFileList([]);
if (formApiRef.current) {
formApiRef.current.setValue('vertex_files', []);
}
setInputs((prev) => ({ ...prev, vertex_files: [] }));
}}
>
{t('手动输入')}
</Button>
</Space>
</div>
)}
{batch && (
<Banner
type='info'
description={t('批量创建模式下仅支持文件上传,不支持手动输入')}
className='!rounded-lg mb-3'
/>
)}
{useManualInput && !batch ? (
<Form.TextArea
field='key'
label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
placeholder={t('请输入 JSON 格式的密钥内容,例如:\n{\n "type": "service_account",\n "project_id": "your-project-id",\n "private_key_id": "...",\n "private_key": "...",\n "client_email": "...",\n "client_id": "...",\n "auth_uri": "...",\n "token_uri": "...",\n "auth_provider_x509_cert_url": "...",\n "client_x509_cert_url": "..."\n}')}
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
extraText={
<div className="flex items-center gap-2">
<Text type="tertiary" size="small">
{t('请输入完整的 JSON 格式密钥内容')}
</Text>
{batchExtra}
</div>
}
autosize
showClear
/>
) : (
<Form.Upload
field='vertex_files'
label={t('密钥文件 (.json)')}
accept='.json'
draggable
dragIcon={<IconBolt />}
dragMainText={t('点击上传文件或拖拽文件到这里')}
dragSubText={t('仅支持 JSON 文件')}
style={{ marginTop: 10 }}
uploadTrigger='custom'
beforeUpload={() => false}
onChange={handleVertexUploadChange}
fileList={vertexFileList}
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
extraText={batchExtra}
/>
)}
</>
) : ( ) : (
<Form.Input <Form.Input
field='key' field='key'