231 lines
8.9 KiB
TypeScript
231 lines
8.9 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { Locale } from '@/lib/i18n';
|
|||
|
|
import { getSEOData, SEOData } from '@/lib/seo';
|
|||
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
|
|||
|
|
interface TDKConfigManagerProps {
|
|||
|
|
locale: Locale;
|
|||
|
|
page: string;
|
|||
|
|
onSave?: (data: SEOData) => void;
|
|||
|
|
className?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function TDKConfigManager({
|
|||
|
|
locale,
|
|||
|
|
page,
|
|||
|
|
onSave,
|
|||
|
|
className = '',
|
|||
|
|
}: TDKConfigManagerProps) {
|
|||
|
|
const [seoData, setSeoData] = useState<SEOData>({
|
|||
|
|
title: '',
|
|||
|
|
description: '',
|
|||
|
|
keywords: [],
|
|||
|
|
});
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [newKeyword, setNewKeyword] = useState('');
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const loadSEOData = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const data = await getSEOData(locale, page);
|
|||
|
|
setSeoData(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to load SEO data:', error);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
loadSEOData();
|
|||
|
|
}, [locale, page]);
|
|||
|
|
|
|||
|
|
const handleSave = async () => {
|
|||
|
|
if (onSave) {
|
|||
|
|
setSaving(true);
|
|||
|
|
try {
|
|||
|
|
await onSave(seoData);
|
|||
|
|
alert(
|
|||
|
|
locale === 'zh-CN'
|
|||
|
|
? '保存成功!'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '儲存成功!'
|
|||
|
|
: 'Saved successfully!',
|
|||
|
|
);
|
|||
|
|
} catch (error) {
|
|||
|
|
alert(
|
|||
|
|
locale === 'zh-CN'
|
|||
|
|
? '保存失败!'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '儲存失敗!'
|
|||
|
|
: 'Save failed!',
|
|||
|
|
);
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const addKeyword = () => {
|
|||
|
|
if (newKeyword.trim() && !seoData.keywords.includes(newKeyword.trim())) {
|
|||
|
|
setSeoData({
|
|||
|
|
...seoData,
|
|||
|
|
keywords: [...seoData.keywords, newKeyword.trim()],
|
|||
|
|
});
|
|||
|
|
setNewKeyword('');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const removeKeyword = (index: number) => {
|
|||
|
|
setSeoData({
|
|||
|
|
...seoData,
|
|||
|
|
keywords: seoData.keywords.filter((_, i) => i !== index),
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className={`animate-pulse ${className}`}>
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="h-4 bg-gray-200 rounded"></div>
|
|||
|
|
<div className="h-20 bg-gray-200 rounded"></div>
|
|||
|
|
<div className="h-4 bg-gray-200 rounded"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className={`tdk-config-manager ${className}`}>
|
|||
|
|
<h3 className="text-lg font-semibold mb-4">
|
|||
|
|
{locale === 'zh-CN'
|
|||
|
|
? 'TDK配置管理'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? 'TDK配置管理'
|
|||
|
|
: 'TDK Configuration'}
|
|||
|
|
</h3>
|
|||
|
|
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{/* Title */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
{locale === 'zh-CN' ? '标题' : locale === 'zh-TW' ? '標題' : 'Title'}
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={seoData.title}
|
|||
|
|
onChange={(e) => setSeoData({ ...seoData, title: e.target.value })}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder={
|
|||
|
|
locale === 'zh-CN'
|
|||
|
|
? '输入页面标题'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '輸入頁面標題'
|
|||
|
|
: 'Enter page title'
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div className="text-xs text-gray-500 mt-1">
|
|||
|
|
{seoData.title.length}/60{' '}
|
|||
|
|
{locale === 'zh-CN' ? '字符' : locale === 'zh-TW' ? '字元' : 'characters'}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Description */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
{locale === 'zh-CN' ? '描述' : locale === 'zh-TW' ? '描述' : 'Description'}
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={seoData.description}
|
|||
|
|
onChange={(e) => setSeoData({ ...seoData, description: e.target.value })}
|
|||
|
|
rows={3}
|
|||
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder={
|
|||
|
|
locale === 'zh-CN'
|
|||
|
|
? '输入页面描述'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '輸入頁面描述'
|
|||
|
|
: 'Enter page description'
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div className="text-xs text-gray-500 mt-1">
|
|||
|
|
{seoData.description.length}/160{' '}
|
|||
|
|
{locale === 'zh-CN' ? '字符' : locale === 'zh-TW' ? '字元' : 'characters'}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Keywords */}
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
{locale === 'zh-CN' ? '关键词' : locale === 'zh-TW' ? '關鍵詞' : 'Keywords'}
|
|||
|
|
</label>
|
|||
|
|
<div className="flex gap-2 mb-2">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={newKeyword}
|
|||
|
|
onChange={(e) => setNewKeyword(e.target.value)}
|
|||
|
|
onKeyPress={(e) => e.key === 'Enter' && addKeyword()}
|
|||
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
placeholder={
|
|||
|
|
locale === 'zh-CN'
|
|||
|
|
? '添加关键词'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '新增關鍵詞'
|
|||
|
|
: 'Add keyword'
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
onClick={addKeyword}
|
|||
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
{locale === 'zh-CN' ? '添加' : locale === 'zh-TW' ? '新增' : 'Add'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
{seoData.keywords.map((keyword, index) => (
|
|||
|
|
<span
|
|||
|
|
key={index}
|
|||
|
|
className="inline-flex items-center gap-1 bg-blue-100 text-blue-800 text-sm px-2 py-1 rounded"
|
|||
|
|
>
|
|||
|
|
{keyword}
|
|||
|
|
<button
|
|||
|
|
onClick={() => removeKeyword(index)}
|
|||
|
|
className="text-blue-600 hover:text-blue-800"
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Save Button */}
|
|||
|
|
{onSave && (
|
|||
|
|
<button
|
|||
|
|
onClick={handleSave}
|
|||
|
|
disabled={saving}
|
|||
|
|
className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{saving
|
|||
|
|
? locale === 'zh-CN'
|
|||
|
|
? '保存中...'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '儲存中...'
|
|||
|
|
: 'Saving...'
|
|||
|
|
: locale === 'zh-CN'
|
|||
|
|
? '保存配置'
|
|||
|
|
: locale === 'zh-TW'
|
|||
|
|
? '儲存配置'
|
|||
|
|
: 'Save Configuration'}
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|