feat(marquee): 首页轮播种子预览改内联常驻、随数据源模式实时刷新 (#40)
系统配置「首页轮播种子」页:预览从点按钮弹窗改为常驻在「数据源」开关下方, 切换「混播/只真实/只种子」即时重拉、所见即所得。 - 预览请求带上当前选中的 mode(配套 app-server 预览接口的 mode 入参),不依赖 PATCH 落库时序;拨动开关(乐观更新)即刻预览对应模式,切换失败回滚预览也随之回滚。 - 移除原「预览效果」弹窗及相关状态;「只真实」无数据时给空态提示。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #40 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #40.
This commit is contained in:
@@ -42,13 +42,15 @@ interface BulkVals {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface PreviewItem {
|
||||
interface RealRecord {
|
||||
masked_user: string;
|
||||
saved_amount_cents: number;
|
||||
time: string;
|
||||
created_at: string;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
const yuan = (cents: number) => (cents / 100).toFixed(2);
|
||||
const MODE_LABEL: Record<string, string> = { mixed: '混播', real: '只真实', seed: '只种子' };
|
||||
|
||||
/** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「系统配置 / 首页」里的一个区块。 */
|
||||
export default function HomeMarqueeSeeds() {
|
||||
@@ -66,9 +68,12 @@ export default function HomeMarqueeSeeds() {
|
||||
const [bulkOpen, setBulkOpen] = useState(false);
|
||||
const [bulkForm] = Form.useForm<BulkVals>();
|
||||
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewItems, setPreviewItems] = useState<PreviewItem[]>([]);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
// 「可展示的真实记录」分页浏览(替代原随机预览):翻遍所有 success+省>0 的真实记录
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
@@ -94,6 +99,7 @@ export default function HomeMarqueeSeeds() {
|
||||
const changeMode = async (m: string) => {
|
||||
const prev = mode;
|
||||
setMode(m);
|
||||
setRecPage(0); // 切模式回第 1 页(不同模式条数不同,避免停在越界页)
|
||||
setModeSaving(true);
|
||||
try {
|
||||
await api.patch('/admin/api/marquee-seeds/mode', { mode: m });
|
||||
@@ -168,22 +174,29 @@ export default function HomeMarqueeSeeds() {
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 预览实际 feed =====
|
||||
const fetchPreview = async () => {
|
||||
setPreviewLoading(true);
|
||||
// ===== 可展示的记录:按「当前模式」分页浏览全部(不去重)=====
|
||||
// 只真实=真实记录;只种子=各启用种子按生成逻辑各出一行;混播=真实+种子。与 app 同口径洗牌+去连簇,
|
||||
// 后端固定种子 → 翻页稳定、能翻遍全部。
|
||||
const fetchRecords = async (page: number, m: string) => {
|
||||
setRecLoading(true);
|
||||
try {
|
||||
const { data } = await api.get<{ items: PreviewItem[] }>('/admin/api/marquee-seeds/preview?limit=8');
|
||||
setPreviewItems(data.items);
|
||||
const { data } = await api.get<{ items: RealRecord[]; total: number }>(
|
||||
'/admin/api/marquee-seeds/real-records',
|
||||
{ params: { offset: page * REC_PAGE_SIZE, limit: REC_PAGE_SIZE, mode: m } },
|
||||
);
|
||||
setRecItems(data.items);
|
||||
setRecTotal(data.total);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
setRecLoading(false);
|
||||
}
|
||||
};
|
||||
const openPreview = () => {
|
||||
setPreviewOpen(true);
|
||||
fetchPreview();
|
||||
};
|
||||
// 页码或模式变化都重拉(切模式时 changeMode 会把页码重置到 0)。
|
||||
useEffect(() => {
|
||||
fetchRecords(recPage, mode);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [recPage, mode]);
|
||||
|
||||
const toggle = async (s: Seed, enabled: boolean) => {
|
||||
try {
|
||||
@@ -300,6 +313,84 @@ export default function HomeMarqueeSeeds() {
|
||||
混播=真实优先+种子补位;只真实=不掺假数据(真实不足则少显示);只种子=只用下方种子/合成(演示用)
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
{/* 可展示的真实记录:分页浏览全部(success+省>0,不去重、稳定倒序);app 轮播即从这些记录
|
||||
随机打乱、同一用户去连簇后展示。翻页可看全所有真实用户的展示信息。 */}
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
可展示的记录 · {MODE_LABEL[mode] ?? mode} · 共 {recTotal} 条
|
||||
<span style={{ color: '#999', fontWeight: 400, fontSize: 12, marginLeft: 8 }}>
|
||||
当前模式下 app 轮播能露出的全部记录(不去重):只真实=真实、只种子=各种子生成、混播=真实+种子;
|
||||
已按 app 口径洗牌+去连簇,顺序固定便于翻页看全
|
||||
</span>
|
||||
</span>
|
||||
<Space size="small">
|
||||
<Button size="small" disabled={recPage <= 0} onClick={() => setRecPage((p) => Math.max(0, p - 1))}>
|
||||
上一页
|
||||
</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>
|
||||
<Spin spinning={recLoading}>
|
||||
<div style={{ minHeight: 120 }}>
|
||||
{recItems.length === 0 && !recLoading ? (
|
||||
<div style={{ color: '#bbb', textAlign: 'center', padding: '36px 0', fontSize: 13 }}>
|
||||
当前模式暂无可展示的记录
|
||||
</div>
|
||||
) : (
|
||||
recItems.map((it, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '6px 4px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{it.masked_user} 比价后节省{' '}
|
||||
<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 style={{ color: '#999', fontSize: 12 }}>{it.created_at}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Button type="primary" size="small" onClick={openAdd}>
|
||||
新增种子
|
||||
@@ -307,9 +398,6 @@ export default function HomeMarqueeSeeds() {
|
||||
<Button size="small" onClick={openBulk}>
|
||||
批量生成
|
||||
</Button>
|
||||
<Button size="small" onClick={openPreview}>
|
||||
预览效果
|
||||
</Button>
|
||||
<span style={{ borderLeft: '1px solid #eee', height: 18 }} />
|
||||
<Button size="small" disabled={!selectedRowKeys.length} onClick={() => batchSetEnabled(true)}>
|
||||
批量启用
|
||||
@@ -413,48 +501,6 @@ export default function HomeMarqueeSeeds() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 预览实际 feed */}
|
||||
<Modal
|
||||
title="预览轮播效果"
|
||||
open={previewOpen}
|
||||
onCancel={() => setPreviewOpen(false)}
|
||||
footer={
|
||||
<Space>
|
||||
<Button size="small" onClick={fetchPreview}>
|
||||
换一批
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={() => setPreviewOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<p style={{ color: '#999', marginTop: 0 }}>
|
||||
客户端实际会看到的混播结果:真实记录会插队,种子随机抽取、金额/用户名每次都不同。点「换一批」可重新随机。
|
||||
</p>
|
||||
<Spin spinning={previewLoading}>
|
||||
<div style={{ minHeight: 120 }}>
|
||||
{previewItems.map((it, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '7px 4px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{it.masked_user} 比价后节省{' '}
|
||||
<b style={{ color: '#fa541c' }}>{yuan(it.saved_amount_cents)} 元</b>
|
||||
</span>
|
||||
<span style={{ color: '#999' }}>{it.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user