music(ui): 简化只留「和弦谱」一个抓取 tab,简谱/字母版废弃
deploy music / build-and-deploy (push) Successful in 1m54s

This commit is contained in:
Fam Zheng
2026-05-10 21:32:49 +01:00
parent e5f3a95aa9
commit 5674be1cfd
3 changed files with 34 additions and 48 deletions
+1 -4
View File
@@ -106,8 +106,7 @@
<span>角色</span> <span>角色</span>
<select v-model="uploadRole"> <select v-model="uploadRole">
<option :value="null"> 自动 / 通用图 </option> <option :value="null"> 自动 / 通用图 </option>
<option value="chord_letters">和弦谱字母 G/Em/C</option> <option value="chord">和弦谱数字级数 1/4/5</option>
<option value="chord_functional">简谱数字 1/4/5</option>
<option value="numbered">简谱手动上传</option> <option value="numbered">简谱手动上传</option>
<option value="staff">五线谱</option> <option value="staff">五线谱</option>
</select> </select>
@@ -339,8 +338,6 @@ function kindLabel(k) {
function roleLabel(r) { function roleLabel(r) {
return ({ return ({
chord: '和弦谱', chord: '和弦谱',
chord_letters: '和弦谱',
chord_functional: '简谱',
numbered: '简谱', numbered: '简谱',
staff: '五线谱', staff: '五线谱',
})[r] || r })[r] || r
+31 -35
View File
@@ -131,10 +131,10 @@
>{{ line.text }}</div> >{{ line.text }}</div>
</div> </div>
<!-- 弹唱谱 / 功能谱 共用渲染 --> <!-- 和弦 -->
<div v-show="['chord', 'functional'].includes(activeTab)" class="sheet-box"> <div v-show="activeTab === 'chord'" class="sheet-box">
<img <img
v-for="att in chordTabAttachments(activeTab)" v-for="att in chordTabAttachments('chord')"
:key="att.id" :key="att.id"
:src="attachmentUrl(att.id)" :src="attachmentUrl(att.id)"
:alt="att.filename" :alt="att.filename"
@@ -142,34 +142,33 @@
@click="fullscreenSrc = attachmentUrl(att.id)" @click="fullscreenSrc = attachmentUrl(att.id)"
/> />
<div <div
v-if="chordTabAttachments(activeTab).length === 0" v-if="chordTabAttachments('chord').length === 0"
class="auto-fetch" class="auto-fetch"
> >
<p v-if="chordStateOf(activeTab) === 'idle'" class="hint-line"> <p v-if="chordStateOf('chord') === 'idle'" class="hint-line">
<span v-if="activeTab === 'chord'"> yopu.co <b>和弦谱字母 G/Em/C + 六线谱</b></span> yopu.co <b>和弦谱歌词 + 数字级数 1/4/5/6m</b>
<span v-else> yopu.co <b>简谱数字 1/4/5/6m 级数</b></span>
</p> </p>
<p v-else-if="['pending','processing'].includes(chordStateOf(activeTab))" class="hint-line"> <p v-else-if="['pending','processing'].includes(chordStateOf('chord'))" class="hint-line">
正在抓取 30-60s 正在抓取 30-60s
</p> </p>
<p v-else-if="chordStateOf(activeTab) === 'failed'" class="hint-line err"> <p v-else-if="chordStateOf('chord') === 'failed'" class="hint-line err">
抓取失败{{ chordErrors[modeForTab(activeTab)] }} 抓取失败{{ chordErrors.chord }}
</p> </p>
<button <button
class="btn-fetch" class="btn-fetch"
:disabled="['pending','processing'].includes(chordStateOf(activeTab))" :disabled="['pending','processing'].includes(chordStateOf('chord'))"
@click="startChordFetch(modeForTab(activeTab))" @click="startChordFetch('chord')"
> >
<span v-if="['pending','processing'].includes(chordStateOf(activeTab))" class="spin"></span> <span v-if="['pending','processing'].includes(chordStateOf('chord'))" class="spin"></span>
<span v-else>🎸 自动抓取{{ activeTab === 'chord' ? '和弦谱' : '简谱' }}</span> <span v-else>🎸 自动抓取和弦谱</span>
</button> </button>
</div> </div>
</div> </div>
<!-- 五线手动上传的图 --> <!-- 手动上传的图 / 五线谱 -->
<div v-show="activeTab === 'staff'" class="sheet-box"> <div v-show="['numbered', 'staff'].includes(activeTab)" class="sheet-box">
<img <img
v-for="att in roleAttachments('staff')" v-for="att in roleAttachments(activeTab)"
:key="att.id" :key="att.id"
:src="attachmentUrl(att.id)" :src="attachmentUrl(att.id)"
:alt="att.filename" :alt="att.filename"
@@ -480,11 +479,11 @@ async function runInspire() {
} }
} }
// chord —— 两个 mode 各自独立 state // chord —— mode(只抓 yopu 默认数字级数版)
const chordStates = ref({ letters: 'idle', functional: 'idle' }) const chordStates = ref({ chord: 'idle' })
const chordErrors = ref({ letters: '', functional: '' }) const chordErrors = ref({ chord: '' })
const chordPollTimers = { letters: null, functional: null } const chordPollTimers = { chord: null }
const chordPollStarted = { letters: 0, functional: 0 } const chordPollStarted = { chord: 0 }
function chordStateOf(tab) { function chordStateOf(tab) {
return chordStates.value[modeForTab(tab)] || 'idle' return chordStates.value[modeForTab(tab)] || 'idle'
} }
@@ -524,19 +523,15 @@ function roleAttachments(role) {
) )
} }
// 和弦谱 tab = 字母版 (chord_letters + 历史 chord) // 和弦谱 tab = role='chord'(数字级数版,yopu 自动抓的就这种)
// 简谱 tab = 数字级数版 (chord_functional) + 用户手动上传 numbered
function chordTabAttachments(tab) { function chordTabAttachments(tab) {
const set = tab === 'chord'
? new Set(['chord', 'chord_letters'])
: new Set(['chord_functional', 'numbered'])
return (selected.value?.attachments || []).filter( return (selected.value?.attachments || []).filter(
a => a.kind === 'image' && set.has(a.role), a => a.kind === 'image' && a.role === 'chord',
) )
} }
function modeForTab(tab) { function modeForTab(_tab) {
return tab === 'functional' ? 'functional' : 'letters' return 'chord' // 唯一抓取模式
} }
const tabs = computed(() => { const tabs = computed(() => {
@@ -544,7 +539,8 @@ const tabs = computed(() => {
const list = [] const list = []
if (selected.value.lyrics) list.push({ key: 'lyrics', label: '歌词', count: 0 }) if (selected.value.lyrics) list.push({ key: 'lyrics', label: '歌词', count: 0 })
list.push({ key: 'chord', label: '和弦谱', count: chordTabAttachments('chord').length }) list.push({ key: 'chord', label: '和弦谱', count: chordTabAttachments('chord').length })
list.push({ key: 'functional', label: '简谱', count: chordTabAttachments('functional').length }) const num = roleAttachments('numbered').length
if (num) list.push({ key: 'numbered', label: '简谱(自传)', count: num })
const staff = roleAttachments('staff').length const staff = roleAttachments('staff').length
if (staff) list.push({ key: 'staff', label: '五线谱', count: staff }) if (staff) list.push({ key: 'staff', label: '五线谱', count: staff })
if (pdfAttachments.value.length) list.push({ key: 'pdf', label: '乐谱 PDF', count: pdfAttachments.value.length }) if (pdfAttachments.value.length) list.push({ key: 'pdf', label: '乐谱 PDF', count: pdfAttachments.value.length })
@@ -662,9 +658,9 @@ async function promptNewPlaylist() {
async function loadPiece(id) { async function loadPiece(id) {
selected.value = null selected.value = null
notesDraft.value = '' notesDraft.value = ''
stopChordPoll('letters'); stopChordPoll('functional') stopChordPoll('chord')
chordStates.value = { letters: 'idle', functional: 'idle' } chordStates.value = { chord: 'idle' }
chordErrors.value = { letters: '', functional: '' } chordErrors.value = { chord: '' }
abortChat() abortChat()
chatMessages.value = [] chatMessages.value = []
chatStreamText.value = '' chatStreamText.value = ''
@@ -1002,7 +998,7 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('keydown', onKeyDown) document.removeEventListener('keydown', onKeyDown)
if (notesTimer) clearTimeout(notesTimer) if (notesTimer) clearTimeout(notesTimer)
stopChordPoll('letters'); stopChordPoll('functional') stopChordPoll('chord')
abortChat() abortChat()
}) })
</script> </script>
+2 -9
View File
@@ -1549,17 +1549,10 @@ async fn upload_attachments(
) -> Result<JsonResp<Value>, AppError> { ) -> Result<JsonResp<Value>, AppError> {
let role = match q.role.as_deref().map(str::trim).filter(|s| !s.is_empty()) { let role = match q.role.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
None => None, None => None,
Some(r) Some(r) if matches!(r, "chord" | "jianpu" | "numbered" | "staff") => Some(r.to_string()),
if matches!(
r,
"chord" | "chord_letters" | "chord_functional" | "numbered" | "staff"
) =>
{
Some(r.to_string())
}
Some(other) => { Some(other) => {
return Err(AppError::bad_request(format!( return Err(AppError::bad_request(format!(
"unsupported role '{other}', expect one of: chord / chord_letters / chord_functional / numbered / staff" "unsupported role '{other}', expect one of: chord / jianpu / numbered / staff"
))); )));
} }
}; };