611 lines
20 KiB
JavaScript
611 lines
20 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import {
|
|||
|
|
Box,
|
|||
|
|
Typography,
|
|||
|
|
Button,
|
|||
|
|
Card,
|
|||
|
|
CardContent,
|
|||
|
|
Switch,
|
|||
|
|
FormControlLabel,
|
|||
|
|
TextField,
|
|||
|
|
IconButton,
|
|||
|
|
Tooltip,
|
|||
|
|
Divider,
|
|||
|
|
Alert,
|
|||
|
|
CircularProgress,
|
|||
|
|
Dialog,
|
|||
|
|
DialogTitle,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogActions,
|
|||
|
|
Grid
|
|||
|
|
} from '@mui/material';
|
|||
|
|
import {
|
|||
|
|
Add as AddIcon,
|
|||
|
|
Delete as DeleteIcon,
|
|||
|
|
AutoFixHigh as AutoFixHighIcon,
|
|||
|
|
Save as SaveIcon
|
|||
|
|
} from '@mui/icons-material';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import i18n from '@/lib/i18n';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* GA Pairs Manager Component
|
|||
|
|
* @param {Object} props
|
|||
|
|
* @param {string} props.projectId - Project ID
|
|||
|
|
* @param {string} props.fileId - File ID
|
|||
|
|
* @param {Function} props.onGaPairsChange - Callback when GA pairs change
|
|||
|
|
*/
|
|||
|
|
export default function GaPairsManager({ projectId, fileId, onGaPairsChange }) {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [gaPairs, setGaPairs] = useState([]);
|
|||
|
|
const [backupGaPairs, setBackupGaPairs] = useState([]); // 备份状态
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [generating, setGenerating] = useState(false);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [error, setError] = useState(null);
|
|||
|
|
const [success, setSuccess] = useState(null);
|
|||
|
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|||
|
|
const [newGaPair, setNewGaPair] = useState({
|
|||
|
|
genreTitle: '',
|
|||
|
|
genreDesc: '',
|
|||
|
|
audienceTitle: '',
|
|||
|
|
audienceDesc: '',
|
|||
|
|
isActive: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadGaPairs();
|
|||
|
|
}, [projectId, fileId]);
|
|||
|
|
|
|||
|
|
const loadGaPairs = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
setError(null);
|
|||
|
|
|
|||
|
|
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`);
|
|||
|
|
|
|||
|
|
// 检查响应状态
|
|||
|
|
if (!response.ok) {
|
|||
|
|
if (response.status === 404) {
|
|||
|
|
console.warn('GA Pairs API not found, using empty data');
|
|||
|
|
setGaPairs([]);
|
|||
|
|
setBackupGaPairs([]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
throw new Error(`HTTP ${response.status}: Failed to load GA pairs`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
console.log('Load GA pairs result:', result);
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
const loadedData = result.data || [];
|
|||
|
|
setGaPairs(loadedData);
|
|||
|
|
setBackupGaPairs([...loadedData]); // 创建备份
|
|||
|
|
onGaPairsChange?.(loadedData);
|
|||
|
|
} else {
|
|||
|
|
throw new Error(result.error || 'Failed to load GA pairs');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Load GA pairs error:', error);
|
|||
|
|
setError(t('gaPairs.loadError', { error: error.message }));
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const generateGaPairs = async () => {
|
|||
|
|
try {
|
|||
|
|
setGenerating(true);
|
|||
|
|
setError(null);
|
|||
|
|
|
|||
|
|
console.log('Starting GA pairs generation...');
|
|||
|
|
|
|||
|
|
// Get current language from i18n
|
|||
|
|
const currentLanguage = i18n.language === 'en' ? 'en' : '中文';
|
|||
|
|
|
|||
|
|
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
regenerate: false,
|
|||
|
|
appendMode: true, // 新增:启用追加模式
|
|||
|
|
language: currentLanguage
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
let errorMessage = t('gaPairs.generateError');
|
|||
|
|
|
|||
|
|
if (response.status === 404) {
|
|||
|
|
errorMessage = t('gaPairs.serviceNotAvailable');
|
|||
|
|
} else if (response.status === 400) {
|
|||
|
|
try {
|
|||
|
|
const errorResult = await response.json();
|
|||
|
|
if (errorResult.error?.includes('No active AI model')) {
|
|||
|
|
errorMessage = t('gaPairs.noActiveModel');
|
|||
|
|
} else if (errorResult.error?.includes('content might be too short')) {
|
|||
|
|
errorMessage = t('gaPairs.contentTooShort');
|
|||
|
|
} else {
|
|||
|
|
errorMessage = errorResult.error || errorMessage;
|
|||
|
|
}
|
|||
|
|
} catch (parseError) {
|
|||
|
|
errorMessage = t('gaPairs.requestFailed', { status: response.status });
|
|||
|
|
}
|
|||
|
|
} else if (response.status === 500) {
|
|||
|
|
try {
|
|||
|
|
const errorResult = await response.json();
|
|||
|
|
if (errorResult.error?.includes('model configuration') || errorResult.error?.includes('Module not found')) {
|
|||
|
|
errorMessage = t('gaPairs.configError');
|
|||
|
|
} else {
|
|||
|
|
errorMessage = errorResult.error || 'Internal server error occurred.';
|
|||
|
|
}
|
|||
|
|
} catch (parseError) {
|
|||
|
|
console.error('Failed to parse error response:', parseError);
|
|||
|
|
errorMessage = errorResult.error || t('gaPairs.internalServerError');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new Error(errorMessage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理成功响应
|
|||
|
|
const responseText = await response.text();
|
|||
|
|
if (!responseText || responseText.trim() === '') {
|
|||
|
|
throw new Error(t('gaPairs.emptyResponse'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = JSON.parse(responseText);
|
|||
|
|
console.log('Generate GA pairs result:', result);
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
// 在追加模式下,后端只返回新生成的GA对
|
|||
|
|
const newGaPairs = result.data || [];
|
|||
|
|
|
|||
|
|
// 将新生成的GA对追加到现有的GA对
|
|||
|
|
const updatedGaPairs = [...gaPairs, ...newGaPairs];
|
|||
|
|
|
|||
|
|
setGaPairs(updatedGaPairs);
|
|||
|
|
setBackupGaPairs([...updatedGaPairs]); // 更新备份
|
|||
|
|
onGaPairsChange?.(updatedGaPairs);
|
|||
|
|
setSuccess(
|
|||
|
|
t('gaPairs.additionalPairsGenerated', {
|
|||
|
|
count: newGaPairs.length,
|
|||
|
|
total: updatedGaPairs.length
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
throw new Error(result.error || t('gaPairs.generationFailed'));
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Generate GA pairs error:', error);
|
|||
|
|
setError(error.message);
|
|||
|
|
} finally {
|
|||
|
|
setGenerating(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const saveGaPairs = async () => {
|
|||
|
|
try {
|
|||
|
|
setSaving(true);
|
|||
|
|
setError(null);
|
|||
|
|
|
|||
|
|
// 验证GA对数据
|
|||
|
|
const validatedGaPairs = gaPairs.map((pair, index) => {
|
|||
|
|
// 处理不同的数据格式
|
|||
|
|
let genreTitle, genreDesc, audienceTitle, audienceDesc;
|
|||
|
|
|
|||
|
|
if (pair.genre && typeof pair.genre === 'object') {
|
|||
|
|
genreTitle = pair.genre.title;
|
|||
|
|
genreDesc = pair.genre.description;
|
|||
|
|
} else {
|
|||
|
|
genreTitle = pair.genreTitle || pair.genre;
|
|||
|
|
genreDesc = pair.genreDesc || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (pair.audience && typeof pair.audience === 'object') {
|
|||
|
|
audienceTitle = pair.audience.title;
|
|||
|
|
audienceDesc = pair.audience.description;
|
|||
|
|
} else {
|
|||
|
|
audienceTitle = pair.audienceTitle || pair.audience;
|
|||
|
|
audienceDesc = pair.audienceDesc || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证必填字段
|
|||
|
|
if (!genreTitle || !audienceTitle) {
|
|||
|
|
throw new Error(t('gaPairs.validationError', { number: index + 1 }));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: pair.id,
|
|||
|
|
genreTitle: genreTitle.trim(),
|
|||
|
|
genreDesc: genreDesc.trim(),
|
|||
|
|
audienceTitle: audienceTitle.trim(),
|
|||
|
|
audienceDesc: audienceDesc.trim(),
|
|||
|
|
isActive: pair.isActive !== undefined ? pair.isActive : true
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('Saving validated GA pairs:', validatedGaPairs);
|
|||
|
|
|
|||
|
|
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
updates: validatedGaPairs
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
let errorMessage = t('gaPairs.saveError');
|
|||
|
|
|
|||
|
|
if (response.status === 404) {
|
|||
|
|
errorMessage = 'GA Pairs save service is not available.';
|
|||
|
|
} else {
|
|||
|
|
try {
|
|||
|
|
const errorResult = await response.json();
|
|||
|
|
errorMessage = errorResult.error || errorMessage;
|
|||
|
|
} catch (parseError) {
|
|||
|
|
errorMessage = t('gaPairs.serverError', { status: response.status });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw new Error(errorMessage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const responseText = await response.text();
|
|||
|
|
const result = responseText ? JSON.parse(responseText) : { success: true };
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
// 更新本地状态为服务器返回的数据
|
|||
|
|
const savedData = result.data || validatedGaPairs;
|
|||
|
|
setGaPairs(savedData);
|
|||
|
|
|
|||
|
|
// 根据保存的GA对数量显示不同的成功消息
|
|||
|
|
if (savedData.length === 0) {
|
|||
|
|
setSuccess(t('gaPairs.allPairsDeleted'));
|
|||
|
|
} else {
|
|||
|
|
setSuccess(t('gaPairs.pairsSaved', { count: savedData.length }));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onGaPairsChange?.(savedData);
|
|||
|
|
} else {
|
|||
|
|
throw new Error(result.error || t('gaPairs.saveOperationFailed'));
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Save GA pairs error:', error);
|
|||
|
|
setError(error.message);
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleGaPairChange = (index, field, value) => {
|
|||
|
|
const updatedGaPairs = [...gaPairs];
|
|||
|
|
|
|||
|
|
// 确保对象存在
|
|||
|
|
if (!updatedGaPairs[index]) {
|
|||
|
|
console.error(`GA pair at index ${index} does not exist`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updatedGaPairs[index] = {
|
|||
|
|
...updatedGaPairs[index],
|
|||
|
|
[field]: value
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
setGaPairs(updatedGaPairs);
|
|||
|
|
// 不立即调用 onGaPairsChange,等用户点击保存时再调用
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeleteGaPair = index => {
|
|||
|
|
const updatedGaPairs = gaPairs.filter((_, i) => i !== index);
|
|||
|
|
setGaPairs(updatedGaPairs);
|
|||
|
|
onGaPairsChange?.(updatedGaPairs);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleAddGaPair = () => {
|
|||
|
|
// 验证输入
|
|||
|
|
if (!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()) {
|
|||
|
|
setError(t('gaPairs.requiredFields'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建新的GA对对象
|
|||
|
|
const newPair = {
|
|||
|
|
id: `temp_${Date.now()}`, // 临时ID
|
|||
|
|
genreTitle: newGaPair.genreTitle.trim(),
|
|||
|
|
genreDesc: newGaPair.genreDesc?.trim() || '',
|
|||
|
|
audienceTitle: newGaPair.audienceTitle.trim(),
|
|||
|
|
audienceDesc: newGaPair.audienceDesc?.trim() || '',
|
|||
|
|
isActive: true
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const updatedGaPairs = [...gaPairs, newPair];
|
|||
|
|
setGaPairs(updatedGaPairs);
|
|||
|
|
onGaPairsChange?.(updatedGaPairs);
|
|||
|
|
|
|||
|
|
// 重置表单并关闭对话框
|
|||
|
|
setNewGaPair({
|
|||
|
|
genreTitle: '',
|
|||
|
|
genreDesc: '',
|
|||
|
|
audienceTitle: '',
|
|||
|
|
audienceDesc: '',
|
|||
|
|
isActive: true
|
|||
|
|
});
|
|||
|
|
setAddDialogOpen(false);
|
|||
|
|
setError(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const resetMessages = () => {
|
|||
|
|
setError(null);
|
|||
|
|
setSuccess(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const recoverFromBackup = () => {
|
|||
|
|
setGaPairs([...backupGaPairs]);
|
|||
|
|
setError(null);
|
|||
|
|
setSuccess(t('gaPairs.restoredFromBackup'));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (error || success) {
|
|||
|
|
const timer = setTimeout(resetMessages, 5000);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
}
|
|||
|
|
}, [error, success]);
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
|
|||
|
|
<CircularProgress />
|
|||
|
|
<Typography sx={{ ml: 2 }}>{t('gaPairs.loading')}</Typography>
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Box sx={{ p: 2 }}>
|
|||
|
|
{/* Header with action buttons */}
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
|||
|
|
<Typography variant="h6">{t('gaPairs.title')}</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|||
|
|
{/* 右上角按钮为手动添加GA对 */}
|
|||
|
|
<Button
|
|||
|
|
variant="outlined"
|
|||
|
|
startIcon={<AddIcon />}
|
|||
|
|
onClick={() => setAddDialogOpen(true)}
|
|||
|
|
disabled={generating || saving}
|
|||
|
|
>
|
|||
|
|
{t('gaPairs.addPair')}
|
|||
|
|
</Button>
|
|||
|
|
<Button variant="contained" startIcon={<SaveIcon />} onClick={saveGaPairs} disabled={generating || saving}>
|
|||
|
|
{saving ? <CircularProgress size={20} /> : t('gaPairs.saveChanges')}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* Error/Success Messages */}
|
|||
|
|
{error && (
|
|||
|
|
<Alert
|
|||
|
|
severity="error"
|
|||
|
|
sx={{ mb: 2 }}
|
|||
|
|
action={
|
|||
|
|
backupGaPairs.length > 0 && (
|
|||
|
|
<Button color="inherit" size="small" onClick={recoverFromBackup}>
|
|||
|
|
{t('gaPairs.restoreBackup')}
|
|||
|
|
</Button>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
onClose={resetMessages}
|
|||
|
|
>
|
|||
|
|
{error}
|
|||
|
|
</Alert>
|
|||
|
|
)}
|
|||
|
|
{success && (
|
|||
|
|
<Alert severity="success" sx={{ mb: 2 }} onClose={resetMessages}>
|
|||
|
|
{success}
|
|||
|
|
</Alert>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Generate GA Pairs Section - 只在没有GA对时显示 */}
|
|||
|
|
{gaPairs.length === 0 && (
|
|||
|
|
<Card sx={{ mb: 3 }}>
|
|||
|
|
<CardContent sx={{ textAlign: 'center' }}>
|
|||
|
|
<Typography variant="h6" gutterBottom>
|
|||
|
|
{t('gaPairs.noGaPairsTitle')}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography color="textSecondary" sx={{ mb: 2 }}>
|
|||
|
|
{t('gaPairs.noGaPairsDescription')}
|
|||
|
|
</Typography>
|
|||
|
|
<Button
|
|||
|
|
variant="contained"
|
|||
|
|
startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
|
|||
|
|
onClick={generateGaPairs}
|
|||
|
|
disabled={generating}
|
|||
|
|
size="large"
|
|||
|
|
>
|
|||
|
|
{generating ? t('gaPairs.generating') : t('gaPairs.generateGaPairs')}
|
|||
|
|
</Button>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* GA Pairs List */}
|
|||
|
|
{gaPairs.length > 0 && (
|
|||
|
|
<Box>
|
|||
|
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
|||
|
|
{t('gaPairs.activePairs', {
|
|||
|
|
active: gaPairs.filter(pair => pair.isActive).length,
|
|||
|
|
total: gaPairs.length
|
|||
|
|
})}
|
|||
|
|
</Typography>
|
|||
|
|
|
|||
|
|
<Grid container spacing={2}>
|
|||
|
|
{gaPairs.map((pair, index) => (
|
|||
|
|
<Grid item xs={12} key={pair.id || index}>
|
|||
|
|
<Card variant="outlined">
|
|||
|
|
<CardContent>
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
|||
|
|
<Typography variant="subtitle2" color="primary">
|
|||
|
|
{t('gaPairs.pairNumber', { number: index + 1 })}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={
|
|||
|
|
<Switch
|
|||
|
|
checked={pair.isActive}
|
|||
|
|
onChange={e => handleGaPairChange(index, 'isActive', e.target.checked)}
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
}
|
|||
|
|
label={t('gaPairs.active')}
|
|||
|
|
/>
|
|||
|
|
{/* 添加删除按钮 */}
|
|||
|
|
<Tooltip title={t('gaPairs.deleteTooltip')}>
|
|||
|
|
<IconButton size="small" color="error" onClick={() => handleDeleteGaPair(index)}>
|
|||
|
|
<DeleteIcon fontSize="small" />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.genre')}
|
|||
|
|
value={pair.genreTitle || pair.genre || ''}
|
|||
|
|
onChange={e => handleGaPairChange(index, 'genreTitle', e.target.value)}
|
|||
|
|
multiline
|
|||
|
|
rows={2}
|
|||
|
|
fullWidth
|
|||
|
|
disabled={!pair.isActive}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.genreDescription')}
|
|||
|
|
value={pair.genreDesc || ''}
|
|||
|
|
onChange={e => handleGaPairChange(index, 'genreDesc', e.target.value)}
|
|||
|
|
multiline
|
|||
|
|
rows={2}
|
|||
|
|
fullWidth
|
|||
|
|
disabled={!pair.isActive}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.audience')}
|
|||
|
|
value={pair.audienceTitle || pair.audience || ''}
|
|||
|
|
onChange={e => handleGaPairChange(index, 'audienceTitle', e.target.value)}
|
|||
|
|
multiline
|
|||
|
|
rows={2}
|
|||
|
|
fullWidth
|
|||
|
|
disabled={!pair.isActive}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.audienceDescription')}
|
|||
|
|
value={pair.audienceDesc || ''}
|
|||
|
|
onChange={e => handleGaPairChange(index, 'audienceDesc', e.target.value)}
|
|||
|
|
multiline
|
|||
|
|
rows={2}
|
|||
|
|
fullWidth
|
|||
|
|
disabled={!pair.isActive}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</Grid>
|
|||
|
|
))}
|
|||
|
|
</Grid>
|
|||
|
|
|
|||
|
|
{/* 在GA对列表下方添加生成按钮 */}
|
|||
|
|
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
|||
|
|
<Button
|
|||
|
|
variant="outlined"
|
|||
|
|
startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
|
|||
|
|
onClick={generateGaPairs}
|
|||
|
|
disabled={generating}
|
|||
|
|
>
|
|||
|
|
{generating ? t('gaPairs.generating') : t('gaPairs.generateMore')}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Add GA Pair Dialog */}
|
|||
|
|
<Dialog open={addDialogOpen} onClose={() => setAddDialogOpen(false)} maxWidth="md" fullWidth>
|
|||
|
|
<DialogTitle>{t('gaPairs.addDialogTitle')}</DialogTitle>
|
|||
|
|
<DialogContent>
|
|||
|
|
<Box sx={{ pt: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.genreTitle')}
|
|||
|
|
value={newGaPair.genreTitle || ''}
|
|||
|
|
onChange={e => setNewGaPair({ ...newGaPair, genreTitle: e.target.value })}
|
|||
|
|
fullWidth
|
|||
|
|
required
|
|||
|
|
placeholder={t('gaPairs.genreTitlePlaceholder')}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.genreDescription')}
|
|||
|
|
value={newGaPair.genreDesc || ''}
|
|||
|
|
onChange={e => setNewGaPair({ ...newGaPair, genreDesc: e.target.value })}
|
|||
|
|
multiline
|
|||
|
|
rows={3}
|
|||
|
|
fullWidth
|
|||
|
|
placeholder={t('gaPairs.genreDescPlaceholder')}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.audienceTitle')}
|
|||
|
|
value={newGaPair.audienceTitle || ''}
|
|||
|
|
onChange={e => setNewGaPair({ ...newGaPair, audienceTitle: e.target.value })}
|
|||
|
|
fullWidth
|
|||
|
|
required
|
|||
|
|
placeholder={t('gaPairs.audienceTitlePlaceholder')}
|
|||
|
|
/>
|
|||
|
|
<TextField
|
|||
|
|
label={t('gaPairs.audienceDescription')}
|
|||
|
|
value={newGaPair.audienceDesc || ''}
|
|||
|
|
onChange={e => setNewGaPair({ ...newGaPair, audienceDesc: e.target.value })}
|
|||
|
|
multiline
|
|||
|
|
rows={3}
|
|||
|
|
fullWidth
|
|||
|
|
placeholder={t('gaPairs.audienceDescPlaceholder')}
|
|||
|
|
/>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={
|
|||
|
|
<Switch
|
|||
|
|
checked={newGaPair.isActive}
|
|||
|
|
onChange={e => setNewGaPair({ ...newGaPair, isActive: e.target.checked })}
|
|||
|
|
/>
|
|||
|
|
}
|
|||
|
|
label={t('gaPairs.active')}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
</DialogContent>
|
|||
|
|
<DialogActions>
|
|||
|
|
<Button
|
|||
|
|
onClick={() => {
|
|||
|
|
setAddDialogOpen(false);
|
|||
|
|
// 重置表单
|
|||
|
|
setNewGaPair({
|
|||
|
|
genreTitle: '',
|
|||
|
|
genreDesc: '',
|
|||
|
|
audienceTitle: '',
|
|||
|
|
audienceDesc: '',
|
|||
|
|
isActive: true
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{t('gaPairs.cancel')}
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
onClick={handleAddGaPair}
|
|||
|
|
variant="contained"
|
|||
|
|
disabled={!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()}
|
|||
|
|
>
|
|||
|
|
{t('gaPairs.addPairButton')}
|
|||
|
|
</Button>
|
|||
|
|
</DialogActions>
|
|||
|
|
</Dialog>
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
}
|