feat(force-onboarding): 用户列表加「开启新手引导」一次性操作 (#13)
Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #13 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #13.
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Card, Image, Input, 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 { FeedbackQrConfig as QrConfig } from '@/lib/types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ACCEPT = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
/** 反馈页「加群二维码」卡配置。改完 App 意见反馈页下次进入即同步。「反馈工单」页的一个区块。 */
|
||||
export default function FeedbackQrConfig() {
|
||||
const [cfg, setCfg] = useState<QrConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// 本地编辑态(文案 + 开关),保存时一次性 PATCH
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [title, setTitle] = useState('');
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [subtitle, setSubtitle] = useState('');
|
||||
|
||||
const canEdit = canDo(['operator']);
|
||||
|
||||
const sync = (c: QrConfig) => {
|
||||
setCfg(c);
|
||||
setEnabled(c.enabled);
|
||||
setTitle(c.title);
|
||||
setGroupName(c.group_name);
|
||||
setSubtitle(c.subtitle);
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await api.get<QrConfig>('/admin/api/feedback-config');
|
||||
sync(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
if (!title.trim() || !groupName.trim() || !subtitle.trim()) {
|
||||
message.warning('三行文案都需填写');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const { data } = await api.patch<QrConfig>('/admin/api/feedback-config', {
|
||||
enabled,
|
||||
title: title.trim(),
|
||||
group_name: groupName.trim(),
|
||||
subtitle: subtitle.trim(),
|
||||
});
|
||||
sync(data);
|
||||
message.success('已保存,App 反馈页下次进入即可见');
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// antd Upload:beforeUpload 里自行 POST(multipart),返回 false 阻止其默认上传。
|
||||
const beforeUpload = (file: File) => {
|
||||
if (!ACCEPT.includes(file.type)) {
|
||||
message.error('仅支持 JPEG / PNG / WebP 图片');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
message.error('图片不能超过 5MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
void uploadImage(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
const uploadImage = async (file: File) => {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
setUploading(true);
|
||||
try {
|
||||
const { data } = await api.post<QrConfig>('/admin/api/feedback-config/image', form);
|
||||
sync(data);
|
||||
message.success('二维码已更新,App 反馈页下次进入即可见');
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = async () => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const { data } = await api.delete<QrConfig>('/admin/api/feedback-config/image');
|
||||
sync(data);
|
||||
message.success('已移除二维码,App 将显示本地兜底图');
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title="反馈页二维码卡(App 意见反馈页底部)"
|
||||
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 }}>
|
||||
改这里的二维码、文案、显隐,App 意见反馈页底部那张「加群二维码」卡会同步(用户下次进入反馈页生效)。
|
||||
每次改动进审计日志。
|
||||
</p>
|
||||
{loading || !cfg ? (
|
||||
<Spin style={{ display: 'block', margin: '24px 0' }} />
|
||||
) : (
|
||||
<Space align="start" size={32} wrap>
|
||||
{/* 左:所见即所得预览(对齐 App 卡片布局) */}
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
App 实际效果预览
|
||||
</Text>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
width: 320,
|
||||
display: 'flex',
|
||||
gap: 14,
|
||||
alignItems: 'center',
|
||||
padding: 14,
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 14,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
|
||||
opacity: enabled ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
{cfg.image_url ? (
|
||||
<Image
|
||||
src={mediaUrl(cfg.image_url)}
|
||||
width={88}
|
||||
height={88}
|
||||
style={{ objectFit: 'cover', borderRadius: 8, border: '1px solid #f0f0f0' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 88,
|
||||
height: 88,
|
||||
borderRadius: 8,
|
||||
border: '1px dashed #d9d9d9',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#bbb',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
本地兜底图
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 13, color: '#1a1a1a', lineHeight: '21px' }}>
|
||||
<div>{title || '(标题)'}</div>
|
||||
<div>
|
||||
直通
|
||||
<b>「{groupName || '群名'}」</b>
|
||||
</div>
|
||||
<div>{subtitle || '(标语)'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{!enabled && (
|
||||
<div style={{ color: '#fa8c16', fontSize: 12, marginTop: 6 }}>
|
||||
当前为「隐藏」:App 反馈页不显示这张卡
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右:编辑控件 */}
|
||||
<Space direction="vertical" size="middle" style={{ minWidth: 320 }}>
|
||||
<Space>
|
||||
<span>显示该卡片:</span>
|
||||
<Switch checked={enabled} disabled={!canEdit} onChange={setEnabled} />
|
||||
{!enabled && <Tag color="orange">已隐藏</Tag>}
|
||||
</Space>
|
||||
|
||||
<Space wrap>
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={beforeUpload} disabled={!canEdit}>
|
||||
<Button icon={<UploadOutlined />} loading={uploading} disabled={!canEdit}>
|
||||
{cfg.image_url ? '更换二维码' : '上传二维码'}
|
||||
</Button>
|
||||
</Upload>
|
||||
{cfg.image_url && (
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
loading={uploading}
|
||||
disabled={!canEdit}
|
||||
onClick={removeImage}
|
||||
>
|
||||
移除二维码
|
||||
</Button>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
JPEG/PNG/WebP,≤5MB
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
第一行(操作提示)
|
||||
</Text>
|
||||
<Input
|
||||
value={title}
|
||||
maxLength={40}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="如:长按图片保存二维码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
第二行加粗群名(App 显示为:直通「群名」)
|
||||
</Text>
|
||||
<Input
|
||||
value={groupName}
|
||||
maxLength={40}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => setGroupName(e.target.value)}
|
||||
placeholder="如:傻瓜比价官方群"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
第三行(标语)
|
||||
</Text>
|
||||
<Input
|
||||
value={subtitle}
|
||||
maxLength={60}
|
||||
disabled={!canEdit}
|
||||
onChange={(e) => setSubtitle(e.target.value)}
|
||||
placeholder="如:一起唠嗑共创、解锁新玩法"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="primary" loading={saving} disabled={!canEdit} onClick={save}>
|
||||
保存文案 / 开关
|
||||
</Button>
|
||||
{!canEdit && <Text type="secondary">仅 operator / super_admin 可修改</Text>}
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user