feat: improve input validation

This commit is contained in:
ws50529
2026-05-08 16:31:10 +08:00
parent 9c535432fe
commit 0ee69c4a49
2 changed files with 56 additions and 22 deletions
+49 -17
View File
@@ -109,8 +109,16 @@ createApp({
const targetA = this.baseA;
for (let i = 0; i < 7; i++) {
if (Math.abs((currentB[i] || 0) - (targetB[i] || 0)) > 1e-8) return true;
if (Math.abs((currentA[i] || 0) - (targetA[i] || 0)) > 1e-8) return true;
const bVal = currentB[i] || 0;
const bTarget = targetB[i] || 0;
// 使用相對誤差,避免數值較大時 (如 K > 10000) 產生的精確度誤判
const epsB = Math.max(1e-8, Math.abs(bTarget) * 1e-10);
if (Math.abs(bVal - bTarget) > epsB) return true;
const aVal = currentA[i] || 0;
const aTarget = targetA[i] || 0;
const epsA = Math.max(1e-8, Math.abs(aTarget) * 1e-10);
if (Math.abs(aVal - aTarget) > epsA) return true;
}
return false;
}
@@ -235,6 +243,17 @@ createApp({
// 當文字改變時,反向計算滑桿位置
this.calcSlidersFromText();
},
onlyNumberKey(e, allowDot = true) {
// 允許的控制鍵
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Home', 'End'];
if (allowedKeys.includes(e.key) || e.ctrlKey || e.metaKey) return;
// 檢查字元是否合法
const charRegex = allowDot ? /[0-9,.\-\s]/ : /[0-9,\-\s]/;
if (!charRegex.test(e.key)) {
e.preventDefault();
}
},
updateFromFixed(idx, type, val) {
const intVal = parseInt(val);
if (isNaN(intVal)) return;
@@ -284,10 +303,17 @@ createApp({
this.applyFilterDesign();
},
resetFilterParams() {
this.systemGain = 1.0;
this.fixedOverrides = { a: {}, b: {} };
this.bSliders = [0, 0, 0, 0, 0, 0, 0];
this.aSliders = [0, 0, 0, 0, 0, 0];
this.outOfRangeB = [false, false, false, false, false, false, false];
this.outOfRangeA = [false, false, false, false, false, false, false];
if (this.filterType === "(無) 手動自訂") {
this.b_str = "1.0, 0.0, 0.0, 0.0";
this.a_str = "1.0, 0.0, 0.0, 0.0";
this.updateFromText();
this.updateBodePlot();
} else {
this.params = {
fc: 1000.0, order: 1,
@@ -455,20 +481,26 @@ createApp({
const layout = {
paper_bgcolor: 'transparent', plot_bgcolor: 'transparent',
margin: isSmallScreen ? { t: 80, b: 70, l: 55, r: 15 } : { t: 70, b: 60, l: 70, r: 30 },
showlegend: true,
legend: {
orientation: 'h',
y: 1.1,
x: 0.5,
xanchor: 'center',
font: { size: isSmallScreen ? 10 : 12, color: textColor }
},
grid: { rows: 2, columns: 1, pattern: 'independent', roworder: 'top to bottom', ygap: isSmallScreen ? 0.35 : 0.35 },
margin: isSmallScreen ? { t: 80, b: 120, l: 55, r: 15 } : { t: 70, b: 120, l: 70, r: 30 },
showlegend: false,
grid: { rows: 2, columns: 1, pattern: 'independent', roworder: 'top to bottom', ygap: isSmallScreen ? 0.35 : 0.3 },
font: { color: textColor, family: 'Inter, sans-serif' },
annotations: [
{ text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
{ text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.2, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
{ text: 'Magnitude Response (dB)', xref: 'x domain', yref: 'y domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.15, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
{ text: 'Phase Response (Deg)', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: isSmallScreen ? 1.25 : 1.15, showarrow: false, font: { size: isSmallScreen ? 14 : 16, color: titleColor, weight: 'bold' } },
// 手動 Magnitude 圖例 (位於上方圖表下方)
{ xref: 'paper', yref: 'paper', x: 0.35, y: isSmallScreen ? 0.52 : 0.54, text: '一一一', font: { color: isDark ? '#3b82f6' : '#2563eb', size: 10 }, showarrow: false },
{ xref: 'paper', yref: 'paper', x: 0.42, y: isSmallScreen ? 0.52 : 0.54, text: 'Ideal (Float)', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' },
{ xref: 'paper', yref: 'paper', x: 0.58, y: isSmallScreen ? 0.52 : 0.54, text: '· · · ·', font: { color: isDark ? '#10b981' : '#059669', size: 14 }, showarrow: false },
{ xref: 'paper', yref: 'paper', x: 0.63, y: isSmallScreen ? 0.52 : 0.54, text: `Fixed (Q${this.shiftBits})`, font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' },
// 手動 Phase 圖例 (位於下方圖表下方)
{ xref: 'paper', yref: 'paper', x: 0.35, y: -0.15, text: '一一一', font: { color: isDark ? '#f97316' : '#ea580c', size: 10 }, showarrow: false },
{ xref: 'paper', yref: 'paper', x: 0.42, y: -0.15, text: 'Ideal Phase', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' },
{ xref: 'paper', yref: 'paper', x: 0.58, y: -0.15, text: '· · · ·', font: { color: isDark ? '#f43f5e' : '#e11d48', size: 14 }, showarrow: false },
{ xref: 'paper', yref: 'paper', x: 0.63, y: -0.15, text: 'Fixed Phase', font: { color: textColor, size: 11 }, showarrow: false, xanchor: 'left' },
...annotations
],
xaxis: { ...xAxisCommon },
@@ -481,8 +513,8 @@ createApp({
const traceMagIdeal = { x: freq, y: magIdeal, name: 'Ideal (Float)', type: 'scatter', line: { color: isDark ? '#3b82f6' : '#2563eb', width: 2.5 } };
const traceMagFixed = { x: freq, y: magFixed, name: `Fixed (Q${this.shiftBits})`, type: 'scatter', line: { color: isDark ? '#10b981' : '#059669', width: 2, dash: 'dot' } };
const tracePhaseIdeal = { x: freq, y: phaseIdeal, name: 'Ideal Phase', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2', showlegend: false };
const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2', showlegend: false };
const tracePhaseIdeal = { x: freq, y: phaseIdeal, name: 'Ideal Phase', type: 'scatter', line: { color: isDark ? '#f97316' : '#ea580c', width: 2.5 }, xaxis: 'x2', yaxis: 'y2' };
const tracePhaseFixed = { x: freq, y: phaseFixed, name: 'Fixed Phase', type: 'scatter', line: { color: isDark ? '#f43f5e' : '#e11d48', width: 2, dash: 'dot' }, xaxis: 'x2', yaxis: 'y2' };
Plotly.react('bodePlot', [traceMagIdeal, traceMagFixed, tracePhaseIdeal, tracePhaseFixed], layout, { responsive: true });
},
+7 -5
View File
@@ -235,11 +235,13 @@
(Hz)</span><span class="text-emerald-600 dark:text-emerald-400">{{ formatK(fs)
}}</span></label>
<input type="number" v-model.number="fs" @input="debouncedApply"
@keydown="onlyNumberKey"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
</div>
<div>
<label class="text-sm text-slate-500 dark:text-gray-400 flex justify-between mb-1"><span>系統增益 K (System Gain)</span><span class="text-blue-600 dark:text-blue-400">{{ systemGain }}x</span></label>
<input type="number" v-model.number="systemGain" @input="onGainChange" step="0.1"
@keydown="onlyNumberKey"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
</div>
</div>
@@ -269,7 +271,7 @@
<span class="text-[10px] font-mono text-blue-800 dark:text-blue-300">{{ item.val }}</span>
</div>
</div>
<textarea v-model="b_str" @input="debouncedUpdateFromText('b')" rows="2"
<textarea v-model="b_str" @input="debouncedUpdateFromText('b')" @keydown="onlyNumberKey" rows="2"
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-blue-700 dark:text-blue-300"></textarea>
</div>
<!-- b 係數倍率微調 -->
@@ -309,7 +311,7 @@
<span class="text-[10px] font-mono text-orange-800 dark:text-orange-300">{{ item.val }}</span>
</div>
</div>
<textarea v-model="a_str" @input="debouncedUpdateFromText('a')" rows="2"
<textarea v-model="a_str" @input="debouncedUpdateFromText('a')" @keydown="onlyNumberKey" rows="2"
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-orange-700 dark:text-orange-300"></textarea>
</div>
<!-- a 係數倍率微調 -->
@@ -363,7 +365,7 @@
<div class="mb-4">
<label class="text-sm text-slate-500 dark:text-gray-400 mb-1 block">左移位元數 (Shift Bits: 0~14)</label>
<div class="flex items-center gap-2">
<input type="number" v-model.number="shiftBits" min="0" max="14" @change="clearFixedOverrides"
<input type="number" v-model.number="shiftBits" min="0" max="14" @change="clearFixedOverrides" @keydown="onlyNumberKey"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base focus:border-blue-500 outline-none transition-all text-slate-900 dark:text-gray-100">
</div>
<p class="text-[10px] text-slate-400 mt-1">Scaling: 2^{{ shiftBits }} ({{ Math.pow(2, shiftBits) }})</p>
@@ -381,7 +383,7 @@
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
</div>
</div>
<textarea v-model="b_int_str" rows="2"
<textarea v-model="b_int_str" @keydown="onlyNumberKey($event, false)" rows="2"
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-blue-700 dark:text-blue-300"></textarea>
</div>
<div>
@@ -395,7 +397,7 @@
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
</div>
</div>
<textarea v-model="a_int_str" rows="2"
<textarea v-model="a_int_str" @keydown="onlyNumberKey($event, false)" rows="2"
class="w-full bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-sm focus:border-blue-500 outline-none font-mono resize-none leading-relaxed text-orange-700 dark:text-orange-300"></textarea>
</div>
</div>