Files
OneOS1.2/src/common/oneos-app-shell/RuoYiSettingsDrawer.tsx

352 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useState } from 'react';
import {
Check,
Copy,
Maximize2,
Minimize2,
Moon,
RefreshCw,
RotateCcw,
Settings,
Sun,
X,
Monitor,
Trash2,
} from 'lucide-react';
export interface RuoYiSettings {
themeMode: 'light' | 'dark' | 'system';
darkSidebar: boolean;
darkNavbar: boolean;
primaryColor: string;
borderRadius: number;
fontSize: number;
activeTab: 'appearance' | 'layout' | 'shortcuts' | 'general';
}
export const RUOYI_SETTINGS_STORAGE_KEY = 'ruoyi-oneos-settings-v2';
export const DEFAULT_RUOYI_SETTINGS: RuoYiSettings = {
themeMode: 'light',
darkSidebar: false,
darkNavbar: false,
primaryColor: '#533AFD',
borderRadius: 0.5,
fontSize: 14,
activeTab: 'appearance',
};
export const PRESET_COLORS = [
{ name: '默认', value: '#533AFD' },
{ name: '紫罗兰', value: '#722ED1' },
{ name: '樱花粉', value: '#EB2F96' },
{ name: '柠檬黄', value: '#FADB14' },
{ name: '天蓝色', value: '#409EFF' },
{ name: '浅绿色', value: '#13C2C2' },
{ name: '特色灰', value: '#595959' },
{ name: '深绿色', value: '#009688' },
{ name: '深蓝色', value: '#111936' },
{ name: '橙黄色', value: '#FA8C16' },
{ name: '玫瑰红', value: '#F5222D' },
{ name: '中性色', value: '#262626' },
{ name: '石板灰', value: '#1E293B' },
{ name: '中灰色', value: '#4B5563' },
];
export function readStoredRuoYiSettings(): RuoYiSettings {
if (typeof window === 'undefined') return DEFAULT_RUOYI_SETTINGS;
try {
const raw = window.localStorage.getItem(RUOYI_SETTINGS_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
return { ...DEFAULT_RUOYI_SETTINGS, ...parsed };
}
} catch {
/* ignore parse error */
}
return DEFAULT_RUOYI_SETTINGS;
}
export function persistRuoYiSettings(settings: RuoYiSettings): void {
if (typeof window === 'undefined') return;
try {
window.localStorage.setItem(RUOYI_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch {
/* ignore */
}
}
export type RuoYiSettingsDrawerProps = {
open: boolean;
onClose: () => void;
settings: RuoYiSettings;
onChange: (next: RuoYiSettings) => void;
onReset: () => void;
};
export function RuoYiSettingsDrawer({
open,
onClose,
settings,
onChange,
onReset,
}: RuoYiSettingsDrawerProps) {
const [copied, setCopied] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && open) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, onClose]);
if (!open) return null;
const updateSetting = <K extends keyof RuoYiSettings>(key: K, value: RuoYiSettings[K]) => {
const next = { ...settings, [key]: value };
onChange(next);
};
const handleCopySettings = () => {
const jsonStr = JSON.stringify(settings, null, 2);
navigator.clipboard.writeText(jsonStr).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
return (
<div className="ruoyi-settings-overlay" onClick={onClose}>
<aside
className={`ruoyi-settings-drawer${isFullscreen ? ' is-fullscreen' : ''}`}
onClick={(e) => e.stopPropagation()}
aria-label="偏好设置"
>
{/* Header */}
<header className="ruoyi-settings-header">
<div className="ruoyi-settings-header__title">
<h3></h3>
<p> &amp; </p>
</div>
<div className="ruoyi-settings-header__actions">
<button
type="button"
className="ruoyi-settings-icon-btn"
title="重置偏好设置"
onClick={onReset}
>
<RotateCcw size={15} />
</button>
<button
type="button"
className="ruoyi-settings-icon-btn"
title={isFullscreen ? '还原' : '全屏'}
onClick={() => setIsFullscreen((v) => !v)}
>
{isFullscreen ? <Minimize2 size={15} /> : <Maximize2 size={15} />}
</button>
<button
type="button"
className="ruoyi-settings-icon-btn"
title="关闭"
onClick={onClose}
>
<X size={16} />
</button>
</div>
</header>
{/* Tabs */}
<nav className="ruoyi-settings-tabs">
{[
{ id: 'appearance', label: '外观' },
{ id: 'layout', label: '布局' },
{ id: 'shortcuts', label: '快捷键' },
{ id: 'general', label: '通用' },
].map((tab) => (
<button
key={tab.id}
type="button"
className={`ruoyi-settings-tab${settings.activeTab === tab.id ? ' is-active' : ''}`}
onClick={() => updateSetting('activeTab', tab.id as RuoYiSettings['activeTab'])}
>
{tab.label}
</button>
))}
</nav>
{/* Drawer Body */}
<div className="ruoyi-settings-body">
{settings.activeTab === 'appearance' && (
<div className="ruoyi-settings-group">
{/* 1. 主题模式 */}
<div className="ruoyi-settings-item">
<label className="ruoyi-settings-label"></label>
<div className="ruoyi-settings-options-segmented">
<button
type="button"
className={`ruoyi-segmented-btn${settings.themeMode === 'light' ? ' is-active' : ''}`}
onClick={() => updateSetting('themeMode', 'light')}
>
<Sun size={14} />
</button>
<button
type="button"
className={`ruoyi-segmented-btn${settings.themeMode === 'dark' ? ' is-active' : ''}`}
onClick={() => updateSetting('themeMode', 'dark')}
>
<Moon size={14} />
</button>
<button
type="button"
className={`ruoyi-segmented-btn${settings.themeMode === 'system' ? ' is-active' : ''}`}
onClick={() => updateSetting('themeMode', 'system')}
>
<Monitor size={14} />
</button>
</div>
</div>
{/* 2. 深色侧边栏 */}
<div className="ruoyi-settings-item ruoyi-settings-item--row">
<span className="ruoyi-settings-label"></span>
<label className="ruoyi-switch">
<input
type="checkbox"
checked={settings.darkSidebar}
onChange={(e) => updateSetting('darkSidebar', e.target.checked)}
/>
<span className="ruoyi-switch-slider" />
</label>
</div>
{/* 3. 深色顶栏 */}
<div className="ruoyi-settings-item ruoyi-settings-item--row">
<span className="ruoyi-settings-label"></span>
<label className="ruoyi-switch">
<input
type="checkbox"
checked={settings.darkNavbar}
onChange={(e) => updateSetting('darkNavbar', e.target.checked)}
/>
<span className="ruoyi-switch-slider" />
</label>
</div>
{/* 4. 内置主题推荐色盘 */}
<div className="ruoyi-settings-item">
<label className="ruoyi-settings-label"></label>
<div className="ruoyi-colors-grid">
{PRESET_COLORS.map((c) => {
const selected =
settings.primaryColor.toLowerCase() === c.value.toLowerCase();
return (
<button
key={c.value}
type="button"
className={`ruoyi-color-tile${selected ? ' is-selected' : ''}`}
onClick={() => updateSetting('primaryColor', c.value)}
title={`${c.name} (${c.value})`}
>
<span className="ruoyi-color-swatch" style={{ background: c.value }}>
{selected && <Check size={12} color="#fff" strokeWidth={3} />}
</span>
<span className="ruoyi-color-name">{c.name}</span>
</button>
);
})}
{/* 自定义颜色 */}
<label className="ruoyi-color-tile ruoyi-color-tile--custom" title="自定义颜色">
<span
className="ruoyi-color-swatch"
style={{ background: settings.primaryColor }}
>
<input
type="color"
value={settings.primaryColor}
onChange={(e) => updateSetting('primaryColor', e.target.value)}
className="ruoyi-color-picker-input"
/>
</span>
<span className="ruoyi-color-name"></span>
</label>
</div>
</div>
{/* 5. 圆角 Radius */}
<div className="ruoyi-settings-item">
<label className="ruoyi-settings-label"></label>
<div className="ruoyi-settings-options-segmented">
{[0, 0.25, 0.5, 0.75, 1].map((r) => (
<button
key={r}
type="button"
className={`ruoyi-segmented-btn${settings.borderRadius === r ? ' is-active' : ''}`}
onClick={() => updateSetting('borderRadius', r)}
>
{r}
</button>
))}
</div>
</div>
{/* 6. 字体大小 Font Size */}
<div className="ruoyi-settings-item">
<label className="ruoyi-settings-label"></label>
<div className="ruoyi-settings-options-segmented">
{[12, 13, 14, 15, 16].map((s) => (
<button
key={s}
type="button"
className={`ruoyi-segmented-btn${settings.fontSize === s ? ' is-active' : ''}`}
onClick={() => updateSetting('fontSize', s)}
>
{s}px
</button>
))}
</div>
</div>
</div>
)}
{settings.activeTab !== 'appearance' && (
<div className="ruoyi-settings-empty">
<p></p>
</div>
)}
</div>
{/* Footer */}
<footer className="ruoyi-settings-footer">
<button
type="button"
className="ruoyi-btn-copy"
onClick={handleCopySettings}
>
<Copy size={14} />
{copied ? '已复制偏好设置' : '复制偏好设置'}
</button>
<button
type="button"
className="ruoyi-btn-reset-cache"
onClick={() => {
onReset();
alert('已清空主题缓存并还原默认设置');
}}
>
<Trash2 size={13} />
&amp;
</button>
</footer>
</aside>
</div>
);
}