157 lines
7.4 KiB
React
157 lines
7.4 KiB
React
import React, { useEffect, useRef, useState } from 'react';
|
|
import Paper from '@mui/material/Paper';
|
|
import Typography from '@mui/material/Typography';
|
|
import TextField from '@mui/material/TextField';
|
|
import Button from '@mui/material/Button';
|
|
import Box from '@mui/material/Box';
|
|
import Slider from '@mui/material/Slider';
|
|
import ToggleButton from '@mui/material/ToggleButton';
|
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
import Switch from '@mui/material/Switch';
|
|
import { getSettings, saveSettings } from '../../api/settings.js';
|
|
import { uploadWallpaper } from '../../api/upload.js';
|
|
import { showSnack } from '../snack.jsx';
|
|
|
|
/** 主题设置:主题色/壁纸(上传或URL)/导航卡片样式/玻璃模糊透明度/强制深色(迁移自 v1 主题卡片) */
|
|
export default function ThemeSettings() {
|
|
const [form, setForm] = useState({
|
|
primary_color: '#6750a4',
|
|
theme_wallpaper: '',
|
|
theme_wallpaper_scale: 'cover',
|
|
theme_wallpaper_enabled: true,
|
|
nav_style: 'default',
|
|
card_style: 'default',
|
|
glass_blur: '20',
|
|
glass_opacity: '0.6',
|
|
theme_force_dark: false,
|
|
});
|
|
const [saving, setSaving] = useState(false);
|
|
const fileRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
getSettings()
|
|
.then((s) => setForm({
|
|
primary_color: s.primary_color || '#6750a4',
|
|
theme_wallpaper: s.theme_wallpaper || '',
|
|
theme_wallpaper_scale: s.theme_wallpaper_scale || 'cover',
|
|
theme_wallpaper_enabled: s.theme_wallpaper_enabled !== '0',
|
|
nav_style: s.nav_style || 'default',
|
|
card_style: s.card_style || 'default',
|
|
glass_blur: s.glass_blur || '20',
|
|
glass_opacity: s.glass_opacity || '0.6',
|
|
theme_force_dark: s.theme_force_dark === '1',
|
|
}))
|
|
.catch((e) => showSnack(e.message, 'error'));
|
|
}, []);
|
|
|
|
const set = (k) => (e) => setForm((p) => ({ ...p, [k]: e.target.value }));
|
|
|
|
const handleWallpaperUpload = async (e) => {
|
|
const file = e.target.files && e.target.files[0];
|
|
e.target.value = '';
|
|
if (!file) return;
|
|
try {
|
|
const data = await uploadWallpaper(file);
|
|
setForm((p) => ({ ...p, theme_wallpaper: data.url }));
|
|
showSnack('壁纸已上传');
|
|
} catch (err) {
|
|
showSnack(err.message, 'error');
|
|
}
|
|
};
|
|
|
|
const save = async () => {
|
|
setSaving(true);
|
|
try {
|
|
await saveSettings({
|
|
primary_color: form.primary_color,
|
|
theme_wallpaper: form.theme_wallpaper.trim(),
|
|
theme_wallpaper_scale: form.theme_wallpaper_scale,
|
|
theme_wallpaper_enabled: form.theme_wallpaper_enabled ? '1' : '0',
|
|
theme_force_dark: form.theme_force_dark ? '1' : '0',
|
|
nav_style: form.nav_style,
|
|
card_style: form.card_style,
|
|
glass_blur: form.glass_blur,
|
|
glass_opacity: form.glass_opacity,
|
|
});
|
|
showSnack('主题设置已保存');
|
|
} catch (e) {
|
|
showSnack(e.message, 'error');
|
|
}
|
|
setSaving(false);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ maxWidth: 640 }}>
|
|
<Typography variant="h5" sx={{ mb: 2 }}>主题设置</Typography>
|
|
|
|
<Paper sx={{ p: 3, mb: 2 }}>
|
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>主题色</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<input type="color" value={form.primary_color} onChange={set('primary_color')} style={{ width: 48, height: 48, border: 'none', background: 'none', cursor: 'pointer' }} />
|
|
<TextField label="主色调" value={form.primary_color} onChange={set('primary_color')} size="small" sx={{ width: 160 }} />
|
|
</Box>
|
|
</Paper>
|
|
|
|
<Paper sx={{ p: 3, mb: 2 }}>
|
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>壁纸背景</Typography>
|
|
<FormControlLabel
|
|
control={<Switch checked={form.theme_wallpaper_enabled} onChange={(e) => setForm((p) => ({ ...p, theme_wallpaper_enabled: e.target.checked }))} />}
|
|
label="启用壁纸背景"
|
|
sx={{ mb: 1 }}
|
|
/>
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
|
<Button variant="outlined" onClick={() => fileRef.current && fileRef.current.click()}>上传图片</Button>
|
|
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleWallpaperUpload} />
|
|
<Typography variant="caption" color="text.secondary">上传后自动填入 URL</Typography>
|
|
</Box>
|
|
<TextField fullWidth label="图片 URL" type="url" value={form.theme_wallpaper} onChange={set('theme_wallpaper')} margin="normal" placeholder="https://example.com/wallpaper.jpg" />
|
|
{form.theme_wallpaper && (
|
|
<Box sx={{ mt: 1 }}>
|
|
<img src={form.theme_wallpaper} alt="wallpaper" style={{ width: '100%', maxHeight: 120, objectFit: 'cover', borderRadius: 8 }} />
|
|
</Box>
|
|
)}
|
|
<TextField fullWidth label="缩放方式" select value={form.theme_wallpaper_scale} onChange={set('theme_wallpaper_scale')} margin="normal" sx={{ maxWidth: 280 }}>
|
|
<option value="cover">cover - 覆盖填充</option>
|
|
<option value="contain">contain - 完整显示</option>
|
|
<option value="repeat">repeat - 平铺重复</option>
|
|
<option value="stretch">stretch - 拉伸填充</option>
|
|
</TextField>
|
|
</Paper>
|
|
|
|
<Paper sx={{ p: 3, mb: 2 }}>
|
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>样式设置</Typography>
|
|
<Typography variant="body2" sx={{ mb: 0.5 }}>导航栏样式</Typography>
|
|
<ToggleButtonGroup exclusive value={form.nav_style} onChange={(e, v) => v && setForm((p) => ({ ...p, nav_style: v }))} size="small" sx={{ mb: 2 }}>
|
|
<ToggleButton value="default">默认</ToggleButton>
|
|
<ToggleButton value="glass">磨砂玻璃</ToggleButton>
|
|
<ToggleButton value="capsule">胶囊</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
<Typography variant="body2" sx={{ mb: 0.5 }}>卡片样式</Typography>
|
|
<ToggleButtonGroup exclusive value={form.card_style} onChange={(e, v) => v && setForm((p) => ({ ...p, card_style: v }))} size="small">
|
|
<ToggleButton value="default">默认</ToggleButton>
|
|
<ToggleButton value="glass">磨砂玻璃</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Paper>
|
|
|
|
<Paper sx={{ p: 3, mb: 2 }}>
|
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>玻璃效果</Typography>
|
|
<Typography variant="body2" color="text.secondary">模糊强度: {form.glass_blur}px</Typography>
|
|
<Slider min={5} max={40} value={parseInt(form.glass_blur, 10) || 20} onChange={(e, v) => setForm((p) => ({ ...p, glass_blur: String(v) }))} sx={{ maxWidth: 400 }} />
|
|
<Typography variant="body2" color="text.secondary">透明度: {form.glass_opacity}</Typography>
|
|
<Slider min={0.1} max={0.95} step={0.05} value={parseFloat(form.glass_opacity) || 0.6} onChange={(e, v) => setForm((p) => ({ ...p, glass_opacity: String(v) }))} sx={{ maxWidth: 400 }} />
|
|
</Paper>
|
|
|
|
<Paper sx={{ p: 3, mb: 2 }}>
|
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>深色模式</Typography>
|
|
<FormControlLabel
|
|
control={<Switch checked={form.theme_force_dark} onChange={(e) => setForm((p) => ({ ...p, theme_force_dark: e.target.checked }))} />}
|
|
label="强制深色模式(启用后用户无法切换为浅色)"
|
|
/>
|
|
</Paper>
|
|
|
|
<Button variant="contained" onClick={save} disabled={saving}>保存主题设置</Button>
|
|
</Box>
|
|
);
|
|
}
|