9722473974
配置页增加「新手引导视频」分区(GuideVideoConfig.tsx):上传 MP4、设置 生效次数、开关启停。types.ts 补齐对应类型。nginx 配置放宽该上传接口的 client_max_body_size,否则大视频在网关就被 413 挡下、到不了后端。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
223 lines
7.7 KiB
TypeScript
223 lines
7.7 KiB
TypeScript
'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<GuideCfg | null>(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<GuideCfg>('/admin/api/guide-video');
|
||
sync(data);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
useEffect(() => {
|
||
load();
|
||
}, []);
|
||
|
||
const save = async () => {
|
||
setSaving(true);
|
||
try {
|
||
const { data } = await api.patch<GuideCfg>('/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<GuideCfg>('/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<GuideCfg>('/admin/api/guide-video/video');
|
||
sync(data);
|
||
message.success('已移除引导视频,领券浮层恢复为只放广告');
|
||
} catch (e) {
|
||
message.error(errMsg(e));
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card
|
||
size="small"
|
||
title="领券引导视频(App 领券等候浮层,前 3 次替代广告)"
|
||
style={{ marginBottom: 16 }}
|
||
extra={
|
||
cfg?.updated_at ? (
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')}
|
||
</Text>
|
||
) : null
|
||
}
|
||
>
|
||
<p style={{ color: '#999', marginTop: 0 }}>
|
||
用户点「一键自动领取」后出现的等候浮层,下方那块广告位<b>前 3 次</b>改放这支引导视频。
|
||
视频<b>不可快进</b>;播完或中途关闭都算看完,各发一次 <b>120 金币</b>;播完若浮层还开着,会自动接着放广告。
|
||
次数<b>按账号</b>计(换设备不重置),<b>视频一开播就算用掉一次</b>。
|
||
次数与金币走固定值、后台不开放调整;这里只管<b>开关</b>和<b>换片</b>,每次改动进审计日志。
|
||
</p>
|
||
{loading || !cfg ? (
|
||
<Spin style={{ display: 'block', margin: '24px 0' }} />
|
||
) : (
|
||
<Space align="start" size={32} wrap>
|
||
{/* 左:视频预览 */}
|
||
<div>
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
当前引导视频
|
||
</Text>
|
||
<div style={{ marginTop: 8, width: 240 }}>
|
||
{cfg.video_url ? (
|
||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||
<video
|
||
src={mediaUrl(cfg.video_url)}
|
||
controls
|
||
style={{
|
||
width: 240,
|
||
maxHeight: 420,
|
||
borderRadius: 12,
|
||
background: '#000',
|
||
border: '1px solid #f0f0f0',
|
||
}}
|
||
/>
|
||
) : (
|
||
<div
|
||
style={{
|
||
width: 240,
|
||
height: 320,
|
||
borderRadius: 12,
|
||
border: '1px dashed #d9d9d9',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: '#bbb',
|
||
fontSize: 13,
|
||
textAlign: 'center',
|
||
padding: 16,
|
||
}}
|
||
>
|
||
未上传视频
|
||
<br />
|
||
(领券浮层照旧只放广告)
|
||
</div>
|
||
)}
|
||
</div>
|
||
{!enabled && cfg.video_url && (
|
||
<div style={{ color: '#fa8c16', fontSize: 12, marginTop: 6 }}>
|
||
当前为「关闭」:浮层不放引导视频,直接放广告
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右:编辑控件 */}
|
||
<Space direction="vertical" size="middle" style={{ minWidth: 360 }}>
|
||
<Space>
|
||
<span>启用引导视频:</span>
|
||
<Switch checked={enabled} disabled={!canEdit} onChange={setEnabled} />
|
||
{!enabled && <Tag color="orange">已关闭</Tag>}
|
||
</Space>
|
||
|
||
<Space wrap>
|
||
<Upload accept="video/mp4,.mp4" showUploadList={false} beforeUpload={beforeUpload} disabled={!canEdit}>
|
||
<Button icon={<UploadOutlined />} loading={uploading} disabled={!canEdit}>
|
||
{cfg.video_url ? '更换视频' : '上传视频'}
|
||
</Button>
|
||
</Upload>
|
||
{cfg.video_url && (
|
||
<Button
|
||
icon={<DeleteOutlined />}
|
||
danger
|
||
loading={uploading}
|
||
disabled={!canEdit}
|
||
onClick={removeVideo}
|
||
>
|
||
移除视频
|
||
</Button>
|
||
)}
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
MP4(H.264),≤100MB
|
||
</Text>
|
||
</Space>
|
||
|
||
<Button type="primary" loading={saving} disabled={!canEdit} onClick={save}>
|
||
保存开关
|
||
</Button>
|
||
{!canEdit && <Text type="secondary">仅 operator / super_admin 可修改</Text>}
|
||
</Space>
|
||
</Space>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|