From c564e9df120144f3a5677ecf6099971b2a8e7f4d Mon Sep 17 00:00:00 2001 From: zuochenyong Date: Wed, 29 Jul 2026 15:18:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E5=A2=9E=E5=8A=A0=E4=B8=A4?= =?UTF-8?q?=E5=A5=97=E5=BC=95=E5=AF=BC=E8=A7=86=E9=A2=91=E5=A5=96=E5=8A=B1?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: guke Co-authored-by: exinglang Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-admin-web/pulls/96 Co-authored-by: zuochenyong Co-committed-by: zuochenyong --- src/app/(main)/config/GuideVideoConfig.tsx | 217 +++++++++------------ src/app/(main)/config/page.tsx | 2 +- src/lib/types.ts | 1 + 3 files changed, 96 insertions(+), 124 deletions(-) diff --git a/src/app/(main)/config/GuideVideoConfig.tsx b/src/app/(main)/config/GuideVideoConfig.tsx index dcf8b86..706a636 100644 --- a/src/app/(main)/config/GuideVideoConfig.tsx +++ b/src/app/(main)/config/GuideVideoConfig.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Button, Card, Space, Spin, Switch, Tag, Typography, Upload, message } from 'antd'; +import { Button, Card, InputNumber, 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'; @@ -9,55 +9,58 @@ 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; +type Scene = 'coupon' | 'comparison'; -/** - * 领券等候浮层的「新手引导视频」配置。 - * - * 用户点首页「一键自动领取」→ 出等候浮层,浮层下方那块位置**前 3 次**放这支引导视频 - * (而不是广告),每次固定发 120 金币;播完若浮层还开着,自动接着放广告(原逻辑)。 - * 「系统配置 → 领券引导视频」tab 的一个区块。 - * - * 后台只管两件事:**开关** 和 **换片**。次数(3)/ 金币(120)走服务端默认值,产品已拍板不再 - * 开放配置,所以这里不渲染输入框、PATCH 也不带这两个字段(后端仍保留字段与默认值)。 - */ -export default function GuideVideoConfig() { +const META: Record = { + coupon: { title: '一键自动领取引导视频', action: '一键自动领取' }, + comparison: { title: '开始比价引导视频', action: '开始比价' }, +}; + +function SceneConfig({ scene }: { scene: Scene }) { + const meta = META[scene]; 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 [maxPlays, setMaxPlays] = useState(3); + const [rewardCoin, setRewardCoin] = useState(100); const canEdit = canDo(['operator']); - const sync = (c: GuideCfg) => { - setCfg(c); - setEnabled(c.enabled); + const sync = (value: GuideCfg) => { + setCfg(value); + setEnabled(value.enabled); + setMaxPlays(value.max_plays); + setRewardCoin(value.reward_coin); }; const load = async () => { setLoading(true); try { - const { data } = await api.get('/admin/api/guide-video'); + const { data } = await api.get('/admin/api/guide-video', { params: { scene } }); sync(data); + } catch (e) { + message.error(errMsg(e)); } finally { setLoading(false); } }; + useEffect(() => { - load(); - }, []); + void load(); + }, [scene]); const save = async () => { setSaving(true); try { - const { data } = await api.patch('/admin/api/guide-video', { enabled }); + const { data } = await api.patch( + '/admin/api/guide-video', + { enabled, max_plays: maxPlays, reward_coin: rewardCoin }, + { params: { scene } }, + ); sync(data); - message.success('已保存,用户下一次进入领券浮层即生效'); + message.success(`${meta.title}配置已保存,下次触发即生效`); } catch (e) { message.error(errMsg(e)); } finally { @@ -65,11 +68,27 @@ export default function GuideVideoConfig() { } }; - // antd Upload:beforeUpload 里自行 POST(multipart),返回 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, + { params: { scene } }, + ); + sync(data); + message.success(`${meta.title}已更新`); + } catch (e) { + message.error(errMsg(e)); + } finally { + setUploading(false); + } + }; + const beforeUpload = (file: File) => { - // 有些系统给 .mp4 的 type 是空串,不能只看 type,再兜一层扩展名。 - const looksMp4 = file.type === 'video/mp4' || /\.mp4$/i.test(file.name); - if (!looksMp4) { + if (file.type !== 'video/mp4' && !/\.mp4$/i.test(file.name)) { message.error('仅支持 MP4 视频(H.264 编码)'); return Upload.LIST_IGNORE; } @@ -81,27 +100,15 @@ export default function GuideVideoConfig() { 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'); + const { data } = await api.delete( + '/admin/api/guide-video/video', + { params: { scene } }, + ); sync(data); - message.success('已移除引导视频,领券浮层恢复为只放广告'); + message.success(`已移除${meta.title}`); } catch (e) { message.error(errMsg(e)); } finally { @@ -112,82 +119,48 @@ export default function GuideVideoConfig() { return ( - 更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')} - - ) : null - } + extra={cfg?.updated_at ? 更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')} : null} > -

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

+ Android 用户点击「{meta.action}」后,前若干次在等候浮层广告位播放本视频并奖励金币。 + 次数按账号、按功能分别计算;开播即计次,播完或中途关闭均发放当次配置的金币。

{loading || !cfg ? ( ) : ( - {/* 左:视频预览 */} -
- - 当前引导视频 - -
- {cfg.video_url ? ( - // eslint-disable-next-line jsx-a11y/media-has-caption -
- {!enabled && cfg.video_url && ( -
- 当前为「关闭」:浮层不放引导视频,直接放广告 +
+ {cfg.video_url ? ( + // eslint-disable-next-line jsx-a11y/media-has-caption +
- - {/* 右:编辑控件 */} - + - 启用引导视频: + 启用: {!enabled && 已关闭} - + + 播放次数: + setMaxPlays(v ?? 0)} /> + 每个账号前 N 次(默认 3) + + + 每次金币: + setRewardCoin(v ?? 0)} /> + 默认 100 + {cfg.video_url && ( - )} - - MP4(H.264),≤100MB - + MP4(H.264),≤100MB - - - {!canEdit && 仅 operator / super_admin 可修改} + + 累计播放 {cfg.total_plays} 次,已发币 {cfg.granted_plays} 次 )} ); } + +export default function GuideVideoConfig() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/config/page.tsx b/src/app/(main)/config/page.tsx index c7a0344..30e416b 100644 --- a/src/app/(main)/config/page.tsx +++ b/src/app/(main)/config/page.tsx @@ -165,7 +165,7 @@ export default function ConfigPage() { }, { key: 'welfare', label: '福利页', children: welfareConfig }, { key: 'feedback-qr', label: '反馈二维码', children: }, - { key: 'guide-video', label: '领券引导视频', children: }, + { key: 'guide-video', label: '引导视频奖励', children: }, ]} />
diff --git a/src/lib/types.ts b/src/lib/types.ts index 69d329e..de37144 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -417,6 +417,7 @@ export interface FeedbackQrConfig { /** 领券等候浮层的「新手引导视频」配置(前 3 次用它替代广告,每次固定 120 金币)。 * 后台只开放 enabled + 换片;下面几个字段接口仍返回,但页面不展示、也不回传。 */ export interface GuideVideoConfig { + scene: 'coupon' | 'comparison'; enabled: boolean; video_url: string | null; // /media/guide_video/xxx.mp4;null = 未配片(浮层照旧只放广告) max_plays: number; // 每个账号前 N 次浮层放引导视频(服务端默认 3,后台不开放调整)