feat: 增加友链申请、独立页面、投票/悬赏/抽奖帖与侧栏签到,并统一开发数据目录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 06:23:54 +08:00
parent df19752a1e
commit 2208af7070
80 changed files with 9620 additions and 641 deletions

View File

@@ -1,30 +1,59 @@
/** 新建帖本地草稿localStorage */
const STORAGE_KEY = 'j13-compose-draft-v1';
const STORAGE_KEY = 'j13-compose-draft-v2';
export type ComposeDraftPostType = 'normal' | 'question' | 'poll' | 'bounty' | 'lottery';
export type ComposeDraft = {
title: string;
tags: string;
content: string;
boardId: string;
postType: 'normal' | 'question';
postType: ComposeDraftPostType;
pollOptions?: string[];
pollMulti?: boolean;
pollMaxChoices?: number;
pollEndsAt?: string;
pollNoEndTime?: boolean;
savedAt: number;
};
const VALID_POST_TYPES: ComposeDraftPostType[] = ['normal', 'question', 'poll', 'bounty', 'lottery'];
function normalizePostType(value: unknown): ComposeDraftPostType {
if (typeof value === 'string' && VALID_POST_TYPES.includes(value as ComposeDraftPostType)) {
return value as ComposeDraftPostType;
}
return 'normal';
}
export function loadComposeDraft(): ComposeDraft | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const data = JSON.parse(raw) as Partial<ComposeDraft>;
if (!data || typeof data !== 'object') return null;
return {
const postType = normalizePostType(data.postType);
const draft: ComposeDraft = {
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 : '',
postType: data.postType === 'question' ? 'question' : 'normal',
postType,
savedAt: typeof data.savedAt === 'number' ? data.savedAt : Date.now(),
};
if (postType === 'poll') {
if (Array.isArray(data.pollOptions) && data.pollOptions.every(o => typeof o === 'string')) {
draft.pollOptions = data.pollOptions.length >= 2 ? data.pollOptions : ['', ''];
}
if (typeof data.pollMulti === 'boolean') draft.pollMulti = data.pollMulti;
if (typeof data.pollMaxChoices === 'number' && data.pollMaxChoices > 0) {
draft.pollMaxChoices = data.pollMaxChoices;
}
if (typeof data.pollEndsAt === 'string') draft.pollEndsAt = data.pollEndsAt;
if (typeof data.pollNoEndTime === 'boolean') draft.pollNoEndTime = data.pollNoEndTime;
}
return draft;
} catch {
return null;
}
@@ -42,6 +71,8 @@ export function saveComposeDraft(draft: Omit<ComposeDraft, 'savedAt'>): void {
export function clearComposeDraft(): void {
try {
localStorage.removeItem(STORAGE_KEY);
// 清理旧版草稿键
localStorage.removeItem('j13-compose-draft-v1');
} catch {
// ignore
}
@@ -50,10 +81,14 @@ export function clearComposeDraft(): void {
/** 草稿是否有实质内容 */
export function composeDraftHasContent(d: ComposeDraft | null | undefined): boolean {
if (!d) return false;
const hasPollOptions = d.postType === 'poll'
&& Array.isArray(d.pollOptions)
&& d.pollOptions.some(o => o.trim());
return !!(
d.title.trim()
|| d.tags.trim()
|| d.content.trim()
|| (d.content && d.content.replace(/<[^>]*>/g, '').trim())
|| hasPollOptions
);
}