'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(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('/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('/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('/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('/admin/api/feedback-config/image'); sync(data); message.success('已移除二维码,App 将显示本地兜底图'); } catch (e) { message.error(errMsg(e)); } finally { setUploading(false); } }; return ( 更新于 {new Date(cfg.updated_at).toLocaleString('zh-CN')} ) : null } >

改这里的二维码、文案、显隐,App 意见反馈页底部那张「加群二维码」卡会同步(用户下次进入反馈页生效)。 每次改动进审计日志。

{loading || !cfg ? ( ) : ( {/* 左:所见即所得预览(对齐 App 卡片布局) */}
App 实际效果预览
{cfg.image_url ? ( ) : (
本地兜底图
)}
{title || '(标题)'}
直通 「{groupName || '群名'}」
{subtitle || '(标语)'}
{!enabled && (
当前为「隐藏」:App 反馈页不显示这张卡
)}
{/* 右:编辑控件 */} 显示该卡片: {!enabled && 已隐藏} {cfg.image_url && ( )} JPEG/PNG/WebP,≤5MB
第一行(操作提示) setTitle(e.target.value)} placeholder="如:长按图片保存二维码" />
第二行加粗群名(App 显示为:直通「群名」) setGroupName(e.target.value)} placeholder="如:傻瓜比价官方群" />
第三行(标语) setSubtitle(e.target.value)} placeholder="如:一起唠嗑共创、解锁新玩法" />
{!canEdit && 仅 operator / super_admin 可修改}
)}
); }