Files
shaguabijia-admin-web/src/app/(main)/config/GuideVideoConfig.tsx
T
左辰勇 9722473974 后台前端:新增新手引导视频配置
配置页增加「新手引导视频」分区(GuideVideoConfig.tsx):上传 MP4、设置
生效次数、开关启停。types.ts 补齐对应类型。nginx 配置放宽该上传接口的
client_max_body_size,否则大视频在网关就被 413 挡下、到不了后端。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 22:51:07 +08:00

223 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 }}>
MP4H.264100MB
</Text>
</Space>
<Button type="primary" loading={saving} disabled={!canEdit} onClick={save}>
</Button>
{!canEdit && <Text type="secondary"> operator / super_admin </Text>}
</Space>
</Space>
)}
</Card>
);
}