From 4eb2554c2032170f22384ca860488f50b4131116 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 19 May 2026 13:15:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=89=B9=E9=87=8F=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/resource-batch-drawer.vue | 317 ++++++++++++++++-- 1 file changed, 287 insertions(+), 30 deletions(-) diff --git a/apps/src/views/props/components/resource-batch-drawer.vue b/apps/src/views/props/components/resource-batch-drawer.vue index f1ed6c7..c0fe07e 100644 --- a/apps/src/views/props/components/resource-batch-drawer.vue +++ b/apps/src/views/props/components/resource-batch-drawer.vue @@ -26,11 +26,13 @@ import { addPropsSource } from '#/api/legacy/props'; import { BADGE_DISPLAY_TYPE_OPTIONS, + getPropsTypeName, PROPS_RESOURCE_CONFIG_TYPES, } from '../shared'; type AssetKind = 'cover' | 'expand' | 'source'; type RowStatus = 'error' | 'ready' | 'saved' | 'saving' | 'uploading'; +type StructuredAssetKind = 'cover' | 'source'; interface BatchRow { adminFree: boolean; @@ -55,6 +57,25 @@ interface BatchRow { sourceUploading: boolean; sourceUrl: string; status: RowStatus; + type: string; + typeTouched: boolean; +} + +interface FindRowOptions { + adminFree?: boolean; + amount?: number | string; + amountTouched?: boolean; + code?: string; + type?: string; + typeTouched?: boolean; +} + +interface StructuredResourceFile { + amount: number; + assetKind: StructuredAssetKind; + code: string; + name: string; + type: string; } const props = defineProps<{ @@ -97,23 +118,112 @@ const badgeDisplayTypeOptions = BADGE_DISPLAY_TYPE_OPTIONS.map((item) => ({ value: item.value as any, })); -const isImageSourceType = computed(() => - ['CHAT_BUBBLE', 'LAYOUT', 'THEME'].includes(batchForm.type), +const STRUCTURED_RESOURCE_TYPE_MAP: Record = { + 勋章: 'BADGE', + 坐骑: 'RIDE', + 头像框: 'AVATAR_FRAME', +}; +const STRUCTURED_ASSET_KIND_MAP: Record = { + animation: 'source', + cover: 'cover', +}; +const RESOURCE_NAME_TRANSLATION_ENTRIES = ([ + ['头像框', 'avatar frame'], + ['座驾', 'ride'], + ['坐骑', 'mount'], + ['勋章', 'badge'], + ['徽章', 'badge'], + ['管理员', 'admin'], + ['圣诞', 'christmas'], + ['新年', 'new year'], + ['情人节', 'valentine'], + ['万圣节', 'halloween'], + ['锦鲤', 'koi'], + ['熊猫', 'panda'], + ['独角兽', 'unicorn'], + ['天使', 'angel'], + ['恶魔', 'devil'], + ['皇冠', 'crown'], + ['国王', 'king'], + ['女王', 'queen'], + ['玫瑰', 'rose'], + ['星光', 'starlight'], + ['星星', 'star'], + ['月亮', 'moon'], + ['太阳', 'sun'], + ['彩虹', 'rainbow'], + ['火焰', 'flame'], + ['冰雪', 'snow'], + ['蓝色', 'blue'], + ['红色', 'red'], + ['紫色', 'purple'], + ['粉色', 'pink'], + ['金色', 'gold'], + ['银色', 'silver'], + ['黑色', 'black'], + ['白色', 'white'], + ['绿色', 'green'], + ['黄色', 'yellow'], + ['钻石', 'diamond'], + ['水晶', 'crystal'], + ['幸运', 'lucky'], + ['浪漫', 'romantic'], + ['荣耀', 'glory'], + ['守护', 'guard'], + ['贵族', 'noble'], + ['梦幻', 'dream'], + ['闪耀', 'shining'], + ['限定', 'limited'], + ['豪华', 'deluxe'], + ['高级', 'premium'], + ['经典', 'classic'], + ['热气球', 'hot air balloon'], + ['跑车', 'sports car'], + ['飞船', 'spaceship'], + ['游艇', 'yacht'], + ['摩托', 'motorcycle'], + ['龙', 'dragon'], + ['凤', 'phoenix'], + ['虎', 'tiger'], + ['兔', 'rabbit'], + ['猫', 'cat'], + ['狗', 'dog'], + ['鱼', 'fish'], +] as Array<[string, string]>).toSorted( + (current, next) => next[0].length - current[0].length, ); -const isAmountRequired = computed( - () => !['CUSTOMIZE', 'FRAGMENTS'].includes(batchForm.type), + +function isImageSourceTypeByType(type: string) { + return ['CHAT_BUBBLE', 'LAYOUT', 'THEME'].includes(type); +} + +function isAmountRequiredByType(type: string) { + return !['CUSTOMIZE', 'FRAGMENTS'].includes(type); +} + +function isSourceRequiredByType(type: string) { + return !['BADGE', 'CUSTOMIZE', 'FRAGMENTS'].includes(type); +} + +function showExpandUploadByType(type: string) { + return type === 'CHAT_BUBBLE'; +} + +const isImageSourceType = computed(() => isImageSourceTypeByType(batchForm.type)); +const isAmountRequired = computed(() => isAmountRequiredByType(batchForm.type)); +const showExpandUpload = computed(() => showExpandUploadByType(batchForm.type)); +const hasExpandColumn = computed( + () => + showExpandUpload.value + || rows.value.some((row) => showExpandUploadByType(getRowType(row))), ); -const isSourceRequired = computed( - () => !['BADGE', 'CUSTOMIZE', 'FRAGMENTS'].includes(batchForm.type), -); -const showExpandUpload = computed(() => batchForm.type === 'CHAT_BUBBLE'); const columns = computed(() => { const baseColumns = [ { dataIndex: 'cover', key: 'cover', title: '封面', width: 116 }, { dataIndex: 'sourceUrl', key: 'sourceUrl', title: '资源', width: 180 }, ]; - if (showExpandUpload.value) { + if (hasExpandColumn.value) { baseColumns.push({ dataIndex: 'expand', key: 'expand', @@ -123,6 +233,7 @@ const columns = computed(() => { } return [ ...baseColumns, + { dataIndex: 'type', key: 'type', title: '类型', width: 132 }, { dataIndex: 'name', key: 'name', title: '名称', width: 180 }, { dataIndex: 'code', key: 'code', title: '编码', width: 180 }, { dataIndex: 'amount', key: 'amount', title: '底价', width: 116 }, @@ -228,6 +339,11 @@ watch( watch( () => [batchForm.type, batchForm.sysOrigin, batchForm.badgeDisplayType], () => { + rows.value.forEach((row) => { + if (!row.typeTouched) { + row.type = batchForm.type; + } + }); refreshAllRows(); }, ); @@ -247,7 +363,33 @@ function normalizeBaseName(value: string) { } function normalizeCode(value: string) { - return normalizeBaseName(value).replaceAll(/\s+/g, '_'); + const baseName = normalizeBaseName(value); + const translated = translateResourceName(baseName); + return slugifyResourceCode(translated) || slugifyResourceCode(baseName) || 'resource'; +} + +function translateResourceName(value: string) { + let result = value.trim(); + RESOURCE_NAME_TRANSLATION_ENTRIES.forEach(([source, target]) => { + result = result.replaceAll(source, ` ${target} `); + }); + return [...result] + .map((char) => { + if (/[\u4E00-\u9FFF]/.test(char)) { + return ` u${char.codePointAt(0)?.toString(16) || ''} `; + } + return char; + }) + .join(''); +} + +function slugifyResourceCode(value: string) { + return value + .normalize('NFKC') + .replaceAll(/[^\d A-Za-z]+/g, ' ') + .trim() + .replaceAll(/\s+/g, '_') + .toLowerCase(); } function getPathFileName(path: string) { @@ -311,17 +453,93 @@ function getFileBaseName(file: File) { return normalizeBaseName(getPathFileName(getFilePath(file))); } -function findOrCreateRow(baseName: string) { - const code = normalizeCode(baseName); - const matched = rows.value.find((row) => row.code === code || row.name === baseName); +function parseStructuredResourceFile(file: File): null | StructuredResourceFile { + if (!isImageFile(file) && !isResourceFile(file)) { + return null; + } + const fileName = getPathFileName(getFilePath(file)); + const baseName = fileName.replace(/\.[^.]+$/, '').trim(); + const parts = baseName + .split('_') + .map((part) => part.trim()) + .filter(Boolean); + const type = STRUCTURED_RESOURCE_TYPE_MAP[parts[0] || '']; + const assetKind = + STRUCTURED_ASSET_KIND_MAP[String(parts[parts.length - 1] || '').toLowerCase()]; + if (!type || !assetKind) { + return null; + } + if (assetKind === 'cover' && !isImageFile(file)) { + return null; + } + if (type === 'BADGE') { + const name = parts.slice(1, -1).join('_').trim(); + if (!name) { + return null; + } + return { + amount: 0, + assetKind, + code: normalizeCode(name), + name, + type, + }; + } + if (parts.length < 5) { + return null; + } + const amount = Number(parts[parts.length - 3]); + const name = parts.slice(1, -3).join('_').trim(); + if (!name || !Number.isFinite(amount) || amount < 0) { + return null; + } + return { + amount: normalizeAmount(amount), + assetKind, + code: normalizeCode(name), + name, + type, + }; +} + +function getRowType(row: BatchRow) { + return String(row.type || batchForm.type || ''); +} + +function findOrCreateRow(baseName: string, options: FindRowOptions = {}) { + const name = baseName.trim() || 'resource'; + const type = String(options.type || batchForm.type || ''); + const code = options.code || normalizeCode(name); + const matched = rows.value.find( + (row) => + getRowType(row) === type + && (String(row.code || '') === code || String(row.name || '') === name), + ); if (matched) { + if (options.adminFree !== undefined) { + matched.adminFree = options.adminFree; + } + if (options.amount !== undefined) { + matched.amount = normalizeAmount(options.amount); + matched.amountTouched = options.amountTouched ?? true; + } + if (options.code) { + matched.code = options.code; + } + if (options.type) { + matched.type = options.type; + matched.typeTouched = options.typeTouched ?? true; + } matched.saved = false; return matched; } const row: BatchRow = { - adminFree: batchForm.adminFree, - amount: normalizeAmount(batchForm.amount), - amountTouched: false, + adminFree: options.adminFree ?? batchForm.adminFree, + amount: + options.amount === undefined + ? normalizeAmount(batchForm.amount) + : normalizeAmount(options.amount), + amountTouched: options.amountTouched ?? false, code, cover: '', coverError: '', @@ -333,7 +551,7 @@ function findOrCreateRow(baseName: string) { expandFileName: '', expandUploading: false, key: `batch-${Date.now()}-${sequence.value++}`, - name: baseName, + name, saved: false, saving: false, sourceError: '', @@ -341,6 +559,8 @@ function findOrCreateRow(baseName: string) { sourceUploading: false, sourceUrl: '', status: 'error', + type, + typeTouched: options.typeTouched ?? false, }; rows.value.push(row); if (!activeRowKey.value) { @@ -419,6 +639,20 @@ async function uploadAsset( function processFiles(files: File[]) { let accepted = 0; files.forEach((file) => { + const structuredFile = parseStructuredResourceFile(file); + if (structuredFile) { + const row = findOrCreateRow(structuredFile.name, { + adminFree: false, + amount: structuredFile.amount, + amountTouched: true, + code: structuredFile.code, + type: structuredFile.type, + typeTouched: true, + }); + setRowAsset(row, structuredFile.assetKind, file); + accepted += 1; + return; + } const kind = classifyFile(file); if (!kind) { return; @@ -515,7 +749,8 @@ function getRowErrors( if (!batchForm.sysOrigin) { errors.push('请选择系统'); } - if (!batchForm.type) { + const rowType = getRowType(row); + if (!rowType) { errors.push('请选择类型'); } if (!String(row.name || '').trim()) { @@ -542,14 +777,14 @@ function getRowErrors( if (!row.cover && !row.coverUploading) { errors.push('缺少封面'); } - if (isSourceRequired.value && !row.sourceUrl && !row.sourceUploading) { + if (isSourceRequiredByType(rowType) && !row.sourceUrl && !row.sourceUploading) { errors.push('缺少资源'); } - if (showExpandUpload.value && !row.expand && !row.expandUploading) { + if (showExpandUploadByType(rowType) && !row.expand && !row.expandUploading) { errors.push('缺少 Android 资源'); } if ( - isAmountRequired.value + isAmountRequiredByType(rowType) && (!Number.isFinite(Number(row.amount)) || Number(row.amount) < 0) ) { errors.push('底价不正确'); @@ -594,6 +829,13 @@ function markAmountTouched(record: Record) { refreshAllRows(); } +function markTypeTouched(record: Record) { + const row = record as BatchRow; + row.typeTouched = true; + row.saved = false; + refreshAllRows(); +} + function markRowEdited(record: Record) { const row = record as BatchRow; row.saved = false; @@ -722,17 +964,18 @@ function buildBadgeExpand() { } function buildPayload(row: BatchRow) { + const rowType = getRowType(row); return { adminFree: row.adminFree, - amount: isAmountRequired.value ? normalizeAmount(row.amount) : 0, + amount: isAmountRequiredByType(rowType) ? normalizeAmount(row.amount) : 0, code: String(row.code || '').trim(), cover: row.cover, - expand: batchForm.type === 'BADGE' ? buildBadgeExpand() : row.expand, + expand: rowType === 'BADGE' ? buildBadgeExpand() : row.expand, id: '', name: String(row.name || '').trim(), sourceUrl: row.sourceUrl, sysOrigin: batchForm.sysOrigin, - type: batchForm.type, + type: rowType, }; } @@ -907,7 +1150,7 @@ function customRow(record: Record) { @drop.prevent="handleDrop" >
拖拽文件到这里
-
封面和资源按文件名自动配对
+
选择文件夹后按命名规则自动配对