🎉 初始化项目

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-02-27 21:52:00 +08:00
commit f4e166c5ee
482 changed files with 55079 additions and 0 deletions

View 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"> JPGPNG 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>
)
}