修复无板块时空态导航失效,去除发帖本地草稿,并种子默认板块。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Home, Star, LayoutDashboard, FolderGit2, ArrowLeft,
|
Home, Star, LayoutDashboard, FolderGit2, FolderKanban, ArrowLeft,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import type { Board } from '../api/types';
|
import type { Board } from '../api/types';
|
||||||
@@ -150,6 +150,13 @@ export default function Sidebar({
|
|||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
|
) : isAdmin ? (
|
||||||
|
<>
|
||||||
|
<div className="sidebar-section sidebar-section--boards">板块</div>
|
||||||
|
<nav className="sidebar-nav">
|
||||||
|
{navItem('boards-empty', '创建第一个板块', <FolderKanban aria-hidden />, () => nav('/admin/boards'))}
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ interface Props {
|
|||||||
boardId?: number;
|
boardId?: number;
|
||||||
/** 当前板块名 */
|
/** 当前板块名 */
|
||||||
boardName?: string;
|
boardName?: string;
|
||||||
|
/** 站点是否尚无任何板块(全新安装) */
|
||||||
|
noBoards?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VirtualPostList({
|
export default function VirtualPostList({
|
||||||
@@ -55,6 +57,7 @@ export default function VirtualPostList({
|
|||||||
keyword = '',
|
keyword = '',
|
||||||
boardId = 0,
|
boardId = 0,
|
||||||
boardName = '',
|
boardName = '',
|
||||||
|
noBoards = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const nav = useNavigate();
|
const nav = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -81,6 +84,7 @@ export default function VirtualPostList({
|
|||||||
const isEmpty = !loading && posts.length === 0;
|
const isEmpty = !loading && posts.length === 0;
|
||||||
const isSearchEmpty = isEmpty && !!keyword.trim();
|
const isSearchEmpty = isEmpty && !!keyword.trim();
|
||||||
const composeTarget = boardId > 0 ? `/compose?board=${boardId}` : '/compose';
|
const composeTarget = boardId > 0 ? `/compose?board=${boardId}` : '/compose';
|
||||||
|
const isAdmin = user?.role === 'admin';
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (resetScrollKey <= 0) return;
|
if (resetScrollKey <= 0) return;
|
||||||
@@ -116,7 +120,17 @@ export default function VirtualPostList({
|
|||||||
|
|
||||||
const emptyActions = (
|
const emptyActions = (
|
||||||
<div className="empty-feed-actions">
|
<div className="empty-feed-actions">
|
||||||
{isSearchEmpty ? (
|
{noBoards ? (
|
||||||
|
isAdmin ? (
|
||||||
|
<Button type="button" size="sm" onClick={() => nav('/admin/boards')}>
|
||||||
|
去创建板块
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button type="button" size="sm" variant="outline" onClick={() => nav('/')}>
|
||||||
|
刷新看看
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
) : isSearchEmpty ? (
|
||||||
<>
|
<>
|
||||||
<Button type="button" size="sm" variant="outline" onClick={() => nav('/')}>
|
<Button type="button" size="sm" variant="outline" onClick={() => nav('/')}>
|
||||||
返回全部帖子
|
返回全部帖子
|
||||||
@@ -155,13 +169,21 @@ export default function VirtualPostList({
|
|||||||
{isSearchEmpty
|
{isSearchEmpty
|
||||||
? <SearchX className="empty-feed-icon" aria-hidden size={36} strokeWidth={1.5} />
|
? <SearchX className="empty-feed-icon" aria-hidden size={36} strokeWidth={1.5} />
|
||||||
: <Inbox className="empty-feed-icon" aria-hidden size={36} strokeWidth={1.5} />}
|
: <Inbox className="empty-feed-icon" aria-hidden size={36} strokeWidth={1.5} />}
|
||||||
<p>{isSearchEmpty ? '没有匹配的帖子' : '暂无帖子'}</p>
|
<p>
|
||||||
|
{noBoards
|
||||||
|
? '暂无板块'
|
||||||
|
: isSearchEmpty
|
||||||
|
? '没有匹配的帖子'
|
||||||
|
: '暂无帖子'}
|
||||||
|
</p>
|
||||||
<p className="empty-feed-hint">
|
<p className="empty-feed-hint">
|
||||||
{isSearchEmpty
|
{noBoards
|
||||||
? '试试更短的关键词,或浏览标签云 / 板块'
|
? (isAdmin ? '创建第一个板块后即可开始发帖' : '管理员创建板块后即可参与讨论')
|
||||||
: boardName
|
: isSearchEmpty
|
||||||
? `「${boardName}」还没有内容,来发第一篇吧`
|
? '试试更短的关键词,或浏览标签云 / 板块'
|
||||||
: '换个板块看看,或发第一篇内容'}
|
: boardName
|
||||||
|
? `「${boardName}」还没有内容,来发第一篇吧`
|
||||||
|
: '换个板块看看,或发第一篇内容'}
|
||||||
</p>
|
</p>
|
||||||
{emptyActions}
|
{emptyActions}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -283,11 +283,12 @@ export default function MainLayout() {
|
|||||||
keyword: outletKeyword,
|
keyword: outletKeyword,
|
||||||
setBoardId,
|
setBoardId,
|
||||||
boards,
|
boards,
|
||||||
|
boardsLoading,
|
||||||
stats,
|
stats,
|
||||||
refreshBoards,
|
refreshBoards,
|
||||||
isMobile,
|
isMobile,
|
||||||
setPostOutline: setPostOutlineSafe,
|
setPostOutline: setPostOutlineSafe,
|
||||||
}), [boardId, outletKeyword, boards, stats, refreshBoards, isMobile, setPostOutlineSafe]);
|
}), [boardId, outletKeyword, boards, boardsLoading, stats, refreshBoards, isMobile, setPostOutlineSafe]);
|
||||||
|
|
||||||
const selectBoardChip = (id: number) => {
|
const selectBoardChip = (id: number) => {
|
||||||
setBoardId(id);
|
setBoardId(id);
|
||||||
@@ -696,6 +697,7 @@ export type LayoutCtx = {
|
|||||||
keyword: string;
|
keyword: string;
|
||||||
setBoardId: (id: number) => void;
|
setBoardId: (id: number) => void;
|
||||||
boards: Board[];
|
boards: Board[];
|
||||||
|
boardsLoading: boolean;
|
||||||
stats: ForumStats | null;
|
stats: ForumStats | null;
|
||||||
refreshBoards: () => void;
|
refreshBoards: () => void;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate, useSearchParams, useParams, useOutletContext } from 'react-router-dom';
|
import { useNavigate, useSearchParams, useParams, useOutletContext } from 'react-router-dom';
|
||||||
import { ArrowLeft, Send, Pencil } from 'lucide-react';
|
import { ArrowLeft, Send, Pencil } from 'lucide-react';
|
||||||
import { notify } from '@/lib/notify';
|
import { notify } from '@/lib/notify';
|
||||||
@@ -17,12 +17,6 @@ import type { LayoutCtx } from '../layouts/MainLayout';
|
|||||||
import { loginPath } from '../utils/authRedirect';
|
import { loginPath } from '../utils/authRedirect';
|
||||||
import { useNoIndexSEO } from '../hooks/usePageSEO';
|
import { useNoIndexSEO } from '../hooks/usePageSEO';
|
||||||
import { parsePermalinkID, postPath } from '../utils/permalink';
|
import { parsePermalinkID, postPath } from '../utils/permalink';
|
||||||
import {
|
|
||||||
loadComposeDraft,
|
|
||||||
saveComposeDraft,
|
|
||||||
clearComposeDraft,
|
|
||||||
draftHasContent,
|
|
||||||
} from '../utils/composeDraft';
|
|
||||||
|
|
||||||
interface ComposeBaseline {
|
interface ComposeBaseline {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -77,9 +71,6 @@ export default function ComposePage() {
|
|||||||
);
|
);
|
||||||
const [baseline, setBaseline] = useState<ComposeBaseline | null>(null);
|
const [baseline, setBaseline] = useState<ComposeBaseline | null>(null);
|
||||||
const [editWindowHint, setEditWindowHint] = useState('');
|
const [editWindowHint, setEditWindowHint] = useState('');
|
||||||
const [draftHint, setDraftHint] = useState('');
|
|
||||||
const draftReadyRef = useRef(false);
|
|
||||||
const draftTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authLoading) return;
|
if (authLoading) return;
|
||||||
@@ -90,7 +81,6 @@ export default function ComposePage() {
|
|||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
draftReadyRef.current = false;
|
|
||||||
const cached = resolveBoards(layoutCtx?.boards);
|
const cached = resolveBoards(layoutCtx?.boards);
|
||||||
const boardsPromise = cached.length > 0
|
const boardsPromise = cached.length > 0
|
||||||
? Promise.resolve({ boards: cached })
|
? Promise.resolve({ boards: cached })
|
||||||
@@ -120,6 +110,9 @@ export default function ComposePage() {
|
|||||||
};
|
};
|
||||||
setBoardId(loadedBoardId);
|
setBoardId(loadedBoardId);
|
||||||
setBaseline(serverBaseline);
|
setBaseline(serverBaseline);
|
||||||
|
setTitle(serverBaseline.title);
|
||||||
|
setTags(serverBaseline.tags);
|
||||||
|
setContent(serverBaseline.content);
|
||||||
|
|
||||||
const windowHours = postData.post_edit_window_hours ?? 0;
|
const windowHours = postData.post_edit_window_hours ?? 0;
|
||||||
if (user.role !== 'admin' && windowHours > 0) {
|
if (user.role !== 'admin' && windowHours > 0) {
|
||||||
@@ -127,28 +120,6 @@ export default function ComposePage() {
|
|||||||
} else {
|
} else {
|
||||||
setEditWindowHint('');
|
setEditWindowHint('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const draft = loadComposeDraft(editId);
|
|
||||||
const useDraft = draft
|
|
||||||
&& draftHasContent(draft)
|
|
||||||
&& (
|
|
||||||
draft.title !== serverBaseline.title
|
|
||||||
|| draft.tags !== serverBaseline.tags
|
|
||||||
|| draft.content !== serverBaseline.content
|
|
||||||
);
|
|
||||||
if (useDraft && draft) {
|
|
||||||
setTitle(draft.title);
|
|
||||||
setTags(draft.tags);
|
|
||||||
setContent(draft.content);
|
|
||||||
setDraftHint('已恢复未保存的编辑草稿');
|
|
||||||
notify.success('已恢复未保存的编辑草稿');
|
|
||||||
} else {
|
|
||||||
setTitle(serverBaseline.title);
|
|
||||||
setTags(serverBaseline.tags);
|
|
||||||
setContent(serverBaseline.content);
|
|
||||||
setDraftHint('');
|
|
||||||
}
|
|
||||||
draftReadyRef.current = true;
|
|
||||||
})
|
})
|
||||||
.catch((e: unknown) => {
|
.catch((e: unknown) => {
|
||||||
notify.error(e instanceof Error ? e.message : '加载帖子失败');
|
notify.error(e instanceof Error ? e.message : '加载帖子失败');
|
||||||
@@ -158,39 +129,17 @@ export default function ComposePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
draftReadyRef.current = false;
|
|
||||||
const applyNewBaseline = (list: Board[], initialBoardId: string) => {
|
const applyNewBaseline = (list: Board[], initialBoardId: string) => {
|
||||||
setBoards(list);
|
setBoards(list);
|
||||||
if (!defaultBoard) setBoardId(initialBoardId);
|
if (!defaultBoard) setBoardId(initialBoardId);
|
||||||
const boardForBaseline = defaultBoard || initialBoardId;
|
const boardForBaseline = defaultBoard || initialBoardId;
|
||||||
setBoardId(prev => prev || boardForBaseline);
|
setBoardId(prev => prev || boardForBaseline);
|
||||||
|
setBaseline({
|
||||||
const draft = loadComposeDraft(null);
|
title: '',
|
||||||
if (draft && draftHasContent(draft)) {
|
tags: '',
|
||||||
setTitle(draft.title);
|
content: '',
|
||||||
setTags(draft.tags);
|
boardId: boardForBaseline,
|
||||||
setContent(draft.content);
|
});
|
||||||
if (draft.boardId && list.some(b => String(b.id) === draft.boardId)) {
|
|
||||||
setBoardId(draft.boardId);
|
|
||||||
}
|
|
||||||
setBaseline({
|
|
||||||
title: '',
|
|
||||||
tags: '',
|
|
||||||
content: '',
|
|
||||||
boardId: draft.boardId || boardForBaseline,
|
|
||||||
});
|
|
||||||
setDraftHint('已恢复本地草稿');
|
|
||||||
notify.success('已恢复本地草稿');
|
|
||||||
} else {
|
|
||||||
setBaseline({
|
|
||||||
title: '',
|
|
||||||
tags: '',
|
|
||||||
content: '',
|
|
||||||
boardId: boardForBaseline,
|
|
||||||
});
|
|
||||||
setDraftHint('');
|
|
||||||
}
|
|
||||||
draftReadyRef.current = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = resolveBoards(layoutCtx?.boards);
|
const list = resolveBoards(layoutCtx?.boards);
|
||||||
@@ -209,27 +158,11 @@ export default function ComposePage() {
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setBoards([]);
|
setBoards([]);
|
||||||
}).finally(() => setBoardsReady(true));
|
}).finally(() => setBoardsReady(true));
|
||||||
}, [user, authLoading, nav, defaultBoard, isEdit, editId, layoutCtx?.boards]);
|
}, [user, authLoading, nav, defaultBoard, isEdit, editId, layoutCtx?.boards, limits]);
|
||||||
|
|
||||||
// 防抖自动保存草稿
|
|
||||||
useEffect(() => {
|
|
||||||
if (!draftReadyRef.current || !user) return;
|
|
||||||
clearTimeout(draftTimerRef.current);
|
|
||||||
draftTimerRef.current = setTimeout(() => {
|
|
||||||
saveComposeDraft(isEdit ? editId : null, {
|
|
||||||
title,
|
|
||||||
tags,
|
|
||||||
content,
|
|
||||||
boardId,
|
|
||||||
});
|
|
||||||
if (title.trim() || tags.trim() || content.trim()) {
|
|
||||||
setDraftHint('草稿已自动保存');
|
|
||||||
}
|
|
||||||
}, 800);
|
|
||||||
return () => clearTimeout(draftTimerRef.current);
|
|
||||||
}, [title, tags, content, boardId, isEdit, editId, user]);
|
|
||||||
|
|
||||||
const isDirty = useMemo(() => {
|
const isDirty = useMemo(() => {
|
||||||
|
// 无可发帖板块时不拦截导航(空态页本身没有可保存内容)
|
||||||
|
if (!isEdit && boards.length === 0) return false;
|
||||||
if (!baseline) return false;
|
if (!baseline) return false;
|
||||||
return (
|
return (
|
||||||
title !== baseline.title
|
title !== baseline.title
|
||||||
@@ -237,7 +170,7 @@ export default function ComposePage() {
|
|||||||
|| content !== baseline.content
|
|| content !== baseline.content
|
||||||
|| boardId !== baseline.boardId
|
|| boardId !== baseline.boardId
|
||||||
);
|
);
|
||||||
}, [baseline, title, tags, content, boardId]);
|
}, [baseline, title, tags, content, boardId, isEdit, boards.length]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dialogOpen,
|
dialogOpen,
|
||||||
@@ -247,6 +180,11 @@ export default function ComposePage() {
|
|||||||
markSaved,
|
markSaved,
|
||||||
} = useUnsavedChangesGuard({ isDirty });
|
} = useUnsavedChangesGuard({ isDirty });
|
||||||
|
|
||||||
|
const leaveTo = (path: string) => {
|
||||||
|
markSaved();
|
||||||
|
nav(path);
|
||||||
|
};
|
||||||
|
|
||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="compose-page compose-page--empty">
|
<div className="compose-page compose-page--empty">
|
||||||
@@ -267,24 +205,32 @@ export default function ComposePage() {
|
|||||||
|
|
||||||
if (!isEdit && boards.length === 0) {
|
if (!isEdit && boards.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="compose-page compose-page--empty">
|
<>
|
||||||
<div className="compose-empty-card">
|
<div className="compose-page compose-page--empty">
|
||||||
<div className="compose-empty-icon" aria-hidden>
|
<div className="compose-empty-card">
|
||||||
<Pencil size={28} strokeWidth={1.5} />
|
<div className="compose-empty-icon" aria-hidden>
|
||||||
|
<Pencil size={28} strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<h2>暂无可发帖板块</h2>
|
||||||
|
<p>需要管理员先创建板块后才能发布内容</p>
|
||||||
|
{user.role === 'admin' ? (
|
||||||
|
<button type="button" className="compose-primary-btn" onClick={() => leaveTo('/admin/boards')}>
|
||||||
|
去创建板块
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="compose-ghost-btn" onClick={() => leaveTo('/')}>
|
||||||
|
返回首页
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h2>暂无可发帖板块</h2>
|
|
||||||
<p>需要管理员先创建板块后才能发布内容</p>
|
|
||||||
{user.role === 'admin' ? (
|
|
||||||
<button type="button" className="compose-primary-btn" onClick={() => nav('/admin/boards')}>
|
|
||||||
去创建板块
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button type="button" className="compose-ghost-btn" onClick={() => nav('/')}>
|
|
||||||
返回首页
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<UnsavedChangesDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onStay={stayOnPage}
|
||||||
|
onLeave={discardAndLeave}
|
||||||
|
isEdit={isEdit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,13 +251,11 @@ export default function ComposePage() {
|
|||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await api.updatePost(editId!, payload);
|
await api.updatePost(editId!, payload);
|
||||||
notify.success(user?.role === 'admin' ? '帖子已更新' : '已更新并重新提交审核');
|
notify.success(user?.role === 'admin' ? '帖子已更新' : '已更新并重新提交审核');
|
||||||
clearComposeDraft(editId);
|
|
||||||
markSaved();
|
markSaved();
|
||||||
nav(postPath(editId!, limits));
|
nav(postPath(editId!, limits));
|
||||||
} else {
|
} else {
|
||||||
const res = await api.createPost(payload);
|
const res = await api.createPost(payload);
|
||||||
notify.success(res.message || (res.status === 'pending' ? '已提交审核' : '发帖成功'));
|
notify.success(res.message || (res.status === 'pending' ? '已提交审核' : '发帖成功'));
|
||||||
clearComposeDraft(null);
|
|
||||||
markSaved();
|
markSaved();
|
||||||
nav(postPath(res.post_id, limits));
|
nav(postPath(res.post_id, limits));
|
||||||
}
|
}
|
||||||
@@ -340,9 +284,9 @@ export default function ComposePage() {
|
|||||||
<span>返回</span>
|
<span>返回</span>
|
||||||
</button>
|
</button>
|
||||||
<h1 className="compose-header-title">{isEdit ? '编辑帖子' : '写新帖'}</h1>
|
<h1 className="compose-header-title">{isEdit ? '编辑帖子' : '写新帖'}</h1>
|
||||||
{(draftHint || editWindowHint) && (
|
{editWindowHint && (
|
||||||
<span className="compose-draft-hint" title={editWindowHint || draftHint}>
|
<span className="compose-draft-hint" title={editWindowHint}>
|
||||||
{editWindowHint || draftHint}
|
{editWindowHint}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ export default function HomePage() {
|
|||||||
keyword={keyword}
|
keyword={keyword}
|
||||||
boardId={boardId}
|
boardId={boardId}
|
||||||
boardName={ctx?.boards?.find(b => b.id === boardId)?.name || ''}
|
boardName={ctx?.boards?.find(b => b.id === boardId)?.name || ''}
|
||||||
|
noBoards={!ctx?.boardsLoading && (ctx?.boards?.length ?? 0) === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
export interface ComposeDraft {
|
|
||||||
title: string;
|
|
||||||
tags: string;
|
|
||||||
content: string;
|
|
||||||
boardId: string;
|
|
||||||
savedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PREFIX = 'j13-compose-draft:';
|
|
||||||
|
|
||||||
function draftKey(editId: number | null): string {
|
|
||||||
return editId == null ? `${PREFIX}new` : `${PREFIX}edit:${editId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 读取发帖/编辑草稿 */
|
|
||||||
export function loadComposeDraft(editId: number | null): ComposeDraft | null {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(draftKey(editId));
|
|
||||||
if (!raw) return null;
|
|
||||||
const data = JSON.parse(raw) as ComposeDraft;
|
|
||||||
if (!data || typeof data !== 'object') return null;
|
|
||||||
return {
|
|
||||||
title: typeof data.title === 'string' ? data.title : '',
|
|
||||||
tags: typeof data.tags === 'string' ? data.tags : '',
|
|
||||||
content: typeof data.content === 'string' ? data.content : '',
|
|
||||||
boardId: typeof data.boardId === 'string' ? data.boardId : '',
|
|
||||||
savedAt: typeof data.savedAt === 'number' ? data.savedAt : 0,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 写入发帖/编辑草稿 */
|
|
||||||
export function saveComposeDraft(editId: number | null, draft: Omit<ComposeDraft, 'savedAt'>): void {
|
|
||||||
try {
|
|
||||||
const payload: ComposeDraft = { ...draft, savedAt: Date.now() };
|
|
||||||
localStorage.setItem(draftKey(editId), JSON.stringify(payload));
|
|
||||||
} catch {
|
|
||||||
// 配额不足等场景静默忽略
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 清除发帖/编辑草稿 */
|
|
||||||
export function clearComposeDraft(editId: number | null): void {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(draftKey(editId));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 草稿是否相对 baseline 有实质内容 */
|
|
||||||
export function draftHasContent(draft: ComposeDraft): boolean {
|
|
||||||
return Boolean(
|
|
||||||
draft.title.trim()
|
|
||||||
|| draft.tags.trim()
|
|
||||||
|| draft.content.trim()
|
|
||||||
|| draft.boardId.trim(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -44,6 +44,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
|
|||||||
authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
|
authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
|
||||||
userSvc := service.NewUserService(filter, settingsSvc)
|
userSvc := service.NewUserService(filter, settingsSvc)
|
||||||
boardSvc := service.NewBoardService()
|
boardSvc := service.NewBoardService()
|
||||||
|
boardSvc.EnsureDefaultBoard()
|
||||||
postSvc := service.NewPostService(filter, settingsSvc)
|
postSvc := service.NewPostService(filter, settingsSvc)
|
||||||
commentSvc := service.NewCommentService(filter, settingsSvc)
|
commentSvc := service.NewCommentService(filter, settingsSvc)
|
||||||
messageSvc := service.NewMessageService(filter, settingsSvc)
|
messageSvc := service.NewMessageService(filter, settingsSvc)
|
||||||
|
|||||||
@@ -76,3 +76,12 @@ func (s *BoardService) Delete(id uint) error {
|
|||||||
}
|
}
|
||||||
return model.DB.Delete(&model.Board{}, id).Error
|
return model.DB.Delete(&model.Board{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureDefaultBoard 若尚无板块则创建默认「综合讨论」,便于全新安装后直接发帖
|
||||||
|
func (s *BoardService) EnsureDefaultBoard() {
|
||||||
|
var n int64
|
||||||
|
if err := model.DB.Model(&model.Board{}).Count(&n).Error; err != nil || n > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = s.Create("综合讨论", "默认板块,欢迎发帖交流", "message-square", 0, 0)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user