From ec97291d0e3989ec12433dfc2a1592fbaf524228 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 19 May 2026 18:25:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B9=B6=E5=8F=91=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operate/components/gift-batch-drawer.vue | 44 ++++- .../components/resource-batch-drawer.vue | 44 ++++- apps/src/views/props/send-tool.vue | 169 ++++++++++++------ apps/src/views/props/shared.ts | 4 - 4 files changed, 204 insertions(+), 57 deletions(-) diff --git a/apps/src/views/operate/components/gift-batch-drawer.vue b/apps/src/views/operate/components/gift-batch-drawer.vue index babcc6d..6fcc623 100644 --- a/apps/src/views/operate/components/gift-batch-drawer.vue +++ b/apps/src/views/operate/components/gift-batch-drawer.vue @@ -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(); +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; diff --git a/apps/src/views/props/components/resource-batch-drawer.vue b/apps/src/views/props/components/resource-batch-drawer.vue index a07e16a..e44d9fe 100644 --- a/apps/src/views/props/components/resource-batch-drawer.vue +++ b/apps/src/views/props/components/resource-batch-drawer.vue @@ -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(); +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) { 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; diff --git a/apps/src/views/props/send-tool.vue b/apps/src/views/props/send-tool.vue index 0a1da06..19399b8 100644 --- a/apps/src/views/props/send-tool.vue +++ b/apps/src/views/props/send-tool.vue @@ -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; 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(() => { const options: TypeOption[] = []; @@ -135,6 +136,14 @@ const typeOptions = computed(() => { 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 = { 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() {