feat(huya): 协作台账支持皮肤筛选与主页截图拖拽/粘贴上传

- 外部单列表新增礼包/皮肤精确筛选,并提供去重下拉选项
- 主页截图支持拖拽、多选及 Ctrl/Cmd+V 剪贴板粘贴上传
This commit is contained in:
yml2213
2026-10-17 12:49:42 +08:00
parent 1dd5c4a279
commit 26b8d6b054
8 changed files with 173 additions and 29 deletions
@@ -3,9 +3,10 @@
*
* - 第一张就是封面(列表和详情都按第一张当封面);
* - 新建时还没落库,所以预览用的地址来自上传接口返回的 thumbnail_url / url;
* - 放进 `Form.Item name="home_images"`,值就是 object_key 数组。
* - 放进 `Form.Item name="home_images"`,值就是 object_key 数组;
* - 支持点击选择、拖拽,以及剪贴板里的截图直接 Ctrl/Cmd+V 粘贴。
*/
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Image, Popconfirm, Space, Tag, Tooltip, Typography, Upload } from 'antd';
import { CloudUploadOutlined, DeleteOutlined, SwapOutlined } from '@ant-design/icons';
import { fileApi } from '../api/files';
@@ -14,9 +15,28 @@ import { message } from '../utils/antdMessage';
import { getErrorMessage } from '../utils/error';
const { Text } = Typography;
const { Dragger } = Upload;
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
const ACCEPT_ATTR = ACCEPTED_IMAGE_TYPES.join(',');
type Preview = Pick<FileUploadResult, 'url' | 'thumbnail_url'>;
/** 从剪贴板里取图片文件;items 拿不到时退回 files。 */
function clipboardImageFiles(data: DataTransfer | null): File[] {
if (!data) return [];
const fromItems: File[] = [];
for (const item of Array.from(data.items ?? [])) {
if (item.kind !== 'file' || !ACCEPTED_IMAGE_TYPES.includes(item.type)) continue;
const file = item.getAsFile();
if (file) fromItems.push(file);
}
if (fromItems.length > 0) return fromItems;
return Array.from(data.files ?? []).filter((file) =>
ACCEPTED_IMAGE_TYPES.includes(file.type),
);
}
export default function HuyaCollabHomeImageUploader({
value = [],
max = 20,
@@ -39,24 +59,69 @@ export default function HuyaCollabHomeImageUploader({
const keys = value ?? [];
const full = keys.length >= max;
// 上传串行执行:多选/拖拽/一次粘贴多张时,封面顺序和用户选择顺序一致
const uploadQueue = useRef<Promise<void>>(Promise.resolve());
const uploadOne = async (file: File) => {
setBusy(true);
try {
const uploaded = await fileApi.upload(file, 'collab');
setPreviews((prev) => ({
...prev,
[uploaded.object_key]: { url: uploaded.url, thumbnail_url: uploaded.thumbnail_url },
}));
const next = [...keysRef.current, uploaded.object_key];
keysRef.current = next;
onChange?.(next);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setBusy(false);
}
};
const enqueueUploads = useCallback(
(files: File[]) => {
const images = files.filter((file) => ACCEPTED_IMAGE_TYPES.includes(file.type));
if (images.length === 0) {
if (files.length > 0) message.warning('只支持 PNG / JPG / WebP 图片');
return;
}
if (images.length < files.length) {
message.warning('已忽略非 PNG / JPG / WebP 文件');
}
uploadQueue.current = uploadQueue.current.catch(() => {}).then(async () => {
const remaining = Math.max(0, max - keysRef.current.length);
if (remaining === 0) {
message.warning(`最多上传 ${max} 张主页截图`);
return;
}
const accepted = images.slice(0, remaining);
if (accepted.length < images.length) {
message.warning(`最多上传 ${max} 张,已忽略多余图片`);
}
setBusy(true);
try {
for (const file of accepted) {
try {
const uploaded = await fileApi.upload(file, 'collab');
setPreviews((prev) => ({
...prev,
[uploaded.object_key]: {
url: uploaded.url,
thumbnail_url: uploaded.thumbnail_url,
},
}));
const next = [...keysRef.current, uploaded.object_key];
keysRef.current = next;
onChange?.(next);
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
}
} finally {
setBusy(false);
}
});
},
[max, onChange],
);
useEffect(() => {
if (disabled) return undefined;
const handlePaste = (event: ClipboardEvent) => {
const files = clipboardImageFiles(event.clipboardData);
if (files.length === 0) return;
event.preventDefault();
enqueueUploads(files);
};
window.addEventListener('paste', handlePaste);
return () => window.removeEventListener('paste', handlePaste);
}, [disabled, enqueueUploads]);
const remove = (key: string) => {
const next = keysRef.current.filter((item) => item !== key);
@@ -123,20 +188,34 @@ export default function HuyaCollabHomeImageUploader({
</Image.PreviewGroup>
)}
{!disabled && (
<Upload
accept="image/*"
<Dragger
className="huya-collab-home-drop"
accept={ACCEPT_ATTR}
multiple
showUploadList={false}
disabled={busy || full}
beforeUpload={(file) => {
void uploadOne(file as File);
beforeUpload={(file, fileList) => {
// beforeUpload 会按文件逐个调用;只在第一批触发一次,保持选择顺序
if (file === fileList[0]) enqueueUploads(fileList as File[]);
return false;
}}
>
<Button icon={<CloudUploadOutlined />} loading={busy} disabled={full}>
{full ? `最多 ${max} 张` : '上传主页截图'}
</Button>
</Upload>
<Space direction="vertical" size={2}>
<CloudUploadOutlined style={{ fontSize: 22 }} />
<Text strong>
{full
? `最多 ${max} 张`
: busy
? '上传中…'
: '点击、拖拽或 Ctrl/Cmd+V 粘贴主页截图'}
</Text>
{!full && (
<Text type="secondary" style={{ fontSize: 12 }}>
支持多张图片,第一张作封面
</Text>
)}
</Space>
</Dragger>
)}
</Space>
);