修改并发上传
This commit is contained in:
parent
d2a994cfa9
commit
ec97291d0e
@ -54,6 +54,13 @@ interface ParsedGiftFile {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UploadTask {
|
||||
file: File;
|
||||
fileName: string;
|
||||
kind: AssetKind;
|
||||
row: BatchGiftRow;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
initialSysOrigin?: number | string;
|
||||
open: boolean;
|
||||
@ -82,6 +89,9 @@ const sequence = ref(0);
|
||||
const showOnlyErrors = ref(false);
|
||||
const submitting = ref(false);
|
||||
const droppedFilePathMap = new WeakMap<File, string>();
|
||||
const uploadQueue: UploadTask[] = [];
|
||||
const MAX_UPLOAD_CONCURRENCY = 2;
|
||||
let activeUploadCount = 0;
|
||||
|
||||
const giftTabOptions = GIFT_CONFIG_TAB_OPTIONS.map((item) => ({
|
||||
label: item.name,
|
||||
@ -171,6 +181,7 @@ function resetBatch() {
|
||||
activeRowKey.value = '';
|
||||
showOnlyErrors.value = false;
|
||||
sequence.value = 0;
|
||||
uploadQueue.length = 0;
|
||||
}
|
||||
|
||||
watch(
|
||||
@ -342,7 +353,26 @@ function setRowAsset(row: BatchGiftRow, kind: AssetKind, file: File) {
|
||||
}
|
||||
row.saved = false;
|
||||
refreshAllRows();
|
||||
void uploadAsset(row, kind, file, fileName);
|
||||
enqueueUpload({ file, fileName, kind, row });
|
||||
}
|
||||
|
||||
function enqueueUpload(task: UploadTask) {
|
||||
uploadQueue.push(task);
|
||||
runUploadQueue();
|
||||
}
|
||||
|
||||
function runUploadQueue() {
|
||||
while (activeUploadCount < MAX_UPLOAD_CONCURRENCY && uploadQueue.length > 0) {
|
||||
const task = uploadQueue.shift();
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
activeUploadCount += 1;
|
||||
void uploadAsset(task.row, task.kind, task.file, task.fileName).finally(() => {
|
||||
activeUploadCount -= 1;
|
||||
runUploadQueue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAsset(
|
||||
@ -1131,6 +1161,18 @@ async function submitBatch() {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.asset-cover {
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-cover :deep(.ant-image-img) {
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.asset-empty {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
|
||||
@ -79,6 +79,13 @@ interface StructuredResourceFile {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface UploadTask {
|
||||
file: File;
|
||||
fileName: string;
|
||||
kind: AssetKind;
|
||||
row: BatchRow;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
initialSysOrigin?: number | string;
|
||||
initialType?: number | string;
|
||||
@ -109,6 +116,9 @@ const sequence = ref(0);
|
||||
const showOnlyErrors = ref(false);
|
||||
const submitting = ref(false);
|
||||
const droppedFilePathMap = new WeakMap<File, string>();
|
||||
const uploadQueue: UploadTask[] = [];
|
||||
const MAX_UPLOAD_CONCURRENCY = 2;
|
||||
let activeUploadCount = 0;
|
||||
|
||||
const propsTypeOptions = PROPS_RESOURCE_CONFIG_TYPES.map((item) => ({
|
||||
label: item.name,
|
||||
@ -243,6 +253,7 @@ function resetBatch() {
|
||||
activeRowKey.value = '';
|
||||
showOnlyErrors.value = false;
|
||||
sequence.value = 0;
|
||||
uploadQueue.length = 0;
|
||||
}
|
||||
|
||||
watch(
|
||||
@ -456,7 +467,26 @@ function setRowAsset(row: BatchRow, kind: AssetKind, file: File) {
|
||||
}
|
||||
row.saved = false;
|
||||
refreshAllRows();
|
||||
void uploadAsset(row, kind, file, fileName);
|
||||
enqueueUpload({ file, fileName, kind, row });
|
||||
}
|
||||
|
||||
function enqueueUpload(task: UploadTask) {
|
||||
uploadQueue.push(task);
|
||||
runUploadQueue();
|
||||
}
|
||||
|
||||
function runUploadQueue() {
|
||||
while (activeUploadCount < MAX_UPLOAD_CONCURRENCY && uploadQueue.length > 0) {
|
||||
const task = uploadQueue.shift();
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
activeUploadCount += 1;
|
||||
void uploadAsset(task.row, task.kind, task.file, task.fileName).finally(() => {
|
||||
activeUploadCount -= 1;
|
||||
runUploadQueue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAsset(
|
||||
@ -1410,6 +1440,18 @@ function customRow(record: Record<string, any>) {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.asset-cover {
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-cover :deep(.ant-image-img) {
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.asset-empty {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
|
||||
@ -13,12 +13,12 @@ import {
|
||||
InputNumber,
|
||||
message,
|
||||
Pagination,
|
||||
Select,
|
||||
Spin,
|
||||
Tag,
|
||||
} from 'antdv-next';
|
||||
|
||||
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
|
||||
import { listGiftBySysOrigin } from '#/api/legacy/gift';
|
||||
import {
|
||||
listNotFamilyBySysOriginType,
|
||||
sendPropsGiveUser,
|
||||
@ -31,7 +31,7 @@ import { BADGE_TYPE_OPTIONS, PROPS_TYPES } from './shared';
|
||||
|
||||
defineOptions({ name: 'PropsSendTool' });
|
||||
|
||||
type ResourceKind = 'badge' | 'props';
|
||||
type ResourceKind = 'badge' | 'gift' | 'props';
|
||||
|
||||
type TypeOption = {
|
||||
icon: string;
|
||||
@ -50,6 +50,7 @@ type DisplayResource = {
|
||||
name: string;
|
||||
raw: Record<string, any>;
|
||||
secondaryType: string;
|
||||
type: string;
|
||||
typeLabel: string;
|
||||
};
|
||||
|
||||
@ -62,9 +63,11 @@ type RecipientProfile = {
|
||||
};
|
||||
|
||||
const BADGE_TYPE_VALUE = '__BADGE__';
|
||||
const GIFT_TYPE_VALUE = '__GIFT__';
|
||||
const MAX_BATCH_ACCOUNTS = 15;
|
||||
const RESOURCE_PAGE_SIZE = 72;
|
||||
const QUICK_DAYS = [1, 7, 15, 30, -1, -7];
|
||||
const QUICK_GIFT_QUANTITIES = [1, 5, 10, 50, 100];
|
||||
const USER_AVATAR_FALLBACK =
|
||||
'https://dummyimage.com/40x40/e2e8f0/64748b&text=U';
|
||||
|
||||
@ -76,14 +79,7 @@ const sysOriginOptions = computed(() => {
|
||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||
});
|
||||
|
||||
const propsTypeOptions = PROPS_TYPES.filter(
|
||||
(item) => item.value !== 'CUSTOMIZE' && item.value !== 'FRAGMENTS',
|
||||
);
|
||||
const vipOriginOptions = [
|
||||
{ label: '系统赠送', value: 'SYSTEM_GIVE' as any },
|
||||
{ label: '购买或朋友赠送', value: 'BUY_OR_GIVE' as any },
|
||||
{ label: '活动奖励', value: 'ACTIVITY_AWARD' as any },
|
||||
];
|
||||
const propsTypeOptions = PROPS_TYPES.filter((item) => item.value !== 'CUSTOMIZE');
|
||||
|
||||
const propsFormLoading = ref(false);
|
||||
const sourceLoading = ref(false);
|
||||
@ -98,10 +94,10 @@ let recipientSearchTimer: number | undefined;
|
||||
|
||||
const form = reactive({
|
||||
exchangeDays: '',
|
||||
giftQuantity: 1,
|
||||
receiverAccounts: '',
|
||||
secondaryType: '',
|
||||
sysOrigin: sysOriginOptions.value[0]?.value ?? '',
|
||||
vipOrigin: '',
|
||||
});
|
||||
|
||||
function hasPermission(code: string) {
|
||||
@ -113,7 +109,12 @@ const canSendProps = computed(() => hasPermission('send:props'));
|
||||
const canSendBadge = computed(
|
||||
() => canSendProps.value || hasPermission('send:props:badge'),
|
||||
);
|
||||
const canUsePage = computed(() => canSendProps.value || canSendBadge.value);
|
||||
const canSendGift = computed(
|
||||
() => canSendProps.value || hasPermission('send:props:GIFT'),
|
||||
);
|
||||
const canUsePage = computed(
|
||||
() => canSendProps.value || canSendBadge.value || canSendGift.value,
|
||||
);
|
||||
|
||||
const typeOptions = computed<TypeOption[]>(() => {
|
||||
const options: TypeOption[] = [];
|
||||
@ -135,6 +136,14 @@ const typeOptions = computed<TypeOption[]>(() => {
|
||||
value: BADGE_TYPE_VALUE,
|
||||
});
|
||||
}
|
||||
if (canSendGift.value) {
|
||||
options.push({
|
||||
icon: 'lucide:gift',
|
||||
kind: 'gift',
|
||||
label: '礼物',
|
||||
value: GIFT_TYPE_VALUE,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
@ -156,14 +165,27 @@ const displayedRecipients = computed(() =>
|
||||
);
|
||||
}),
|
||||
);
|
||||
const sendActionText = computed(() => (Number(form.exchangeDays) < 0 ? '回收' : '赠送'));
|
||||
const selectedCount = computed(() => selectedResources.value.length);
|
||||
const needsVipOrigin = computed(
|
||||
() =>
|
||||
selectedResources.value.some(
|
||||
(item) => item.kind === 'props' && item.secondaryType === 'NOBLE_VIP',
|
||||
),
|
||||
const selectedHasGift = computed(() =>
|
||||
selectedResources.value.some((item) => item.kind === 'gift'),
|
||||
);
|
||||
const selectedHasNonGift = computed(() =>
|
||||
selectedResources.value.some((item) => item.kind !== 'gift'),
|
||||
);
|
||||
const currentTypeIsGift = computed(() => currentTypeOption.value?.kind === 'gift');
|
||||
const showGiftQuantity = computed(() => selectedHasGift.value || currentTypeIsGift.value);
|
||||
const showExchangeDays = computed(
|
||||
() => selectedHasNonGift.value || (!selectedHasGift.value && !currentTypeIsGift.value),
|
||||
);
|
||||
const sendActionText = computed(() => {
|
||||
if (selectedHasGift.value && selectedHasNonGift.value) {
|
||||
return Number(form.exchangeDays) < 0 ? '礼物赠送 / 道具回收' : '赠送';
|
||||
}
|
||||
if (selectedHasGift.value) {
|
||||
return '赠送';
|
||||
}
|
||||
return Number(form.exchangeDays) < 0 ? '回收' : '赠送';
|
||||
});
|
||||
const selectedTypeSummary = computed(() => {
|
||||
const labels = [
|
||||
...new Set(selectedResources.value.map((item) => item.typeLabel).filter(Boolean)),
|
||||
@ -331,17 +353,19 @@ function normalizeDisplayResource(
|
||||
secondaryType: string,
|
||||
typeLabel: string,
|
||||
): DisplayResource {
|
||||
const id =
|
||||
kind === 'badge'
|
||||
? firstPresent(item, ['badgeConfigId', 'id', 'badgeId'])
|
||||
: firstPresent(item, ['id', 'resourceId', 'propsSourceId', 'propsId']);
|
||||
const name =
|
||||
kind === 'badge'
|
||||
? firstPresent(item, ['badgeName', 'name'])
|
||||
: firstPresent(item, ['name', 'resourceName', 'sourceName']);
|
||||
let id = firstPresent(item, ['id', 'resourceId', 'propsSourceId', 'propsId']);
|
||||
let name = firstPresent(item, ['name', 'resourceName', 'sourceName']);
|
||||
if (kind === 'badge') {
|
||||
id = firstPresent(item, ['badgeConfigId', 'id', 'badgeId']);
|
||||
name = firstPresent(item, ['badgeName', 'name']);
|
||||
} else if (kind === 'gift') {
|
||||
id = firstPresent(item, ['id', 'giftId']);
|
||||
name = firstPresent(item, ['giftName', 'name']);
|
||||
}
|
||||
const cover = firstPresent(item, [
|
||||
'cover',
|
||||
'coverUrl',
|
||||
'giftPhoto',
|
||||
'resourceCover',
|
||||
'resourceCoverUrl',
|
||||
'selectUrl',
|
||||
@ -349,7 +373,7 @@ function normalizeDisplayResource(
|
||||
]);
|
||||
|
||||
return {
|
||||
amount: firstPresent(item, ['amount', 'price', 'goldAmount']),
|
||||
amount: firstPresent(item, ['amount', 'giftCandy', 'price', 'goldAmount']),
|
||||
badgeTypeLabel: kind === 'badge' ? typeLabel : '',
|
||||
cover: String(cover || ''),
|
||||
id: id as number | string,
|
||||
@ -358,6 +382,7 @@ function normalizeDisplayResource(
|
||||
name: String(name || (id ? `资源 ${id}` : '-')),
|
||||
raw: item,
|
||||
secondaryType,
|
||||
type: secondaryType,
|
||||
typeLabel,
|
||||
};
|
||||
}
|
||||
@ -387,12 +412,15 @@ function setQuickDays(value: number) {
|
||||
form.exchangeDays = String(value);
|
||||
}
|
||||
|
||||
function setQuickGiftQuantity(value: number) {
|
||||
form.giftQuantity = value;
|
||||
}
|
||||
|
||||
function selectType(option: TypeOption) {
|
||||
if (form.secondaryType === option.value) {
|
||||
return;
|
||||
}
|
||||
form.secondaryType = option.value;
|
||||
form.vipOrigin = option.value === 'NOBLE_VIP' ? form.vipOrigin : '';
|
||||
resourceKeyword.value = '';
|
||||
resourcePage.value = 1;
|
||||
sourceOptions.value = [];
|
||||
@ -429,6 +457,14 @@ async function loadSources() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type.kind === 'gift') {
|
||||
const rows = await listGiftBySysOrigin(form.sysOrigin);
|
||||
sourceOptions.value = (rows || []).map((item) =>
|
||||
normalizeDisplayResource(item, 'gift', 'GIFT', '礼物'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = await listNotFamilyBySysOriginType(form.sysOrigin, type.value);
|
||||
sourceOptions.value = (rows || []).map((item) =>
|
||||
normalizeDisplayResource(item, 'props', type.value, type.label),
|
||||
@ -483,18 +519,37 @@ function validateBeforeSend() {
|
||||
message.warning('请选择资源');
|
||||
return null;
|
||||
}
|
||||
if (form.exchangeDays === '' || Number.isNaN(Number(form.exchangeDays))) {
|
||||
if (
|
||||
selectedHasNonGift.value &&
|
||||
(form.exchangeDays === '' || Number.isNaN(Number(form.exchangeDays)))
|
||||
) {
|
||||
message.warning('请输入有效天数');
|
||||
return null;
|
||||
}
|
||||
if (needsVipOrigin.value && !form.vipOrigin) {
|
||||
message.warning('请选择贵族来源');
|
||||
if (
|
||||
selectedHasGift.value &&
|
||||
(!Number.isFinite(Number(form.giftQuantity)) || Number(form.giftQuantity) <= 0)
|
||||
) {
|
||||
message.warning('请输入礼物数量');
|
||||
return null;
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function buildSendPayload(resource: DisplayResource, accounts: string[]) {
|
||||
if (resource.kind === 'gift') {
|
||||
const quantity = Number(form.giftQuantity);
|
||||
return {
|
||||
content: resource.id,
|
||||
exchangeDays: quantity,
|
||||
quantity,
|
||||
receiverAccounts: accounts.join(','),
|
||||
secondaryType: 'GIFT',
|
||||
sysOrigin: form.sysOrigin,
|
||||
type: 'GIFT',
|
||||
};
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
content: resource.id,
|
||||
exchangeDays: Number(form.exchangeDays),
|
||||
@ -504,11 +559,6 @@ function buildSendPayload(resource: DisplayResource, accounts: string[]) {
|
||||
type: resource.kind === 'badge' ? 'BADGE' : 'PROPS',
|
||||
};
|
||||
|
||||
if (resource.kind === 'props') {
|
||||
payload.vipOrigin =
|
||||
resource.secondaryType === 'NOBLE_VIP' ? form.vipOrigin : null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
@ -542,7 +592,7 @@ function resetCurrentForm() {
|
||||
form.receiverAccounts = '';
|
||||
form.secondaryType = '';
|
||||
form.exchangeDays = '';
|
||||
form.vipOrigin = '';
|
||||
form.giftQuantity = 1;
|
||||
resourceKeyword.value = '';
|
||||
resourcePage.value = 1;
|
||||
sourceOptions.value = [];
|
||||
@ -551,7 +601,7 @@ function resetCurrentForm() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="道具赠送">
|
||||
<Page title="资源赠送">
|
||||
<div v-if="canUsePage" class="send-tool">
|
||||
<div class="send-layout">
|
||||
<section class="send-main">
|
||||
@ -687,7 +737,7 @@ function resetCurrentForm() {
|
||||
<aside class="send-summary">
|
||||
<div class="summary-title">
|
||||
<IconifyIcon icon="lucide:package-check" />
|
||||
<span>赠送道具</span>
|
||||
<span>赠送资源</span>
|
||||
</div>
|
||||
|
||||
<div class="selected-panel">
|
||||
@ -732,7 +782,7 @@ function resetCurrentForm() {
|
||||
<div v-else class="summary-empty">未选择资源</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-control summary-control--days">
|
||||
<div v-if="showExchangeDays" class="summary-control summary-control--days">
|
||||
<div class="summary-control__row">
|
||||
<label class="summary-control__label" for="props-send-days">
|
||||
有效天数
|
||||
@ -760,15 +810,33 @@ function resetCurrentForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="needsVipOrigin" class="summary-control">
|
||||
<label class="summary-control__label">贵族来源</label>
|
||||
<Select
|
||||
v-model:value="form.vipOrigin"
|
||||
:options="vipOriginOptions"
|
||||
class="summary-vip-origin"
|
||||
option-label-prop="label"
|
||||
placeholder="请选择贵族来源"
|
||||
/>
|
||||
<div v-if="showGiftQuantity" class="summary-control summary-control--days">
|
||||
<div class="summary-control__row">
|
||||
<label class="summary-control__label" for="gift-send-quantity">
|
||||
礼物数量
|
||||
</label>
|
||||
<InputNumber
|
||||
id="gift-send-quantity"
|
||||
v-model:value="form.giftQuantity"
|
||||
:controls="false"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
class="summary-days-input"
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
<div class="quick-days summary-quick-days">
|
||||
<button
|
||||
v-for="quantity in QUICK_GIFT_QUANTITIES"
|
||||
:key="quantity"
|
||||
:class="{ 'quick-day--active': Number(form.giftQuantity) === quantity }"
|
||||
class="quick-day"
|
||||
type="button"
|
||||
@click="setQuickGiftQuantity(quantity)"
|
||||
>
|
||||
{{ quantity }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="summary-list">
|
||||
@ -1256,8 +1324,7 @@ function resetCurrentForm() {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-days-input,
|
||||
.summary-vip-origin {
|
||||
.summary-days-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@ -8,12 +8,8 @@ export interface OptionItem {
|
||||
export const PROPS_TYPES: OptionItem[] = [
|
||||
{ value: 'AVATAR_FRAME', name: '头像框' },
|
||||
{ value: 'RIDE', name: '座驾' },
|
||||
{ value: 'NOBLE_VIP', name: '贵族' },
|
||||
{ value: 'THEME', name: '主题' },
|
||||
{ value: 'LAYOUT', name: '装扮' },
|
||||
{ value: 'CHAT_BUBBLE', name: '聊天气泡' },
|
||||
{ value: 'FLOAT_PICTURE', name: '飘窗' },
|
||||
{ value: 'FRAGMENTS', name: '碎片' },
|
||||
{ value: 'DATA_CARD', name: '资料卡' },
|
||||
{ value: 'SPECIAL_ID', name: '靓号' },
|
||||
{ value: 'CUSTOMIZE', name: '自定义(只读)' },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user