feat(douyu): 兑换成功后生成结果图片并自动复制到剪贴板

- 账号表格新增兑换图片列,展示每账号最近兑换成功的结果卡片
- 新完成兑换任务自动生成图片并复制到剪贴板(批量时最后完成者覆盖)
- canvas 绘制兑换卡片:商品图/商品名/游戏角色/区服/兑换账号/时间
- 后端兑换成功结果补充 game_name/game_channel/account_name 字段
- 不支持剪贴板的环境降级为点击查看大图
This commit is contained in:
yml2213
2026-08-01 13:23:22 +08:00
parent 3e95cf208d
commit 592eef3582
4 changed files with 406 additions and 2 deletions
@@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import { Button, Spin } from 'antd';
import type { DouyuTaskItem } from '../api/types';
import { getExchangeImage } from '../utils/exchangeImage';
interface Props {
task: DouyuTaskItem;
onClick?: (task: DouyuTaskItem, url: string) => void;
}
export default function ExchangeResultImage({ task, onClick }: Props) {
const [url, setUrl] = useState('');
const [failed, setFailed] = useState(false);
const [gen, setGen] = useState(0);
useEffect(() => {
let alive = true;
setFailed(false);
getExchangeImage(task)
.then(({ url }) => { if (alive) setUrl(url); })
.catch(() => { if (alive) setFailed(true); });
return () => { alive = false; };
}, [task, gen]);
if (failed) {
return (
<Button
size="small"
type="text"
onClick={() => setGen((g) => g + 1)}
>
</Button>
);
}
if (!url) return <Spin size="small" />;
return (
<div
onClick={() => onClick?.(task, url)}
style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0 }}
title="点击查看大图并复制"
>
<img
src={url}
width={56}
height={82}
alt="兑换结果"
style={{ objectFit: 'cover', borderRadius: 6, border: '1px solid #f0f0f0' }}
/>
</div>
);
}