feat(huya): 协作台账支持皮肤筛选与主页截图拖拽/粘贴上传
- 外部单列表新增礼包/皮肤精确筛选,并提供去重下拉选项 - 主页截图支持拖拽、多选及 Ctrl/Cmd+V 剪贴板粘贴上传
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user