revert(cards): 撤掉卡片外观配置与分组页卡片外壳,改用文字对比度

- 回退 card.opacity / card.blur 设置与后台「卡片外观」面板
- 分组页内容恢复为直接压在背景图上,不再套卡片
- 分组页文字:次级文字提一档 + 同主题描边(深色暗描边、浅色亮描边)
- 保留上一轮:背景缓存、搜索记忆、明暗图标、卡片描边/阴影、侧栏实底
This commit is contained in:
2026-11-05 14:18:18 +08:00
parent 4ce1b7e7c1
commit 93f80af893
7 changed files with 14 additions and 135 deletions
-2
View File
@@ -113,7 +113,6 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等
"header": { "enabled": true, "speed": 24, "gap": 26, "items": [...] }, "header": { "enabled": true, "speed": 24, "gap": 26, "items": [...] },
"sidebar": { "show_profile": true, "show_service": false, "show_nav": true }, "sidebar": { "show_profile": true, "show_service": false, "show_nav": true },
"background": { "light": "", "dark": "" }, // 空 = 内置背景 "background": { "light": "", "dark": "" }, // 空 = 内置背景
"card": { "opacity": 100, "blur": false }, // 卡片不透明度 40–100 + 毛玻璃
"search": { "search": {
"default": "internal", "default": "internal",
"engines": [ "engines": [
@@ -148,7 +147,6 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等
- 搜索框:默认搜索项、五个外部引擎的名称 / URL / 启停、恢复默认; - 搜索框:默认搜索项、五个外部引擎的名称 / URL / 启停、恢复默认;
- 侧边栏:资料卡 / 本地服务 / 分组导航显隐; - 侧边栏:资料卡 / 本地服务 / 分组导航显隐;
- 内容卡片:卡片 + 勾选分组 + 预览条数 + 排序; - 内容卡片:卡片 + 勾选分组 + 预览条数 + 排序;
- 卡片外观:前台所有卡片的不透明度(40–100)与毛玻璃开关;
- **资料管理**:头像、名称、角色、简介、位置; - **资料管理**:头像、名称、角色、简介、位置;
- **系统管理**:改密码、下载备份。 - **系统管理**:改密码、下载备份。
+1 -1
View File
@@ -54,7 +54,7 @@ cargo run -- --root ./sites --host 0.0.0.0 --port 3000
- 登录后点「站点设置」进入 `/admin`,左侧导航 + 右侧内容: - 登录后点「站点设置」进入 `/admin`,左侧导航 + 右侧内容:
- **分组管理**:树形表格,可建根 / 子分组、图标(字符或 iconfont 图标名)、排序权重、启停、删除; - **分组管理**:树形表格,可建根 / 子分组、图标(字符或 iconfont 图标名)、排序权重、启停、删除;
- **数据管理**:选分组后列出条目(含子分组),每条可单独设格式,可新建 / 编辑 / 删除; - **数据管理**:选分组后列出条目(含子分组),每条可单独设格式,可新建 / 编辑 / 删除;
- **首页管理**(子项):**页头**(站点信息 / 操作工具 / 滚动信息三个页签)、**侧边栏**(三块侧栏卡片显隐)、**内容卡片**(左侧可拖拽排序的卡片列表 + 右侧分组表格:状态、预览条数、顺序、展示)、**卡片外观**(所有卡片的不透明度 40–100 与毛玻璃开关); - **首页管理**(子项):**页头**(站点信息 / 操作工具 / 滚动信息三个页签)、**侧边栏**(三块侧栏卡片显隐)、**内容卡片**(左侧可拖拽排序的卡片列表 + 右侧分组表格:状态、预览条数、顺序、展示);
- **资料管理**与**系统管理**(改密码、下载整库备份); - **资料管理**与**系统管理**(改密码、下载整库备份);
- 分组的「状态」是**唯一可见性来源**:停用的分组在侧栏和卡片里都不再出现,层层往下生效; - 分组的「状态」是**唯一可见性来源**:停用的分组在侧栏和卡片里都不再出现,层层往下生效;
- 详情页的 `[编辑]` 只在登录后显示;保存后局部刷新,`updated_at` 乐观锁; - 详情页的 `[编辑]` 只在登录后显示;保存后局部刷新,`updated_at` 乐观锁;
-35
View File
@@ -13,8 +13,6 @@ pub const SETTINGS_KEY: &str = "site";
pub const MAX_HEADER_ITEMS: usize = 8; pub const MAX_HEADER_ITEMS: usize = 8;
pub const MIN_SPEED: u32 = 8; pub const MIN_SPEED: u32 = 8;
pub const MAX_SPEED: u32 = 120; pub const MAX_SPEED: u32 = 120;
pub const MIN_CARD_OPACITY: u32 = 40;
pub const MAX_CARD_OPACITY: u32 = 100;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@@ -121,25 +119,6 @@ pub struct BackgroundSettings {
pub dark: String, pub dark: String,
} }
/// 卡片外观:透明度与毛玻璃。前台所有卡片共用一套(首页 / 分组页 / 详情页 / 侧栏)。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct CardSettings {
/// 卡片底色的不透明度(百分比,40–100)
pub opacity: u32,
/// 毛玻璃:背景模糊 + 提饱和度
pub blur: bool,
}
impl Default for CardSettings {
fn default() -> Self {
Self {
opacity: 100,
blur: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default)] #[serde(default)]
pub struct SearchEngineSettings { pub struct SearchEngineSettings {
@@ -309,7 +288,6 @@ pub struct Settings {
pub header: HeaderSettings, pub header: HeaderSettings,
pub sidebar: SidebarSettings, pub sidebar: SidebarSettings,
pub background: BackgroundSettings, pub background: BackgroundSettings,
pub card: CardSettings,
pub search: SearchSettings, pub search: SearchSettings,
pub profile: ProfileSettings, pub profile: ProfileSettings,
pub site: SiteMetaSettings, pub site: SiteMetaSettings,
@@ -376,11 +354,6 @@ pub fn validate(settings: &Settings) -> Result<(), StashError> {
"暗色背景地址最多 500 个字符".into(), "暗色背景地址最多 500 个字符".into(),
)); ));
} }
if !(MIN_CARD_OPACITY..=MAX_CARD_OPACITY).contains(&settings.card.opacity) {
return Err(StashError::InvalidDirectory(format!(
"卡片不透明度需在 {MIN_CARD_OPACITY}–{MAX_CARD_OPACITY} 之间"
)));
}
if settings.search.engines.len() > 10 { if settings.search.engines.len() > 10 {
return Err(StashError::InvalidDirectory("搜索引擎最多 10 个".into())); return Err(StashError::InvalidDirectory("搜索引擎最多 10 个".into()));
} }
@@ -578,14 +551,6 @@ mod tests {
let mut empty = Settings::default(); let mut empty = Settings::default();
empty.header.items.clear(); empty.header.items.clear();
assert!(save(&mut conn, empty, 0).is_err()); assert!(save(&mut conn, empty, 0).is_err());
let mut too_transparent = Settings::default();
too_transparent.card.opacity = MIN_CARD_OPACITY - 1;
assert!(save(&mut conn, too_transparent, 0).is_err());
let mut clear = Settings::default();
clear.card.opacity = MAX_CARD_OPACITY;
assert!(save(&mut conn, clear, 0).is_ok());
} }
#[test] #[test]
+10 -20
View File
@@ -14,26 +14,6 @@
--su-card-border: var(--su-border-strong); --su-card-border: var(--su-border-strong);
} }
/* 卡片外观(后台「卡片外观」可调):底色透明度 + 毛玻璃,前台所有卡片共用一套。
变量由 AppNext 按站点设置写到 <html> 上;没设置时就是实底、无毛玻璃。 */
.app .section-card,
.app .profile-card,
.app .service-panel,
.app .nav-card,
.app .group-own-block,
.app .group-child-block,
.app .detail-nav,
.app .item-card,
.app .article-body-card,
.app .related-card {
background-color: color-mix(
in srgb,
var(--su-bg-card) var(--stash-card-alpha, 100%),
transparent
);
backdrop-filter: var(--stash-card-blur, none);
}
/* 卡片阴影默认会被滚动容器在左右边缘一刀切掉(卡片正好贴着容器边)。 /* 卡片阴影默认会被滚动容器在左右边缘一刀切掉(卡片正好贴着容器边)。
负边距把容器撑宽 16px、再补同样的内边距,内容位置不变,阴影有地方画。 */ 负边距把容器撑宽 16px、再补同样的内边距,内容位置不变,阴影有地方画。 */
#page-scroll { #page-scroll {
@@ -396,6 +376,16 @@
margin-top: var(--su-space-2); margin-top: var(--su-space-2);
} }
/* 分组页的内容直接压在背景图上(没有卡片底):靠文字自己的对比来保证可读,
次级文字提一档,再加一层同主题的描边,落在图片亮部也不会糊成一片。 */
.detail-view .group-own-block,
.detail-view .group-child-block,
.detail-hero {
--su-text-sub: var(--su-text-secondary);
--su-text-muted: var(--su-text-sub);
text-shadow: 0 1px 3px color-mix(in srgb, var(--su-bg-page) 70%, transparent);
}
/* 明细页的分组标题:与首页卡片标题同一套「色条 + 标题」 */ /* 明细页的分组标题:与首页卡片标题同一套「色条 + 标题」 */
.group-title { .group-title {
display: flex; display: flex;
+3 -13
View File
@@ -333,7 +333,6 @@ export default function AppNext() {
show_nav: true, show_nav: true,
}, },
background: cachedBackground ?? { light: '', dark: '' }, background: cachedBackground ?? { light: '', dark: '' },
card: { opacity: 100, blur: false },
search: { search: {
default: 'internal', default: 'internal',
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })), engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),
@@ -409,13 +408,6 @@ export default function AppNext() {
} }
}, [scheme]) }, [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 toggleTheme = () => {
const next: ThemeChoice = isDark ? 'light' : 'dark' const next: ThemeChoice = isDark ? 'light' : 'dark'
@@ -668,9 +660,7 @@ export default function AppNext() {
</div> </div>
{group.entries.length > 0 ? ( {group.entries.length > 0 ? (
<Card padding="md" className="group-own-block"> <div className="group-own-block">{renderEntries(group, group.entries)}</div>
{renderEntries(group, group.entries)}
</Card>
) : group.children.some((child) => groupEntryCount(child) > 0) ? null : ( ) : group.children.some((child) => groupEntryCount(child) > 0) ? null : (
<Empty text="这个分组还没有内容" /> <Empty text="这个分组还没有内容" />
)} )}
@@ -679,7 +669,7 @@ export default function AppNext() {
{group.children {group.children
.filter((child) => child.enabled !== false) .filter((child) => child.enabled !== false)
.map((child) => ( .map((child) => (
<Card key={child.key} padding="md" className="group-child-block"> <section key={child.key} className="group-child-block">
<div className="group-title"> <div className="group-title">
<span <span
className="group-bar" className="group-bar"
@@ -702,7 +692,7 @@ export default function AppNext() {
) : ( ) : (
<Empty text="暂无内容" /> <Empty text="暂无内容" />
)} )}
</Card> </section>
))} ))}
</Space> </Space>
</div> </div>
@@ -83,7 +83,6 @@ type TabKey =
| 'content' | 'content'
| 'home-sidebar' | 'home-sidebar'
| 'home-cards' | 'home-cards'
| 'home-cards-look'
| 'home-header' | 'home-header'
| 'home-background' | 'home-background'
| 'home-search' | 'home-search'
@@ -103,7 +102,6 @@ const ADMIN_NAV: MenuItem[] = [
{ key: 'home-search', label: '搜索框' }, { key: 'home-search', label: '搜索框' },
{ key: 'home-sidebar', label: '侧边栏' }, { key: 'home-sidebar', label: '侧边栏' },
{ key: 'home-cards', label: '内容卡片' }, { key: 'home-cards', label: '内容卡片' },
{ key: 'home-cards-look', label: '卡片外观' },
], ],
}, },
{ key: 'profile', label: '资料管理' }, { key: 'profile', label: '资料管理' },
@@ -392,16 +390,6 @@ export function AdminNext() {
/> />
)} )}
{tab === 'home-cards-look' && (
<section className="admin-home-section">
<Title level={4}>卡片外观</Title>
<CardLookPanel
value={draft.card}
onChange={(card) => setDraft({ ...draft, card })}
/>
</section>
)}
{tab === 'home-background' && ( {tab === 'home-background' && (
<BackgroundPanel <BackgroundPanel
value={draft.background} value={draft.background}
@@ -428,7 +416,6 @@ export function AdminNext() {
{(tab === 'home-header' || {(tab === 'home-header' ||
tab === 'home-background' || tab === 'home-background' ||
tab === 'home-search' || tab === 'home-search' ||
tab === 'home-cards-look' ||
tab === 'profile') && ( tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions"> <Space gap="sm" justify="end" className="admin-actions">
<Button <Button
@@ -1407,48 +1394,6 @@ function SidebarPanel({
) )
} }
// ---------- 卡片外观(透明度 / 毛玻璃) ----------
function CardLookPanel({
value,
onChange,
}: {
value: SiteSettings['card']
onChange: (card: SiteSettings['card']) => void
}) {
return (
<Space direction="column" gap="md" align="stretch" className="admin-form">
<Text size="sm" tone="muted">
说明:前台所有卡片共用这一套外观(首页、分组页、详情页、侧栏)。不透明度越低,背景图透得越多;
毛玻璃要配合透明才看得出来。
</Text>
<FormField
label="卡片不透明度"
htmlFor="card-opacity"
hint="百分比 40–100;100 为实底,40 最透"
>
<Input
id="card-opacity"
type="number"
min={40}
max={100}
value={String(value.opacity)}
onChange={(event) => {
const opacity = Number(event.target.value)
if (Number.isFinite(opacity)) onChange({ ...value, opacity })
}}
/>
</FormField>
<ToggleRow
label="毛玻璃"
checked={value.blur === true}
onChange={(blur) => onChange({ ...value, blur })}
switchLabel="卡片使用毛玻璃"
/>
</Space>
)
}
// ---------- 首页卡片(勾选分组) ---------- // ---------- 首页卡片(勾选分组) ----------
function CardsPanel({ function CardsPanel({
-9
View File
@@ -30,13 +30,6 @@ export type BackgroundSettings = {
dark: string dark: string
} }
export type CardSettings = {
/** 卡片底色的不透明度(百分比,40–100) */
opacity: number
/** 毛玻璃:背景模糊 + 提饱和度 */
blur: boolean
}
const BACKGROUND_CACHE_KEY = 'stash-background' const BACKGROUND_CACHE_KEY = 'stash-background'
/** /**
@@ -133,7 +126,6 @@ export type SiteSettings = {
show_nav: boolean show_nav: boolean
} }
background: BackgroundSettings background: BackgroundSettings
card: CardSettings
search: SearchSettings search: SearchSettings
profile: ProfileSettings profile: ProfileSettings
/** 站点级:标签标题 / favicon / 品牌 / 页头操作工具 */ /** 站点级:标签标题 / favicon / 品牌 / 页头操作工具 */
@@ -203,7 +195,6 @@ export function defaultSettings(): SiteSettings {
show_nav: true, show_nav: true,
}, },
background: { light: '', dark: '' }, background: { light: '', dark: '' },
card: { opacity: 100, blur: false },
search: { search: {
default: 'internal', default: 'internal',
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })), engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),