'use client'; import { useEffect, useState } from 'react'; import { Button, Card, Space, Spin, Switch, Tag, Typography, Upload, message } from 'antd'; import { DeleteOutlined, UploadOutlined } from '@ant-design/icons'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; import { mediaUrl } from '@/lib/media'; import type { GuideVideoConfig as GuideCfg } from '@/lib/types'; const { Text } = Typography; /** 后端 media.save_guide_video 只认 MP4 魔数;这里先在浏览器挡一道,省得白传 100MB。 */ const MAX_BYTES = 100 * 1024 * 1024; /** * 领券等候浮层的「新手引导视频」配置。 * * 用户点首页「一键自动领取」→ 出等候浮层,浮层下方那块位置**前 3 次**放这支引导视频 * (而不是广告),每次固定发 120 金币;播完若浮层还开着,自动接着放广告(原逻辑)。 * 「系统配置 → 领券引导视频」tab 的一个区块。 * * 后台只管两件事:**开关** 和 **换片**。次数(3)/ 金币(120)走服务端默认值,产品已拍板不再 * 开放配置,所以这里不渲染输入框、PATCH 也不带这两个字段(后端仍保留字段与默认值)。 */ export default function GuideVideoConfig() { const [cfg, setCfg] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); // 本地编辑态,保存时一次性 PATCH const [enabled, setEnabled] = useState(true); const canEdit = canDo(['operator']); const sync = (c: GuideCfg) => { setCfg(c); setEnabled(c.enabled); }; const load = async () => { setLoading(true); try { const { data } = await api.get('/admin/api/guide-video'); sync(data); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const save = async () => { setSaving(true); try { const { data } = await api.patch('/admin/api/guide-video', { enabled }); sync(data); message.success('已保存,用户下一次进入领券浮层即生效'); } catch (e) { message.error(errMsg(e)); } finally { setSaving(false); } }; // antd Upload:beforeUpload 里自行 POST(multipart),返回 false 阻止其默认上传。 const beforeUpload = (file: File) => { // 有些系统给 .mp4 的 type 是空串,不能只看 type,再兜一层扩展名。 const looksMp4 = file.type === 'video/mp4' || /\.mp4$/i.test(file.name); if (!looksMp4) { message.error('仅支持 MP4 视频(H.264 编码)'); return Upload.LIST_IGNORE; } if (file.size > MAX_BYTES) { message.error('视频不能超过 100MB'); return Upload.LIST_IGNORE; } void uploadVideo(file); return false; }; const uploadVideo = async (file: File) => { const form = new FormData(); form.append('file', file); setUploading(true); try { const { data } = await api.post('/admin/api/guide-video/video', form); sync(data); message.success('引导视频已更新,用户下一次进入领券浮层即生效'); } catch (e) { message.error(errMsg(e)); } finally { setUploading(false); } }; const removeVideo = async () => { setUploading(true); try { const { data } = await api.delete('/admin/api/guide-video/video'); sync(data); message.success('已移除引导视频,领券浮层恢复为只放广告'); } catch (e) { message.error(errMsg(e)); } finally { setUploading(false); } }; return ( 更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')} ) : null } >

用户点「一键自动领取」后出现的等候浮层,下方那块广告位前 3 次改放这支引导视频。 视频不可快进;播完或中途关闭都算看完,各发一次 120 金币;播完若浮层还开着,会自动接着放广告。 次数按账号计(换设备不重置),视频一开播就算用掉一次。 次数与金币走固定值、后台不开放调整;这里只管开关换片,每次改动进审计日志。

{loading || !cfg ? ( ) : ( {/* 左:视频预览 */}
当前引导视频
{cfg.video_url ? ( // eslint-disable-next-line jsx-a11y/media-has-caption
{!enabled && cfg.video_url && (
当前为「关闭」:浮层不放引导视频,直接放广告
)}
{/* 右:编辑控件 */} 启用引导视频: {!enabled && 已关闭} {cfg.video_url && ( )} MP4(H.264),≤100MB {!canEdit && 仅 operator / super_admin 可修改}
)}
); }