Compare commits
10
Commits
7bfeffaaa0
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb9592d56 | ||
|
|
8a08932d78 | ||
|
|
b152a2a9c9 | ||
|
|
4af71185d5 | ||
|
|
6d60b10c14 | ||
|
|
cf4c7d4ca4 | ||
|
|
5acc461e88 | ||
|
|
391cd1bdae | ||
|
|
5b166ef0e2 | ||
|
|
4f89c63b28 |
@@ -186,6 +186,7 @@ function seedDefaults() {
|
|||||||
smtp_from_name: 'RainWeb',
|
smtp_from_name: 'RainWeb',
|
||||||
theme_wallpaper: '',
|
theme_wallpaper: '',
|
||||||
theme_wallpaper_scale: 'cover',
|
theme_wallpaper_scale: 'cover',
|
||||||
|
theme_wallpaper_enabled: '1',
|
||||||
nav_style: 'default',
|
nav_style: 'default',
|
||||||
card_style: 'default',
|
card_style: 'default',
|
||||||
glass_blur: '20',
|
glass_blur: '20',
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<div id="musicEmbed"></div>
|
<div id="musicEmbed"></div>
|
||||||
<div id="snackbar" class="snackbar"></div>
|
<div id="snackbar" class="snackbar" role="status" aria-live="polite"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function ThemeSettings() {
|
|||||||
primary_color: '#6750a4',
|
primary_color: '#6750a4',
|
||||||
theme_wallpaper: '',
|
theme_wallpaper: '',
|
||||||
theme_wallpaper_scale: 'cover',
|
theme_wallpaper_scale: 'cover',
|
||||||
|
theme_wallpaper_enabled: true,
|
||||||
nav_style: 'default',
|
nav_style: 'default',
|
||||||
card_style: 'default',
|
card_style: 'default',
|
||||||
glass_blur: '20',
|
glass_blur: '20',
|
||||||
@@ -34,6 +35,7 @@ export default function ThemeSettings() {
|
|||||||
primary_color: s.primary_color || '#6750a4',
|
primary_color: s.primary_color || '#6750a4',
|
||||||
theme_wallpaper: s.theme_wallpaper || '',
|
theme_wallpaper: s.theme_wallpaper || '',
|
||||||
theme_wallpaper_scale: s.theme_wallpaper_scale || 'cover',
|
theme_wallpaper_scale: s.theme_wallpaper_scale || 'cover',
|
||||||
|
theme_wallpaper_enabled: s.theme_wallpaper_enabled !== '0',
|
||||||
nav_style: s.nav_style || 'default',
|
nav_style: s.nav_style || 'default',
|
||||||
card_style: s.card_style || 'default',
|
card_style: s.card_style || 'default',
|
||||||
glass_blur: s.glass_blur || '20',
|
glass_blur: s.glass_blur || '20',
|
||||||
@@ -65,6 +67,7 @@ export default function ThemeSettings() {
|
|||||||
primary_color: form.primary_color,
|
primary_color: form.primary_color,
|
||||||
theme_wallpaper: form.theme_wallpaper.trim(),
|
theme_wallpaper: form.theme_wallpaper.trim(),
|
||||||
theme_wallpaper_scale: form.theme_wallpaper_scale,
|
theme_wallpaper_scale: form.theme_wallpaper_scale,
|
||||||
|
theme_wallpaper_enabled: form.theme_wallpaper_enabled ? '1' : '0',
|
||||||
theme_force_dark: form.theme_force_dark ? '1' : '0',
|
theme_force_dark: form.theme_force_dark ? '1' : '0',
|
||||||
nav_style: form.nav_style,
|
nav_style: form.nav_style,
|
||||||
card_style: form.card_style,
|
card_style: form.card_style,
|
||||||
@@ -92,6 +95,11 @@ export default function ThemeSettings() {
|
|||||||
|
|
||||||
<Paper sx={{ p: 3, mb: 2 }}>
|
<Paper sx={{ p: 3, mb: 2 }}>
|
||||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>壁纸背景</Typography>
|
<Typography variant="subtitle1" sx={{ mb: 1 }}>壁纸背景</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Switch checked={form.theme_wallpaper_enabled} onChange={(e) => setForm((p) => ({ ...p, theme_wallpaper_enabled: e.target.checked }))} />}
|
||||||
|
label="启用壁纸背景"
|
||||||
|
sx={{ mb: 1 }}
|
||||||
|
/>
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
||||||
<Button variant="outlined" onClick={() => fileRef.current && fileRef.current.click()}>上传图片</Button>
|
<Button variant="outlined" onClick={() => fileRef.current && fileRef.current.click()}>上传图片</Button>
|
||||||
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleWallpaperUpload} />
|
<input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }} onChange={handleWallpaperUpload} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import * as captchaApi from '../api/captcha.js';
|
import * as captchaApi from '../api/captcha.js';
|
||||||
|
import { useDialog } from '../lib/utils.js';
|
||||||
|
|
||||||
// ── 模块级单例:showCaptcha 挂起请求,由 CaptchaModalHost 消费 ──
|
// ── 模块级单例:showCaptcha 挂起请求,由 CaptchaModalHost 消费 ──
|
||||||
let pending = null; // { action, type, resolve }
|
let pending = null; // { action, type, resolve }
|
||||||
@@ -140,15 +141,16 @@ function BuiltinCaptcha({ onFinish }) {
|
|||||||
onChange={(e) => setAnswer(e.target.value)}
|
onChange={(e) => setAnswer(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
|
onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
|
||||||
placeholder="输入验证码"
|
placeholder="输入验证码"
|
||||||
|
aria-label="输入验证码"
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
style={{ flex: 1, textAlign: 'center', fontSize: 20, letterSpacing: 6, textTransform: 'uppercase' }}
|
style={{ flex: 1, textAlign: 'center', fontSize: 20, letterSpacing: 6, textTransform: 'uppercase' }}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="btn btn-icon" title="刷新" onClick={loadImage} style={{ flexShrink: 0 }}>
|
<button type="button" className="btn btn-icon" title="刷新" aria-label="刷新验证码" onClick={loadImage} style={{ flexShrink: 0 }}>
|
||||||
<span className="material-icons">refresh</span>
|
<span className="material-icons">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error && <div style={{ color: 'var(--md-ref-error)', fontSize: 13, marginTop: 8 }}>{error}</div>}
|
{error && <div role="alert" style={{ color: 'var(--md-ref-error)', fontSize: 13, marginTop: 8 }}>{error}</div>}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 8, fontSize: 13, color: 'var(--md-ref-on-surface-variant)' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 8, fontSize: 13, color: 'var(--md-ref-on-surface-variant)' }}>
|
||||||
{powDone ? (
|
{powDone ? (
|
||||||
<>
|
<>
|
||||||
@@ -264,13 +266,21 @@ export default function CaptchaModalHost() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 弹窗键盘/焦点管理(B3):Esc 取消、聚焦首个输入、关闭后焦点还给触发元素
|
||||||
|
const { dialogRef, onKeyDown } = useDialog(!!request, () => finish(null));
|
||||||
|
|
||||||
if (!request) return null;
|
if (!request) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="验证码"
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) finish(null); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) finish(null); }}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
{request.type === 'recaptcha' ? (
|
{request.type === 'recaptcha' ? (
|
||||||
<ThirdPartyCaptcha type="recaptcha" onFinish={finish} />
|
<ThirdPartyCaptcha type="recaptcha" onFinish={finish} />
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function Layout() {
|
|||||||
const wallpaper = settings.theme_wallpaper || '';
|
const wallpaper = settings.theme_wallpaper || '';
|
||||||
const scale = settings.theme_wallpaper_scale || 'cover';
|
const scale = settings.theme_wallpaper_scale || 'cover';
|
||||||
|
|
||||||
if (wallpaper) {
|
if (wallpaper && settings.theme_wallpaper_enabled !== '0') {
|
||||||
body.classList.add('has-wallpaper');
|
body.classList.add('has-wallpaper');
|
||||||
body.style.setProperty('--wallpaper', `url(${wallpaper})`);
|
body.style.setProperty('--wallpaper', `url(${wallpaper})`);
|
||||||
const bgSizeMap = { cover: 'cover', contain: 'contain', repeat: 'auto', stretch: '100% 100%' };
|
const bgSizeMap = { cover: 'cover', contain: 'contain', repeat: 'auto', stretch: '100% 100%' };
|
||||||
@@ -167,6 +167,7 @@ export default function Layout() {
|
|||||||
<button
|
<button
|
||||||
className="btn-icon"
|
className="btn-icon"
|
||||||
onClick={handleToggleTheme}
|
onClick={handleToggleTheme}
|
||||||
|
aria-label="切换主题"
|
||||||
title={settings.theme_force_dark === '1' ? '已强制深色模式' : '切换主题'}
|
title={settings.theme_force_dark === '1' ? '已强制深色模式' : '切换主题'}
|
||||||
>
|
>
|
||||||
<span className="material-icons">{theme === 'dark' ? 'light_mode' : 'dark_mode'}</span>
|
<span className="material-icons">{theme === 'dark' ? 'light_mode' : 'dark_mode'}</span>
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ export default function MusicEmbed({ settings }) {
|
|||||||
>
|
>
|
||||||
{/* 保持挂载以保证折叠后音乐继续播放(display 由 .music-embed.collapsed 规则控制) */}
|
{/* 保持挂载以保证折叠后音乐继续播放(display 由 .music-embed.collapsed 规则控制) */}
|
||||||
<div className="music-embed-player" dangerouslySetInnerHTML={{ __html: code }} />
|
<div className="music-embed-player" dangerouslySetInnerHTML={{ __html: code }} />
|
||||||
<div className="music-embed-icon" onClick={expand} title="展开播放器">
|
<button type="button" className="music-embed-icon" onClick={expand} title="展开播放器" aria-label="展开播放器">
|
||||||
<span className="material-icons">music_note</span>
|
<span className="material-icons">music_note</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
/** HTML 转义:& < > " ' 全转义 */
|
/** HTML 转义:& < > " ' 全转义 */
|
||||||
export function escapeHtml(str) {
|
export function escapeHtml(str) {
|
||||||
return String(str == null ? '' : str).replace(/[&<>"']/g, (ch) => {
|
return String(str == null ? '' : str).replace(/[&<>"']/g, (ch) => {
|
||||||
@@ -45,3 +47,42 @@ export function showSnackbar(msg) {
|
|||||||
setTimeout(() => el.classList.remove('show', 'hide'), 300);
|
setTimeout(() => el.classList.remove('show', 'hide'), 300);
|
||||||
}, 2500);
|
}, 2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 弹窗焦点管理(B3):把焦点移到容器内首个可聚焦元素(input/select/textarea/button) */
|
||||||
|
export function focusDialog(container) {
|
||||||
|
if (!container) return;
|
||||||
|
const el = container.querySelector('input, select, textarea, button, [tabindex]:not([tabindex="-1"])');
|
||||||
|
if (el && typeof el.focus === 'function') el.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗键盘/焦点管理(B3):
|
||||||
|
* - 打开时记录触发元素,并把焦点移到弹窗内首个可聚焦元素
|
||||||
|
* - Esc 关闭弹窗
|
||||||
|
* - 关闭后焦点还给触发元素
|
||||||
|
* 返回 { dialogRef, onKeyDown }:dialogRef 绑到弹窗容器(.dialog-overlay),onKeyDown 绑其键盘事件。
|
||||||
|
*/
|
||||||
|
export function useDialog(open, onClose) {
|
||||||
|
const dialogRef = useRef(null);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
triggerRef.current = document.activeElement;
|
||||||
|
focusDialog(dialogRef.current);
|
||||||
|
} else if (triggerRef.current && typeof triggerRef.current.focus === 'function' && document.body.contains(triggerRef.current)) {
|
||||||
|
triggerRef.current.focus();
|
||||||
|
triggerRef.current = null;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onClose) onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return { dialogRef, onKeyDown };
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export default function Blog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="homepage-layout blog-layout">
|
<div className="homepage-layout blog-layout">
|
||||||
|
<h1 className="sr-only">博客</h1>
|
||||||
<BlogSidebar settings={settings} />
|
<BlogSidebar settings={settings} />
|
||||||
<div className="homepage-content">
|
<div className="homepage-content">
|
||||||
{/* 工具条:搜索框 + 归档 */}
|
{/* 工具条:搜索框 + 归档 */}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export default function BlogDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="article-actions">
|
<div className="article-actions">
|
||||||
<button className={'btn like-btn' + (like.liked ? ' liked' : '')} onClick={toggleLike} title="点赞">
|
<button className={'btn like-btn' + (like.liked ? ' liked' : '')} onClick={toggleLike} title="点赞" aria-label="点赞" aria-pressed={like.liked}>
|
||||||
<span className="material-icons" style={{ fontSize: 18 }}>{like.liked ? 'favorite' : 'favorite_border'}</span>
|
<span className="material-icons" style={{ fontSize: 18 }}>{like.liked ? 'favorite' : 'favorite_border'}</span>
|
||||||
<span className="like-count">{like.count}</span>
|
<span className="like-count">{like.count}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -225,7 +225,7 @@ export default function BlogDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<hr style={{ border: 'none', borderTop: '1px solid var(--md-ref-outline-variant)', margin: '32px 0' }} />
|
<hr style={{ border: 'none', borderTop: '1px solid var(--md-ref-outline-variant)', margin: '32px 0' }} />
|
||||||
<h4 style={{ fontWeight: 500, marginBottom: 16 }}>评论 ({comments.length})</h4>
|
<h2 style={{ fontWeight: 500, marginBottom: 16 }}>评论 ({comments.length})</h2>
|
||||||
|
|
||||||
{comments.length === 0 ? (
|
{comments.length === 0 ? (
|
||||||
<p className="text-muted" style={{ fontSize: 14 }}>暂无评论</p>
|
<p className="text-muted" style={{ fontSize: 14 }}>暂无评论</p>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default function Embed() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="embed-page" style={{ height: 'calc(100vh - 56px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div className="embed-page" style={{ height: 'calc(100vh - 56px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
|
<h1 className="sr-only">嵌入页面</h1>
|
||||||
<div
|
<div
|
||||||
className="embed-toolbar"
|
className="embed-toolbar"
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', height: 48, background: 'var(--md-ref-surface-container)', borderBottom: '1px solid var(--md-ref-outline-variant)', flexShrink: 0 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', height: 48, background: 'var(--md-ref-surface-container)', borderBottom: '1px solid var(--md-ref-outline-variant)', flexShrink: 0 }}
|
||||||
@@ -67,7 +68,14 @@ export default function Embed() {
|
|||||||
Ignore X-Frame-Headers
|
Ignore X-Frame-Headers
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
扩展,或点击右上角「原站」在新标签页打开
|
扩展,或点击右上角「原站」在新标签页打开
|
||||||
<span onClick={() => setShowHint(false)} style={{ cursor: 'pointer', marginLeft: 8, fontWeight: 600 }}>✕</span>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowHint(false)}
|
||||||
|
aria-label="关闭提示"
|
||||||
|
style={{ cursor: 'pointer', marginLeft: 8, fontWeight: 600, height: 'auto', padding: '2px 8px', fontSize: 'inherit', fontFamily: 'inherit', background: 'transparent', border: 'none', color: 'inherit' }}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getToken } from '../api/client.js';
|
|||||||
import { uploadFile } from '../api/upload.js';
|
import { uploadFile } from '../api/upload.js';
|
||||||
import { required as captchaRequired, applyCaptchaResult } from '../api/captcha.js';
|
import { required as captchaRequired, applyCaptchaResult } from '../api/captcha.js';
|
||||||
import { showCaptcha } from '../components/CaptchaModal.jsx';
|
import { showCaptcha } from '../components/CaptchaModal.jsx';
|
||||||
import { showSnackbar } from '../lib/utils.js';
|
import { showSnackbar, useDialog } from '../lib/utils.js';
|
||||||
|
|
||||||
/** 板块的子分类列表 */
|
/** 板块的子分类列表 */
|
||||||
function subCatsOf(cat) {
|
function subCatsOf(cat) {
|
||||||
@@ -133,8 +133,12 @@ export default function Forum() {
|
|||||||
const currentCat = categories.find((c) => c.id === currentCatId);
|
const currentCat = categories.find((c) => c.id === currentCatId);
|
||||||
const subCats = subCatsOf(currentCat);
|
const subCats = subCatsOf(currentCat);
|
||||||
|
|
||||||
|
// 发帖弹窗焦点管理(B3):Esc 关闭、聚焦首个输入、关闭后焦点还给触发按钮
|
||||||
|
const { dialogRef: npDialogRef, onKeyDown: npDialogKey } = useDialog(showNewPost, () => setShowNewPost(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="forum-layout">
|
<div className="forum-layout">
|
||||||
|
<h1 className="sr-only">论坛</h1>
|
||||||
<aside className="forum-sidebar">
|
<aside className="forum-sidebar">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 16 }}>板块</span>
|
<span style={{ fontWeight: 600, fontSize: 16 }}>板块</span>
|
||||||
@@ -144,7 +148,8 @@ export default function Forum() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="forum-cat-list">
|
<div className="forum-cat-list">
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
key={c.id}
|
key={c.id}
|
||||||
className={'forum-cat-item' + (c.id === currentCatId ? ' active' : '')}
|
className={'forum-cat-item' + (c.id === currentCatId ? ' active' : '')}
|
||||||
onClick={() => selectCategory(c.id)}
|
onClick={() => selectCategory(c.id)}
|
||||||
@@ -153,17 +158,18 @@ export default function Forum() {
|
|||||||
{c.announcement ? (
|
{c.announcement ? (
|
||||||
<span className="material-icons" style={{ fontSize: 14, color: 'var(--md-ref-primary)' }}>campaign</span>
|
<span className="material-icons" style={{ fontSize: 14, color: 'var(--md-ref-primary)' }}>campaign</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<hr style={{ border: 'none', borderTop: '1px solid var(--md-ref-outline-variant)', margin: '12px 0' }} />
|
<hr style={{ border: 'none', borderTop: '1px solid var(--md-ref-outline-variant)', margin: '12px 0' }} />
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className={'forum-cat-item' + (currentCatId === null ? ' active' : '')}
|
className={'forum-cat-item' + (currentCatId === null ? ' active' : '')}
|
||||||
onClick={showAllPosts}
|
onClick={showAllPosts}
|
||||||
style={{ fontWeight: 500 }}
|
style={{ fontWeight: 500 }}
|
||||||
>
|
>
|
||||||
<span className="material-icons" style={{ fontSize: 18 }}>dynamic_feed</span> 全部最新
|
<span className="material-icons" style={{ fontSize: 18 }}>dynamic_feed</span> 全部最新
|
||||||
</div>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="forum-content">
|
<div className="forum-content">
|
||||||
@@ -171,7 +177,7 @@ export default function Forum() {
|
|||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
<h3 style={{ fontWeight: 600, fontSize: 20, margin: 0 }}>{currentCat.name}</h3>
|
<h3 style={{ fontWeight: 600, fontSize: 20, margin: 0 }}>{currentCat.name}</h3>
|
||||||
<span className="chip" style={{ cursor: 'default', fontSize: 12, padding: '1px 8px', color: 'var(--md-ref-on-surface-variant)', border: '1px solid var(--md-ref-outline-variant)' }}>板块</span>
|
<span className="chip chip-static">板块</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted" style={{ fontSize: 14 }}>{currentCat.description || ''}</p>
|
<p className="text-muted" style={{ fontSize: 14 }}>{currentCat.description || ''}</p>
|
||||||
{currentCat.announcement && (
|
{currentCat.announcement && (
|
||||||
@@ -182,9 +188,9 @@ export default function Forum() {
|
|||||||
)}
|
)}
|
||||||
{subCats.length > 0 && (
|
{subCats.length > 0 && (
|
||||||
<div className="chips" style={{ marginBottom: 12, marginTop: 12 }}>
|
<div className="chips" style={{ marginBottom: 12, marginTop: 12 }}>
|
||||||
<span className={'chip' + (!filterSub ? ' active' : '')} onClick={() => { setFilterSub(''); load(currentCatId, ''); }}>全部</span>
|
<button type="button" className={'chip' + (!filterSub ? ' active' : '')} onClick={() => { setFilterSub(''); load(currentCatId, ''); }}>全部</button>
|
||||||
{subCats.map((s) => (
|
{subCats.map((s) => (
|
||||||
<span key={s} className={'chip' + (filterSub === s ? ' active' : '')} onClick={() => { setFilterSub(s); load(currentCatId, s); }}>{s}</span>
|
<button type="button" key={s} className={'chip' + (filterSub === s ? ' active' : '')} onClick={() => { setFilterSub(s); load(currentCatId, s); }}>{s}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -208,9 +214,9 @@ export default function Forum() {
|
|||||||
<span>{p.created_at}</span>
|
<span>{p.created_at}</span>
|
||||||
<span>{p.reply_count || 0} 回复</span>
|
<span>{p.reply_count || 0} 回复</span>
|
||||||
<span style={{ color: 'var(--md-ref-outline)', margin: '0 4px' }}>|</span>
|
<span style={{ color: 'var(--md-ref-outline)', margin: '0 4px' }}>|</span>
|
||||||
<span className="chip" style={{ cursor: 'default', fontSize: 11, padding: '1px 8px', background: 'transparent', border: '1px solid var(--md-ref-outline-variant)', color: 'var(--md-ref-on-surface-variant)' }}>{catName}</span>
|
<span className="chip chip-static">{catName}</span>
|
||||||
{p.sub_category ? (
|
{p.sub_category ? (
|
||||||
<span className="chip" style={{ cursor: 'default', fontSize: 11, padding: '1px 8px', background: 'var(--md-ref-secondary-container)', color: 'var(--md-ref-on-secondary-container)' }}>{p.sub_category}</span>
|
<span className="chip chip-tonal">{p.sub_category}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -223,9 +229,14 @@ export default function Forum() {
|
|||||||
{/* 发帖弹窗 */}
|
{/* 发帖弹窗 */}
|
||||||
{showNewPost && (
|
{showNewPost && (
|
||||||
<div
|
<div
|
||||||
|
ref={npDialogRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="发布新帖"
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) setShowNewPost(false); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) setShowNewPost(false); }}
|
||||||
|
onKeyDown={npDialogKey}
|
||||||
>
|
>
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>发布新帖</h3>
|
<h3>发布新帖</h3>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default function ForumDetail() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 style={{ fontSize: 22, fontWeight: 600 }}>{post.title}</h3>
|
<h1 style={{ fontSize: 22, fontWeight: 600 }}>{post.title}</h1>
|
||||||
<div className="post-meta" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
<div className="post-meta" style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<span>{post.author_name || '匿名'}</span>
|
<span>{post.author_name || '匿名'}</span>
|
||||||
<span>{post.created_at}</span>
|
<span>{post.created_at}</span>
|
||||||
@@ -158,12 +158,14 @@ export default function ForumDetail() {
|
|||||||
<div className="reply-meta">
|
<div className="reply-meta">
|
||||||
<strong>{r.author_name || '匿名'}</strong> · {r.created_at}
|
<strong>{r.author_name || '匿名'}</strong> · {r.created_at}
|
||||||
{canDeleteReply && (
|
{canDeleteReply && (
|
||||||
<span
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="删除回复"
|
||||||
style={{ float: 'right', color: 'var(--md-ref-error)', cursor: 'pointer', fontSize: 13 }}
|
style={{ float: 'right', color: 'var(--md-ref-error)', cursor: 'pointer', fontSize: 13 }}
|
||||||
onClick={() => deleteReply(r.id)}
|
onClick={() => deleteReply(r.id)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</span>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="reply-body">{r.content}</div>
|
<div className="reply-body">{r.content}</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="homepage-layout">
|
<div className="homepage-layout">
|
||||||
|
<h1 className="sr-only">首页</h1>
|
||||||
<BlogSidebar settings={settings} showRecent forceShow />
|
<BlogSidebar settings={settings} showRecent forceShow />
|
||||||
<div className="homepage-content">
|
<div className="homepage-content">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import * as pwApi from '../api/passwords.js';
|
import * as pwApi from '../api/passwords.js';
|
||||||
import { me } from '../api/auth.js';
|
import { me } from '../api/auth.js';
|
||||||
import { getToken } from '../api/client.js';
|
import { getToken } from '../api/client.js';
|
||||||
import { showSnackbar } from '../lib/utils.js';
|
import { showSnackbar, useDialog } from '../lib/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码学安全的随机密码生成器(crypto.getRandomValues,替代 v1 的 Math.random)。
|
* 密码学安全的随机密码生成器(crypto.getRandomValues,替代 v1 的 Math.random)。
|
||||||
@@ -250,6 +250,14 @@ export default function Passwords() {
|
|||||||
|
|
||||||
const detailEntry = entries ? entries.find((x) => x.id === detailId) : null;
|
const detailEntry = entries ? entries.find((x) => x.id === detailId) : null;
|
||||||
|
|
||||||
|
// 各弹窗键盘/焦点管理(B3):Esc 关闭、聚焦首个输入、关闭后焦点还给触发按钮
|
||||||
|
const { dialogRef: pinDlgRef, onKeyDown: pinDlgKey } = useDialog(pinDialog, () => setPinDialog(false));
|
||||||
|
const { dialogRef: pwDlgRef, onKeyDown: pwDlgKey } = useDialog(pwDialog, () => setPwDialog(false));
|
||||||
|
const { dialogRef: detailDlgRef, onKeyDown: detailDlgKey } = useDialog(!!detailEntry, () => setDetailId(null));
|
||||||
|
const { dialogRef: genDlgRef, onKeyDown: genDlgKey } = useDialog(genDialog, () => setGenDialog(false));
|
||||||
|
const { dialogRef: warnDlgRef, onKeyDown: warnDlgKey } = useDialog(pinChangeWarn, () => setPinChangeWarn(false));
|
||||||
|
const { dialogRef: confirmDlgRef, onKeyDown: confirmDlgKey } = useDialog(confirmDialog, () => setConfirmDialog(false));
|
||||||
|
|
||||||
// 加载中
|
// 加载中
|
||||||
if (phase === 'loading') {
|
if (phase === 'loading') {
|
||||||
return <div className="loading"><div className="spinner"></div></div>;
|
return <div className="loading"><div className="spinner"></div></div>;
|
||||||
@@ -313,14 +321,18 @@ export default function Passwords() {
|
|||||||
{entries && entries.length > 0 && (
|
{entries && entries.length > 0 && (
|
||||||
<div className="password-grid">
|
<div className="password-grid">
|
||||||
{entries.map((p) => (
|
{entries.map((p) => (
|
||||||
<div key={p.id} className="card password-card" onClick={() => setDetailId(p.id)}>
|
<div key={p.id} className="password-card-wrap">
|
||||||
<div className="pw-title">{p.title}</div>
|
<button type="button" className="card password-card" onClick={() => setDetailId(p.id)}>
|
||||||
<div className="pw-username">{p.username || '无用户名'}</div>
|
<div className="pw-title">{p.title}</div>
|
||||||
|
<div className="pw-username">{p.username || '无用户名'}</div>
|
||||||
|
</button>
|
||||||
<div className="pw-actions">
|
<div className="pw-actions">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="btn-icon"
|
className="btn-icon"
|
||||||
style={{ width: 32, height: 32, fontSize: 16 }}
|
style={{ width: 40, height: 40, fontSize: 16 }}
|
||||||
title="复制密码"
|
title="复制密码"
|
||||||
|
aria-label={`复制 ${p.title} 的密码`}
|
||||||
onClick={(e) => { e.stopPropagation(); copyToClipboard(p.password, '密码'); }}
|
onClick={(e) => { e.stopPropagation(); copyToClipboard(p.password, '密码'); }}
|
||||||
>
|
>
|
||||||
<span className="material-icons" style={{ fontSize: 16 }}>content_copy</span>
|
<span className="material-icons" style={{ fontSize: 16 }}>content_copy</span>
|
||||||
@@ -334,9 +346,14 @@ export default function Passwords() {
|
|||||||
{/* 设置/修改 PIN 弹窗 */}
|
{/* 设置/修改 PIN 弹窗 */}
|
||||||
{pinDialog && (
|
{pinDialog && (
|
||||||
<div
|
<div
|
||||||
|
ref={pinDlgRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="设置 PIN 码"
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) setPinDialog(false); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) setPinDialog(false); }}
|
||||||
|
onKeyDown={pinDlgKey}
|
||||||
>
|
>
|
||||||
<div className="dialog" style={{ maxWidth: 360 }}>
|
<div className="dialog" style={{ maxWidth: 360 }}>
|
||||||
<h3>设置 PIN 码</h3>
|
<h3>设置 PIN 码</h3>
|
||||||
@@ -360,9 +377,14 @@ export default function Passwords() {
|
|||||||
{/* 添加/编辑条目弹窗 */}
|
{/* 添加/编辑条目弹窗 */}
|
||||||
{pwDialog && (
|
{pwDialog && (
|
||||||
<div
|
<div
|
||||||
|
ref={pwDlgRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={editingId ? '编辑密码' : '添加密码'}
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) setPwDialog(false); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) setPwDialog(false); }}
|
||||||
|
onKeyDown={pwDlgKey}
|
||||||
>
|
>
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>{editingId ? '编辑密码' : '添加密码'}</h3>
|
<h3>{editingId ? '编辑密码' : '添加密码'}</h3>
|
||||||
@@ -400,9 +422,14 @@ export default function Passwords() {
|
|||||||
{/* 详情弹窗 */}
|
{/* 详情弹窗 */}
|
||||||
{detailEntry && (
|
{detailEntry && (
|
||||||
<div
|
<div
|
||||||
|
ref={detailDlgRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={detailEntry.title}
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) setDetailId(null); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) setDetailId(null); }}
|
||||||
|
onKeyDown={detailDlgKey}
|
||||||
>
|
>
|
||||||
<div className="dialog" style={{ maxWidth: 500 }}>
|
<div className="dialog" style={{ maxWidth: 500 }}>
|
||||||
<h3>{detailEntry.title}</h3>
|
<h3>{detailEntry.title}</h3>
|
||||||
@@ -410,16 +437,16 @@ export default function Passwords() {
|
|||||||
<div className="pw-field">
|
<div className="pw-field">
|
||||||
<span className="pw-label">用户名</span>
|
<span className="pw-label">用户名</span>
|
||||||
<span className="pw-value">{detailEntry.username || ''}</span>
|
<span className="pw-value">{detailEntry.username || ''}</span>
|
||||||
<span className="pw-copy" onClick={() => copyToClipboard(detailEntry.username || '', '用户名')}>
|
<button type="button" className="pw-copy" aria-label="复制用户名" onClick={() => copyToClipboard(detailEntry.username || '', '用户名')}>
|
||||||
<span className="material-icons" style={{ fontSize: 18 }}>content_copy</span>
|
<span className="material-icons" style={{ fontSize: 18 }}>content_copy</span>
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="pw-field">
|
<div className="pw-field">
|
||||||
<span className="pw-label">密码</span>
|
<span className="pw-label">密码</span>
|
||||||
<span className="pw-value">{detailEntry.password}</span>
|
<span className="pw-value">{detailEntry.password}</span>
|
||||||
<span className="pw-copy" onClick={() => copyToClipboard(detailEntry.password, '密码')}>
|
<button type="button" className="pw-copy" aria-label="复制密码" onClick={() => copyToClipboard(detailEntry.password, '密码')}>
|
||||||
<span className="material-icons" style={{ fontSize: 18 }}>content_copy</span>
|
<span className="material-icons" style={{ fontSize: 18 }}>content_copy</span>
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{detailEntry.url ? (
|
{detailEntry.url ? (
|
||||||
<div className="pw-field">
|
<div className="pw-field">
|
||||||
@@ -447,7 +474,7 @@ export default function Passwords() {
|
|||||||
|
|
||||||
{/* 密码生成器弹窗 */}
|
{/* 密码生成器弹窗 */}
|
||||||
{genDialog && (
|
{genDialog && (
|
||||||
<div className="dialog-overlay active" style={{ display: 'flex', zIndex: 9999 }}>
|
<div ref={genDlgRef} className="dialog-overlay active" role="dialog" aria-modal="true" aria-label="生成随机密码" style={{ display: 'flex', zIndex: 9999 }} onKeyDown={genDlgKey}>
|
||||||
<div className="dialog" style={{ maxWidth: 380 }}>
|
<div className="dialog" style={{ maxWidth: 380 }}>
|
||||||
<h3>生成随机密码</h3>
|
<h3>生成随机密码</h3>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -476,7 +503,7 @@ export default function Passwords() {
|
|||||||
|
|
||||||
{/* 修改 PIN 警告确认弹窗 */}
|
{/* 修改 PIN 警告确认弹窗 */}
|
||||||
{pinChangeWarn && (
|
{pinChangeWarn && (
|
||||||
<div className="dialog-overlay active" style={{ display: 'flex', zIndex: 9999 }}>
|
<div ref={warnDlgRef} className="dialog-overlay active" role="dialog" aria-modal="true" aria-label="确认修改 PIN" style={{ display: 'flex', zIndex: 9999 }} onKeyDown={warnDlgKey}>
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>确认修改 PIN</h3>
|
<h3>确认修改 PIN</h3>
|
||||||
<p style={{ marginBottom: 24, fontSize: 16 }}>⚠️ 修改 PIN 后,现有密码条目将无法解密,需要重新添加。确定继续?</p>
|
<p style={{ marginBottom: 24, fontSize: 16 }}>⚠️ 修改 PIN 后,现有密码条目将无法解密,需要重新添加。确定继续?</p>
|
||||||
@@ -490,7 +517,7 @@ export default function Passwords() {
|
|||||||
|
|
||||||
{/* 删除确认弹窗 */}
|
{/* 删除确认弹窗 */}
|
||||||
{confirmDialog && (
|
{confirmDialog && (
|
||||||
<div className="dialog-overlay active" style={{ display: 'flex', zIndex: 9999 }}>
|
<div ref={confirmDlgRef} className="dialog-overlay active" role="dialog" aria-modal="true" aria-label="确认操作" style={{ display: 'flex', zIndex: 9999 }} onKeyDown={confirmDlgKey}>
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>确认操作</h3>
|
<h3>确认操作</h3>
|
||||||
<p style={{ marginBottom: 24, fontSize: 16 }}>确定删除此密码记录?</p>
|
<p style={{ marginBottom: 24, fontSize: 16 }}>确定删除此密码记录?</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as profileApi from '../api/profile.js';
|
|||||||
import * as authApi from '../api/auth.js';
|
import * as authApi from '../api/auth.js';
|
||||||
import { avatarUrl, uploadAvatar } from '../api/upload.js';
|
import { avatarUrl, uploadAvatar } from '../api/upload.js';
|
||||||
import { getToken, notifyAuthChange } from '../api/client.js';
|
import { getToken, notifyAuthChange } from '../api/client.js';
|
||||||
import { showSnackbar } from '../lib/utils.js';
|
import { showSnackbar, useDialog, focusDialog } from '../lib/utils.js';
|
||||||
|
|
||||||
/** FileReader + Image 加载图片(头像裁剪前读取) */
|
/** FileReader + Image 加载图片(头像裁剪前读取) */
|
||||||
function loadImage(file) {
|
function loadImage(file) {
|
||||||
@@ -38,6 +38,13 @@ export default function Profile() {
|
|||||||
const [confirmPw, setConfirmPw] = useState('');
|
const [confirmPw, setConfirmPw] = useState('');
|
||||||
const [pwBusy, setPwBusy] = useState(false);
|
const [pwBusy, setPwBusy] = useState(false);
|
||||||
|
|
||||||
|
// 修改密码弹窗键盘/焦点管理(B3):Esc 关闭、聚焦首个输入、关闭后焦点还给触发按钮
|
||||||
|
const { dialogRef: pwDlgRef, onKeyDown: pwDlgKey } = useDialog(pwDialog, () => setPwDialog(false));
|
||||||
|
// 发送验证码成功切到第二步时,把焦点移到验证码输入框
|
||||||
|
useEffect(() => {
|
||||||
|
if (pwDialog && pwStep === 2) focusDialog(pwDlgRef.current);
|
||||||
|
}, [pwDialog, pwStep]);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -186,9 +193,14 @@ export default function Profile() {
|
|||||||
{/* 修改密码弹窗 */}
|
{/* 修改密码弹窗 */}
|
||||||
{pwDialog && (
|
{pwDialog && (
|
||||||
<div
|
<div
|
||||||
|
ref={pwDlgRef}
|
||||||
className="dialog-overlay active"
|
className="dialog-overlay active"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="修改密码"
|
||||||
style={{ display: 'flex', zIndex: 9999 }}
|
style={{ display: 'flex', zIndex: 9999 }}
|
||||||
onMouseDown={(e) => { if (e.target === e.currentTarget) setPwDialog(false); }}
|
onMouseDown={(e) => { if (e.target === e.currentTarget) setPwDialog(false); }}
|
||||||
|
onKeyDown={pwDlgKey}
|
||||||
>
|
>
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h3>修改密码</h3>
|
<h3>修改密码</h3>
|
||||||
|
|||||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "rainweb-links",
|
"name": "rainweb-links",
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "rainweb-links",
|
"name": "rainweb-links",
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
@@ -506,7 +506,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emotion/utils": {
|
"node_modules/@emotion/utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
||||||
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
|
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -1320,7 +1320,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
"engines": [
|
"engines": [
|
||||||
@@ -1480,7 +1480,7 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -1557,7 +1557,7 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2557,7 +2557,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rainweb-links",
|
"name": "rainweb-links",
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"description": "链接聚合管理平台",
|
"description": "链接聚合管理平台",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+270
-25
@@ -188,6 +188,27 @@ button:active, .btn:active {
|
|||||||
filter: brightness(0.9);
|
filter: brightness(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 键盘焦点可见(B1):所有可交互元素 focus-visible 时显示焦点环,补偿上方 outline: none */
|
||||||
|
button:focus-visible, .btn:focus-visible, a:focus-visible, [tabindex]:focus-visible {
|
||||||
|
outline: 2px solid var(--md-ref-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 屏幕阅读器专用(B6):只读不出、不占布局的辅助标题 */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-filled {
|
.btn-filled {
|
||||||
background: var(--md-ref-primary);
|
background: var(--md-ref-primary);
|
||||||
color: var(--md-ref-on-primary);
|
color: var(--md-ref-on-primary);
|
||||||
@@ -522,6 +543,10 @@ table tr:hover td {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
/* div/span → button 后补偿:不被 button 全局样式的固定高度/字体/对齐影响 */
|
||||||
|
font-family: inherit;
|
||||||
|
height: auto;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.active {
|
.chip.active {
|
||||||
@@ -531,14 +556,41 @@ table tr:hover td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip:hover {
|
.chip:hover {
|
||||||
|
background: var(--md-ref-surface-variant);
|
||||||
border-color: var(--md-ref-outline);
|
border-color: var(--md-ref-outline);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 选中态优先级:hover 不覆盖 active(保持 primary-container 底) */
|
||||||
|
.chip.active:hover {
|
||||||
|
background: var(--md-ref-primary-container);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.chip:active {
|
.chip:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 装饰性静态徽标(替代内联样式):不可点、紧凑尺寸;
|
||||||
|
static=描边中性,tonal=次级色容器(均无交互态) */
|
||||||
|
.chip.chip-static {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--md-ref-outline-variant);
|
||||||
|
color: var(--md-ref-on-surface-variant);
|
||||||
|
cursor: default;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.chip-tonal {
|
||||||
|
background: var(--md-ref-secondary-container);
|
||||||
|
color: var(--md-ref-on-secondary-container);
|
||||||
|
border-color: transparent;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes chipPop {
|
@keyframes chipPop {
|
||||||
from { transform: scale(0.92); }
|
from { transform: scale(0.92); }
|
||||||
50% { transform: scale(1.06); }
|
50% { transform: scale(1.06); }
|
||||||
@@ -979,6 +1031,17 @@ table tr:hover td {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
/* div/span → button 后补偿:保持块级填充、左对齐与自然高度 */
|
||||||
|
font-family: inherit;
|
||||||
|
min-height: 40px;
|
||||||
|
height: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: inherit;
|
||||||
|
/* B2 div→button 后补偿:清除 UA 默认浅色按钮底/描边;同时基础态保留
|
||||||
|
主题化底色(surface-container)而非透明——否则深色模式下非选中按钮
|
||||||
|
与页面暗底融为一体,点击切换分类后"背景消失"(沿用 .chip 的既有模式) */
|
||||||
|
background: var(--md-ref-surface-container);
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forum-cat-item:hover { background: var(--md-ref-surface-variant); }
|
.forum-cat-item:hover { background: var(--md-ref-surface-variant); }
|
||||||
@@ -1135,10 +1198,25 @@ table tr:hover td {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 密码卡片的外层定位容器:复制按钮与卡片同级(避免 button 嵌套 button),
|
||||||
|
用绝对定位贴在卡片右上角 */
|
||||||
|
.password-card-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.password-card {
|
.password-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
/* div/span → button 后补偿:卡片保留块级布局、继承文字色与左对齐 */
|
||||||
|
font-family: inherit;
|
||||||
|
color: inherit;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-card .pw-title {
|
.password-card .pw-title {
|
||||||
@@ -1192,6 +1270,12 @@ table tr:hover td {
|
|||||||
color: var(--md-ref-primary);
|
color: var(--md-ref-primary);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
/* span → button 后补偿:保持小图标按钮外观(不被 button 全局样式撑高) */
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: auto;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-detail .pw-field .pw-copy:hover { background: var(--md-ref-primary-container); }
|
.password-detail .pw-field .pw-copy:hover { background: var(--md-ref-primary-container); }
|
||||||
@@ -1266,7 +1350,7 @@ table tr:hover td {
|
|||||||
|
|
||||||
.nav-version {
|
.nav-version {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--md-ref-outline);
|
color: var(--md-ref-on-surface-variant);
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
@@ -1444,6 +1528,7 @@ table tr:hover td {
|
|||||||
/* ===== Markdown toggle ===== */
|
/* ===== Markdown toggle ===== */
|
||||||
.toggle-group { display: flex; gap: 8px; margin-bottom: 16px; }
|
.toggle-group { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||||
.toggle-btn { padding: 6px 16px; border-radius: 100px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--md-ref-outline-variant); background: transparent; color: var(--md-ref-on-surface-variant); }
|
.toggle-btn { padding: 6px 16px; border-radius: 100px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--md-ref-outline-variant); background: transparent; color: var(--md-ref-on-surface-variant); }
|
||||||
|
.toggle-btn:hover { background: var(--md-ref-surface-variant); }
|
||||||
.toggle-btn.active { background: var(--md-ref-primary-container); color: var(--md-ref-on-primary-container); border-color: transparent; }
|
.toggle-btn.active { background: var(--md-ref-primary-container); color: var(--md-ref-on-primary-container); border-color: transparent; }
|
||||||
|
|
||||||
/* ===== Captcha ===== */
|
/* ===== Captcha ===== */
|
||||||
@@ -1505,22 +1590,23 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
.has-wallpaper .forum-post-card,
|
.has-wallpaper .forum-post-card,
|
||||||
.has-wallpaper .blog-card,
|
.has-wallpaper .blog-card,
|
||||||
.has-wallpaper .panel-card,
|
.has-wallpaper .panel-card,
|
||||||
.has-wallpaper .password-card {
|
.has-wallpaper .password-card,
|
||||||
background: var(--wallpaper-overlay, rgba(0,0,0,0.25));
|
.has-wallpaper .link-card {
|
||||||
|
background: var(--glass-bg, rgba(255,251,254,0.75));
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-wallpaper .nav-bar {
|
.has-wallpaper .nav-bar {
|
||||||
background: var(--wallpaper-overlay, rgba(0,0,0,0.2));
|
background: var(--glass-bg, rgba(255,251,254,0.8));
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-wallpaper .dialog {
|
.has-wallpaper .dialog {
|
||||||
background: var(--wallpaper-overlay, rgba(0,0,0,0.4));
|
background: var(--glass-bg-heavy, rgba(255,251,254,0.9));
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
-webkit-backdrop-filter: blur(24px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
}
|
}
|
||||||
@@ -1528,14 +1614,27 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
.has-wallpaper input,
|
.has-wallpaper input,
|
||||||
.has-wallpaper select,
|
.has-wallpaper select,
|
||||||
.has-wallpaper textarea {
|
.has-wallpaper textarea {
|
||||||
background: var(--wallpaper-overlay, rgba(0,0,0,0.15));
|
background: var(--glass-bg, rgba(255,251,254,0.6));
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .has-wallpaper::before {
|
[data-theme="dark"] .has-wallpaper::before {
|
||||||
background: var(--wallpaper-overlay, rgba(0,0,0,0.4));
|
background: var(--wallpaper-overlay, rgba(0,0,0,0.4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 壁纸开启时整体提高玻璃不透明度(内容可读性):
|
||||||
|
--glass-bg / --glass-bg-heavy 的所有使用者(玻璃卡/导航/表格/弹窗)自动加强;
|
||||||
|
无壁纸时保持原有半透明质感不受影响 */
|
||||||
|
body.has-wallpaper {
|
||||||
|
--glass-bg: rgba(255,251,254,0.82);
|
||||||
|
--glass-bg-heavy: rgba(255,251,254,0.92);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] body.has-wallpaper {
|
||||||
|
--glass-bg: rgba(28,27,31,0.74);
|
||||||
|
--glass-bg-heavy: rgba(28,27,31,0.84);
|
||||||
|
}
|
||||||
|
|
||||||
/* Glass card style */
|
/* Glass card style */
|
||||||
.card.glass-card,
|
.card.glass-card,
|
||||||
.glass-card {
|
.glass-card {
|
||||||
@@ -1559,9 +1658,42 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Forum sidebar glass with wallpaper */
|
/* 壁纸下内容区统一磨砂背景(可读性核心):
|
||||||
.has-wallpaper .forum-sidebar .forum-cat-item {
|
用 .page::before 实现而不是直接在 .page 上 backdrop-filter——
|
||||||
backdrop-filter: blur(4px);
|
直接加会使 .page 成为 position:fixed 后代的包含块,
|
||||||
|
破坏详情页悬浮目录(.article-toc-side)的视口定位。
|
||||||
|
半透明 + 模糊给文字稳固底色,壁纸在面板边缘仍清晰可见 */
|
||||||
|
|
||||||
|
/* 壁纸下页脚同源加强(页脚在 .page 之外,单独覆盖) */
|
||||||
|
body.has-wallpaper .footer-classic {
|
||||||
|
background: rgba(255, 251, 254, 0.82);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] body.has-wallpaper .footer-classic {
|
||||||
|
background: rgba(28, 27, 31, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 壁纸下详情页正文卡 / 目录卡加强:阅读面需要更高不透明度(其余卡片由
|
||||||
|
body.has-wallpaper 的 --glass-bg 变量统一加强) */
|
||||||
|
body.has-wallpaper .blog-article,
|
||||||
|
body.has-wallpaper .article-toc-side {
|
||||||
|
background: rgba(255, 251, 254, 0.78);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] body.has-wallpaper .blog-article,
|
||||||
|
[data-theme="dark"] body.has-wallpaper .article-toc-side {
|
||||||
|
background: rgba(28, 27, 31, 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 壁纸下论坛侧栏玻璃底(B9):仿 .blog-article 的半透明 + 模糊 + 圆角,
|
||||||
|
保证壁纸下分类文字可读(深色主题用深色底) */
|
||||||
|
.has-wallpaper .forum-sidebar {
|
||||||
|
background: rgba(255, 251, 254, 0.5);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .has-wallpaper .forum-sidebar {
|
||||||
|
background: rgba(28, 27, 31, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blog waterfall cards with wallpaper */
|
/* Blog waterfall cards with wallpaper */
|
||||||
@@ -1602,7 +1734,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.blog-waterfall .blog-card { padding: 16px; }
|
.blog-waterfall .blog-card { padding: 16px; }
|
||||||
.nav-tab { font-size: 13px; padding: 4px 10px; }
|
.nav-tab { font-size: 13px; padding: 8px 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Homepage Layout ===== */
|
/* ===== Homepage Layout ===== */
|
||||||
@@ -1763,6 +1895,18 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
/* 折叠触发按钮:div → button 后补偿,保持 48×48 圆角矩形图标样式 */
|
||||||
|
.music-embed-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--md-ref-on-surface-variant);
|
||||||
|
}
|
||||||
.music-embed.right { right: 24px; }
|
.music-embed.right { right: 24px; }
|
||||||
.music-embed.left { left: 24px; }
|
.music-embed.left { left: 24px; }
|
||||||
.music-embed iframe {
|
.music-embed iframe {
|
||||||
@@ -1841,6 +1985,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
.footer-classic .fc-version {
|
.footer-classic .fc-version {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: var(--md-ref-on-surface-variant);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1895,7 +2040,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--md-ref-outline);
|
color: var(--md-ref-on-surface-variant);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.fc-col .col-links { display: flex; flex-direction: column; gap: 8px; }
|
.fc-col .col-links { display: flex; flex-direction: column; gap: 8px; }
|
||||||
@@ -2048,8 +2193,8 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
.search-clear {
|
.search-clear {
|
||||||
width: 28px;
|
width: 40px;
|
||||||
height: 28px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -2103,7 +2248,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.sri-meta { font-size: 12px; color: var(--md-ref-outline); display: flex; gap: 6px; flex-wrap: wrap; }
|
.sri-meta { font-size: 12px; color: var(--md-ref-on-surface-variant); display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
.sri-meta .sep { color: var(--md-ref-outline-variant); }
|
.sri-meta .sep { color: var(--md-ref-outline-variant); }
|
||||||
.sri-tags { color: var(--md-ref-primary); }
|
.sri-tags { color: var(--md-ref-primary); }
|
||||||
|
|
||||||
@@ -2142,7 +2287,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
}
|
}
|
||||||
.article-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
.article-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
||||||
.article-tag-chip { text-decoration: none; transition: all 0.2s; }
|
.article-tag-chip { text-decoration: none; transition: all 0.2s; }
|
||||||
.article-tag-chip:hover { border-color: var(--md-ref-primary); background: var(--md-ref-primary-container); }
|
.article-tag-chip:hover { border-color: var(--md-ref-primary); background: var(--md-ref-surface-variant); }
|
||||||
.article-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
|
.article-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
|
||||||
.like-btn {
|
.like-btn {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
@@ -2159,12 +2304,12 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0.0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||||
}
|
}
|
||||||
.like-btn:hover { border-color: #d32f2f; color: #d32f2f; background: rgba(211, 47, 47, 0.06); }
|
.like-btn:hover { border-color: var(--md-ref-error); color: var(--md-ref-error); background: color-mix(in srgb, var(--md-ref-error) 10%, transparent); }
|
||||||
.like-btn .material-icons { font-size: 18px; }
|
.like-btn .material-icons { font-size: 18px; }
|
||||||
.like-btn.liked {
|
.like-btn.liked {
|
||||||
border-color: #d32f2f;
|
border-color: var(--md-ref-error);
|
||||||
color: #d32f2f;
|
color: var(--md-ref-error);
|
||||||
background: rgba(211, 47, 47, 0.1);
|
background: color-mix(in srgb, var(--md-ref-error) 10%, transparent);
|
||||||
}
|
}
|
||||||
.like-btn:active { transform: scale(0.94); }
|
.like-btn:active { transform: scale(0.94); }
|
||||||
|
|
||||||
@@ -2249,7 +2394,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
border-color: var(--md-ref-primary);
|
border-color: var(--md-ref-primary);
|
||||||
box-shadow: 0 4px 16px var(--md-shadow);
|
box-shadow: 0 4px 16px var(--md-shadow);
|
||||||
}
|
}
|
||||||
.pn-label { font-size: 12px; font-weight: 500; color: var(--md-ref-outline); }
|
.pn-label { font-size: 12px; font-weight: 500; color: var(--md-ref-on-surface-variant); }
|
||||||
.pn-title {
|
.pn-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -2284,7 +2429,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
}
|
}
|
||||||
.reply-btn {
|
.reply-btn {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
height: 28px;
|
height: 40px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--md-ref-on-surface-variant);
|
color: var(--md-ref-on-surface-variant);
|
||||||
@@ -2346,7 +2491,7 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.archive-item:hover .archive-item-title { color: var(--md-ref-primary); }
|
.archive-item:hover .archive-item-title { color: var(--md-ref-primary); }
|
||||||
.archive-item-date { font-size: 12px; color: var(--md-ref-outline); flex-shrink: 0; }
|
.archive-item-date { font-size: 12px; color: var(--md-ref-on-surface-variant); flex-shrink: 0; }
|
||||||
|
|
||||||
/* ===== 博客增强响应式 ===== */
|
/* ===== 博客增强响应式 ===== */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
@@ -2567,3 +2712,103 @@ button.glass-card, .btn.glass-card, .nav-tab.glass-card, .chip.glass-card {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Markdown Indigo 定制(.md-body)
|
||||||
|
—— 只做文本级渲染;除代码块(okaidia 暗底)外全部背景透明,
|
||||||
|
由卡片磨砂玻璃接管;颜色全部走主题 CSS 变量,
|
||||||
|
浅/深色随 data-theme 自动切换。
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* 正文基调:透明背景,文字色走主题变量 */
|
||||||
|
.md-body {
|
||||||
|
color: var(--md-ref-on-surface);
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题:Indigo 排版(字重 500 / 行高 1.3),h1/h2 带左侧竖线 + 下边框 */
|
||||||
|
.md-body h1, .md-body h2, .md-body h3, .md-body h4, .md-body h5, .md-body h6 {
|
||||||
|
color: var(--md-ref-on-surface);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 1.5em 0 .6em;
|
||||||
|
}
|
||||||
|
.md-body h1, .md-body h2 {
|
||||||
|
padding: .25em 0 .25em .75em;
|
||||||
|
border-left: 6px solid var(--md-ref-primary);
|
||||||
|
border-bottom: 1px solid var(--md-ref-outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加粗 / 删除线:不加 Indigo 的「」引号,仅保留字重;删除线走主题灰 */
|
||||||
|
.md-body strong { font-weight: 700; }
|
||||||
|
.md-body del, .md-body s { color: var(--md-ref-outline); }
|
||||||
|
|
||||||
|
/* 链接:主题主色 + Indigo 下边框式下划线 */
|
||||||
|
.md-body a {
|
||||||
|
color: var(--md-ref-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--md-ref-primary) 40%, transparent);
|
||||||
|
}
|
||||||
|
.md-body a:hover { border-bottom-color: var(--md-ref-primary); }
|
||||||
|
|
||||||
|
/* 行内代码:主题容器色背景(可换肤),透明背景不出现 */
|
||||||
|
.md-body code {
|
||||||
|
background: var(--md-ref-surface-container);
|
||||||
|
color: var(--md-ref-on-surface);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: .9em;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 引用块:Indigo 左粗线(主题主色)+ 主题浅底 */
|
||||||
|
.md-body blockquote {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: .5em 1em;
|
||||||
|
border-left: 4px solid var(--md-ref-primary);
|
||||||
|
background: var(--md-ref-surface-container-low);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
color: color-mix(in srgb, var(--md-ref-primary) 70%, var(--md-ref-on-surface));
|
||||||
|
}
|
||||||
|
.md-body blockquote > :last-child { margin-bottom: 0; }
|
||||||
|
.md-body blockquote > :first-child { margin-top: 0; }
|
||||||
|
|
||||||
|
/* 代码块:唯一保留背景的元素 —— okaidia 暗色主题 */
|
||||||
|
.md-body pre {
|
||||||
|
background: #272822;
|
||||||
|
color: #f8f8f2;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, .3);
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
.md-body pre code {
|
||||||
|
background: none;
|
||||||
|
color: #f8f8f2;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, .3);
|
||||||
|
}
|
||||||
|
.md-body pre::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .18); border-radius: 2px; }
|
||||||
|
|
||||||
|
/* okaidia 语法高亮(token 出现时生效,如后续接入 highlight.js/prism) */
|
||||||
|
.md-body .token.cdata, .md-body .token.comment, .md-body .token.doctype, .md-body .token.prolog { color: #8292a2; }
|
||||||
|
.md-body .token.punctuation { color: #f8f8f2; }
|
||||||
|
.md-body .token.namespace { opacity: .7; }
|
||||||
|
.md-body .token.constant, .md-body .token.deleted, .md-body .token.property, .md-body .token.symbol, .md-body .token.tag { color: #f92672; }
|
||||||
|
.md-body .token.boolean, .md-body .token.number { color: #ae81ff; }
|
||||||
|
.md-body .token.attr-name, .md-body .token.builtin, .md-body .token.char, .md-body .token.inserted, .md-body .token.selector, .md-body .token.string { color: #a6e22e; }
|
||||||
|
.md-body .token.entity, .md-body .token.operator, .md-body .token.url, .md-body .token.variable { color: #f8f8f2; }
|
||||||
|
.md-body .token.atrule, .md-body .token.attr-value, .md-body .token.class-name, .md-body .token.function { color: #e6db74; }
|
||||||
|
.md-body .token.keyword { color: #66d9ef; }
|
||||||
|
.md-body .token.important, .md-body .token.regex { color: #fd971f; }
|
||||||
|
.md-body .token.bold, .md-body .token.important { font-weight: 700; }
|
||||||
|
.md-body .token.italic { font-style: italic; }
|
||||||
|
|
||||||
|
/* 深色模式:文本色跟随 on-surface 浅色变体;代码块保持 okaidia 暗底不变 */
|
||||||
|
[data-theme="dark"] .md-body { color: var(--md-ref-on-surface); }
|
||||||
|
[data-theme="dark"] .md-body blockquote { background: var(--md-ref-surface-container); }
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ const router = express.Router();
|
|||||||
|
|
||||||
const PUBLIC_KEYS = ['site_name','site_description','site_url','primary_color',
|
const PUBLIC_KEYS = ['site_name','site_description','site_url','primary_color',
|
||||||
'recaptcha_site_key','turnstile_site_key',
|
'recaptcha_site_key','turnstile_site_key',
|
||||||
'theme_wallpaper','theme_wallpaper_scale','nav_style','card_style',
|
'theme_wallpaper','theme_wallpaper_scale','theme_wallpaper_enabled','nav_style','card_style',
|
||||||
'glass_blur','glass_opacity','theme_force_dark',
|
'glass_blur','glass_opacity','theme_force_dark',
|
||||||
'captcha_type','captcha_login','captcha_register','captcha_forum',
|
'captcha_type','captcha_login','captcha_register','captcha_forum',
|
||||||
'homepage_avatar','homepage_bio','homepage_content','blog_show_sidebar',
|
'homepage_avatar','homepage_bio','homepage_content','blog_show_sidebar',
|
||||||
@@ -15,7 +15,7 @@ const PUBLIC_KEYS = ['site_name','site_description','site_url','primary_color',
|
|||||||
|
|
||||||
const ALL_KEYS = ['site_name','site_description','site_url','primary_color','recaptcha_site_key','turnstile_site_key',
|
const ALL_KEYS = ['site_name','site_description','site_url','primary_color','recaptcha_site_key','turnstile_site_key',
|
||||||
'smtp_host','smtp_port','smtp_user','smtp_from_email','smtp_from_name',
|
'smtp_host','smtp_port','smtp_user','smtp_from_email','smtp_from_name',
|
||||||
'theme_wallpaper','theme_wallpaper_scale','nav_style','card_style','glass_blur','glass_opacity','theme_force_dark',
|
'theme_wallpaper','theme_wallpaper_scale','theme_wallpaper_enabled','nav_style','card_style','glass_blur','glass_opacity','theme_force_dark',
|
||||||
'captcha_type','captcha_login','captcha_register','captcha_forum',
|
'captcha_type','captcha_login','captcha_register','captcha_forum',
|
||||||
'homepage_avatar','homepage_bio','homepage_content','blog_show_sidebar',
|
'homepage_avatar','homepage_bio','homepage_content','blog_show_sidebar',
|
||||||
'site_favicon','homepage_contacts','music_embed_enabled','music_embed_code','music_embed_position','music_embed_autohide','music_embed_idle_timeout',
|
'site_favicon','homepage_contacts','music_embed_enabled','music_embed_code','music_embed_position','music_embed_autohide','music_embed_idle_timeout',
|
||||||
|
|||||||
Reference in New Issue
Block a user