我正在开发一个为音乐 composer 设计的React Web应用程序。该应用程序具有动态UI,允许用户选择音阶和和弦,并提供视觉反馈以指示所选和弦和音阶之间的兼容性。然而,我遇到了一些与颜色突出显示逻辑相关的错误,我正在努力解决。
应用描述:
该应用程序以表格格式显示一组音符,音阶和和弦。用户可以选择和弦和音阶,应用程序会根据选择以红色突出显示不兼容的和弦或音阶。以下是关键功能:
**选择和弦:**选择和弦时,未包含和弦所有音符的音阶以及未完全包含在任何非红色高亮显示音阶中的和弦将以红色高亮显示。
**选择音阶:**选择音阶会以红色突出显示未在所选音阶中包含所有音符的和弦,以及不包括至少一个非红色突出显示和弦的所有音符的音阶。
**取消选择和弦:**取消选择和弦将从仅受该和弦选择影响的和弦和音阶中删除红色高光。
**取消选择音阶:**取消选择音阶将从仅受该音阶选择影响的和弦和音阶中删除红色高光。
(https://i.stack.imgur.com/te9R0.png)
已识别错误:
- 如果我先按一个和弦,然后按一个音阶,由于和弦选择而标记为红色的音阶将停止显示为红色。预期的行为是,应用程序应保持已经突出显示为红色的音阶,并根据需要标记其他音阶。
- 当选择一个和弦,然后选择一个音阶,然后取消选择音阶时,只有那些在选择音阶时变成红色的和弦/音阶才应该恢复为白色。同样,如果选择一个音阶,然后选择一个和弦,然后取消选择和弦,只有那些在选择和弦时变成红色的音阶/和弦才应该恢复为白色。
注意事项:当只选择和弦或音阶时,以及在和弦之前选择音阶时,应用程序可以正常工作。当在和弦之后选择音阶时,会出现问题。
我在下面包含了我的React组件(App.js)和CSS(App.css)代码以供参考。我正在寻找如何调试和修复这些问题的指导。
App.js
import React, { useState } from 'react';
import './App.css';
const notes = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'];
const scaleQualities = ['Mayor', 'Menor', "Pent Mayor"]; // Used only for scales
const chordQualities = ['Mayor', 'Menor', 'Sus2', 'Sus4']; // Used for chords
// Get the index of a note within an octave
const getIndexInOctave = (note) => {
return notes.indexOf(note);
};
// Define intervals for different scale qualities
const scaleIntervals = {
'Mayor': [2, 2, 1, 2, 2, 2, 1],
'Menor': [2, 1, 2, 2, 1, 2, 2],
'Pent Mayor': [2, 2, 3, 2, 3]
};
// Generate a scale based on a root note and quality
const generateScale = (rootNote, quality) => {
let scale = [rootNote];
let intervals = scaleIntervals[quality];
let currentNote = rootNote;
intervals.forEach(interval => {
currentNote = getNoteAtInterval(currentNote, interval);
scale.push(currentNote);
});
return scale;
};
const getNoteAtInterval = (startNote, interval) => {
let startIndex = getIndexInOctave(startNote);
let targetIndex = (startIndex + interval) % notes.length;
return notes[targetIndex];
};
// Define intervals for different chord qualities
const chordIntervals = {
'Mayor': [4, 3],
'Menor': [3, 4],
'Sus2': [2, 5],
'Sus4': [5, 2]
};
// Generate a chord based on a root note and quality
const generateChord = (rootNote, quality) => {
let chord = [rootNote];
let intervals = chordIntervals[quality];
let currentNote = rootNote;
intervals.forEach(interval => {
currentNote = getNoteAtInterval(currentNote, interval);
chord.push(currentNote);
});
return chord;
};
// Generate all major and minor scales
let generatedScales = {};
notes.forEach(note => {
scaleQualities.forEach(quality => {
let scaleName = `${note} ${quality}`;
generatedScales[scaleName] = generateScale(note, quality);
});
});
// Generate all chords
let generatedChords = {};
notes.forEach(note => {
chordQualities.forEach(quality => {
let chordName = `${note} ${quality}`;
generatedChords[chordName] = generateChord(note, quality);
});
});
// Initial state for the application
const initialState = {
buttons: notes.reduce((acc, note) => {
scaleQualities.forEach(quality => {
acc[`scale-${note} ${quality}`] = 'white';
});
chordQualities.forEach(quality => {
acc[`chord-${note} ${quality}`] = 'white';
});
return acc;
}, {}),
selectedChord: '',
selectedScale: ''
};
const App = () => {
const [selectedChord, setSelectedChord] = useState('');
const [selectedScale, setSelectedScale] = useState('');
const [buttons, setButtons] = useState(initialState.buttons); // Agregado
// Check if a chord is compatible with a scale
const isChordCompatibleWithScale = (chord, scale) => {
if (!generatedChords[chord] || !generatedScales[scale]) {
return false;
}
return generatedChords[chord].every(note => generatedScales[scale].includes(note));
};
// Check if any chord is incompatible with a scale
const isAnyChordIncompatibleWithScale = (buttons, scale) => {
return Object.keys(buttons).some(key => {
if (key.startsWith("chord-") && buttons[key] === 'green') {
const chord = key.substring(6);
return !isChordCompatibleWithScale(chord, scale);
}
return false;
});
};
// Check if any scale is incompatible with a chord
const isAnyScaleIncompatibleWithChord = (buttons, chord) => {
return Object.keys(buttons).some(key => {
if (key.startsWith("scale-") && buttons[key] === 'green') {
const scale = key.substring(6);
return !isChordCompatibleWithScale(chord, scale);
}
return false;
});
};
// Handle click on a chord button
const handleChordClick = (chord) => {
const chordKey = `chord-${chord}`;
if (buttons[chordKey] === 'red') {
return; // Do nothing if the button is red
}
let newButtons = { ...buttons };
const wasSelected = newButtons[chordKey] === 'green';
newButtons[chordKey] = wasSelected ? 'white' : 'green';
// Update the compatibility of scales with the selected/deselected chord
Object.keys(newButtons).forEach(key => {
if (key.startsWith("scale-")) {
if (!wasSelected) {
newButtons[key] = isChordCompatibleWithScale(chord, key.substring(6)) ? newButtons[key] : 'red';
} else if (newButtons[key] === 'red') {
newButtons[key] = isAnyChordIncompatibleWithScale(newButtons, key.substring(6)) ? 'red' : 'white';
}
}
});
// Update the compatibility of chords with the updated scales
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-") && key !== chordKey) {
const currentChord = key.substring(6);
const isCompatibleWithAnyNonRedScale = Object.keys(newButtons).some(scaleKey => {
return scaleKey.startsWith("scale-") && newButtons[scaleKey] !== 'red' && isChordCompatibleWithScale(currentChord, scaleKey.substring(6));
});
newButtons[key] = newButtons[key] === 'green' || isCompatibleWithAnyNonRedScale ? newButtons[key] : 'red';
}
});
// If a chord is deselected, check all chords and update their color state
if (wasSelected) {
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-")) {
const currentChord = key.substring(6);
// Check if the chord is not currently selected
if (newButtons[key] !== 'green') {
const isCompatibleWithAnyNonRedScale = Object.keys(newButtons).some(scaleKey => {
return scaleKey.startsWith("scale-") && newButtons[scaleKey] !== 'red' && isChordCompatibleWithScale(currentChord, scaleKey.substring(6));
});
newButtons[key] = isCompatibleWithAnyNonRedScale ? 'white' : 'red';
}
}
});
}
setButtons(newButtons);
};
// Handle click on a scale button
const handleScaleClick = (scale) => {
const scaleKey = `scale-${scale}`;
if (buttons[scaleKey] === 'red') {
return; // Do nothing if the button is red
}
let newButtons = { ...buttons };
// Toggle the state of the current scale between selected and unselected
newButtons[scaleKey] = newButtons[scaleKey] === 'green' ? 'white' : 'green';
// Check compatibility of all chords with the current scale
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-")) {
const currentChord = key.substring(6);
if (newButtons[scaleKey] === 'green') {
newButtons[key] = isChordCompatibleWithScale(currentChord, scale) ? newButtons[key] : 'red';
} else {
newButtons[key] = isAnyScaleIncompatibleWithChord(newButtons, currentChord) ? 'red' : (newButtons[key] === 'green' ? 'green' : 'white');
}
}
});
// Update the state of scales based on the chords not marked in red
Object.keys(newButtons).forEach(scaleKey => {
if (scaleKey.startsWith("scale-")) {
const currentScale = scaleKey.substring(6);
newButtons[scaleKey] = !Object.keys(generatedChords).some(chordKey => {
if (newButtons[`chord-${chordKey}`] !== 'red') {
return generatedChords[chordKey].every(note => generatedScales[currentScale].includes(note));
}
return false;
}) ? 'red' : (newButtons[scaleKey] === 'green' ? 'green' : 'white');
}
});
setButtons(newButtons);
};
// Generate cells for a row of chords or scales
const generateRowCells = (type, rowQuality) => {
return notes.map((note) => {
const combination = `${note} ${rowQuality}`;
const buttonType = type === 'chord' ? `chord-${combination}` : `scale-${combination}`;
const buttonColor = buttons[buttonType];
const isButtonIncompatible = buttonColor === 'red';
return (
<td
key={note}
onClick={() => {
if (type === 'chord') {
handleChordClick(combination);
} else {
handleScaleClick(combination);
}
}}
style={{ backgroundColor: buttonColor }}
className={isButtonIncompatible ? "incompatible" : ""}
>
{combination}
</td>
);
});
};
// Generate table rows for chords or scales
const generateTableRows = (type) => {
const qualities = type === 'chord' ? chordQualities : scaleQualities;
return qualities.map((quality) => (
<tr key={quality}>
<th>{quality}</th>
{generateRowCells(type, quality)}
</tr>
));
};
return (
<div className="App">
<h1 className="title">ChordProg</h1>
<div className="table-container">
<h2>Acordes</h2>
<table className="chord-table">
<thead>
<tr>
<th></th> {/* Empty cell in the corner */}
{notes.map((note) => (
<th key={note}>{note}</th>
))}
</tr>
</thead>
<tbody>{generateTableRows('chord')}</tbody>
</table>
</div>
<div className="table-container">
<h2>Escalas</h2>
<table className="scale-table">
<thead>
<tr>
<th></th> {/* Empty cell in the corner */}
{notes.map((note) => (
<th key={note}>{note}</th>
))}
</tr>
</thead>
<tbody>{generateTableRows('scale')}</tbody>
</table>
</div>
</div>
);
};
export default App;
字符串
App.css
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
body, h1, h2, h3, p, figure, ul, li, table, td, th {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #f0f2f5;
color: #333;
line-height: 1.6;
}
.App {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
text-align: center;
zoom: 0.70;
}
.title {
font-size: 2.5em;
margin-bottom: 30px;
color: #4a90e2;
}
.table-container {
margin-top: 30px;
width: 90%;
max-width: 1000px;
}
.chord-table, .scale-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: center;
}
th {
background-color: #eaeaea;
font-weight: 700;
}
td {
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
td:hover {
color: #0401d6;
}
.selected {
background-color: #ff6347;
color: white;
}
.table-container h2 {
margin-bottom: 15px;
color: #333;
font-size: 1.5em;
}
@media (max-width: 768px) {
.title {
font-size: 2em;
}
.table-container {
width: 100%;
}
}`
.selected {
background-color: #4CAF50;
color: white;
}
.compatible {
}
.incompatible {
cursor: not-allowed;
background-color: #ff4a4a;
color: white;
}
型
**我尝试了什么:**我已经实现了基于兼容性更改代表和弦和音阶的按钮的颜色状态的逻辑。该逻辑涉及迭代按钮并根据选择和兼容性标准将其颜色状态更新为“红色”或“白色”。当选择和弦或音阶时,应用程序会检查与其他和弦和音阶的兼容性,并相应地更新UI。
**我的期望:**我的期望是,在选择和弦或音阶时,应用程序将正确识别并以红色突出显示与所选和弦或音阶不兼容的音阶和和弦。此外,在取消选择和弦或音阶时,只有受该特定选择影响的高光应恢复为白色,而其他红色高光保持不变。
**实际情况:**问题出在操作顺序上。如果我先选择一个和弦,然后选择一个音阶,当选择音阶时,由于和弦选择而应用的红色高亮(表示不兼容)被错误地清除。相反,如果我先选择音阶,然后选择和弦,应用程序的行为与预期的一样。这种不一致表明,当以特定顺序与按钮交互时,处理按钮状态更新的逻辑可能存在缺陷。问题似乎在于应用程序在进行新选择时处理状态更新的方式,特别是当涉及与音阶之后的和弦交互时。
1条答案
按热度按时间pzfprimi1#
我会重新设计按钮的颜色状态,使其根本不是状态。
首先,我使用
selectedChords
和selectedScales
作为我们的“事实来源”,按钮已经被选中,通过减少点击事件处理程序:字符串
然后,为了防止任何不兼容的按钮执行任何功能,我会修改
generateRowCells()
:型
为了检查哪些按钮代表不兼容的组合,我们可以首先设置哪些音阶与所选和弦不兼容,以及哪些和弦与所选音阶不兼容:
型
这只会让你点击的其他面板高亮显示。我们现在需要复制你的原始逻辑,即获取哪些和弦与所选和弦的剩余可用音阶不兼容,反之亦然。
型
我们可以将其稍微重构为:
型
这只是一个初步的尝试,但它应该合理的按钮行为适当。当然,优化/重构的味道。