feat(cards): 卡片外观可配置(不透明度 / 毛玻璃)
- 站点设置新增 card.opacity(40–100)与 card.blur,后台「首页管理 → 卡片外观」可调 - 前台所有卡片(首页 / 分组页 / 详情 / 侧栏)共用一套 CSS 变量 - 顺带带上上一轮:背景缓存、搜索记忆、明暗图标跟随实际状态、卡片描边/阴影、分组页卡片外壳
This commit is contained in:
@@ -32,6 +32,8 @@ import { fetchSession, logout, type SessionState } from './auth'
|
||||
import {
|
||||
DEFAULT_SEARCH_ENGINES,
|
||||
fetchSettings,
|
||||
readCachedBackground,
|
||||
writeCachedBackground,
|
||||
type SettingsDocument,
|
||||
type SiteSettings,
|
||||
} from './settings'
|
||||
@@ -69,6 +71,25 @@ type Route =
|
||||
|
||||
const INTERNAL_SEARCH = { id: 'internal', name: '站内搜索', url: '' } as const
|
||||
|
||||
const SEARCH_ENGINE_KEY = 'stash-search-engine'
|
||||
|
||||
/** 上次选的搜索引擎:只存在这台浏览器,刷新后仍用它;失效时回落到站点默认 */
|
||||
function readSearchEngine(): string | null {
|
||||
try {
|
||||
return window.localStorage.getItem(SEARCH_ENGINE_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeSearchEngine(id: string): void {
|
||||
try {
|
||||
window.localStorage.setItem(SEARCH_ENGINE_KEY, id)
|
||||
} catch {
|
||||
// 隐私模式下 localStorage 不可写,忽略即可
|
||||
}
|
||||
}
|
||||
|
||||
function parseRoute(pathname: string): Route {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
if (segments[0] === 'interface') return { kind: 'api' }
|
||||
@@ -214,11 +235,13 @@ export default function AppNext() {
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [session, setSession] = useState<SessionState | null>(null)
|
||||
const [settings, setSettings] = useState<SettingsDocument | null>(null)
|
||||
/** 上次缓存的背景:设置回来之前先用它上屏,避免刷新时先闪内置背景 */
|
||||
const [cachedBackground] = useState(readCachedBackground)
|
||||
const [loginOpen, setLoginOpen] = useState(false)
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const searchRef = useRef<HTMLInputElement>(null)
|
||||
const [searchEngineId, setSearchEngineId] = useState<string | null>(null)
|
||||
const [searchEngineId, setSearchEngineId] = useState<string | null>(readSearchEngine)
|
||||
const [engineOpen, setEngineOpen] = useState(false)
|
||||
const engineRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -246,6 +269,11 @@ export default function AppNext() {
|
||||
return () => source.close()
|
||||
}, [])
|
||||
|
||||
// 背景地址跟着设置存一份到本机:下次刷新首帧直接用它,不用等接口回来再换
|
||||
useEffect(() => {
|
||||
if (settings !== null) writeCachedBackground(settings.settings.background)
|
||||
}, [settings])
|
||||
|
||||
useEffect(() => {
|
||||
const content = document.getElementById('content-scroll')
|
||||
const main = content?.parentElement
|
||||
@@ -304,7 +332,8 @@ export default function AppNext() {
|
||||
show_service: true,
|
||||
show_nav: true,
|
||||
},
|
||||
background: { light: '', dark: '' },
|
||||
background: cachedBackground ?? { light: '', dark: '' },
|
||||
card: { opacity: 100, blur: false },
|
||||
search: {
|
||||
default: 'internal',
|
||||
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),
|
||||
@@ -380,6 +409,13 @@ export default function AppNext() {
|
||||
}
|
||||
}, [scheme])
|
||||
|
||||
// 卡片外观:透明度和毛玻璃写在 <html> 的 CSS 变量上,前台所有卡片共用一套
|
||||
useEffect(() => {
|
||||
const root = document.documentElement.style
|
||||
root.setProperty('--stash-card-alpha', `${site.card.opacity}%`)
|
||||
root.setProperty('--stash-card-blur', site.card.blur ? 'blur(14px) saturate(1.15)' : 'none')
|
||||
}, [site.card.opacity, site.card.blur])
|
||||
|
||||
/** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */
|
||||
const toggleTheme = () => {
|
||||
const next: ThemeChoice = isDark ? 'light' : 'dark'
|
||||
@@ -435,6 +471,7 @@ export default function AppNext() {
|
||||
|
||||
const chooseEngine = (next: string) => {
|
||||
setSearchEngineId(next)
|
||||
writeSearchEngine(next)
|
||||
setEngineOpen(false)
|
||||
searchRef.current?.focus()
|
||||
}
|
||||
@@ -631,7 +668,9 @@ export default function AppNext() {
|
||||
</div>
|
||||
|
||||
{group.entries.length > 0 ? (
|
||||
renderEntries(group, group.entries)
|
||||
<Card padding="md" className="group-own-block">
|
||||
{renderEntries(group, group.entries)}
|
||||
</Card>
|
||||
) : group.children.some((child) => groupEntryCount(child) > 0) ? null : (
|
||||
<Empty text="这个分组还没有内容" />
|
||||
)}
|
||||
@@ -640,7 +679,7 @@ export default function AppNext() {
|
||||
{group.children
|
||||
.filter((child) => child.enabled !== false)
|
||||
.map((child) => (
|
||||
<section key={child.key} className="group-child-block">
|
||||
<Card key={child.key} padding="md" className="group-child-block">
|
||||
<div className="group-title">
|
||||
<span
|
||||
className="group-bar"
|
||||
@@ -663,7 +702,7 @@ export default function AppNext() {
|
||||
) : (
|
||||
<Empty text="暂无内容" />
|
||||
)}
|
||||
</section>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user