清除业务契约双读:抽奖/草稿/时间戳/邮件只认新格式,并删除 legacy 短代码改写。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -248,7 +248,7 @@ func MatchPasswordBlocks(content, password string) ([]int, error) {
|
|||||||
return hit, nil
|
return hit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapContentAsHide 将整篇正文包进单一隐藏块(旧帖迁移用)
|
// WrapContentAsHide 将整篇正文包进单一隐藏块
|
||||||
func WrapContentAsHide(kind string, points int, content string) string {
|
func WrapContentAsHide(kind string, points int, content string) string {
|
||||||
body := strings.TrimRight(content, "\n")
|
body := strings.TrimRight(content, "\n")
|
||||||
open := openMarkerLine(kind, points, "", false)
|
open := openMarkerLine(kind, points, "", false)
|
||||||
@@ -258,7 +258,7 @@ func WrapContentAsHide(kind string, points int, content string) string {
|
|||||||
return open + "\n" + body + "\n[/hide]\n"
|
return open + "\n" + body + "\n[/hide]\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasHideBlocks 快速判断正文是否已含隐藏块(迁移幂等)
|
// HasHideBlocks 判断正文是否已含隐藏块
|
||||||
func HasHideBlocks(content string) bool {
|
func HasHideBlocks(content string) bool {
|
||||||
blocks, err := ParseHideBlocks(content)
|
blocks, err := ParseHideBlocks(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -177,20 +177,3 @@ func TestTimelineDoesNotBreakHideValidate(t *testing.T) {
|
|||||||
t.Fatalf("timeline-only should pass: %v", err)
|
t.Fatalf("timeline-only should pass: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRewriteLegacyDirectives(t *testing.T) {
|
|
||||||
src := "公开\n\n:::hide login\n秘\n:::\n\n:::timeline\n## 2026-01-01 A\n:::\n"
|
|
||||||
got := RewriteLegacyDirectives(src)
|
|
||||||
if strings.Contains(got, ":::") {
|
|
||||||
t.Fatalf("legacy remains: %q", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, "[hide login]") || !strings.Contains(got, "[/hide]") {
|
|
||||||
t.Fatalf("hide missing: %q", got)
|
|
||||||
}
|
|
||||||
if !strings.Contains(got, "[timeline]") || !strings.Contains(got, "[/timeline]") {
|
|
||||||
t.Fatalf("timeline missing: %q", got)
|
|
||||||
}
|
|
||||||
if err := ValidateHideContent(got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
package markdown
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RewriteLegacyDirectives 将旧式 :::hide / :::timeline / 裸 ::: 改写为行级 BBCode。
|
|
||||||
// 幂等:已是 BBCode 的内容原样返回(若无旧标记)。代码围栏内不改写。
|
|
||||||
func RewriteLegacyDirectives(content string) string {
|
|
||||||
if content == "" {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
if !strings.Contains(content, ":::") {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
lines := splitLines(content)
|
|
||||||
var out []string
|
|
||||||
inCode := false
|
|
||||||
i := 0
|
|
||||||
for i < len(lines) {
|
|
||||||
trimmed := strings.TrimSpace(lines[i])
|
|
||||||
if strings.HasPrefix(trimmed, "```") {
|
|
||||||
inCode = !inCode
|
|
||||||
out = append(out, lines[i])
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inCode {
|
|
||||||
out = append(out, lines[i])
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if kind, pts, pwd, locked, ok := parseLegacyHideOpen(trimmed); ok {
|
|
||||||
bodyLines := make([]string, 0)
|
|
||||||
j := i + 1
|
|
||||||
innerCode := false
|
|
||||||
found := false
|
|
||||||
for j < len(lines) {
|
|
||||||
inner := strings.TrimSpace(lines[j])
|
|
||||||
if strings.HasPrefix(inner, "```") {
|
|
||||||
innerCode = !innerCode
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if innerCode {
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inner == ":::" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
out = append(out, openMarkerLine(kind, pts, pwd, locked))
|
|
||||||
out = append(out, bodyLines...)
|
|
||||||
if found {
|
|
||||||
out = append(out, "[/hide]")
|
|
||||||
i = j + 1
|
|
||||||
} else {
|
|
||||||
// 未闭合:仍写出已转换开标记与正文,避免丢内容
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmed == ":::timeline" || strings.HasPrefix(trimmed, ":::timeline ") {
|
|
||||||
bodyLines := make([]string, 0)
|
|
||||||
j := i + 1
|
|
||||||
innerCode := false
|
|
||||||
found := false
|
|
||||||
for j < len(lines) {
|
|
||||||
inner := strings.TrimSpace(lines[j])
|
|
||||||
if strings.HasPrefix(inner, "```") {
|
|
||||||
innerCode = !innerCode
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if innerCode {
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inner == ":::" {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
bodyLines = append(bodyLines, lines[j])
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
out = append(out, "[timeline]")
|
|
||||||
out = append(out, bodyLines...)
|
|
||||||
if found {
|
|
||||||
out = append(out, "[/timeline]")
|
|
||||||
i = j + 1
|
|
||||||
} else {
|
|
||||||
i = j
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out = append(out, lines[i])
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return strings.Join(out, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseLegacyHideOpen 解析旧式 :::hide <kind> [arg] [locked]
|
|
||||||
func parseLegacyHideOpen(trimmed string) (kind string, points int, password string, locked bool, ok bool) {
|
|
||||||
if !strings.HasPrefix(trimmed, ":::hide") {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
rest := strings.TrimSpace(trimmed[len(":::hide"):])
|
|
||||||
if rest == "" {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
parts := strings.Fields(rest)
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
kind = parts[0]
|
|
||||||
switch kind {
|
|
||||||
case HideKindLogin, HideKindReply, HideKindPoints, HideKindPassword:
|
|
||||||
default:
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
idx := 1
|
|
||||||
if kind == HideKindPoints {
|
|
||||||
if idx >= len(parts) {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
n, err := strconv.Atoi(parts[idx])
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
points = n
|
|
||||||
idx++
|
|
||||||
} else if kind == HideKindPassword {
|
|
||||||
if idx < len(parts) && parts[idx] != "locked" {
|
|
||||||
password = parts[idx]
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ; idx < len(parts); idx++ {
|
|
||||||
if parts[idx] == "locked" {
|
|
||||||
locked = true
|
|
||||||
} else {
|
|
||||||
return "", 0, "", false, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return kind, points, password, locked, true
|
|
||||||
}
|
|
||||||
@@ -113,9 +113,6 @@ func (o *Operations) smtp(ctx context.Context, c MailConfig, p *mailPayload, id
|
|||||||
|
|
||||||
const defaultMailSubject = "{{site_name}} · {{purpose}}"
|
const defaultMailSubject = "{{site_name}} · {{purpose}}"
|
||||||
|
|
||||||
// 旧版内置正文:读取时若仍是此值,升级为新默认模板。
|
|
||||||
const legacyMailBody = "{{logo}}<h1>{{site_name}} · {{purpose}}</h1><p>您的验证码:</p><p><strong>{{code}}</strong></p><p>15 分钟内有效。如果不是您本人操作,请忽略本邮件。</p><p><a href=\"{{link}}\">前往站点完成{{purpose}}</a>(请粘贴以上验证码)</p>"
|
|
||||||
|
|
||||||
const defaultMailBody = `<div class="email-wrap" style="margin:0;padding:32px 16px;background:#f5f7fb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;color:#1e293b;">
|
const defaultMailBody = `<div class="email-wrap" style="margin:0;padding:32px 16px;background:#f5f7fb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;color:#1e293b;">
|
||||||
<style>
|
<style>
|
||||||
.email-wrap{margin:0;padding:32px 16px;background:#f5f7fb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;color:#1e293b;}
|
.email-wrap{margin:0;padding:32px 16px;background:#f5f7fb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;line-height:1.6;color:#1e293b;}
|
||||||
@@ -158,8 +155,7 @@ func (c *MailConfig) applyTemplateDefaults() {
|
|||||||
if strings.TrimSpace(c.SubjectTemplate) == "" {
|
if strings.TrimSpace(c.SubjectTemplate) == "" {
|
||||||
c.SubjectTemplate = defaultMailSubject
|
c.SubjectTemplate = defaultMailSubject
|
||||||
}
|
}
|
||||||
body := strings.TrimSpace(c.BodyTemplate)
|
if strings.TrimSpace(c.BodyTemplate) == "" {
|
||||||
if body == "" || body == legacyMailBody {
|
|
||||||
c.BodyTemplate = defaultMailBody
|
c.BodyTemplate = defaultMailBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -589,10 +589,11 @@ func TestMailTemplateGuards(t *testing.T) {
|
|||||||
if cfg.BodyTemplate != defaultMailBody || cfg.SubjectTemplate != defaultMailSubject {
|
if cfg.BodyTemplate != defaultMailBody || cfg.SubjectTemplate != defaultMailSubject {
|
||||||
t.Fatal("defaults not applied")
|
t.Fatal("defaults not applied")
|
||||||
}
|
}
|
||||||
cfg.BodyTemplate = legacyMailBody
|
customBody := "<p>{{code}}</p>"
|
||||||
|
cfg.BodyTemplate = customBody
|
||||||
cfg.applyTemplateDefaults()
|
cfg.applyTemplateDefaults()
|
||||||
if cfg.BodyTemplate != defaultMailBody {
|
if cfg.BodyTemplate != customBody {
|
||||||
t.Fatal("legacy default not upgraded")
|
t.Fatal("自定义正文不应被默认模板覆盖")
|
||||||
}
|
}
|
||||||
o := &Operations{cfg: &config.Config{SiteURL: "https://forum.example.com", DevMode: true}}
|
o := &Operations{cfg: &config.Config{SiteURL: "https://forum.example.com", DevMode: true}}
|
||||||
if e := o.validate(nil, "mail", &MailConfig{Port: 465, TLS: "tls", Timeout: 10, Retention: 30, BodyTemplate: "<script></script><p>no code</p>"}); e == nil || !strings.Contains(e.Error(), "{{code}}") {
|
if e := o.validate(nil, "mail", &MailConfig{Port: 465, TLS: "tls", Timeout: 10, Retention: 30, BodyTemplate: "<script></script><p>no code</p>"}); e == nil || !strings.Contains(e.Error(), "{{code}}") {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 兼容旧引用:默认体积/个数(实际限额读 SettingService)
|
// 附件默认体积/个数上限(实际限额读 SettingService;此处供校验与测试引用)
|
||||||
const (
|
const (
|
||||||
FileMaxBytes = 20 << 20 // 默认 20 MiB(缺省配置)
|
FileMaxBytes = 20 << 20 // 默认 20 MiB
|
||||||
MaxPostAttachments = 10
|
MaxPostAttachments = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type LotteryPrize struct {
|
|||||||
|
|
||||||
type LotteryMeta struct {
|
type LotteryMeta struct {
|
||||||
Prizes []LotteryPrize `json:"prizes"`
|
Prizes []LotteryPrize `json:"prizes"`
|
||||||
Slots int `json:"slots"` // 由 prizes 数量合计推导,兼容旧数据
|
Slots int `json:"slots"` // 由 prizes 数量合计,仅便于展示/校验
|
||||||
Drawn bool `json:"drawn"`
|
Drawn bool `json:"drawn"`
|
||||||
WinnerIDs []uint `json:"winner_ids"`
|
WinnerIDs []uint `json:"winner_ids"`
|
||||||
Closed bool `json:"closed"`
|
Closed bool `json:"closed"`
|
||||||
@@ -122,13 +122,7 @@ func parseLotteryMeta(raw string) (*LotteryMeta, error) {
|
|||||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||||
return nil, errors.New("抽奖配置无效")
|
return nil, errors.New("抽奖配置无效")
|
||||||
}
|
}
|
||||||
// 旧数据只有 slots:合成单一奖品项
|
m.Slots = lotterySlotsFromPrizes(m.Prizes)
|
||||||
if len(m.Prizes) == 0 && m.Slots > 0 {
|
|
||||||
m.Prizes = []LotteryPrize{{Name: "奖品", Count: m.Slots}}
|
|
||||||
}
|
|
||||||
if len(m.Prizes) > 0 {
|
|
||||||
m.Slots = lotterySlotsFromPrizes(m.Prizes)
|
|
||||||
}
|
|
||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,16 +586,12 @@ func (s *PostService) buildQuestionState(post *model.Post, viewerID uint, canMan
|
|||||||
}
|
}
|
||||||
floor := FloorNumber(s.db, post.ID, m.AcceptedCommentID)
|
floor := FloorNumber(s.db, post.ID, m.AcceptedCommentID)
|
||||||
answer := s.loadAcceptedAnswer(post.ID, m.AcceptedCommentID, floor, viewerID, isMod)
|
answer := s.loadAcceptedAnswer(post.ID, m.AcceptedCommentID, floor, viewerID, isMod)
|
||||||
solvedAt := m.SolvedAt
|
|
||||||
if m.Solved && solvedAt == "" && answer != nil && !answer.CreatedAt.IsZero() {
|
|
||||||
solvedAt = answer.CreatedAt.UTC().Format(time.RFC3339) // 旧数据:无 solved_at 时用采纳评论时间
|
|
||||||
}
|
|
||||||
return &QuestionState{
|
return &QuestionState{
|
||||||
Solved: m.Solved,
|
Solved: m.Solved,
|
||||||
AcceptedCommentID: m.AcceptedCommentID,
|
AcceptedCommentID: m.AcceptedCommentID,
|
||||||
AcceptedFloor: floor,
|
AcceptedFloor: floor,
|
||||||
AcceptedAnswer: answer,
|
AcceptedAnswer: answer,
|
||||||
SolvedAt: solvedAt,
|
SolvedAt: m.SolvedAt,
|
||||||
CanAccept: canManage && !m.Solved,
|
CanAccept: canManage && !m.Solved,
|
||||||
CanSolve: canManage && !m.Solved,
|
CanSolve: canManage && !m.Solved,
|
||||||
// 已采纳答案后不可重新打开;仅「手动标已解决」可撤回
|
// 已采纳答案后不可重新打开;仅「手动标已解决」可撤回
|
||||||
@@ -688,17 +678,13 @@ func (s *PostService) buildPollState(post *model.Post, viewerID uint, canClose b
|
|||||||
opts[i] = stat
|
opts[i] = stat
|
||||||
}
|
}
|
||||||
voters := len(voterSet)
|
voters := len(voterSet)
|
||||||
closedAt := m.ClosedAt
|
|
||||||
if m.Closed && closedAt == "" {
|
|
||||||
closedAt = m.EndsAt // 旧数据:无 closed_at 时用截止时刻兜底
|
|
||||||
}
|
|
||||||
return &PollState{
|
return &PollState{
|
||||||
Options: opts,
|
Options: opts,
|
||||||
Multi: m.Multi,
|
Multi: m.Multi,
|
||||||
Closed: m.Closed,
|
Closed: m.Closed,
|
||||||
Anonymous: m.Anonymous,
|
Anonymous: m.Anonymous,
|
||||||
EndsAt: m.EndsAt,
|
EndsAt: m.EndsAt,
|
||||||
ClosedAt: closedAt,
|
ClosedAt: m.ClosedAt,
|
||||||
TotalVotes: total,
|
TotalVotes: total,
|
||||||
Voters: voters,
|
Voters: voters,
|
||||||
MyOptions: my,
|
MyOptions: my,
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ type ComposeDraft = {
|
|||||||
tags: string;
|
tags: string;
|
||||||
boardId: number;
|
boardId: number;
|
||||||
postType?: string;
|
postType?: string;
|
||||||
/** @deprecated 旧草稿仅有 JSON 字符串;新草稿用 typeForm */
|
|
||||||
typeMeta?: string;
|
|
||||||
typeForm?: TypeFormSnapshot;
|
typeForm?: TypeFormSnapshot;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
@@ -90,70 +88,12 @@ function serializeTags(tags: string[]): string {
|
|||||||
return tags.join(", ");
|
return tags.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从旧版 typeMeta JSON 尽量还原类型表单(不完整则用默认) */
|
|
||||||
function typeFormFromMeta(
|
|
||||||
postType: PostTypeValue,
|
|
||||||
raw: string | undefined
|
|
||||||
): Partial<TypeFormSnapshot> {
|
|
||||||
if (!raw?.trim()) return {};
|
|
||||||
try {
|
|
||||||
const m = JSON.parse(raw) as Record<string, unknown>;
|
|
||||||
const out: Partial<TypeFormSnapshot> = {};
|
|
||||||
const endsAt = typeof m.ends_at === "string" ? m.ends_at : "";
|
|
||||||
const expireDays = Number(m.expire_days);
|
|
||||||
if (endsAt) {
|
|
||||||
const t = Date.parse(endsAt);
|
|
||||||
if (Number.isFinite(t)) {
|
|
||||||
out.deadlineMode = "custom";
|
|
||||||
out.customEndsLocal = toLocalInputValue(new Date(t));
|
|
||||||
}
|
|
||||||
} else if (Number.isFinite(expireDays) && expireDays > 0) {
|
|
||||||
out.deadlineMode = "preset";
|
|
||||||
}
|
|
||||||
if (postType === "poll") {
|
|
||||||
if (Array.isArray(m.options)) {
|
|
||||||
const opts = m.options.map((o) => String(o ?? ""));
|
|
||||||
out.pollOptions = opts.length >= 2 ? opts : ["", ""];
|
|
||||||
}
|
|
||||||
out.pollMulti = !!m.multi;
|
|
||||||
out.pollAnonymous = m.anonymous !== false;
|
|
||||||
if ((POLL_EXPIRE_DAYS as readonly number[]).includes(expireDays)) {
|
|
||||||
out.pollExpireDays = expireDays;
|
|
||||||
}
|
|
||||||
} else if (postType === "bounty") {
|
|
||||||
const p = Number(m.points);
|
|
||||||
out.bountyPoints = p > 0 ? Math.floor(p) : "";
|
|
||||||
if ((BOUNTY_EXPIRE_DAYS as readonly number[]).includes(expireDays)) {
|
|
||||||
out.bountyExpireDays = expireDays;
|
|
||||||
}
|
|
||||||
} else if (postType === "lottery") {
|
|
||||||
const prizes = Array.isArray(m.prizes)
|
|
||||||
? (m.prizes as { name?: string; count?: number }[]).map((p) => ({
|
|
||||||
name: String(p.name || "").slice(0, 32),
|
|
||||||
count:
|
|
||||||
typeof p.count === "number" && p.count >= 1
|
|
||||||
? Math.floor(p.count)
|
|
||||||
: ("" as const),
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
out.lotteryPrizes =
|
|
||||||
prizes.length > 0 ? prizes : [{ name: "", count: "" as const }];
|
|
||||||
if ((LOTTERY_EXPIRE_DAYS as readonly number[]).includes(expireDays)) {
|
|
||||||
out.lotteryExpireDays = expireDays;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draftHasContent(d: ComposeDraft): boolean {
|
function draftHasContent(d: ComposeDraft): boolean {
|
||||||
if (d.title.trim() || d.content.trim() || d.tags.trim()) return true;
|
if (d.title.trim() || d.content.trim() || d.tags.trim()) return true;
|
||||||
if (d.boardId > 0) return true;
|
if (d.boardId > 0) return true;
|
||||||
const pt = normalizePostType(d.postType);
|
const pt = normalizePostType(d.postType);
|
||||||
if (pt !== "discussion") return true;
|
if (pt !== "discussion") return true;
|
||||||
if (d.typeForm || (d.typeMeta && d.typeMeta.trim())) return true;
|
if (d.typeForm) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,8 +117,14 @@ function readDraft(): ComposeDraft | null {
|
|||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(DRAFT_KEY);
|
const raw = localStorage.getItem(DRAFT_KEY);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const d = JSON.parse(raw) as ComposeDraft;
|
const d = JSON.parse(raw) as ComposeDraft & { typeMeta?: string };
|
||||||
if (!d || typeof d !== "object") return null;
|
if (!d || typeof d !== "object") return null;
|
||||||
|
// 仅含旧 typeMeta、无 typeForm 的草稿已废弃
|
||||||
|
if (d.typeMeta && !d.typeForm) {
|
||||||
|
clearDraft();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
delete d.typeMeta;
|
||||||
return d;
|
return d;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -295,7 +241,6 @@ export default function PostComposer(props: PostComposerProps) {
|
|||||||
try {
|
try {
|
||||||
const raw = JSON.parse(props.initialTypeMeta || "{}") as {
|
const raw = JSON.parse(props.initialTypeMeta || "{}") as {
|
||||||
prizes?: { name?: string; count?: number }[];
|
prizes?: { name?: string; count?: number }[];
|
||||||
slots?: number;
|
|
||||||
};
|
};
|
||||||
if (Array.isArray(raw.prizes) && raw.prizes.length > 0) {
|
if (Array.isArray(raw.prizes) && raw.prizes.length > 0) {
|
||||||
return raw.prizes.map((p) => ({
|
return raw.prizes.map((p) => ({
|
||||||
@@ -303,9 +248,6 @@ export default function PostComposer(props: PostComposerProps) {
|
|||||||
count: Math.max(1, Number(p.count) || 1),
|
count: Math.max(1, Number(p.count) || 1),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// 旧数据仅有 slots:数量可回填,名称留空让作者补全
|
|
||||||
const slots = Number(raw.slots);
|
|
||||||
if (slots > 0) return [{ name: "", count: slots }];
|
|
||||||
return [{ name: "", count: "" }];
|
return [{ name: "", count: "" }];
|
||||||
} catch {
|
} catch {
|
||||||
return [{ name: "", count: "" }];
|
return [{ name: "", count: "" }];
|
||||||
@@ -573,9 +515,7 @@ export default function PostComposer(props: PostComposerProps) {
|
|||||||
const nextType = normalizePostType(draftBanner.postType);
|
const nextType = normalizePostType(draftBanner.postType);
|
||||||
setPostType(nextType);
|
setPostType(nextType);
|
||||||
|
|
||||||
const fromMeta = typeFormFromMeta(nextType, draftBanner.typeMeta);
|
|
||||||
const snap: Partial<TypeFormSnapshot> = {
|
const snap: Partial<TypeFormSnapshot> = {
|
||||||
...fromMeta,
|
|
||||||
...(draftBanner.typeForm || {}),
|
...(draftBanner.typeForm || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -615,9 +555,7 @@ export default function PostComposer(props: PostComposerProps) {
|
|||||||
}
|
}
|
||||||
if (snap.customEndsLocal) setCustomEndsLocal(snap.customEndsLocal);
|
if (snap.customEndsLocal) setCustomEndsLocal(snap.customEndsLocal);
|
||||||
|
|
||||||
// 编辑态用的 typeMeta 字符串一并同步,避免后续误用空串
|
if (draftBanner.typeForm) setTypeMeta("");
|
||||||
if (draftBanner.typeMeta) setTypeMeta(draftBanner.typeMeta);
|
|
||||||
else if (draftBanner.typeForm) setTypeMeta("");
|
|
||||||
|
|
||||||
setDraftBanner(null);
|
setDraftBanner(null);
|
||||||
skipDraftSave.current = false;
|
skipDraftSave.current = false;
|
||||||
|
|||||||
@@ -574,11 +574,7 @@ function LotteryPanel({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prizes = state.prizes?.length
|
const prizes = state.prizes?.length ? state.prizes : [];
|
||||||
? state.prizes
|
|
||||||
: state.slots > 0
|
|
||||||
? [{ name: "奖品", count: state.slots }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const statusBadge = state.drawn
|
const statusBadge = state.drawn
|
||||||
? { cls: "badge-status badge-status-closed", label: "已开奖" }
|
? { cls: "badge-status badge-status-closed", label: "已开奖" }
|
||||||
|
|||||||
Reference in New Issue
Block a user