第11环裂变重写:对齐上线版 split.js 一次LLM出N套完整笔记包
架构从"扇出N个GenerationTask各跑完整管道"改为"一次LLM调用直接出N套
完整笔记包(N=1~5)",落 FissionNote 表 + 独立展示页。
后端:
- 018迁移:fission_notes 表(文案JSON+score+passed+imagePlan+images+status)
- fission_prompt:FISSION_SYSTEM+三档参考度(low/mid/high)+normalize_tags+品类兜底
- fission_pipeline:一次LLM出N套→各评分(@80合格线)→排序→落库,不达标标
needs_optimization 非丢弃;apiports 503 回落 codeproxy gpt-5.5 强档兜底
- fission_images:每套串行调现有生图接口(零改动image_gen/storyboard)
- tasks.py:run_fission_pipeline Celery task,删旧扇出注入
- api/v1/fission:进度聚合FissionNote + GET /fission/{id}/notes(剥内部字段)
前端:FissionProgress对齐状态机 + N套独立展示页 + FissionNoteCard
测试:test_fission_engine(19)+test_fission_pipeline(5) 全过;104 全量回归绿
实测task5(fanout=2,mid)端到端跑通:一次出2套→seq0=85过/seq1=79标优化→
生图codeproxy/edits→1024×1536去AI化→task completed→notes端点返完整数据
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
112
frontend/src/app/fission/[id]/notes/page.tsx
Normal file
112
frontend/src/app/fission/[id]/notes/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
/**
|
||||
* /fission/[id]/notes — N 套裂变笔记包独立展示页(F2,新架构)
|
||||
* 一次 LLM 出的 N 套完整笔记包:标题/正文/标签/封面钩子/imagePlan/生成图/分数。
|
||||
* 数据源 GET /api/v1/fission/{id}/notes(FissionNote 落库结果)。
|
||||
*/
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import { FissionNoteCard } from '@/components/fission/FissionNoteCard';
|
||||
|
||||
export interface FissionImagePlan {
|
||||
role?: string;
|
||||
title?: string;
|
||||
overlayText?: string;
|
||||
text?: string;
|
||||
}
|
||||
export interface FissionNoteData {
|
||||
title?: string;
|
||||
content?: string;
|
||||
tags?: string[];
|
||||
coverTitle?: string;
|
||||
dimension?: string;
|
||||
audience?: string;
|
||||
scene?: string;
|
||||
painPoint?: string;
|
||||
keywords?: string[];
|
||||
imagePlan?: FissionImagePlan[];
|
||||
}
|
||||
export interface FissionImage {
|
||||
role: string;
|
||||
seq?: number;
|
||||
url?: string;
|
||||
error?: string;
|
||||
}
|
||||
export interface FissionNoteItem {
|
||||
seq: number;
|
||||
note: FissionNoteData;
|
||||
images: FissionImage[];
|
||||
score: number | null;
|
||||
passed: boolean;
|
||||
needs_optimization: boolean;
|
||||
dimension: string | null;
|
||||
status: string;
|
||||
}
|
||||
interface NotesResponse {
|
||||
fission_id: number;
|
||||
status: string;
|
||||
fanout_count: number;
|
||||
notes: FissionNoteItem[];
|
||||
}
|
||||
|
||||
export default function FissionNotesPage() {
|
||||
const params = useParams();
|
||||
const fissionId = Number(params?.id);
|
||||
const [data, setData] = useState<NotesResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchNotes = useCallback(async () => {
|
||||
setError(null);
|
||||
try {
|
||||
const res = await api.get<NotesResponse>(`/api/v1/fission/${fissionId}/notes`);
|
||||
setData(res);
|
||||
} catch {
|
||||
setError('加载裂变笔记包失败');
|
||||
}
|
||||
}, [fissionId]);
|
||||
|
||||
useEffect(() => { if (fissionId) fetchNotes(); }, [fissionId, fetchNotes]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-4xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-text-primary">
|
||||
裂变 #{fissionId} · {data?.fanout_count ?? ''} 套笔记包
|
||||
</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
一个爆款裂变出的多套差异化完整笔记,可分发给不同达人。
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/fission" className="text-sm text-text-secondary underline">
|
||||
返回裂变
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
|
||||
{error}
|
||||
<button className="ml-4 underline" onClick={fetchNotes}>重试</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!data && !error && (
|
||||
<div className="py-12 text-center text-text-tertiary text-sm">加载笔记包…</div>
|
||||
)}
|
||||
|
||||
{data && data.notes.length === 0 && (
|
||||
<div className="py-12 text-center text-text-tertiary text-sm">
|
||||
这批裂变还没有产出笔记,可能仍在生成或已失败。
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{data?.notes.map((item) => (
|
||||
<FissionNoteCard key={item.seq} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
frontend/src/app/fission/page.tsx
Normal file
76
frontend/src/app/fission/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
/**
|
||||
* /fission — 独立裂变页(第11环,倩倩姐2026-06-15拍板"新建独立裂变页")
|
||||
* 选一个已通过/已归档的爆款源任务 → 设定参考强度+扇出套数 → 一键裂变成 N 套完整笔记包。
|
||||
* 后端:POST /api/v1/fission,GET /api/v1/fission/{id} 查聚合进度。
|
||||
*/
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { TaskListItem, PaginatedResponse } from '@/types';
|
||||
import { getErrorAction } from '@/types/errors';
|
||||
import { FissionLauncher } from '@/components/fission/FissionLauncher';
|
||||
import { FissionProgress } from '@/components/fission/FissionProgress';
|
||||
|
||||
// 裂变源 = 已验证的爆款,只从通过/归档里选
|
||||
const SOURCE_STATUSES = ['approved', 'archived'];
|
||||
|
||||
export default function FissionPage() {
|
||||
const [sources, setSources] = useState<TaskListItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeFissionId, setActiveFissionId] = useState<number | null>(null);
|
||||
|
||||
const fetchSources = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams({ page: '1', page_size: '50' });
|
||||
SOURCE_STATUSES.forEach((s) => params.append('status', s));
|
||||
const res = await api.get<PaginatedResponse<TaskListItem>>(`/api/v1/tasks?${params}`);
|
||||
setSources(res.items);
|
||||
} catch (e: unknown) {
|
||||
const ae = e as { code?: number };
|
||||
setError(getErrorAction(ae.code ?? 50001).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchSources(); }, [fetchSources]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-3xl">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-text-primary">裂变扩散</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
选一个验证过的爆款笔记,一键裂变成多套差异化的完整笔记包,交给不同达人分发。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
|
||||
{error}
|
||||
<button className="ml-4 underline" onClick={fetchSources}>重试</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFissionId ? (
|
||||
<FissionProgress
|
||||
fissionId={activeFissionId}
|
||||
onReset={() => { setActiveFissionId(null); fetchSources(); }}
|
||||
/>
|
||||
) : loading ? (
|
||||
<div className="py-12 text-center text-text-tertiary text-sm">加载可裂变的爆款…</div>
|
||||
) : sources.length === 0 ? (
|
||||
<div className="py-12 text-center text-text-tertiary text-sm">
|
||||
还没有通过审核的笔记可裂变。先去开新任务并通过审核,再回来裂变。
|
||||
</div>
|
||||
) : (
|
||||
<FissionLauncher
|
||||
sources={sources}
|
||||
onLaunched={(fissionId) => setActiveFissionId(fissionId)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
frontend/src/components/fission/FissionLauncher.tsx
Normal file
112
frontend/src/components/fission/FissionLauncher.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
/**
|
||||
* FissionLauncher — 裂变发起面板
|
||||
* 选源爆款 + 参考强度(low/mid/high) + 扇出套数(1~5) → POST /fission,回调 fission_id。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { TaskListItem } from '@/types';
|
||||
import { getErrorAction } from '@/types/errors';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface Props {
|
||||
sources: TaskListItem[];
|
||||
onLaunched: (fissionId: number) => void;
|
||||
}
|
||||
|
||||
const LEVELS: { key: string; label: string; desc: string }[] = [
|
||||
{ key: 'low', label: '低', desc: '只借结构,文案视觉大改' },
|
||||
{ key: 'mid', label: '中', desc: '保留核心卖点,角度换新' },
|
||||
{ key: 'high', label: '高', desc: '贴近原作,仅做差异微调' },
|
||||
];
|
||||
|
||||
export function FissionLauncher({ sources, onLaunched }: Props) {
|
||||
const [sourceId, setSourceId] = useState<number | null>(sources[0]?.id ?? null);
|
||||
const [level, setLevel] = useState('mid');
|
||||
const [fanout, setFanout] = useState(3);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function launch() {
|
||||
if (!sourceId) { setError('请选择一个源爆款'); return; }
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await api.post<{ fission_id: number }>('/api/v1/fission', {
|
||||
source_task_id: sourceId, reference_level: level, fanout_count: fanout,
|
||||
});
|
||||
onLaunched(res.fission_id);
|
||||
} catch (e: unknown) {
|
||||
const ae = e as { code?: number; message?: string };
|
||||
setError(ae.message || getErrorAction(ae.code ?? 50001).message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* 选源爆款 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">源爆款笔记</label>
|
||||
<select
|
||||
value={sourceId ?? ''}
|
||||
onChange={(e) => setSourceId(Number(e.target.value))}
|
||||
className="w-full border border-border-default rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand-orange"
|
||||
>
|
||||
{sources.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
#{t.id} · {t.theme || '(无主题)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 参考强度 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">参考强度</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{LEVELS.map((l) => (
|
||||
<button
|
||||
key={l.key}
|
||||
type="button"
|
||||
onClick={() => setLevel(l.key)}
|
||||
className={clsx(
|
||||
'border rounded-lg p-3 text-left transition-colors',
|
||||
level === l.key
|
||||
? 'border-brand-orange bg-brand-orange-light'
|
||||
: 'border-border-default hover:border-brand-orange'
|
||||
)}
|
||||
>
|
||||
<div className="text-sm font-medium text-text-primary">{l.label}</div>
|
||||
<div className="text-xs text-text-tertiary mt-1">{l.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 扇出套数 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
裂变套数:<span className="text-brand-orange font-semibold">{fanout}</span> 套
|
||||
</label>
|
||||
<input
|
||||
type="range" min={1} max={5} value={fanout}
|
||||
onChange={(e) => setFanout(Number(e.target.value))}
|
||||
className="w-full accent-brand-orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-sm text-red-600">{error}</div>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={launch}
|
||||
disabled={submitting || !sourceId}
|
||||
className="self-start bg-brand-orange text-white text-sm font-medium px-6 py-2.5 rounded-lg hover:bg-orange-600 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? '裂变中…' : `裂变成 ${fanout} 套笔记包 →`}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
117
frontend/src/components/fission/FissionNoteCard.tsx
Normal file
117
frontend/src/components/fission/FissionNoteCard.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
/**
|
||||
* FissionNoteCard — 单套裂变笔记包卡片(F2 子组件)
|
||||
* 展示:维度/分数徽章 + 标题 + 正文 + 标签 + 生成图 + imagePlan 排版说明。
|
||||
*/
|
||||
import type { FissionNoteItem } from '@/app/fission/[id]/notes/page';
|
||||
|
||||
interface Props {
|
||||
item: FissionNoteItem;
|
||||
}
|
||||
|
||||
// 图片 url 后端给 /uploads/... 相对路径,前端同源直接用
|
||||
function imgSrc(url?: string): string | null {
|
||||
if (!url) return null;
|
||||
return url.startsWith('/uploads/') ? url : `/uploads/${url.replace(/^.*\/uploads\//, '')}`;
|
||||
}
|
||||
|
||||
export function FissionNoteCard({ item }: Props) {
|
||||
const { note, images, score, passed, needs_optimization, dimension, seq } = item;
|
||||
const okImages = images.filter((im) => im.url && !im.error);
|
||||
const failedImages = images.filter((im) => im.error);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border-default bg-surface-primary p-5 flex flex-col gap-4">
|
||||
{/* 头部:序号 + 维度 + 分数 */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-text-primary">第 {seq} 套</span>
|
||||
{(dimension || note.dimension) && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-surface-tertiary text-text-secondary">
|
||||
{dimension || note.dimension}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{score != null && (
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
|
||||
passed ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
{score} 分
|
||||
</span>
|
||||
)}
|
||||
{needs_optimization && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-50 text-amber-600">
|
||||
建议优化
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标题 + 封面钩子 */}
|
||||
{note.coverTitle && (
|
||||
<p className="text-sm font-medium text-brand-orange">钩子:{note.coverTitle}</p>
|
||||
)}
|
||||
{note.title && (
|
||||
<h3 className="text-base font-semibold text-text-primary">{note.title}</h3>
|
||||
)}
|
||||
|
||||
{/* 正文 */}
|
||||
{note.content && (
|
||||
<p className="text-sm text-text-secondary whitespace-pre-wrap leading-relaxed">
|
||||
{note.content}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
{note.tags && note.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{note.tags.map((t, i) => (
|
||||
<span key={i} className="text-xs text-brand-orange">#{t}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生成图 */}
|
||||
{okImages.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{okImages.map((im) => (
|
||||
<div key={im.seq} className="flex flex-col gap-1">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={imgSrc(im.url) || ''}
|
||||
alt={im.role}
|
||||
className="w-full aspect-[3/4] object-cover rounded-lg border border-border-default"
|
||||
/>
|
||||
<span className="text-[11px] text-text-tertiary text-center">{im.role}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{failedImages.length > 0 && (
|
||||
<p className="text-xs text-amber-600">
|
||||
{failedImages.length} 张配图生成失败,可在交付前重试生图。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* imagePlan 排版说明(运营按此排版/叠字) */}
|
||||
{note.imagePlan && note.imagePlan.length > 0 && (
|
||||
<details className="text-sm">
|
||||
<summary className="cursor-pointer text-text-secondary">
|
||||
图片排版说明({note.imagePlan.length} 张)
|
||||
</summary>
|
||||
<ol className="mt-2 flex flex-col gap-2 list-decimal list-inside">
|
||||
{note.imagePlan.map((p, i) => (
|
||||
<li key={i} className="text-xs text-text-tertiary">
|
||||
<span className="text-text-secondary">{p.role || `图${i + 1}`}</span>
|
||||
{p.title && <> · 标题:{p.title}</>}
|
||||
{p.overlayText && <> · 叠字:{p.overlayText}</>}
|
||||
{p.text && <> · 画面:{p.text}</>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
frontend/src/components/fission/FissionProgress.tsx
Normal file
108
frontend/src/components/fission/FissionProgress.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
/**
|
||||
* FissionProgress — 裂变进度(轮询 GET /fission/{id} 聚合 x/N 完成)
|
||||
* 新架构:一次 LLM 出 N 套完整笔记包落 FissionNote,完成后进独立展示页看 N 套。
|
||||
*/
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface FissionStatus {
|
||||
fission_id: number;
|
||||
reference_level: string;
|
||||
fanout_count: number;
|
||||
status: 'generating' | 'completed' | 'failed';
|
||||
progress: { done: number; failed: number; total: number };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
fissionId: number;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export function FissionProgress({ fissionId, onReset }: Props) {
|
||||
const [data, setData] = useState<FissionStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const poll = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.get<FissionStatus>(`/api/v1/fission/${fissionId}`);
|
||||
setData(res);
|
||||
return res.status;
|
||||
} catch {
|
||||
setError('查询裂变进度失败');
|
||||
return 'failed';
|
||||
}
|
||||
}, [fissionId]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let stopped = false;
|
||||
const tick = async () => {
|
||||
const status = await poll();
|
||||
if (!stopped && status === 'generating') timer = setTimeout(tick, 3000);
|
||||
};
|
||||
tick();
|
||||
return () => { stopped = true; clearTimeout(timer); };
|
||||
}, [poll]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
|
||||
{error}
|
||||
<button className="ml-4 underline" onClick={onReset}>返回</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) return <div className="py-12 text-center text-text-tertiary text-sm">启动裂变…</div>;
|
||||
|
||||
const { progress, status } = data;
|
||||
const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-text-primary">
|
||||
裂变 #{data.fission_id} · {data.fanout_count} 套
|
||||
</h2>
|
||||
<span className="text-sm text-text-secondary">
|
||||
{progress.done}/{progress.total || data.fanout_count} 完成
|
||||
{progress.failed > 0 && <span className="text-red-500"> · {progress.failed} 失败</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-2 bg-surface-tertiary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-brand-orange transition-all duration-500"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'generating' && (
|
||||
<p className="text-sm text-text-tertiary">正在一次性生成 {data.fanout_count} 套差异化笔记包,请稍候…</p>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<div className="rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-600">
|
||||
这批裂变全部生成失败,可点下方「再裂变一个」重试,或换一个源爆款。
|
||||
</div>
|
||||
)}
|
||||
{status === 'completed' && (
|
||||
<Link
|
||||
href={`/fission/${data.fission_id}/notes`}
|
||||
className="self-start rounded-lg bg-brand-orange text-white px-5 py-2.5 text-sm font-medium hover:opacity-90 transition-opacity"
|
||||
>
|
||||
查看 {data.fanout_count} 套笔记包 →
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="self-start text-sm text-text-secondary underline mt-2"
|
||||
>
|
||||
再裂变一个
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user