diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx
index ce35c07..29af8ee 100644
--- a/web/src/app/App.tsx
+++ b/web/src/app/App.tsx
@@ -40,7 +40,9 @@ export function AuthBoundary({ children }: { children?: ReactNode }) {
export function MediaLayout() {
const runtime = useRuntime();
const location = useLocation();
- const current = location.pathname.startsWith('/media/music')
+ const current = location.pathname === '/me' || location.pathname.startsWith('/me/')
+ ? 'me'
+ : location.pathname.startsWith('/media/music')
? 'music'
: location.pathname.startsWith('/media/audiobook')
? 'audiobook'
diff --git a/web/src/app/router/routes.tsx b/web/src/app/router/routes.tsx
index 6170376..96d4c2a 100644
--- a/web/src/app/router/routes.tsx
+++ b/web/src/app/router/routes.tsx
@@ -13,6 +13,7 @@ import { ShelfPage } from '../../features/shelf/pages/ShelfPage.tsx';
import { LibraryPage, LibraryBrowsePage, LibraryFilesPage } from '../../features/library/pages/LibraryPage.tsx';
import { SourcesPage } from '../../features/sources/pages/SourcesPage.tsx';
import { ReaderPage } from '../../features/reader/pages/ReaderPage.tsx';
+import { MinePage } from '../../features/account/pages/MinePage.tsx';
import { MediaLibrariesPage, MediaLibraryCreatePage } from '../../features/media/pages/MediaLibrariesPage.tsx';
import { MediaLibraryEditPage, MediaLibraryPermissionsPage } from '../../features/media/pages/MediaLibraryEditorPages.tsx';
import { MediaTasksPage } from '../../features/media/pages/MediaTasksPage.tsx';
@@ -58,6 +59,8 @@ export const routes = [{
// the shelf settings sheet and media settings has its own nested route.
{ path: 'settings', element: },
{ path: 'book/:bookId', element: },
+ // 我的 shares MediaLayout so it follows the chosen theme and keeps the channel rail.
+ { path: 'me', element: , children: [{ index: true, element: }] },
{ path: 'media', element: , children: [
{ index: true, element: },
{ path: 'music', element: },
diff --git a/web/src/features/account/pages/MinePage.tsx b/web/src/features/account/pages/MinePage.tsx
new file mode 100644
index 0000000..eb0ea34
--- /dev/null
+++ b/web/src/features/account/pages/MinePage.tsx
@@ -0,0 +1,70 @@
+import { Link } from 'react-router-dom';
+import { ChevronLeft, ChevronRight, Film, LibraryBig, LogOut, Palette, PlayCircle, Rss, UserRound, type LucideIcon } from 'lucide-react';
+import type { ReactNode } from 'react';
+import { useRuntime } from '../../../app/providers/runtime-context.tsx';
+import { useAuthStore } from '../../../shared/stores/auth.store.ts';
+import { mediaThemes, readMediaTheme } from '../../media/services/theme.ts';
+import '../styles/mine.css';
+
+function Row({ icon: Icon, label, value, to, onClick, danger = false }: { icon: LucideIcon; label: string; value?: string; to?: string; onClick?: () => void; danger?: boolean }) {
+ const body = <>
+
+ {label}
+ {value && {value}}
+ {!danger && }
+ >;
+ const className = danger ? 'mine-row is-danger' : 'mine-row';
+ return to
+ ? {body}
+ : ;
+}
+
+function Group({ title, children }: { title: string; children: ReactNode }) {
+ return
{title}
{children}
;
+}
+
+function serverHost(baseUrl: string): string {
+ try { return new URL(baseUrl).host; } catch { return baseUrl; }
+}
+
+/**
+ * 我的: the one place for who you are, what is connected, and the settings that
+ * already exist elsewhere. It owns no settings of its own: every row links to the
+ * page that does, so there is still exactly one place to change each thing.
+ */
+export function MinePage() {
+ const runtime = useRuntime();
+ const user = useAuthStore(state => state.verifiedUser ?? state.session?.user ?? null);
+ const admin = user?.role === 'admin';
+ const choice = readMediaTheme(runtime.mediaApi.preferenceScope());
+ const themeName = choice === 'system' ? '跟随系统' : (mediaThemes.find(theme => theme.id === choice)?.name ?? '跟随系统');
+ const name = user?.displayName || user?.username || '';
+ return