feat: add sky theme and refresh themed UI

Add the sky theme across the frontend and backend theme APIs, including starfield assets and icon-only branding.

Refresh themed grid backgrounds, admin/login/sidebar styling, and theme-specific video/listing polish.
This commit is contained in:
nianzhibai
2026-06-14 11:53:07 +08:00
parent 139e63eef2
commit 9cc8e02bec
22 changed files with 718 additions and 124 deletions
+3 -3
View File
@@ -352,7 +352,7 @@ type App struct {
// 串行化可以避免启动后台挂载和手动扫盘按需挂载同一个 drive 时重复创建 worker。 // 串行化可以避免启动后台挂载和手动扫盘按需挂载同一个 drive 时重复创建 worker。
driveAttachMu sync.Mutex driveAttachMu sync.Mutex
// 全站主题("dark" | "pink"),从 DB 读 // 全站主题("dark" | "pink" | "sky"),从 DB 读
theme string theme string
// 显式指定的 spider91 上传目标 drive ID。 // 显式指定的 spider91 上传目标 drive ID。
// 空字符串表示本地保存不上传,不再自动挑选 pikpak/p115/p123/onedrive/wopan drive。 // 空字符串表示本地保存不上传,不再自动挑选 pikpak/p115/p123/onedrive/wopan drive。
@@ -451,7 +451,7 @@ func (a *App) Theme() string {
// SetTheme 切换并持久化主题;未知值会返回错误。 // SetTheme 切换并持久化主题;未知值会返回错误。
func (a *App) SetTheme(ctx context.Context, theme string) error { func (a *App) SetTheme(ctx context.Context, theme string) error {
if theme != "dark" && theme != "pink" { if theme != "dark" && theme != "pink" && theme != "sky" {
return fmt.Errorf("unsupported theme %q", theme) return fmt.Errorf("unsupported theme %q", theme)
} }
a.mu.Lock() a.mu.Lock()
@@ -470,7 +470,7 @@ func (a *App) loadTheme(ctx context.Context) {
a.mu.Unlock() a.mu.Unlock()
return return
} }
if v != "pink" && v != "dark" { if v != "pink" && v != "dark" && v != "sky" {
v = "dark" v = "dark"
} }
a.mu.Lock() a.mu.Lock()
+1 -1
View File
@@ -71,7 +71,7 @@ type AdminServer struct {
// enabled=true 时上层应该重新把 pending 预览视频入队(类似旧的全局开关从关到开); // enabled=true 时上层应该重新把 pending 预览视频入队(类似旧的全局开关从关到开);
// enabled=false 时通常不用做事 —— worker 入队前会再次查 catalog,自然停止。 // enabled=false 时通常不用做事 —— worker 入队前会再次查 catalog,自然停止。
OnTeaserEnabledChanged func(driveID string, enabled bool) OnTeaserEnabledChanged func(driveID string, enabled bool)
// Theme 读写("dark" | "pink" // Theme 读写("dark" | "pink" | "sky"
GetTheme func() string GetTheme func() string
SetTheme func(theme string) error SetTheme func(theme string) error
// Spider91 → 115/123/PikPak/OneDrive/Google Drive/联通网盘 上传目标 drive ID 读写 // Spider91 → 115/123/PikPak/OneDrive/Google Drive/联通网盘 上传目标 drive ID 读写
+3 -3
View File
@@ -64,7 +64,7 @@ type Server struct {
tagCacheUntil time.Time tagCacheUntil time.Time
tagCache []TagDTO tagCache []TagDTO
// GetTheme 返回当前生效的主题("dark" | "pink")。前台 /api/settings/theme 用, // GetTheme 返回当前生效的主题("dark" | "pink" | "sky")。前台 /api/settings/theme 用,
// 不需要登录。无注入时返回 "dark"。 // 不需要登录。无注入时返回 "dark"。
GetTheme func() string GetTheme func() string
} }
@@ -160,11 +160,11 @@ func (s *Server) RegisterRoutes(r chi.Router, a *auth.Authenticator) {
} }
// handleGetTheme 返回当前生效的主题。无需登录。响应永远是 // handleGetTheme 返回当前生效的主题。无需登录。响应永远是
// {"theme": "dark"} 或 {"theme": "pink"},便于前端无脑解析。 // {"theme": "dark" | "pink" | "sky"},便于前端无脑解析。
func (s *Server) handleGetTheme(w http.ResponseWriter, r *http.Request) { func (s *Server) handleGetTheme(w http.ResponseWriter, r *http.Request) {
theme := "dark" theme := "dark"
if s.GetTheme != nil { if s.GetTheme != nil {
if v := s.GetTheme(); v == "pink" || v == "dark" { if v := s.GetTheme(); v == "pink" || v == "dark" || v == "sky" {
theme = v theme = v
} }
} }
+3 -2
View File
@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/png" href="/icon.png" />
<link rel="apple-touch-icon" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="91 视频站" /> <meta name="description" content="91 视频站" />
<title>91</title> <title>91</title>
@@ -19,7 +20,7 @@
(function () { (function () {
try { try {
var t = localStorage.getItem("video-site:theme"); var t = localStorage.getItem("video-site:theme");
if (t === "pink" || t === "dark") { if (t === "pink" || t === "dark" || t === "sky") {
document.documentElement.setAttribute("data-theme", t); document.documentElement.setAttribute("data-theme", t);
} else { } else {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

+66 -61
View File
@@ -1,4 +1,5 @@
import { Navigate, Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes } from "react-router-dom";
import { SkyStarfield } from "@/components/SkyStarfield";
import HomePage from "@/pages/HomePage"; import HomePage from "@/pages/HomePage";
import ListingPage from "@/pages/ListingPage"; import ListingPage from "@/pages/ListingPage";
import ShortsPage from "@/pages/ShortsPage"; import ShortsPage from "@/pages/ShortsPage";
@@ -15,69 +16,73 @@ import { ThemePage } from "@/admin/ThemePage";
export default function App() { export default function App() {
return ( return (
<Routes> <>
<Route path="/login" element={<LoginPage />} /> {/* 星空蓝主题的固定位置星星层,仅在 data-theme="sky" 下可见 */}
<SkyStarfield />
<Routes>
<Route path="/login" element={<LoginPage />} />
{/* 主站需要登录 */} {/* 主站需要登录 */}
<Route <Route
path="/" path="/"
element={ element={
<RequireAuth> <RequireAuth>
<HomePage /> <HomePage />
</RequireAuth> </RequireAuth>
} }
/> />
<Route <Route
path="/list" path="/list"
element={ element={
<RequireAuth> <RequireAuth>
<ListingPage /> <ListingPage />
</RequireAuth> </RequireAuth>
} }
/> />
<Route <Route
path="/shorts" path="/shorts"
element={ element={
<RequireAuth> <RequireAuth>
<ShortsPage /> <ShortsPage />
</RequireAuth> </RequireAuth>
} }
/> />
<Route <Route
path="/upload" path="/upload"
element={ element={
<RequireAuth> <RequireAuth>
<UploadPage /> <UploadPage />
</RequireAuth> </RequireAuth>
} }
/> />
<Route <Route
path="/video/:id" path="/video/:id"
element={ element={
<RequireAuth> <RequireAuth>
<VideoDetailPage /> <VideoDetailPage />
</RequireAuth> </RequireAuth>
} }
/> />
{/* 管理后台也需要登录 */} {/* 管理后台也需要登录 */}
<Route <Route
path="/admin" path="/admin"
element={ element={
<RequireAuth> <RequireAuth>
<AdminLayout /> <AdminLayout />
</RequireAuth> </RequireAuth>
} }
> >
<Route index element={<Navigate to="/admin/drives" replace />} /> <Route index element={<Navigate to="/admin/drives" replace />} />
<Route path="drives" element={<DrivesPage />} /> <Route path="drives" element={<DrivesPage />} />
<Route path="crawlers" element={<CrawlersPage />} /> <Route path="crawlers" element={<CrawlersPage />} />
<Route path="videos" element={<VideosPage />} /> <Route path="videos" element={<VideosPage />} />
<Route path="tags" element={<TagsPage />} /> <Route path="tags" element={<TagsPage />} />
<Route path="theme" element={<ThemePage />} /> <Route path="theme" element={<ThemePage />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</>
); );
} }
-7
View File
@@ -4,7 +4,6 @@ import {
HardDrive, HardDrive,
Film, Film,
LogOut, LogOut,
Play,
Home, Home,
Tags, Tags,
Palette, Palette,
@@ -71,12 +70,6 @@ export function AdminLayout() {
return ( return (
<div className="admin-shell"> <div className="admin-shell">
<aside className="admin-sidebar"> <aside className="admin-sidebar">
<div className="admin-sidebar__brand">
<span className="admin-sidebar__brand-mark">
<Play size={14} fill="#000" />
</span>
<span className="admin-sidebar__brand-text">91</span>
</div>
<nav className="admin-nav"> <nav className="admin-nav">
<div className="admin-nav__group admin-nav__group--home"> <div className="admin-nav__group admin-nav__group--home">
<span className="admin-nav__group-label"></span> <span className="admin-nav__group-label"></span>
+5 -3
View File
@@ -79,9 +79,11 @@ export function LoginPage() {
return ( return (
<div className="admin-login"> <div className="admin-login">
<form className="admin-login__card" onSubmit={handleSubmit}> <form className="admin-login__card" onSubmit={handleSubmit}>
<h1 className="admin-login__title"> {setupRequired && (
<Play size={18} fill="currentColor" /> {setupRequired ? "首次设置管理员" : "登录"} <h1 className="admin-login__title">
</h1> <Play size={18} fill="currentColor" />
</h1>
)}
<div className="admin-form"> <div className="admin-form">
<div className="admin-form__row"> <div className="admin-form__row">
<label htmlFor="admin-login-username"></label> <label htmlFor="admin-login-username"></label>
+9 -2
View File
@@ -1,12 +1,12 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Check, Loader2, Moon, Sparkles } from "lucide-react"; import { Check, Loader2, Moon, Sparkles, Star } from "lucide-react";
import * as api from "./api"; import * as api from "./api";
import type { Theme } from "./api"; import type { Theme } from "./api";
import { useToast } from "./ToastContext"; import { useToast } from "./ToastContext";
import { applyTheme, getCurrentTheme } from "@/lib/theme"; import { applyTheme, getCurrentTheme } from "@/lib/theme";
function isTheme(value: unknown): value is Theme { function isTheme(value: unknown): value is Theme {
return value === "dark" || value === "pink"; return value === "dark" || value === "pink" || value === "sky";
} }
type Option = { type Option = {
@@ -32,6 +32,13 @@ const OPTIONS: Option[] = [
description: "柔和奶白底 + 樱花粉主色,清爽温柔,日间使用更舒适。", description: "柔和奶白底 + 樱花粉主色,清爽温柔,日间使用更舒适。",
icon: Sparkles, icon: Sparkles,
}, },
{
id: "sky",
title: "星空蓝 + 暖星黄",
subtitle: "Starry Sky",
description: "浅天空蓝底 + 暖星黄主色,配上淡淡的网格与点点星光,顶级美感。",
icon: Star,
},
]; ];
/** /**
+1 -1
View File
@@ -632,7 +632,7 @@ export function deleteTag(id: number) {
// ---------- Settings ---------- // ---------- Settings ----------
export type Theme = "dark" | "pink"; export type Theme = "dark" | "pink" | "sky";
export type Settings = { export type Settings = {
theme: Theme; theme: Theme;
+1 -3
View File
@@ -3,7 +3,6 @@ import { NavLink } from "react-router-dom";
import { import {
Film, Film,
Menu, Menu,
Play,
Settings, Settings,
Sparkles, Sparkles,
Upload, Upload,
@@ -25,9 +24,8 @@ export function MainNav() {
<div className="container main-nav__inner"> <div className="container main-nav__inner">
<NavLink to="/" className="main-nav__logo"> <NavLink to="/" className="main-nav__logo">
<span className="main-nav__logo-mark"> <span className="main-nav__logo-mark">
<Play size={16} fill="#000" /> <img src="/icon.png" alt="" className="main-nav__logo-img" />
</span> </span>
<span className="main-nav__logo-text">91</span>
</NavLink> </NavLink>
<ul className="main-nav__list" role="menubar"> <ul className="main-nav__list" role="menubar">
+116
View File
@@ -0,0 +1,116 @@
import type { CSSProperties } from "react";
/**
* 星空蓝主题专属:视口级星星贴纸层。
*
* 用 vip.215.im 那套动画 GIF 贴纸:每个 GIF 自带逐帧闪烁动画,
* 比 CSS opacity 呼吸真实得多。桌面和手机分开维护点位,避免首屏密度
* 被页面高度拉伸,也避免手机端星星过大。
*
* - 资源在 public/stickers/star-*.gif,会被打包到 dist/stickers/
* - 渲染在 App 根节点,主站和后台都看得到
* - data-theme!=="sky" 时 CSS display: none,不占布局
* - aria-hidden + pointer-events: none,对可访问性和点击都透明
* - 加 / 减 / 调星只动 DESKTOP_STARS / MOBILE_STARS 数组
*/
const STICKERS = [
"/stickers/star-gold.gif",
"/stickers/star-pink.gif",
"/stickers/star-sparkle.gif",
"/stickers/star-mini.gif",
];
type StarSpec = {
/** 锚点用百分号写,CSS 直接当 top/left/right/bottom 用 */
top?: string;
bottom?: string;
left?: string;
right?: string;
/** 像素,控制 GIF 渲染尺寸 */
size: number;
};
/**
* 桌面:星星偏四周和顶部,主体阅读区保持干净。
* 大星只放边角,小星补顶部和侧边空隙。
*/
const DESKTOP_STARS: StarSpec[] = [
{ top: "6%", left: "5%", size: 44 },
{ top: "4%", left: "24%", size: 26 },
{ top: "8%", right: "12%", size: 48 },
{ top: "17%", right: "31%", size: 30 },
{ top: "24%", left: "8%", size: 34 },
{ top: "28%", right: "5%", size: 38 },
{ top: "43%", left: "3%", size: 24 },
{ top: "49%", right: "9%", size: 28 },
{ top: "63%", left: "11%", size: 32 },
{ top: "66%", right: "18%", size: 44 },
{ bottom: "14%", left: "5%", size: 36 },
{ bottom: "10%", right: "6%", size: 42 },
{ bottom: "4%", left: "33%", size: 24 },
{ bottom: "6%", right: "34%", size: 28 },
{ top: "13%", left: "52%", size: 22 },
{ bottom: "24%", right: "41%", size: 22 },
];
/**
* 手机:数量更少、尺寸更小,只做边缘点缀。
*/
const MOBILE_STARS: StarSpec[] = [
{ top: "7%", left: "6%", size: 30 },
{ top: "11%", right: "7%", size: 28 },
{ top: "24%", right: "3%", size: 22 },
{ top: "39%", left: "4%", size: 22 },
{ top: "57%", right: "6%", size: 26 },
{ bottom: "23%", left: "9%", size: 24 },
{ bottom: "12%", right: "12%", size: 30 },
{ bottom: "5%", left: "48%", size: 20 },
];
export function SkyStarfield() {
return (
<div className="sky-starfield" aria-hidden="true">
{DESKTOP_STARS.map((s, i) => {
const style: CSSProperties = {
top: s.top,
bottom: s.bottom,
left: s.left,
right: s.right,
width: s.size,
height: s.size,
};
const src = STICKERS[i % STICKERS.length];
return (
<img
key={`desktop-${i}`}
className="sky-star sky-star--desktop"
src={src}
alt=""
style={style}
/>
);
})}
{MOBILE_STARS.map((s, i) => {
const style: CSSProperties = {
top: s.top,
bottom: s.bottom,
left: s.left,
right: s.right,
width: s.size,
height: s.size,
};
const src = STICKERS[(i + 1) % STICKERS.length];
return (
<img
key={`mobile-${i}`}
className="sky-star sky-star--mobile"
src={src}
alt=""
style={style}
/>
);
})}
</div>
);
}
+3 -3
View File
@@ -10,13 +10,13 @@
// 公开端点 /api/settings/theme 不需要登录,原因见 backend/internal/api/api.go 中 // 公开端点 /api/settings/theme 不需要登录,原因见 backend/internal/api/api.go 中
// 的注释——登录页本身就要在用户登录之前正确显示主题。 // 的注释——登录页本身就要在用户登录之前正确显示主题。
export type Theme = "dark" | "pink"; export type Theme = "dark" | "pink" | "sky";
export const THEMES: Theme[] = ["dark", "pink"]; export const THEMES: Theme[] = ["dark", "pink", "sky"];
const STORAGE_KEY = "video-site:theme"; const STORAGE_KEY = "video-site:theme";
function isTheme(value: unknown): value is Theme { function isTheme(value: unknown): value is Theme {
return value === "dark" || value === "pink"; return value === "dark" || value === "pink" || value === "sky";
} }
/** /**
+75 -15
View File
@@ -32,8 +32,9 @@
.admin-sidebar__brand { .admin-sidebar__brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; justify-content: center;
padding: 0 12px var(--space-6); gap: 0;
padding: 0 12px var(--space-5);
font-size: var(--font-xl); font-size: var(--font-xl);
font-weight: var(--weight-bold); font-weight: var(--weight-bold);
color: var(--text-strong); color: var(--text-strong);
@@ -52,22 +53,29 @@
.admin-sidebar__brand-mark { .admin-sidebar__brand-mark {
display: grid; display: grid;
place-items: center; place-items: center;
width: 38px; width: 40px;
height: 38px; height: 40px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--accent-gradient); background: transparent;
color: var(--text-on-accent); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
box-shadow: 0 4px 14px var(--accent-glow), var(--shadow-inset); overflow: hidden;
animation: admin-brand-pulse 3s infinite ease-in-out; animation: admin-brand-pulse 3s infinite ease-in-out;
} }
.admin-sidebar__brand-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
}
@keyframes admin-brand-pulse { @keyframes admin-brand-pulse {
0%, 100% { 0%, 100% {
box-shadow: 0 4px 12px var(--accent-glow), var(--shadow-inset); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);
transform: scale(1); transform: scale(1);
} }
50% { 50% {
box-shadow: 0 6px 20px rgba(255, 138, 60, 0.45), var(--shadow-inset); box-shadow: 0 6px 20px var(--accent-glow);
transform: scale(1.04); transform: scale(1.04);
} }
} }
@@ -76,8 +84,10 @@
.admin-nav { .admin-nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 22px; flex: 1;
padding: var(--space-5) 0; justify-content: space-evenly;
gap: var(--space-4);
padding: var(--space-2) 0 var(--space-4);
} }
.admin-nav__group { .admin-nav__group {
@@ -3891,12 +3901,16 @@
.theme-card[data-preview="dark"] .theme-card__preview { .theme-card[data-preview="dark"] .theme-card__preview {
background: background:
radial-gradient(80% 60% at 50% 0%, rgba(255, 138, 60, 0.18), transparent 70%), radial-gradient(80% 60% at 50% 0%, rgba(255, 138, 60, 0.18), transparent 70%),
linear-gradient(to right, rgba(255, 138, 60, 0.14) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(to bottom, rgba(255, 138, 60, 0.14) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(180deg, #14161c 0%, #0b0c10 100%); linear-gradient(180deg, #14161c 0%, #0b0c10 100%);
} }
.theme-card[data-preview="pink"] .theme-card__preview { .theme-card[data-preview="pink"] .theme-card__preview {
background: background:
radial-gradient(80% 60% at 50% 0%, rgba(255, 91, 138, 0.16), transparent 70%), radial-gradient(80% 60% at 50% 0%, rgba(255, 91, 138, 0.16), transparent 70%),
linear-gradient(to right, rgba(255, 91, 138, 0.18) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(to bottom, rgba(255, 91, 138, 0.18) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(180deg, #ffffff 0%, #fff5f7 100%); linear-gradient(180deg, #ffffff 0%, #fff5f7 100%);
} }
@@ -4017,6 +4031,43 @@
border-color: rgba(255, 91, 138, 0.6); border-color: rgba(255, 91, 138, 0.6);
} }
/* ----- 星空蓝预览:浅蓝渐变 + 隐约网格 + 几颗黄星点缀 ----- */
.theme-card[data-preview="sky"] .theme-card__preview {
background:
radial-gradient(80% 60% at 50% 0%, rgba(255, 200, 61, 0.22), transparent 70%),
linear-gradient(to right, rgba(255, 255, 255, 0.5) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(to bottom, rgba(255, 255, 255, 0.5) 1px, transparent 1px) 0 0 / 18px 18px,
linear-gradient(180deg, #d6ecff 0%, #b8dcff 100%);
}
.theme-card[data-preview="sky"] .theme-card__bar {
background: linear-gradient(135deg, #ffe28a 0%, #ffc83d 100%);
box-shadow: 0 0 12px rgba(255, 200, 61, 0.5);
}
.theme-card[data-preview="sky"] .theme-card__player {
box-shadow: 0 0 0 1px rgba(255, 200, 61, 0.5),
0 12px 28px rgba(40, 80, 160, 0.22);
}
.theme-card[data-preview="sky"] .theme-card__line {
background: rgba(40, 70, 140, 0.22);
}
.theme-card[data-preview="sky"] .theme-card__line--lg {
background: rgba(27, 37, 71, 0.55);
}
.theme-card[data-preview="sky"] .theme-card__chip {
background: rgba(40, 70, 140, 0.08);
border: 1px solid rgba(40, 70, 140, 0.22);
}
.theme-card[data-preview="sky"] .theme-card__chip--accent {
background: rgba(255, 200, 61, 0.28);
border-color: rgba(255, 200, 61, 0.7);
}
/* 卡片底部信息区 */ /* 卡片底部信息区 */
.theme-card__body { .theme-card__body {
display: flex; display: flex;
@@ -4878,14 +4929,23 @@
} }
.admin-tag-card__alias-pill { .admin-tag-card__alias-pill {
font-size: 10px; font-size: var(--font-xs);
padding: 1px 6px; font-weight: var(--weight-semibold);
line-height: 1.3;
padding: 2px 7px;
background: var(--bg-sunken); background: var(--bg-sunken);
color: var(--text-muted); color: var(--text-default);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-default);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
} }
:root[data-theme="sky"] .admin-tag-card__alias-pill {
background: rgba(47, 111, 214, 0.13);
border-color: rgba(47, 111, 214, 0.2);
color: #2f436f;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-tag-card__footer { .admin-tag-card__footer {
display: flex; display: flex;
align-items: center; align-items: center;
+11 -15
View File
@@ -49,33 +49,29 @@
} }
.main-nav__logo:hover .main-nav__logo-mark { .main-nav__logo:hover .main-nav__logo-mark {
transform: rotate(6deg) scale(1.05); transform: rotate(3deg) scale(1.04);
box-shadow: 0 6px 20px rgba(255, 138, 60, 0.45), var(--shadow-inset); box-shadow: 0 6px 20px var(--accent-glow);
} }
.main-nav__logo-mark { .main-nav__logo-mark {
display: grid; display: grid;
place-items: center; place-items: center;
width: 32px; width: 34px;
height: 32px; height: 34px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--accent-gradient); background: transparent;
color: var(--text-on-accent); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
box-shadow:
0 4px 14px var(--accent-glow),
var(--shadow-inset);
position: relative; position: relative;
isolation: isolate; isolation: isolate;
overflow: hidden; overflow: hidden;
transition: transform var(--transition-fast), box-shadow var(--transition-fast); transition: transform var(--transition-fast), box-shadow var(--transition-fast);
} }
.main-nav__logo-mark::after { .main-nav__logo-img {
content: ""; width: 100%;
position: absolute; height: 100%;
inset: 0; object-fit: cover;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 60%); border-radius: inherit;
pointer-events: none;
} }
/* ----- 链接列表 ----- */ /* ----- 链接列表 ----- */
+2
View File
@@ -173,6 +173,7 @@
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-3) var(--space-2); padding: var(--space-3) var(--space-2);
margin-top: var(--space-3); margin-top: var(--space-3);
margin-bottom: var(--space-4);
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-md); border-radius: var(--radius-md);
@@ -285,6 +286,7 @@
.sort-toolbar { .sort-toolbar {
padding: var(--space-2); padding: var(--space-2);
gap: var(--space-2); gap: var(--space-2);
margin-bottom: var(--space-3);
} }
.sort-toolbar__group { .sort-toolbar__group {
+405 -5
View File
@@ -1,15 +1,16 @@
/* ========================================================= /* =========================================================
* Design Tokens * Design Tokens
* *
* 套主题 * 套主题
* [data-theme="dark"] 暗黑 + 暖橙默认 / 兜底 * [data-theme="dark"] 暗黑 + 暖橙默认 / 兜底
* [data-theme="pink"] 奶油白 + 樱花粉 * [data-theme="pink"] 奶油白 + 樱花粉
* [data-theme="sky"] 星空蓝 + 暖星黄
* *
* 切换方式 * 切换方式
* document.documentElement.setAttribute("data-theme", "pink" | "dark") * document.documentElement.setAttribute("data-theme", "dark" | "pink" | "sky")
* *
* 设计约束 * 设计约束
* - 套用相同的 token key组件 CSS 一行不动 * - 套用相同的 token key组件 CSS 一行不动
* - 间距 / 圆角 / 字号 / 过渡等"非主题"变量挂在 :root * - 间距 / 圆角 / 字号 / 过渡等"非主题"变量挂在 :root
* ========================================================= */ * ========================================================= */
@@ -152,6 +153,17 @@
--shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04); --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04);
} }
/* 暗黑主题:保留暖橙光晕,再叠一层低对比度网格。 */
:root body::before,
:root[data-theme="dark"] body::before {
background:
linear-gradient(to right, rgba(255, 138, 60, 0.09) 1px, transparent 1px) 0 0 / 88px 88px,
linear-gradient(to bottom, rgba(255, 138, 60, 0.09) 1px, transparent 1px) 0 0 / 88px 88px,
radial-gradient(1200px 600px at 85% -10%, rgba(255, 138, 60, 0.12), transparent 60%),
radial-gradient(900px 500px at 10% 110%, rgba(90, 120, 255, 0.06), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 44%);
}
/* ========================================================= /* =========================================================
* 奶油白 + 樱花粉 * 奶油白 + 樱花粉
* *
@@ -233,11 +245,13 @@
--shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.7); --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.7);
} }
/* 粉白主题下 body::before "暖色光晕"改得更柔粉 /* 粉白主题下 body::before 改成柔粉网格 + 轻光晕
base.css 用的是写死的 rgba 渐变这里放一层覆盖 */ base.css 用的是写死的 rgba 渐变这里放一层覆盖 */
:root[data-theme="pink"] body::before { :root[data-theme="pink"] body::before {
background: background:
radial-gradient(1200px 600px at 85% -10%, rgba(255, 91, 138, 0.1), transparent 60%), linear-gradient(to right, rgba(255, 91, 138, 0.13) 1px, transparent 1px) 0 0 / 82px 82px,
linear-gradient(to bottom, rgba(255, 91, 138, 0.13) 1px, transparent 1px) 0 0 / 82px 82px,
radial-gradient(1200px 600px at 85% -10%, rgba(255, 91, 138, 0.12), transparent 60%),
radial-gradient(900px 500px at 10% 110%, rgba(190, 130, 200, 0.07), transparent 60%); radial-gradient(900px 500px at 10% 110%, rgba(190, 130, 200, 0.07), transparent 60%);
} }
@@ -251,3 +265,389 @@
:root[data-theme="pink"] * { :root[data-theme="pink"] * {
scrollbar-color: rgba(180, 90, 120, 0.28) transparent; scrollbar-color: rgba(180, 90, 120, 0.28) transparent;
} }
/* =========================================================
* 星空蓝 + 暖星黄
*
* 设计要点
* - 页底用浅天空蓝带一点点紫的清透蓝不用纯白避免缺少氛围
* - 卡片提亮到接近白色但带蓝调避免和背景太接近
* - 主色用暖星黄#ffc83d与天蓝形成互补关系呼应背景里的星点
* - 文本用深夜蓝#1b2547避免纯黑割裂梦幻氛围
* - 边框/阴影用蓝调让卡片看起来像漂在天空里
* ========================================================= */
:root[data-theme="sky"] {
/* ----- 表面色 ----- */
--bg-page: #c9e4ff;
--bg-surface: #ffffff;
--bg-elevated: #eaf4ff;
--bg-sunken: #b8d8f5;
--bg-overlay: rgba(210, 230, 250, 0.82);
/* 半透明玻璃(蓝白叠透明) */
--glass-nav: rgba(232, 244, 255, 0.78);
--glass-card: rgba(255, 255, 255, 0.72);
/* ----- 边框(深蓝描边) ----- */
--border-subtle: rgba(60, 100, 170, 0.1);
--border-default: rgba(60, 100, 170, 0.18);
--border-strong: rgba(40, 70, 140, 0.26);
--border-accent: rgba(255, 200, 61, 0.6);
/* ----- 文本 ----- */
--text-strong: #1b2547;
--text-default: #324063;
--text-muted: #6a7898;
--text-faint: #a5b1c8;
--text-disabled: #ccd4e2;
--text-on-accent: #3a2400;
--text-on-dark: #1b2547;
/* ----- 主色(暖星黄) ----- */
--accent: #ffc83d;
--accent-hover: #ffd76b;
--accent-strong: #f0b21f;
--accent-soft: rgba(255, 200, 61, 0.16);
--accent-softer: rgba(255, 200, 61, 0.08);
--accent-glow: rgba(255, 200, 61, 0.35);
--accent-gradient: linear-gradient(135deg, #ffe28a 0%, #ffc83d 100%);
--accent-gradient-strong: linear-gradient(135deg, #ffeca8 0%, #ffc83d 55%, #f0a514 100%);
/* ----- 状态色(浅蓝底下加深一些) ----- */
--success: #1ea974;
--success-soft: rgba(30, 169, 116, 0.16);
--warning: #d99022;
--warning-soft: rgba(217, 144, 34, 0.16);
--danger: #e43b5c;
--danger-soft: rgba(228, 59, 92, 0.14);
--info: #2f6fd6;
--info-soft: rgba(47, 111, 214, 0.14);
/* ----- 网盘品牌色(浅蓝底下重新调谐,整体加深) ----- */
--drive-quark: #4467c8;
--drive-p115: #d8485d;
--drive-p123: #128da0;
--drive-pikpak: #6b4ed4;
--drive-wopan: #dc6d28;
--drive-onedrive: #1f7fc0;
--drive-localstorage: #198866;
/* ----- 阴影(蓝色柔投影) ----- */
--shadow-sm: 0 1px 2px rgba(40, 80, 160, 0.1);
--shadow-md:
0 2px 4px rgba(40, 80, 160, 0.1),
0 8px 20px rgba(40, 80, 160, 0.12);
--shadow-lg:
0 4px 10px rgba(40, 80, 160, 0.12),
0 18px 40px rgba(40, 80, 160, 0.16);
--shadow-xl:
0 8px 16px rgba(40, 80, 160, 0.12),
0 28px 60px rgba(40, 80, 160, 0.2);
--shadow-glow: 0 0 0 1px var(--border-accent), 0 8px 28px var(--accent-glow);
--shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
/* 星空蓝主题下body::before 改为承载"网格"base.css 里它画的是暖光晕这里覆写
* 星星 / 闪光不再用 CSS 瓦片改由 React 组件 <SkyStarfield /> 放固定位置元素
* 见本文件下方 .sky-starfield / .sky-star 规则与 src/components/SkyStarfield.tsx */
:root[data-theme="sky"] body::before {
background:
linear-gradient(to right, rgba(255, 255, 255, 0.55) 1px, transparent 1px) 0 0 / 80px 80px,
linear-gradient(to bottom, rgba(255, 255, 255, 0.55) 1px, transparent 1px) 0 0 / 80px 80px;
}
/* 星空蓝主题:自定义滚动条(覆盖 base.css 的硬编码白色透明) */
:root[data-theme="sky"] ::-webkit-scrollbar-thumb {
background: rgba(60, 100, 170, 0.22);
}
:root[data-theme="sky"] ::-webkit-scrollbar-thumb:hover {
background: rgba(60, 100, 170, 0.36);
}
:root[data-theme="sky"] * {
scrollbar-color: rgba(60, 100, 170, 0.28) transparent;
}
/* =========================================================
* 星空蓝主题专属视口级星星贴纸SkyStarfield 组件
*
* 设计原则
* - 不用 CSS 瓦片 pattern那会让星星在屏幕上"重复堆叠"显脏
* - 桌面和手机分开点位桌面围绕四周手机减少数量并缩小尺寸
* - 使用 fixed 定位让首屏星星密度稳定不被长页面高度稀释
* - pointer-events: none对滚动和点击都透明
* - sky 主题下整层 display: none不渲染也不占位
* ========================================================= */
.sky-starfield {
display: none;
}
:root[data-theme="sky"] .sky-starfield {
/* fixed + 四边显式 0星星锚定视口滚动时保持稳定的背景氛围
* top/left/right/bottom 而不是 inset 是为了避免某些旧浏览器对 inset
* 简写的支持差异Safari 14.1 之前
* overflow: hidden 防止 left 百分比在窄屏下溢出造成水平滚动条 */
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.sky-star {
position: absolute;
pointer-events: none;
display: block;
user-select: none;
max-width: none;
opacity: 0.72;
/* GIF 自身就是动画 + 彩色,不再叠 CSS twinkle / drop-shadow */
}
.sky-star--mobile {
display: none;
}
/* 每颗星的 top/left/right/bottom + width/height 都走 inline style。 */
:root .app-shell,
:root .admin-shell,
:root .admin-main,
:root .admin-login,
:root .admin-loading-screen {
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
:root[data-theme="sky"] .sky-star--desktop {
display: none;
}
:root[data-theme="sky"] .sky-star--mobile {
display: block;
opacity: 0.64;
}
}
@media (min-width: 769px) {
:root .admin-sidebar,
:root[data-theme="dark"] .admin-sidebar {
background:
linear-gradient(90deg, rgba(18, 20, 27, 0.86) 0%, rgba(18, 20, 27, 0.66) 72%, rgba(18, 20, 27, 0.24) 100%),
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 138, 60, 0.008) 100%);
border-right: 0;
box-shadow: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
:root .admin-sidebar__brand,
:root .admin-sidebar__footer,
:root[data-theme="dark"] .admin-sidebar__brand,
:root[data-theme="dark"] .admin-sidebar__footer {
border-color: rgba(255, 255, 255, 0.07);
}
:root .admin-nav__icon,
:root .admin-sidebar__footer .admin-sidebar__home svg,
:root .admin-sidebar__footer .admin-sidebar__check-update svg,
:root .admin-sidebar__footer .admin-sidebar__logout svg,
:root[data-theme="dark"] .admin-nav__icon,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__home svg,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__check-update svg,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__logout svg {
background: rgba(255, 255, 255, 0.055);
border-color: rgba(255, 255, 255, 0.08);
}
:root .admin-nav__link:hover,
:root .admin-sidebar__footer .admin-sidebar__home:hover,
:root .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled),
:root[data-theme="dark"] .admin-nav__link:hover,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__home:hover,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.055);
border-color: rgba(255, 138, 60, 0.065);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.18);
}
:root .admin-nav__link:hover .admin-nav__icon,
:root .admin-sidebar__footer .admin-sidebar__home:hover svg,
:root .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) svg,
:root[data-theme="dark"] .admin-nav__link:hover .admin-nav__icon,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__home:hover svg,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) svg {
background: rgba(255, 255, 255, 0.085);
border-color: rgba(255, 255, 255, 0.13);
}
:root .admin-nav__link.is-active,
:root[data-theme="dark"] .admin-nav__link.is-active {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 138, 60, 0.14);
box-shadow:
0 10px 26px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
:root .admin-nav__link.is-active .admin-nav__icon,
:root[data-theme="dark"] .admin-nav__link.is-active .admin-nav__icon {
background: rgba(255, 138, 60, 0.07);
border-color: rgba(255, 138, 60, 0.2);
color: rgba(255, 176, 112, 0.76);
}
:root .admin-nav__link.is-active::before,
:root[data-theme="dark"] .admin-nav__link.is-active::before {
left: 8px;
width: 2px;
opacity: 0.45;
box-shadow: none;
}
:root .admin-sidebar__footer .admin-sidebar__logout:hover,
:root[data-theme="dark"] .admin-sidebar__footer .admin-sidebar__logout:hover {
background: rgba(241, 85, 108, 0.12);
border-color: rgba(241, 85, 108, 0.25);
}
:root[data-theme="pink"] .admin-sidebar {
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 245, 249, 0.34) 72%, transparent 100%),
linear-gradient(180deg, rgba(255, 91, 138, 0.08) 0%, rgba(255, 255, 255, 0.18) 100%);
border-right: 0;
box-shadow: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
:root[data-theme="pink"] .admin-sidebar__brand,
:root[data-theme="pink"] .admin-sidebar__footer {
border-color: rgba(255, 91, 138, 0.11);
}
:root[data-theme="pink"] .admin-nav__icon,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__home svg,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__check-update svg,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__logout svg {
background: rgba(255, 255, 255, 0.38);
border-color: rgba(255, 91, 138, 0.12);
}
:root[data-theme="pink"] .admin-nav__link:hover,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__home:hover,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.42);
border-color: rgba(255, 91, 138, 0.16);
box-shadow: 0 8px 22px rgba(180, 90, 120, 0.08);
}
:root[data-theme="pink"] .admin-nav__link:hover .admin-nav__icon,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__home:hover svg,
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) svg {
background: rgba(255, 255, 255, 0.58);
border-color: rgba(255, 91, 138, 0.2);
}
:root[data-theme="pink"] .admin-nav__link.is-active {
background: rgba(255, 255, 255, 0.58);
border-color: rgba(255, 91, 138, 0.34);
box-shadow:
0 10px 26px rgba(180, 90, 120, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
:root[data-theme="pink"] .admin-nav__link.is-active .admin-nav__icon {
background: rgba(255, 91, 138, 0.16);
border-color: rgba(255, 91, 138, 0.42);
}
:root[data-theme="pink"] .admin-nav__link.is-active::before {
left: 8px;
width: 2px;
opacity: 0.72;
box-shadow: none;
}
:root[data-theme="pink"] .admin-sidebar__footer .admin-sidebar__logout:hover {
background: rgba(228, 59, 92, 0.1);
border-color: rgba(228, 59, 92, 0.22);
}
:root[data-theme="sky"] .admin-sidebar {
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0.18) 72%, transparent 100%),
linear-gradient(180deg, rgba(255, 255, 255, 0.22) 0%, rgba(232, 244, 255, 0.08) 100%);
border-right: 0;
box-shadow: none;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
:root[data-theme="sky"] .admin-sidebar__brand,
:root[data-theme="sky"] .admin-sidebar__footer {
border-color: rgba(60, 100, 170, 0.1);
}
:root[data-theme="sky"] .admin-nav__icon,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__home svg,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__check-update svg,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__logout svg {
background: rgba(255, 255, 255, 0.28);
border-color: rgba(60, 100, 170, 0.12);
}
:root[data-theme="sky"] .admin-nav__link:hover,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__home:hover,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.26);
border-color: rgba(60, 100, 170, 0.14);
box-shadow: 0 8px 22px rgba(40, 80, 160, 0.08);
}
:root[data-theme="sky"] .admin-nav__link:hover .admin-nav__icon,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__home:hover svg,
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__check-update:hover:not(:disabled) svg {
background: rgba(255, 255, 255, 0.42);
border-color: rgba(60, 100, 170, 0.18);
}
:root[data-theme="sky"] .admin-nav__link.is-active {
background: rgba(255, 255, 255, 0.42);
border-color: rgba(255, 200, 61, 0.42);
box-shadow:
0 10px 26px rgba(40, 80, 160, 0.09),
inset 0 1px 0 rgba(255, 255, 255, 0.58);
}
:root[data-theme="sky"] .admin-nav__link.is-active .admin-nav__icon {
background: rgba(255, 200, 61, 0.2);
border-color: rgba(255, 200, 61, 0.58);
}
:root[data-theme="sky"] .admin-nav__link.is-active::before {
left: 8px;
width: 2px;
opacity: 0.72;
box-shadow: none;
}
:root[data-theme="sky"] .admin-sidebar__footer .admin-sidebar__logout:hover {
background: rgba(228, 59, 92, 0.1);
border-color: rgba(228, 59, 92, 0.22);
}
}
/* 后台和登录页默认会用 var(--bg-page) 填满网格会被遮挡
* 三套主题都让外壳透明露出 body::before 的底纹卡片本身仍是实心表面 */
:root .admin-shell,
:root .admin-main,
:root .admin-login,
:root .admin-loading-screen {
background: transparent;
}
+14
View File
@@ -94,6 +94,13 @@
--video-player-progress-hover: rgba(255, 197, 216, 0.42); --video-player-progress-hover: rgba(255, 197, 216, 0.42);
} }
:root[data-theme="sky"] .video-player {
--video-player-progress: #58b8ff;
--video-player-progress-loaded: rgba(88, 184, 255, 0.36);
--video-player-progress-track: rgba(210, 236, 255, 0.32);
--video-player-progress-hover: rgba(166, 220, 255, 0.45);
}
.video-player__mount { .video-player__mount {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -1441,6 +1448,13 @@
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
:root[data-theme="sky"] .vd-rail__hd {
background: var(--accent);
color: var(--text-on-accent);
border: 0;
box-shadow: 0 4px 10px var(--accent-glow);
}
.vd-rail__body { .vd-rail__body {
min-width: 0; min-width: 0;
display: flex; display: flex;