feat: improve input validation
This commit is contained in:
+49
-17
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user