feat: 友链入口迁至站点区并支持左右页脚独立开关
修复左侧栏/页脚开关无法关闭的问题,改为专用设置接口可靠写入。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -423,8 +423,19 @@ export const api = {
|
|||||||
reciprocal_check_enabled?: boolean;
|
reciprocal_check_enabled?: boolean;
|
||||||
}>(`/api/admin/friend-link-applies${qs ? `?${qs}` : ''}`);
|
}>(`/api/admin/friend-link-applies${qs ? `?${qs}` : ''}`);
|
||||||
},
|
},
|
||||||
adminUpdateFriendLinkSettings: (body: { reciprocal_check_enabled: boolean }) =>
|
adminUpdateFriendLinkSettings: (body: {
|
||||||
request<{ message: string; reciprocal_check_enabled: boolean }>('/api/admin/friend-link-settings', {
|
reciprocal_check_enabled?: boolean;
|
||||||
|
nav_show_friend_links?: boolean;
|
||||||
|
footer_show_friend_links?: boolean;
|
||||||
|
aside_show_friend_links?: boolean;
|
||||||
|
}) =>
|
||||||
|
request<{
|
||||||
|
message: string;
|
||||||
|
reciprocal_check_enabled: boolean;
|
||||||
|
nav_show_friend_links: boolean;
|
||||||
|
footer_show_friend_links: boolean;
|
||||||
|
aside_show_friend_links: boolean;
|
||||||
|
}>('/api/admin/friend-link-settings', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -233,6 +233,10 @@ export interface ForumLimits {
|
|||||||
aside_show_friend_links: boolean;
|
aside_show_friend_links: boolean;
|
||||||
/** 右侧栏可选组件顺序与开关 */
|
/** 右侧栏可选组件顺序与开关 */
|
||||||
aside_widgets: AsideWidget[];
|
aside_widgets: AsideWidget[];
|
||||||
|
/** 左侧栏「站点」展示友情链接入口 */
|
||||||
|
nav_show_friend_links: boolean;
|
||||||
|
/** 页脚展示友情链接入口 */
|
||||||
|
footer_show_friend_links: boolean;
|
||||||
/** 首页列表样式:title 仅标题 / thumbnail 缩略图 */
|
/** 首页列表样式:title 仅标题 / thumbnail 缩略图 */
|
||||||
feed_list_style: 'title' | 'excerpt' | 'thumbnail';
|
feed_list_style: 'title' | 'excerpt' | 'thumbnail';
|
||||||
/** 伪静态(固定链接)开关 */
|
/** 伪静态(固定链接)开关 */
|
||||||
@@ -259,6 +263,8 @@ export interface ForumLimitsPublic {
|
|||||||
aside_show_recent_comments: boolean;
|
aside_show_recent_comments: boolean;
|
||||||
aside_show_friend_links: boolean;
|
aside_show_friend_links: boolean;
|
||||||
aside_widgets: AsideWidget[];
|
aside_widgets: AsideWidget[];
|
||||||
|
nav_show_friend_links: boolean;
|
||||||
|
footer_show_friend_links: boolean;
|
||||||
feed_list_style: 'title' | 'excerpt' | 'thumbnail';
|
feed_list_style: 'title' | 'excerpt' | 'thumbnail';
|
||||||
permalink_enabled: boolean;
|
permalink_enabled: boolean;
|
||||||
permalink_ext: string;
|
permalink_ext: string;
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ export default function Sidebar({
|
|||||||
const isAdmin = user?.role === 'admin';
|
const isAdmin = user?.role === 'admin';
|
||||||
const { navPages } = useSitePages();
|
const { navPages } = useSitePages();
|
||||||
const { limits } = useForumLimits();
|
const { limits } = useForumLimits();
|
||||||
|
const showFriendLinksNav = limits.nav_show_friend_links !== false;
|
||||||
|
const showSiteSection = navPages.length > 0 || showFriendLinksNav;
|
||||||
|
|
||||||
const keyword = params.get('keyword') || '';
|
const keyword = params.get('keyword') || '';
|
||||||
const menuKey = resolveMenuKey(loc.pathname, activeBoard, keyword);
|
const menuKey = resolveMenuKey(loc.pathname, activeBoard, keyword);
|
||||||
@@ -135,7 +137,6 @@ export default function Sidebar({
|
|||||||
{feedNavLink('all', buildHomeUrl(0, sort, permalinkOpts), '全部帖子', <Home aria-hidden />, 0)}
|
{feedNavLink('all', buildHomeUrl(0, sort, permalinkOpts), '全部帖子', <Home aria-hidden />, 0)}
|
||||||
{user && navItem('favorites', '我的收藏', <Star aria-hidden />, () => nav('/favorites'))}
|
{user && navItem('favorites', '我的收藏', <Star aria-hidden />, () => nav('/favorites'))}
|
||||||
{navItem('projects', '开源码桶', <FolderGit2 aria-hidden />, () => nav('/projects'))}
|
{navItem('projects', '开源码桶', <FolderGit2 aria-hidden />, () => nav('/projects'))}
|
||||||
{navItem('links', '友情链接', <Link2 aria-hidden />, () => nav('/links'))}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{(boardsLoading && boards.length === 0) ? (
|
{(boardsLoading && boards.length === 0) ? (
|
||||||
@@ -191,10 +192,11 @@ export default function Sidebar({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(navPages.length > 0) && (
|
{showSiteSection && (
|
||||||
<>
|
<>
|
||||||
<div className="sidebar-section sidebar-section--spaced">站点</div>
|
<div className="sidebar-section sidebar-section--spaced">站点</div>
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
|
{showFriendLinksNav && navItem('links', '友情链接', <Link2 aria-hidden />, () => nav('/links'))}
|
||||||
{navPages.map(p => (
|
{navPages.map(p => (
|
||||||
navItem(`page-${p.slug}`, p.title, <FileText aria-hidden />, () => nav(pagePath(p.slug, limits)))
|
navItem(`page-${p.slug}`, p.title, <FileText aria-hidden />, () => nav(pagePath(p.slug, limits)))
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,18 +34,20 @@ export default function SiteFooter() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="site-footer__nav" aria-label="站点链接">
|
<nav className="site-footer__nav" aria-label="站点链接">
|
||||||
<span className="site-footer__friend">
|
{limits.footer_show_friend_links !== false && (
|
||||||
<Link to="/links">友情链接</Link>
|
<span className="site-footer__friend">
|
||||||
</span>
|
<Link to="/links">友情链接</Link>
|
||||||
{footerPages.map(p => (
|
</span>
|
||||||
|
)}
|
||||||
|
{footerPages.map((p, i) => (
|
||||||
<span key={p.slug} className="site-footer__friend">
|
<span key={p.slug} className="site-footer__friend">
|
||||||
<FooterSep />
|
{(limits.footer_show_friend_links !== false || i > 0) && <FooterSep />}
|
||||||
<Link to={pagePath(p.slug, limits)}>{p.title}</Link>
|
<Link to={pagePath(p.slug, limits)}>{p.title}</Link>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{icp && (
|
{icp && (
|
||||||
<>
|
<>
|
||||||
<FooterSep />
|
{(limits.footer_show_friend_links !== false || footerPages.length > 0) && <FooterSep />}
|
||||||
<a
|
<a
|
||||||
href={icpURL}
|
href={icpURL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const DEFAULT_LIMITS: ForumLimitsPublic = {
|
|||||||
aside_show_recent_comments: false,
|
aside_show_recent_comments: false,
|
||||||
aside_show_friend_links: true,
|
aside_show_friend_links: true,
|
||||||
aside_widgets: DEFAULT_ASIDE_WIDGETS,
|
aside_widgets: DEFAULT_ASIDE_WIDGETS,
|
||||||
|
nav_show_friend_links: true,
|
||||||
|
footer_show_friend_links: true,
|
||||||
feed_list_style: 'title',
|
feed_list_style: 'title',
|
||||||
permalink_enabled: false,
|
permalink_enabled: false,
|
||||||
permalink_ext: 'html',
|
permalink_ext: 'html',
|
||||||
|
|||||||
@@ -21,10 +21,16 @@ import { notify } from '@/lib/notify';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import { useAdminGuard } from '../../layouts/AdminLayout';
|
import { useAdminGuard } from '../../layouts/AdminLayout';
|
||||||
import type { FriendLink, FriendLinkApply, SiteBranding } from '../../api/types';
|
import type { ForumLimits, FriendLink, FriendLinkApply, SiteBranding } from '../../api/types';
|
||||||
import { useSiteBranding, seedSiteBrandingCache, invalidateSiteBrandingCache } from '../../hooks/useSiteBranding';
|
import { useSiteBranding, seedSiteBrandingCache, invalidateSiteBrandingCache } from '../../hooks/useSiteBranding';
|
||||||
|
import { invalidateForumLimitsCache } from '../../hooks/useForumLimits';
|
||||||
import { formatTime } from '../../utils/content';
|
import { formatTime } from '../../utils/content';
|
||||||
import { resolveFriendLinkLogo, isReciprocalChecking, reciprocalStatusLabel } from '../../utils/friendLink';
|
import { resolveFriendLinkLogo, isReciprocalChecking, reciprocalStatusLabel } from '../../utils/friendLink';
|
||||||
|
import {
|
||||||
|
mergeForumLimitsWithAsideWidgets,
|
||||||
|
normalizeAsideWidgets,
|
||||||
|
resolveAsideWidgets,
|
||||||
|
} from '../../utils/asideWidgets';
|
||||||
import AdminSortableList, { SortableDragHandle, SortableMoveButtons } from '../../components/admin/AdminSortableList';
|
import AdminSortableList, { SortableDragHandle, SortableMoveButtons } from '../../components/admin/AdminSortableList';
|
||||||
import { shouldShowSortableMoveButtons } from '../../utils/sortOrder';
|
import { shouldShowSortableMoveButtons } from '../../utils/sortOrder';
|
||||||
|
|
||||||
@@ -126,6 +132,8 @@ export default function AdminLinksPage() {
|
|||||||
const [handlingId, setHandlingId] = useState<number | null>(null);
|
const [handlingId, setHandlingId] = useState<number | null>(null);
|
||||||
const [reciprocalCheckEnabled, setReciprocalCheckEnabled] = useState(false);
|
const [reciprocalCheckEnabled, setReciprocalCheckEnabled] = useState(false);
|
||||||
const [reciprocalCheckSaving, setReciprocalCheckSaving] = useState(false);
|
const [reciprocalCheckSaving, setReciprocalCheckSaving] = useState(false);
|
||||||
|
const [forumLimits, setForumLimits] = useState<ForumLimits | null>(null);
|
||||||
|
const [entrySaving, setEntrySaving] = useState(false);
|
||||||
|
|
||||||
const baselineLinks = branding.friend_links ?? [];
|
const baselineLinks = branding.friend_links ?? [];
|
||||||
const siteURL = branding.site_url;
|
const siteURL = branding.site_url;
|
||||||
@@ -134,10 +142,55 @@ export default function AdminLinksPage() {
|
|||||||
[links, baselineLinks],
|
[links, baselineLinks],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const entryFlags = useMemo(() => {
|
||||||
|
const widgets = resolveAsideWidgets({
|
||||||
|
aside_widgets: forumLimits?.aside_widgets,
|
||||||
|
aside_show_tag_cloud: forumLimits?.aside_show_tag_cloud ?? false,
|
||||||
|
aside_show_recent_comments: forumLimits?.aside_show_recent_comments ?? false,
|
||||||
|
aside_show_friend_links: forumLimits?.aside_show_friend_links ?? true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nav: forumLimits?.nav_show_friend_links !== false,
|
||||||
|
footer: forumLimits?.footer_show_friend_links !== false,
|
||||||
|
aside: widgets.find(w => w.id === 'friend_links')?.enabled ?? true,
|
||||||
|
};
|
||||||
|
}, [forumLimits]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLinks(toLinkRows(baselineLinks));
|
setLinks(toLinkRows(baselineLinks));
|
||||||
}, [baselineLinks]);
|
}, [baselineLinks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ready) return;
|
||||||
|
api.adminSettings()
|
||||||
|
.then(s => {
|
||||||
|
const loadedAsideWidgets = normalizeAsideWidgets(
|
||||||
|
resolveAsideWidgets({
|
||||||
|
aside_widgets: s.limits?.aside_widgets,
|
||||||
|
aside_show_tag_cloud: s.limits?.aside_show_tag_cloud ?? false,
|
||||||
|
aside_show_recent_comments: s.limits?.aside_show_recent_comments ?? false,
|
||||||
|
aside_show_friend_links: s.limits?.aside_show_friend_links ?? true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const base: ForumLimits = {
|
||||||
|
open_posts_in_new_tab: true,
|
||||||
|
open_content_links_in_new_tab: true,
|
||||||
|
aside_show_tag_cloud: false,
|
||||||
|
aside_show_recent_comments: false,
|
||||||
|
aside_show_friend_links: true,
|
||||||
|
aside_widgets: loadedAsideWidgets,
|
||||||
|
nav_show_friend_links: true,
|
||||||
|
footer_show_friend_links: true,
|
||||||
|
feed_list_style: 'title',
|
||||||
|
permalink_enabled: false,
|
||||||
|
permalink_ext: 'html',
|
||||||
|
...s.limits,
|
||||||
|
};
|
||||||
|
setForumLimits(mergeForumLimitsWithAsideWidgets(base, loadedAsideWidgets));
|
||||||
|
})
|
||||||
|
.catch(() => { /* 入口开关稍后仍可按默认展示 */ });
|
||||||
|
}, [ready]);
|
||||||
|
|
||||||
const loadApplies = useCallback(async (status: ApplyStatusTab = applyStatus, page = 1) => {
|
const loadApplies = useCallback(async (status: ApplyStatusTab = applyStatus, page = 1) => {
|
||||||
setAppliesLoading(true);
|
setAppliesLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -272,6 +325,114 @@ export default function AdminLinksPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const patchEntryVisibility = async (patch: {
|
||||||
|
nav?: boolean;
|
||||||
|
footer?: boolean;
|
||||||
|
aside?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (entrySaving) return;
|
||||||
|
const prev = {
|
||||||
|
nav: entryFlags.nav,
|
||||||
|
footer: entryFlags.footer,
|
||||||
|
aside: entryFlags.aside,
|
||||||
|
};
|
||||||
|
const nextFlags = {
|
||||||
|
nav: patch.nav ?? prev.nav,
|
||||||
|
footer: patch.footer ?? prev.footer,
|
||||||
|
aside: patch.aside ?? prev.aside,
|
||||||
|
};
|
||||||
|
// 乐观更新本地 limits,避免开关回弹
|
||||||
|
setForumLimits(fl => {
|
||||||
|
if (!fl) {
|
||||||
|
return {
|
||||||
|
open_posts_in_new_tab: true,
|
||||||
|
open_content_links_in_new_tab: true,
|
||||||
|
aside_show_tag_cloud: false,
|
||||||
|
aside_show_recent_comments: false,
|
||||||
|
aside_show_friend_links: nextFlags.aside,
|
||||||
|
aside_widgets: normalizeAsideWidgets([
|
||||||
|
{ id: 'tag_cloud', enabled: false },
|
||||||
|
{ id: 'recent_comments', enabled: false },
|
||||||
|
{ id: 'recent_users', enabled: false },
|
||||||
|
{ id: 'friend_links', enabled: nextFlags.aside },
|
||||||
|
]),
|
||||||
|
nav_show_friend_links: nextFlags.nav,
|
||||||
|
footer_show_friend_links: nextFlags.footer,
|
||||||
|
feed_list_style: 'title',
|
||||||
|
permalink_enabled: false,
|
||||||
|
permalink_ext: 'html',
|
||||||
|
post_edit_window_hours: 24,
|
||||||
|
comment_edit_window_minutes: 3,
|
||||||
|
rate_limit_post: 10,
|
||||||
|
rate_limit_comment: 10,
|
||||||
|
rate_limit_register: 10,
|
||||||
|
rate_limit_login: 10,
|
||||||
|
rate_limit_window_sec: 60,
|
||||||
|
post_title_max: 128,
|
||||||
|
post_tags_max: 256,
|
||||||
|
post_content_max: 50000,
|
||||||
|
comment_max: 5000,
|
||||||
|
search_keyword_min: 1,
|
||||||
|
search_keyword_max: 50,
|
||||||
|
page_size_default: 30,
|
||||||
|
password_min_len: 6,
|
||||||
|
avatar_max_mb: 2,
|
||||||
|
signature_max: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const widgets = normalizeAsideWidgets(fl.aside_widgets).map(w => (
|
||||||
|
w.id === 'friend_links' ? { ...w, enabled: nextFlags.aside } : w
|
||||||
|
));
|
||||||
|
return mergeForumLimitsWithAsideWidgets({
|
||||||
|
...fl,
|
||||||
|
nav_show_friend_links: nextFlags.nav,
|
||||||
|
footer_show_friend_links: nextFlags.footer,
|
||||||
|
}, widgets);
|
||||||
|
});
|
||||||
|
setEntrySaving(true);
|
||||||
|
try {
|
||||||
|
const body: {
|
||||||
|
nav_show_friend_links?: boolean;
|
||||||
|
footer_show_friend_links?: boolean;
|
||||||
|
aside_show_friend_links?: boolean;
|
||||||
|
} = {};
|
||||||
|
if (patch.nav !== undefined) body.nav_show_friend_links = patch.nav;
|
||||||
|
if (patch.footer !== undefined) body.footer_show_friend_links = patch.footer;
|
||||||
|
if (patch.aside !== undefined) body.aside_show_friend_links = patch.aside;
|
||||||
|
const r = await api.adminUpdateFriendLinkSettings(body);
|
||||||
|
setForumLimits(fl => {
|
||||||
|
const base = fl;
|
||||||
|
if (!base) return fl;
|
||||||
|
const widgets = normalizeAsideWidgets(base.aside_widgets).map(w => (
|
||||||
|
w.id === 'friend_links' ? { ...w, enabled: r.aside_show_friend_links } : w
|
||||||
|
));
|
||||||
|
return mergeForumLimitsWithAsideWidgets({
|
||||||
|
...base,
|
||||||
|
nav_show_friend_links: r.nav_show_friend_links,
|
||||||
|
footer_show_friend_links: r.footer_show_friend_links,
|
||||||
|
aside_show_friend_links: r.aside_show_friend_links,
|
||||||
|
}, widgets);
|
||||||
|
});
|
||||||
|
invalidateForumLimitsCache();
|
||||||
|
notify.success(r.message);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setForumLimits(fl => {
|
||||||
|
if (!fl) return fl;
|
||||||
|
const widgets = normalizeAsideWidgets(fl.aside_widgets).map(w => (
|
||||||
|
w.id === 'friend_links' ? { ...w, enabled: prev.aside } : w
|
||||||
|
));
|
||||||
|
return mergeForumLimitsWithAsideWidgets({
|
||||||
|
...fl,
|
||||||
|
nav_show_friend_links: prev.nav,
|
||||||
|
footer_show_friend_links: prev.footer,
|
||||||
|
}, widgets);
|
||||||
|
});
|
||||||
|
notify.error(e instanceof Error ? e.message : '保存失败');
|
||||||
|
} finally {
|
||||||
|
setEntrySaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitReject = async () => {
|
const submitReject = async () => {
|
||||||
if (!rejectTarget) return;
|
if (!rejectTarget) return;
|
||||||
setRejecting(true);
|
setRejecting(true);
|
||||||
@@ -412,6 +573,37 @@ export default function AdminLinksPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div className="admin-card admin-links-entry-card">
|
||||||
|
<div className="admin-card-head">入口展示</div>
|
||||||
|
<p className="admin-card-desc">
|
||||||
|
控制友情链接入口出现在何处;关闭后仍可直接访问 /links 页面申请与浏览。右侧栏开关与「系统设置 → 右侧栏组件」同源。
|
||||||
|
</p>
|
||||||
|
<div className="admin-card-body admin-links-entry-body">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ key: 'nav' as const, label: '左侧栏(站点)', on: entryFlags.nav },
|
||||||
|
{ key: 'footer' as const, label: '页脚', on: entryFlags.footer },
|
||||||
|
{ key: 'aside' as const, label: '右侧栏', on: entryFlags.aside },
|
||||||
|
]
|
||||||
|
).map(item => (
|
||||||
|
<div key={item.key} className="admin-links-entry-row">
|
||||||
|
<span id={`admin-links-entry-${item.key}`}>{item.label}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={item.on}
|
||||||
|
aria-labelledby={`admin-links-entry-${item.key}`}
|
||||||
|
disabled={entrySaving}
|
||||||
|
className={cn('admin-settings-switch', item.on && 'is-on')}
|
||||||
|
onClick={() => patchEntryVisibility({ [item.key]: !item.on })}
|
||||||
|
>
|
||||||
|
<span className="admin-settings-switch-ui" aria-hidden />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="admin-links-columns">
|
<div className="admin-links-columns">
|
||||||
<div className="admin-card admin-links-apply-card">
|
<div className="admin-card admin-links-apply-card">
|
||||||
<div className="admin-card-head">
|
<div className="admin-card-head">
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ export default function AdminSettingsPage() {
|
|||||||
aside_show_recent_comments: false,
|
aside_show_recent_comments: false,
|
||||||
aside_show_friend_links: true,
|
aside_show_friend_links: true,
|
||||||
aside_widgets: loadedAsideWidgets,
|
aside_widgets: loadedAsideWidgets,
|
||||||
|
nav_show_friend_links: true,
|
||||||
|
footer_show_friend_links: true,
|
||||||
feed_list_style: 'title',
|
feed_list_style: 'title',
|
||||||
permalink_enabled: false,
|
permalink_enabled: false,
|
||||||
permalink_ext: 'html',
|
permalink_ext: 'html',
|
||||||
|
|||||||
@@ -13826,6 +13826,25 @@ button.post-poll__option,
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.admin-links-entry-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.admin-links-entry-body {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 28px;
|
||||||
|
}
|
||||||
|
.admin-links-entry-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--foreground, #334155);
|
||||||
|
}
|
||||||
|
.admin-links-entry-row .admin-settings-switch:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.admin-links-columns {
|
.admin-links-columns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -100,27 +100,64 @@ func (h *Handlers) APIAdminFriendLinkApplies(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIAdminUpdateFriendLinkSettings 更新友链回链检测开关
|
// APIAdminUpdateFriendLinkSettings 更新友链相关开关(回链检测 / 入口展示)
|
||||||
func (h *Handlers) APIAdminUpdateFriendLinkSettings(c *gin.Context) {
|
func (h *Handlers) APIAdminUpdateFriendLinkSettings(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
ReciprocalCheckEnabled *bool `json:"reciprocal_check_enabled"`
|
ReciprocalCheckEnabled *bool `json:"reciprocal_check_enabled"`
|
||||||
|
NavShowFriendLinks *bool `json:"nav_show_friend_links"`
|
||||||
|
FooterShowFriendLinks *bool `json:"footer_show_friend_links"`
|
||||||
|
AsideShowFriendLinks *bool `json:"aside_show_friend_links"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil || req.ReciprocalCheckEnabled == nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.Settings.SetFriendLinkReciprocalCheckEnabled(*req.ReciprocalCheckEnabled); err != nil {
|
if req.ReciprocalCheckEnabled == nil && req.NavShowFriendLinks == nil &&
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
req.FooterShowFriendLinks == nil && req.AsideShowFriendLinks == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
enabled := *req.ReciprocalCheckEnabled
|
if req.ReciprocalCheckEnabled != nil {
|
||||||
msg := "已开启回链检测"
|
if err := h.Settings.SetFriendLinkReciprocalCheckEnabled(*req.ReciprocalCheckEnabled); err != nil {
|
||||||
if !enabled {
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
msg = "已关闭回链检测"
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if req.NavShowFriendLinks != nil {
|
||||||
|
if err := h.Settings.SetNavShowFriendLinks(*req.NavShowFriendLinks); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.FooterShowFriendLinks != nil {
|
||||||
|
if err := h.Settings.SetFooterShowFriendLinks(*req.FooterShowFriendLinks); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.AsideShowFriendLinks != nil {
|
||||||
|
if err := h.Settings.SetAsideFriendLinksEnabled(*req.AsideShowFriendLinks); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "设置已保存"
|
||||||
|
if req.ReciprocalCheckEnabled != nil && req.NavShowFriendLinks == nil &&
|
||||||
|
req.FooterShowFriendLinks == nil && req.AsideShowFriendLinks == nil {
|
||||||
|
if *req.ReciprocalCheckEnabled {
|
||||||
|
msg = "已开启回链检测"
|
||||||
|
} else {
|
||||||
|
msg = "已关闭回链检测"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": msg,
|
"message": msg,
|
||||||
"reciprocal_check_enabled": enabled,
|
"reciprocal_check_enabled": h.Settings.FriendLinkReciprocalCheckEnabled(),
|
||||||
|
"nav_show_friend_links": h.Settings.NavShowFriendLinks(),
|
||||||
|
"footer_show_friend_links": h.Settings.FooterShowFriendLinks(),
|
||||||
|
"aside_show_friend_links": h.Settings.AsideShowFriendLinks(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const (
|
|||||||
SettingAsideShowRecentComments = "aside_show_recent_comments"
|
SettingAsideShowRecentComments = "aside_show_recent_comments"
|
||||||
SettingAsideShowFriendLinks = "aside_show_friend_links"
|
SettingAsideShowFriendLinks = "aside_show_friend_links"
|
||||||
SettingAsideWidgets = "aside_widgets"
|
SettingAsideWidgets = "aside_widgets"
|
||||||
|
SettingNavShowFriendLinks = "nav_show_friend_links"
|
||||||
|
SettingFooterShowFriendLinks = "footer_show_friend_links"
|
||||||
SettingFeedListStyle = "feed_list_style"
|
SettingFeedListStyle = "feed_list_style"
|
||||||
|
|
||||||
// 伪静态键名见 permalink.go:SettingPermalinkEnabled / SettingPermalinkExt
|
// 伪静态键名见 permalink.go:SettingPermalinkEnabled / SettingPermalinkExt
|
||||||
@@ -128,6 +130,8 @@ type ForumLimits struct {
|
|||||||
AsideShowRecentComments bool `json:"aside_show_recent_comments"`
|
AsideShowRecentComments bool `json:"aside_show_recent_comments"`
|
||||||
AsideShowFriendLinks bool `json:"aside_show_friend_links"`
|
AsideShowFriendLinks bool `json:"aside_show_friend_links"`
|
||||||
AsideWidgets []AsideWidget `json:"aside_widgets"`
|
AsideWidgets []AsideWidget `json:"aside_widgets"`
|
||||||
|
NavShowFriendLinks bool `json:"nav_show_friend_links"`
|
||||||
|
FooterShowFriendLinks bool `json:"footer_show_friend_links"`
|
||||||
|
|
||||||
FeedListStyle string `json:"feed_list_style"`
|
FeedListStyle string `json:"feed_list_style"`
|
||||||
|
|
||||||
@@ -177,6 +181,8 @@ type ForumLimitsPublic struct {
|
|||||||
AsideShowRecentComments bool `json:"aside_show_recent_comments"`
|
AsideShowRecentComments bool `json:"aside_show_recent_comments"`
|
||||||
AsideShowFriendLinks bool `json:"aside_show_friend_links"`
|
AsideShowFriendLinks bool `json:"aside_show_friend_links"`
|
||||||
AsideWidgets []AsideWidget `json:"aside_widgets"`
|
AsideWidgets []AsideWidget `json:"aside_widgets"`
|
||||||
|
NavShowFriendLinks bool `json:"nav_show_friend_links"`
|
||||||
|
FooterShowFriendLinks bool `json:"footer_show_friend_links"`
|
||||||
|
|
||||||
FeedListStyle string `json:"feed_list_style"`
|
FeedListStyle string `json:"feed_list_style"`
|
||||||
|
|
||||||
@@ -272,6 +278,8 @@ var storageSettingDefaults = map[string]string{
|
|||||||
|
|
||||||
var friendLinkSettingDefaults = map[string]string{
|
var friendLinkSettingDefaults = map[string]string{
|
||||||
SettingFriendLinkReciprocalCheck: "0", // 默认关闭回链检测
|
SettingFriendLinkReciprocalCheck: "0", // 默认关闭回链检测
|
||||||
|
SettingNavShowFriendLinks: "1",
|
||||||
|
SettingFooterShowFriendLinks: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
var siteBrandingDefaults = map[string]string{
|
var siteBrandingDefaults = map[string]string{
|
||||||
@@ -538,6 +546,8 @@ func (s *ForumSettingsService) Limits() ForumLimits {
|
|||||||
AsideShowRecentComments: bools.recentComments,
|
AsideShowRecentComments: bools.recentComments,
|
||||||
AsideShowFriendLinks: bools.friendLinks,
|
AsideShowFriendLinks: bools.friendLinks,
|
||||||
AsideWidgets: widgets,
|
AsideWidgets: widgets,
|
||||||
|
NavShowFriendLinks: s.NavShowFriendLinks(),
|
||||||
|
FooterShowFriendLinks: s.FooterShowFriendLinks(),
|
||||||
|
|
||||||
FeedListStyle: s.FeedListStyle(),
|
FeedListStyle: s.FeedListStyle(),
|
||||||
|
|
||||||
@@ -569,6 +579,8 @@ func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic {
|
|||||||
AsideShowRecentComments: limits.AsideShowRecentComments,
|
AsideShowRecentComments: limits.AsideShowRecentComments,
|
||||||
AsideShowFriendLinks: limits.AsideShowFriendLinks,
|
AsideShowFriendLinks: limits.AsideShowFriendLinks,
|
||||||
AsideWidgets: limits.AsideWidgets,
|
AsideWidgets: limits.AsideWidgets,
|
||||||
|
NavShowFriendLinks: limits.NavShowFriendLinks,
|
||||||
|
FooterShowFriendLinks: limits.FooterShowFriendLinks,
|
||||||
|
|
||||||
FeedListStyle: limits.FeedListStyle,
|
FeedListStyle: limits.FeedListStyle,
|
||||||
|
|
||||||
@@ -616,6 +628,8 @@ func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
|||||||
SettingAsideShowTagCloud: bools.tagCloud,
|
SettingAsideShowTagCloud: bools.tagCloud,
|
||||||
SettingAsideShowRecentComments: bools.recentComments,
|
SettingAsideShowRecentComments: bools.recentComments,
|
||||||
SettingAsideShowFriendLinks: bools.friendLinks,
|
SettingAsideShowFriendLinks: bools.friendLinks,
|
||||||
|
SettingNavShowFriendLinks: in.NavShowFriendLinks,
|
||||||
|
SettingFooterShowFriendLinks: in.FooterShowFriendLinks,
|
||||||
SettingPermalinkEnabled: in.PermalinkEnabled,
|
SettingPermalinkEnabled: in.PermalinkEnabled,
|
||||||
}
|
}
|
||||||
for key, on := range boolUpdates {
|
for key, on := range boolUpdates {
|
||||||
@@ -705,6 +719,16 @@ func (s *ForumSettingsService) FriendLinkReciprocalCheckEnabled() bool {
|
|||||||
return s.getString(SettingFriendLinkReciprocalCheck, "0") == "1"
|
return s.getString(SettingFriendLinkReciprocalCheck, "0") == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NavShowFriendLinks 左侧栏「站点」是否展示友链入口;缺省开启
|
||||||
|
func (s *ForumSettingsService) NavShowFriendLinks() bool {
|
||||||
|
return s.getString(SettingNavShowFriendLinks, "1") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FooterShowFriendLinks 页脚是否展示友链入口;缺省开启
|
||||||
|
func (s *ForumSettingsService) FooterShowFriendLinks() bool {
|
||||||
|
return s.getString(SettingFooterShowFriendLinks, "1") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ForumSettingsService) SetFriendLinkReciprocalCheckEnabled(enabled bool) error {
|
func (s *ForumSettingsService) SetFriendLinkReciprocalCheckEnabled(enabled bool) error {
|
||||||
v := "0"
|
v := "0"
|
||||||
if enabled {
|
if enabled {
|
||||||
@@ -713,6 +737,52 @@ func (s *ForumSettingsService) SetFriendLinkReciprocalCheckEnabled(enabled bool)
|
|||||||
return s.setString(SettingFriendLinkReciprocalCheck, v)
|
return s.setString(SettingFriendLinkReciprocalCheck, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ForumSettingsService) SetNavShowFriendLinks(enabled bool) error {
|
||||||
|
v := "0"
|
||||||
|
if enabled {
|
||||||
|
v = "1"
|
||||||
|
}
|
||||||
|
return s.setString(SettingNavShowFriendLinks, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForumSettingsService) SetFooterShowFriendLinks(enabled bool) error {
|
||||||
|
v := "0"
|
||||||
|
if enabled {
|
||||||
|
v = "1"
|
||||||
|
}
|
||||||
|
return s.setString(SettingFooterShowFriendLinks, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAsideFriendLinksEnabled 更新右侧栏友链组件开关(与 aside_widgets 同步)
|
||||||
|
func (s *ForumSettingsService) SetAsideFriendLinksEnabled(enabled bool) error {
|
||||||
|
widgets := s.AsideWidgets()
|
||||||
|
found := false
|
||||||
|
for i := range widgets {
|
||||||
|
if widgets[i].ID == AsideWidgetFriendLinks {
|
||||||
|
widgets[i].Enabled = enabled
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
widgets = append(widgets, AsideWidget{ID: AsideWidgetFriendLinks, Enabled: enabled})
|
||||||
|
}
|
||||||
|
widgets = NormalizeAsideWidgets(widgets)
|
||||||
|
bools := asideBoolsFromWidgets(widgets)
|
||||||
|
payload, err := json.Marshal(widgets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.setString(SettingAsideWidgets, string(payload)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v := "0"
|
||||||
|
if bools.friendLinks {
|
||||||
|
v = "1"
|
||||||
|
}
|
||||||
|
return s.setString(SettingAsideShowFriendLinks, v)
|
||||||
|
}
|
||||||
|
|
||||||
type asideWidgetBools struct {
|
type asideWidgetBools struct {
|
||||||
tagCloud bool
|
tagCloud bool
|
||||||
recentComments bool
|
recentComments bool
|
||||||
|
|||||||
Reference in New Issue
Block a user