mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 00:44:30 +08:00
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:
@@ -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()
|
||||||
|
|||||||
@@ -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 读写
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
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
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- 链接列表 ----- */
|
/* ----- 链接列表 ----- */
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user