247
web-app/src/components/SettingsPanel.tsx
Normal file
247
web-app/src/components/SettingsPanel.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import {Image, Palette, RotateCcw, Save, Upload, X} from 'lucide-react'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {type Conversation, conversationApi} from '../api/conversation'
|
||||
|
||||
interface SettingsPanelProps {
|
||||
conversation: Conversation
|
||||
onClose: () => void
|
||||
onUpdate?: (settings: ConversationSettings) => void
|
||||
}
|
||||
|
||||
interface ConversationSettings {
|
||||
themeColor: string
|
||||
backgroundImage?: string
|
||||
}
|
||||
|
||||
const THEME_COLORS = [
|
||||
{ value: '#7C3AED', label: '紫色' },
|
||||
{ value: '#F97316', label: '橙色' },
|
||||
{ value: '#10B981', label: '绿色' },
|
||||
{ value: '#3B82F6', label: '蓝色' },
|
||||
{ value: '#EC4899', label: '粉色' },
|
||||
{ value: '#F59E0B', label: '黄色' },
|
||||
]
|
||||
|
||||
export default function SettingsPanel({ conversation, onClose, onUpdate }: SettingsPanelProps) {
|
||||
const [settings, setSettings] = useState<ConversationSettings>({
|
||||
themeColor: '#7C3AED',
|
||||
})
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [backgroundPreview, setBackgroundPreview] = useState<string>()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [])
|
||||
|
||||
const loadSettings = () => {
|
||||
if (conversation.settings) {
|
||||
try {
|
||||
const parsed = typeof conversation.settings === 'string'
|
||||
? JSON.parse(conversation.settings)
|
||||
: conversation.settings
|
||||
|
||||
// 合并默认设置和保存的设置
|
||||
const mergedSettings = {
|
||||
themeColor: '#7C3AED',
|
||||
...parsed
|
||||
}
|
||||
|
||||
setSettings(mergedSettings)
|
||||
|
||||
if (parsed.backgroundImage) {
|
||||
setBackgroundPreview(parsed.backgroundImage)
|
||||
}
|
||||
|
||||
// 应用主题色
|
||||
if (parsed.themeColor) {
|
||||
document.documentElement.style.setProperty('--color-primary', parsed.themeColor)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析设置失败:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (key: keyof ConversationSettings, value: any) => {
|
||||
setSettings(prev => ({ ...prev, [key]: value }))
|
||||
setHasChanges(true)
|
||||
}
|
||||
|
||||
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('图片大小不能超过 5MB')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const dataUrl = event.target?.result as string
|
||||
setBackgroundPreview(dataUrl)
|
||||
handleChange('backgroundImage', dataUrl)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const handleRemoveBackground = () => {
|
||||
setBackgroundPreview(undefined)
|
||||
handleChange('backgroundImage', undefined)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 保存到后端
|
||||
await conversationApi.updateConversationSettings(conversation.id, settings)
|
||||
|
||||
// 应用主题色到根元素
|
||||
if (settings.themeColor) {
|
||||
document.documentElement.style.setProperty('--color-primary', settings.themeColor)
|
||||
}
|
||||
|
||||
// 通知父组件更新
|
||||
onUpdate?.(settings)
|
||||
setHasChanges(false)
|
||||
alert('设置已保存')
|
||||
} catch (err) {
|
||||
console.error('保存设置失败:', err)
|
||||
alert('保存失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setSettings({
|
||||
themeColor: '#7C3AED',
|
||||
})
|
||||
setBackgroundPreview(undefined)
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="p-4 glass border-b border-white/10 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">对话设置</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 glass-hover rounded-lg cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<div className="glass-hover rounded-2xl p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-primary/20 rounded-lg">
|
||||
<Palette className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-semibold">外观设置</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="text-sm text-white/80 mb-3 block">主题颜色</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{THEME_COLORS.map((color) => (
|
||||
<button
|
||||
key={color.value}
|
||||
onClick={() => handleChange('themeColor', color.value)}
|
||||
className={`p-4 rounded-xl cursor-pointer transition-all ${
|
||||
settings.themeColor === color.value
|
||||
? 'ring-2 ring-white/40 scale-105'
|
||||
: 'ring-1 ring-white/20 hover:ring-white/30'
|
||||
}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
>
|
||||
<div className="text-xs font-medium text-white">{color.label}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-white/80 mb-3 block">对话背景图</label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{backgroundPreview ? (
|
||||
<div className="relative glass rounded-xl p-4">
|
||||
<img
|
||||
src={backgroundPreview}
|
||||
alt="背景预览"
|
||||
className="w-full h-32 object-cover rounded-lg"
|
||||
/>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex-1 px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer flex items-center justify-center gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
更换图片
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemoveBackground}
|
||||
className="flex-1 px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer text-red-400"
|
||||
>
|
||||
移除背景
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full p-8 glass-hover rounded-xl cursor-pointer border-2 border-dashed border-white/20 hover:border-primary/50 transition-all"
|
||||
>
|
||||
<Image className="w-8 h-8 mx-auto mb-2 text-white/40" />
|
||||
<p className="text-sm text-white/60">点击上传背景图片</p>
|
||||
<p className="text-xs text-white/40 mt-1">支持 JPG、PNG,最大 5MB</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-hover rounded-2xl p-4">
|
||||
<div className="text-xs text-white/60 space-y-2">
|
||||
<p>💡 主题颜色会应用到整个界面</p>
|
||||
<p>💡 背景图片仅在对话页面显示</p>
|
||||
<p>💡 AI 模型可在对话界面顶部切换</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 glass border-t border-white/10 flex gap-3">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges}
|
||||
className="flex-1 px-4 py-3 glass-hover rounded-xl text-sm font-medium cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
重置
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges}
|
||||
className="flex-1 px-4 py-3 bg-gradient-to-r from-primary to-secondary rounded-xl text-sm font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user