feat(marquee): 预览改为「按模式分页浏览全部可展示记录」

在原内联预览基础上,把「换一批」随机预览改为分页浏览:
- 上一页/下一页翻遍当前模式下 app 能露出的全部记录(不去重),不再只随机抽一批。
- 跟随数据源模式:只真实=真实记录、只种子=各种子生成、混播=真实+种子;切模式回第 1 页。
- 种子行标「种子」、真实行标 u{id};配套 app-server /marquee-seeds/real-records 接口。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zzhyyyyy
2026-07-07 17:09:44 +08:00
parent f950bcd841
commit ffad461d67
+56 -30
View File
@@ -42,10 +42,11 @@ interface BulkVals {
enabled: boolean; enabled: boolean;
} }
interface PreviewItem { interface RealRecord {
masked_user: string; masked_user: string;
saved_amount_cents: number; saved_amount_cents: number;
time: string; created_at: string;
user_id: number;
} }
const yuan = (cents: number) => (cents / 100).toFixed(2); const yuan = (cents: number) => (cents / 100).toFixed(2);
@@ -67,8 +68,12 @@ export default function HomeMarqueeSeeds() {
const [bulkOpen, setBulkOpen] = useState(false); const [bulkOpen, setBulkOpen] = useState(false);
const [bulkForm] = Form.useForm<BulkVals>(); const [bulkForm] = Form.useForm<BulkVals>();
const [previewItems, setPreviewItems] = useState<PreviewItem[]>([]); // 「可展示的真实记录」分页浏览(替代原随机预览):翻遍所有 success+省>0 的真实记录
const [previewLoading, setPreviewLoading] = useState(false); const REC_PAGE_SIZE = 8;
const [recItems, setRecItems] = useState<RealRecord[]>([]);
const [recTotal, setRecTotal] = useState(0);
const [recPage, setRecPage] = useState(0); // 0-based
const [recLoading, setRecLoading] = useState(false);
const load = async () => { const load = async () => {
setLoading(true); setLoading(true);
@@ -94,6 +99,7 @@ export default function HomeMarqueeSeeds() {
const changeMode = async (m: string) => { const changeMode = async (m: string) => {
const prev = mode; const prev = mode;
setMode(m); setMode(m);
setRecPage(0); // 切模式回第 1 页(不同模式条数不同,避免停在越界页)
setModeSaving(true); setModeSaving(true);
try { try {
await api.patch('/admin/api/marquee-seeds/mode', { mode: m }); await api.patch('/admin/api/marquee-seeds/mode', { mode: m });
@@ -168,27 +174,29 @@ export default function HomeMarqueeSeeds() {
} }
}; };
// ===== 预览实际 feed(常驻内联,跟随数据源模式实时刷新)===== // ===== 可展示的记录:按「当前模式」分页浏览全部(不去重)=====
// 显式把当前选中的 mode 传给后端预览:不依赖 PATCH 落库时序 —— 拨动开关(乐观更新 mode)即刻预览 // 只真实=真实记录;只种子=各启用种子按生成逻辑各出一行;混播=真实+种子。与 app 同口径洗牌+去连簇,
// 对应模式;切换失败回滚 mode 时预览也随之回滚。种子随机抽样,故也可点「换一批」重随机 // 后端固定种子 → 翻页稳定、能翻遍全部
const fetchPreview = async (m: string = mode) => { const fetchRecords = async (page: number, m: string) => {
setPreviewLoading(true); setRecLoading(true);
try { try {
const { data } = await api.get<{ items: PreviewItem[] }>('/admin/api/marquee-seeds/preview', { const { data } = await api.get<{ items: RealRecord[]; total: number }>(
params: { limit: 8, mode: m }, '/admin/api/marquee-seeds/real-records',
}); { params: { offset: page * REC_PAGE_SIZE, limit: REC_PAGE_SIZE, mode: m } },
setPreviewItems(data.items); );
setRecItems(data.items);
setRecTotal(data.total);
} catch (e) { } catch (e) {
message.error(errMsg(e)); message.error(errMsg(e));
} finally { } finally {
setPreviewLoading(false); setRecLoading(false);
} }
}; };
// 模式变化(初次拉到持久化模式 / 拨动开关 / 切换失败回滚)→ 重拉预览,保证「所见=当前模式」 // 页码或模式变化都重拉(切模式时 changeMode 会把页码重置到 0)
useEffect(() => { useEffect(() => {
fetchPreview(mode); fetchRecords(recPage, mode);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode]); }, [recPage, mode]);
const toggle = async (s: Seed, enabled: boolean) => { const toggle = async (s: Seed, enabled: boolean) => {
try { try {
@@ -306,7 +314,8 @@ export default function HomeMarqueeSeeds() {
</span> </span>
</Space> </Space>
{/* 预览:常驻内联,跟随上方「数据源」模式实时刷新(所见=客户端实际效果)。 */} {/* 可展示的真实记录:分页浏览全部(success+省>0,不去重、稳定倒序);app 轮播即从这些记录
随机打乱、同一用户去连簇后展示。翻页可看全所有真实用户的展示信息。 */}
<div <div
style={{ style={{
marginBottom: 16, marginBottom: 16,
@@ -322,33 +331,47 @@ export default function HomeMarqueeSeeds() {
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 6, marginBottom: 6,
flexWrap: 'wrap',
gap: 8,
}} }}
> >
<span style={{ fontSize: 13, fontWeight: 500 }}> <span style={{ fontSize: 13, fontWeight: 500 }}>
· {MODE_LABEL[mode] ?? mode} · {MODE_LABEL[mode] ?? mode} · {recTotal}
<span style={{ color: '#999', fontWeight: 400, fontSize: 12, marginLeft: 8 }}> <span style={{ color: '#999', fontWeight: 400, fontSize: 12, marginLeft: 8 }}>
,;/ app ():===+;
app +,便
</span> </span>
</span> </span>
<Button size="small" onClick={() => fetchPreview()}> <Space size="small">
<Button size="small" disabled={recPage <= 0} onClick={() => setRecPage((p) => Math.max(0, p - 1))}>
</Button>
</Button>
<span style={{ fontSize: 12, color: '#666' }}>
{recTotal === 0 ? 0 : recPage + 1}/{Math.max(1, Math.ceil(recTotal / REC_PAGE_SIZE))}
</span>
<Button
size="small"
disabled={(recPage + 1) * REC_PAGE_SIZE >= recTotal}
onClick={() => setRecPage((p) => p + 1)}
>
</Button>
</Space>
</div> </div>
<Spin spinning={previewLoading}> <Spin spinning={recLoading}>
<div style={{ minHeight: 120 }}> <div style={{ minHeight: 120 }}>
{previewItems.length === 0 && !previewLoading ? ( {recItems.length === 0 && !recLoading ? (
<div style={{ color: '#bbb', textAlign: 'center', padding: '36px 0', fontSize: 13 }}> <div style={{ color: '#bbb', textAlign: 'center', padding: '36px 0', fontSize: 13 }}>
{mode === 'real'
? '当前无真实比价记录 ——「只真实」模式不补种子,故为空'
: '暂无数据'}
</div> </div>
) : ( ) : (
previewItems.map((it, i) => ( recItems.map((it, i) => (
<div <div
key={i} key={i}
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
padding: '6px 4px', padding: '6px 4px',
borderBottom: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0',
}} }}
@@ -356,8 +379,11 @@ export default function HomeMarqueeSeeds() {
<span> <span>
{it.masked_user} {' '} {it.masked_user} {' '}
<b style={{ color: '#fa541c' }}>{yuan(it.saved_amount_cents)} </b> <b style={{ color: '#fa541c' }}>{yuan(it.saved_amount_cents)} </b>
<span style={{ color: '#ccc', fontSize: 12, marginLeft: 8 }}>
{it.user_id > 0 ? `u${it.user_id}` : '种子'}
</span>
</span> </span>
<span style={{ color: '#999' }}>{it.time}</span> <span style={{ color: '#999', fontSize: 12 }}>{it.created_at}</span>
</div> </div>
)) ))
)} )}