36d00a9d68
Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #13 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
274 lines
8.9 KiB
TypeScript
274 lines
8.9 KiB
TypeScript
'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>
|
||
);
|
||
}
|