Skip to content

Commit bab8307

Browse files
committed
feat(import): 导入对话框显示封面预览与检测到的难度列表
- 前端扫描目录里所有 .ugc/.c2s/.sus 文件 - 表单显示封面预览、检测到的全部难度(含定数小数) - api: importMusicCheck/Execute 改为接收 List<File> charts
1 parent 0a93431 commit bab8307

5 files changed

Lines changed: 44 additions & 62 deletions

File tree

ChuChartManager/Front/src/api/index.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,55 +121,45 @@ export function getExportChartUrl(id: number, assetDir: string, diffIndex: numbe
121121

122122
export interface ImportCheckResult {
123123
success: boolean
124-
format: string
125124
alerts: string[]
126125
suggestedId: number
127-
difficulty: number
128-
level: number
129-
levelDecimal: number
130-
designer: string
131126
title: string
132127
artist: string
128+
difficulties: { fileName: string; difficulty: number; level: number; levelDecimal: number; designer: string }[]
133129
}
134130

135131
export interface ImportExecuteResult {
136132
success: boolean
137133
alerts: string[]
138134
}
139135

140-
export async function importMusicCheck(chart: File): Promise<ImportCheckResult> {
136+
export async function importMusicCheck(charts: File[]): Promise<ImportCheckResult> {
141137
const form = new FormData()
142-
form.append('chart', chart)
138+
for (const chart of charts) form.append('charts', chart)
143139
const { data } = await apiClient.post('/api/Music/ImportMusicCheck', form)
144140
return data
145141
}
146142

147143
export async function importMusicExecute(params: {
148-
chart: File
144+
charts: File[]
149145
audio: File
150146
cover?: File
151147
id: number
152148
title: string
153149
artist: string
154150
genreId: number
155151
genreName: string
156-
difficulty: number
157-
level: number
158-
levelDecimal: number
159152
targetDir: string
160153
}): Promise<ImportExecuteResult> {
161154
const form = new FormData()
162-
form.append('chart', params.chart)
155+
for (const chart of params.charts) form.append('charts', chart)
163156
form.append('audio', params.audio)
164157
if (params.cover) form.append('cover', params.cover)
165158
form.append('id', params.id.toString())
166159
form.append('title', params.title)
167160
form.append('artist', params.artist)
168161
form.append('genreId', params.genreId.toString())
169162
form.append('genreName', params.genreName)
170-
form.append('difficulty', params.difficulty.toString())
171-
form.append('level', params.level.toString())
172-
form.append('levelDecimal', params.levelDecimal.toString())
173163
form.append('targetDir', params.targetDir)
174164
const { data } = await apiClient.post('/api/Music/ImportMusicExecute', form, { timeout: 120000 })
175165
return data

ChuChartManager/Front/src/locales/en.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ music:
137137
importFailed: Import failed
138138
importChecking: Checking...
139139
importExecuting: Importing...
140+
importDifficulties: Detected difficulties
140141
importFoundFiles: Files found
141142
importId: Music ID
142143
importDifficulty: Difficulty

ChuChartManager/Front/src/locales/ja.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ music:
137137
importFailed: インポート失敗
138138
importChecking: 確認中...
139139
importExecuting: インポート中...
140+
importDifficulties: 検出された難易度
140141
importFoundFiles: 見つかったファイル
141142
importId: Music ID
142143
importDifficulty: 難易度

ChuChartManager/Front/src/locales/zh.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ music:
137137
importFailed: 导入失败
138138
importChecking: 正在检查...
139139
importExecuting: 正在导入...
140+
importDifficulties: 检测到的难度
140141
importFoundFiles: 已找到文件
141142
importId: Music ID
142143
importDifficulty: 难度

ChuChartManager/Front/src/views/ImportMusicModal.vue

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script setup lang="ts">
22
import { ref, computed, onMounted } from 'vue'
33
import { useI18n } from 'vue-i18n'
4-
import { Button, TextInput, NumberInput, Select, Modal, DropMenu, addToast } from '@munet/ui'
4+
import { Button, TextInput, NumberInput, Select, Modal, addToast } from '@munet/ui'
55
import { getGenreMap, getSources, importMusicCheck, importMusicExecute } from '@/api'
6-
import { selectedSource } from '@/store/refs'
76
import type { ImportCheckResult } from '@/api'
7+
import { selectedSource } from '@/store/refs'
88
import BottomOverlay from '@/components/BottomOverlay.vue'
99
import FileTypeIcon from '@/components/FileTypeIcon.vue'
1010
@@ -14,45 +14,31 @@ const { t } = useI18n()
1414
const loading = ref(false)
1515
const step = ref<'idle' | 'picking' | 'checking' | 'form' | 'executing'>('idle')
1616
17-
const chartFile = ref<File | null>(null)
17+
const chartFiles = ref<File[]>([])
1818
const audioFile = ref<File | null>(null)
1919
const coverFile = ref<File | null>(null)
2020
const checkResult = ref<ImportCheckResult | null>(null)
2121
2222
const title = ref('')
2323
const artist = ref('')
2424
const genreId = ref(0)
25-
const difficulty = ref(3)
26-
const level = ref(10)
27-
const levelDecimal = ref(0)
28-
const constant = computed({
29-
get: () => level.value + levelDecimal.value / 100,
30-
set: (v: number) => {
31-
level.value = Math.floor(v)
32-
levelDecimal.value = Math.round((v - Math.floor(v)) * 100)
33-
},
34-
})
3525
const targetDir = ref('')
3626
const musicId = ref(8000)
3727
28+
const diffNames = ['BASIC', 'ADVANCED', 'EXPERT', 'MASTER', 'ULTIMA']
29+
3830
const genreMap = ref<Record<number, string>>({})
3931
const sources = ref<string[]>([])
4032
4133
const showForm = computed(() => step.value === 'form' || step.value === 'executing')
34+
const coverUrl = computed(() => coverFile.value ? URL.createObjectURL(coverFile.value) : '')
4235
4336
const genreOptions = computed(() =>
4437
Object.entries(genreMap.value).map(([id, name]) => ({ label: name, value: Number(id) }))
4538
)
4639
const sourceOptions = computed(() =>
4740
sources.value.filter(s => s !== 'A000').map(s => ({ label: s, value: s }))
4841
)
49-
const diffOptions = [
50-
{ label: 'BASIC', value: 0 },
51-
{ label: 'ADVANCED', value: 1 },
52-
{ label: 'EXPERT', value: 2 },
53-
{ label: 'MASTER', value: 3 },
54-
{ label: 'ULTIMA', value: 4 },
55-
]
5642
5743
onMounted(async () => {
5844
const [g, s] = await Promise.all([getGenreMap(), getSources()])
@@ -64,6 +50,14 @@ onMounted(async () => {
6450
}
6551
})
6652
53+
function findAllByExt(files: File[], exts: string[]): File[] {
54+
return files.filter(f => exts.some(e => f.name.toLowerCase().endsWith(e)))
55+
}
56+
57+
function findByExt(files: File[], exts: string[]): File | null {
58+
return files.find(f => exts.some(e => f.name.toLowerCase().endsWith(e))) ?? null
59+
}
60+
6761
function parseFolderName(name: string) {
6862
const cleaned = name.replace(/\s*\(v\d+\)\s*$/, '').trim()
6963
const sep = cleaned.indexOf(' - ')
@@ -76,15 +70,11 @@ function parseFolderName(name: string) {
7670
}
7771
}
7872
79-
function findByExt(files: File[], exts: string[]): File | null {
80-
return files.find(f => exts.some(e => f.name.toLowerCase().endsWith(e))) ?? null
81-
}
82-
8373
async function startImport() {
8474
step.value = 'picking'
8575
let dirHandle: FileSystemDirectoryHandle
8676
try {
87-
dirHandle = await (window as any).showDirectoryPicker({ id: 'import-chart', startIn: 'downloads' })
77+
dirHandle = await window.showDirectoryPicker({ id: 'import-chart', startIn: 'downloads' })
8878
} catch {
8979
step.value = 'idle'
9080
return
@@ -93,15 +83,15 @@ async function startImport() {
9383
step.value = 'checking'
9484
9585
const files: File[] = []
96-
for await (const entry of (dirHandle as any).values()) {
97-
if (entry.kind === 'file') files.push(await entry.getFile())
86+
for await (const entry of dirHandle.values()) {
87+
if (entry.kind === 'file') files.push(await (entry as FileSystemFileHandle).getFile())
9888
}
9989
100-
chartFile.value = findByExt(files, ['.ugc', '.c2s', '.sus'])
90+
chartFiles.value = findAllByExt(files, ['.ugc', '.c2s', '.sus'])
10191
audioFile.value = findByExt(files, ['.wav', '.mp3', '.ogg'])
10292
coverFile.value = findByExt(files, ['.png', '.jpg', '.jpeg'])
10393
104-
if (!chartFile.value) {
94+
if (chartFiles.value.length === 0) {
10595
addToast({ message: t('music.importNoChart'), type: 'error' })
10696
step.value = 'idle'
10797
return
@@ -113,11 +103,8 @@ async function startImport() {
113103
}
114104
115105
try {
116-
checkResult.value = await importMusicCheck(chartFile.value)
106+
checkResult.value = await importMusicCheck(chartFiles.value)
117107
musicId.value = checkResult.value.suggestedId
118-
difficulty.value = checkResult.value.difficulty
119-
level.value = checkResult.value.level
120-
levelDecimal.value = checkResult.value.levelDecimal
121108
if (checkResult.value.title) title.value = checkResult.value.title
122109
else parseFolderName(dirHandle.name)
123110
if (checkResult.value.artist) artist.value = checkResult.value.artist
@@ -129,24 +116,21 @@ async function startImport() {
129116
}
130117
131118
async function doImport() {
132-
if (!chartFile.value || !audioFile.value) return
119+
if (chartFiles.value.length === 0 || !audioFile.value) return
133120
step.value = 'executing'
134121
loading.value = true
135122
136123
try {
137124
const genreName = genreMap.value[genreId.value] || ''
138125
const result = await importMusicExecute({
139-
chart: chartFile.value,
126+
charts: chartFiles.value,
140127
audio: audioFile.value,
141128
cover: coverFile.value ?? undefined,
142129
id: musicId.value,
143130
title: title.value,
144131
artist: artist.value,
145132
genreId: genreId.value,
146133
genreName,
147-
difficulty: difficulty.value,
148-
level: level.value,
149-
levelDecimal: levelDecimal.value,
150134
targetDir: targetDir.value,
151135
})
152136
@@ -203,15 +187,28 @@ defineExpose({ startImport })
203187
</div>
204188

205189
<template v-if="showForm">
190+
<div v-if="coverFile" class="flex justify-center">
191+
<img :src="coverUrl" class="w-32 h-32 rounded-lg object-cover" />
192+
</div>
193+
206194
<div class="flex flex-col gap-1 text-sm op-70 bg-white/5 rd p-3">
207195
<div>{{ t('music.importFoundFiles') }}:</div>
208196
<div class="flex gap-3 flex-wrap">
209-
<span v-if="chartFile">{{ chartFile.name }}</span>
197+
<span v-for="f in chartFiles" :key="f.name">{{ f.name }}</span>
210198
<span v-if="audioFile">{{ audioFile.name }}</span>
211199
<span v-if="coverFile">{{ coverFile.name }}</span>
212200
</div>
213201
</div>
214202

203+
<div v-if="checkResult?.difficulties?.length" class="text-sm bg-white/5 rd p-3">
204+
<div class="op-70 mb-2">{{ t('music.importDifficulties') }}:</div>
205+
<div class="flex gap-2 flex-wrap">
206+
<span v-for="d in checkResult.difficulties" :key="d.difficulty" class="px-2 py-0.5 rd bg-white/10">
207+
{{ diffNames[d.difficulty] || `DIFF ${d.difficulty}` }} {{ (d.level + d.levelDecimal / 100).toFixed(1) }}
208+
</span>
209+
</div>
210+
</div>
211+
215212
<div v-if="checkResult?.alerts?.length" class="text-sm c-orange bg-orange/10 rd p-2">
216213
<div v-for="(a, i) in checkResult.alerts" :key="i">{{ a }}</div>
217214
</div>
@@ -229,14 +226,6 @@ defineExpose({ startImport })
229226
<label class="block text-sm op-60 mb-1">{{ t('music.genre') }}</label>
230227
<Select :options="genreOptions" v-model:value="genreId" :disabled="loading" />
231228
</div>
232-
<div>
233-
<label class="block text-sm op-60 mb-1">{{ t('music.importDifficulty') }}</label>
234-
<Select :options="diffOptions" v-model:value="difficulty" :disabled="loading" />
235-
</div>
236-
<div>
237-
<label class="block text-sm op-60 mb-1">{{ t('music.chartConstant') }}</label>
238-
<NumberInput v-model:value="constant" :min="0" :max="15.9" :step="0.1" :decimal="1" :disabled="loading" class="w-full" />
239-
</div>
240229
<div>
241230
<label class="block text-sm op-60 mb-1">{{ t('music.importTargetDir') }}</label>
242231
<Select :options="sourceOptions" v-model:value="targetDir" :disabled="loading" />

0 commit comments

Comments
 (0)