chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,83 @@
import { useThemeStore, type ThemePref } from '../stores/themeStore';
import { SunIcon, MoonIcon, MonitorIcon } from './icons';
/**
* 主题切换。
*
* 两种形态共用一份状态:
* - `compact`(侧栏 / 底部导航):单按钮,点一下翻转
* - 默认(「我的」页):三选一,因为 `system` 只有在能明确选中时才有意义
*
* 单按钮不足以表达三态,但侧栏放不下三个选项;而只给单按钮的话
* 用户一旦点过就永久脱离了「跟随系统」—— 那是个回不去的单向门。
* 所以两个入口都提供,compact 用于快速切换,完整形态用于设定偏好。
*/
const OPTIONS: { value: ThemePref; label: string; hint: string; Icon: (p: { className?: string }) => JSX.Element }[] = [
{ value: 'light', label: '浅色', hint: '始终使用浅色', Icon: SunIcon },
{ value: 'dark', label: '深色', hint: '始终使用深色', Icon: MoonIcon },
{ value: 'system', label: '跟随系统', hint: '随系统的深浅色设置切换', Icon: MonitorIcon }
];
/** 侧栏用的单按钮:点一下在浅/深之间翻转。 */
export function ThemeToggleButton({ className = '' }: { className?: string }) {
const resolved = useThemeStore(s => s.resolved);
const pref = useThemeStore(s => s.pref);
const toggle = useThemeStore(s => s.toggle);
const dark = resolved === 'dark';
return (
<button
onClick={toggle}
title={
pref === 'system'
? `跟随系统(当前${dark ? '深色' : '浅色'})—— 点击固定为${dark ? '浅色' : '深色'}`
: `当前${dark ? '深色' : '浅色'} —— 点击切换`
}
aria-label="切换主题"
className={className}
>
{dark ? <MoonIcon className="w-3.5 h-3.5" /> : <SunIcon className="w-3.5 h-3.5" />}
</button>
);
}
/** 「我的」页用的三选一。 */
export default function ThemePicker() {
const pref = useThemeStore(s => s.pref);
const resolved = useThemeStore(s => s.resolved);
const setPref = useThemeStore(s => s.setPref);
return (
<div>
<div className="flex items-center gap-2 mb-2">
<h3 className="text-sm font-medium text-gray-900">外观</h3>
{pref === 'system' && (
<span className="text-xs text-gray-500">
当前跟随系统:{resolved === 'dark' ? '深色' : '浅色'}
</span>
)}
</div>
<div className="grid grid-cols-3 gap-2">
{OPTIONS.map(o => {
const active = pref === o.value;
return (
<button
key={o.value}
onClick={() => setPref(o.value)}
title={o.hint}
className={`px-3 py-2.5 rounded border text-xs flex flex-col items-center gap-1.5 transition-colors ${
active
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
}`}
>
<o.Icon className="w-4 h-4" />
{o.label}
</button>
);
})}
</div>
</div>
);
}