Files
tcad-bodeplot/src/App.vue
T

886 lines
84 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div id="app" class="flex flex-col h-screen overflow-hidden">
<!-- 頂部導覽列 -->
<header
class="bg-white dark:bg-dark border-b border-slate-200 dark:border-gray-800 p-4 shadow-md flex justify-between items-center z-10">
<div class="flex items-center gap-3">
<img :src="'/ui/logo.png'" alt="Logo" class="h-10 w-10 object-contain rounded-lg shadow-sm">
<h1 class="text-xl font-bold text-slate-900 dark:text-gray-100">
差分方程式分析 (Difference Equation Analyzer)</h1>
</div>
<div class="flex items-center gap-4">
<!-- 模式切換按鈕 -->
<button @click="toggleDarkMode"
class="p-2 rounded-lg bg-slate-100 dark:bg-gray-800 hover:bg-slate-200 dark:hover:bg-gray-700 transition-colors shadow-sm">
<svg v-if="isDarkMode" class="w-5 h-5 role-theme-icon" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0 1 1 0 002 0zM2 10a1 1 0 011-1h1a1 1 0 110 2H3a1 1 0 01-1-1zm14 0a1 1 0 011-1h1a1 1 0 110 2h-1a1 1 0 01-1-1zM5.05 5.05a1 1 0 011.414 0l.707.707a1 1 0 11-1.414 1.414l-.707-.707a1 1 0 010-1.414zm9.9 9.9a1 1 0 011.414 0l.707.707a1 1 0 11-1.414 1.414l-.707-.707a1 1 0 010-1.414zM5.05 14.95a1 1 0 010-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707a1 1 0 01-1.414 0zm9.9-9.9a1 1 0 010-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707a1 1 0 01-1.414 0z"
clip-rule="evenodd" />
</svg>
<svg v-else class="w-5 h-5 text-slate-800" fill="currentColor" viewBox="0 0 20 20">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
</button>
</div>
</header>
<!-- 手機版頁籤列 (桌面版隱藏) -->
<div
class="lg:hidden flex border-b border-slate-200 dark:border-gray-800 bg-white dark:bg-dark z-10 flex-shrink-0">
<button @click="mobileTab = 'settings'" :class="mobileTab === 'settings'
? 'border-b-2 role-border-primary role-text-primary bg-transparent'
: 'text-slate-600 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200'"
class="flex-1 flex items-center justify-center gap-2 py-3 text-sm font-semibold transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
設定
</button>
<button @click="mobileTab = 'chart'" :class="mobileTab === 'chart'
? 'border-b-2 role-border-primary role-text-primary bg-transparent'
: 'text-slate-600 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-200'"
class="flex-1 flex items-center justify-center gap-2 py-3 text-sm font-semibold transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
</path>
</svg>
圖表
<span v-if="loadingBode" class="w-2 h-2 rounded-full role-dot-primary animate-ping"></span>
</button>
</div>
<div class="flex flex-col lg:flex-row flex-1 overflow-hidden">
<!-- 左側控制面板 (RWD: 手機版=設定頁籤全螢幕桌面版=左側邊欄) -->
<aside :class="mobileTab === 'settings' ? 'flex' : 'hidden'"
class="lg:flex flex-col w-full lg:w-96 h-full bg-white dark:bg-dark border-b lg:border-b-0 lg:border-r border-slate-200 dark:border-gray-800 overflow-y-auto p-5 gap-5 custom-scrollbar transition-colors duration-300">
<!-- 濾波器設計工具 -->
<section>
<h2
class="text-sm font-semibold text-slate-600 dark:text-gray-400 uppercase tracking-wider mb-3 flex items-center justify-between">
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
濾波器設計
</div>
<div class="flex items-center gap-2">
<button @click="writeToMCU"
:disabled="writingMCU || !mcuConnected"
class="primary-action text-[10px] font-bold role-bg-primary role-hover-primary role-text-on-fill role-border-primary border px-3 py-1 rounded-full transition-colors uppercase disabled:opacity-50 disabled:cursor-not-allowed">
{{ writingMCU ? '寫入中' : '寫值到 MCU' }}
</button>
<button @click="resetFilterParams"
class="text-[10px] font-bold border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 px-3 py-1 rounded-full transition-colors uppercase">重設設計</button>
</div>
</h2>
<div class="mb-3 space-y-2">
<div v-if="webSerialSupported" class="flex items-center gap-2">
<button v-if="!mcuConnected" @click="connectMCUPort"
class="flex-1 text-xs font-bold role-bg-primary role-hover-primary role-text-on-fill role-border-primary border px-3 py-2 rounded transition-colors uppercase">
選擇連接埠並連線
</button>
<template v-else>
<span class="flex items-center gap-1.5 flex-1 text-xs font-semibold text-emerald-600 dark:text-emerald-400">
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
MCU 已連線
</span>
<button @click="disconnectMCUPort"
class="text-[10px] font-bold bg-slate-200 dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-300 dark:hover:bg-gray-700 px-2 py-2 rounded transition-colors uppercase">
斷線
</button>
</template>
</div>
<p v-if="!webSerialSupported" class="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg px-3 py-2">
此瀏覽器不支援 Web SerialMCU 寫值功能需使用 Chrome Edge且透過 HTTPS 連線
</p>
<p v-if="mcuStatus" class="text-xs text-slate-600 dark:text-gray-400">{{ mcuStatus }}</p>
</div>
<select v-model="filterType" @change="onFilterTypeChange"
class="w-full bg-slate-50 dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base role-focus-primary focus:ring-1 role-focus-ring-primary outline-none transition-all mb-3 text-slate-900 dark:text-gray-100 truncate">
<option v-for="opt in filterOptions" :key="opt" :value="opt">{{ opt }}</option>
</select>
<!-- 動態參數區 -->
<div v-if="filterType !== '(無) 手動自訂'"
class="space-y-3 bg-slate-100 dark:bg-gray-900 p-4 rounded-lg border border-slate-200 dark:border-gray-800 shadow-inner">
<!-- Lowpass / Highpass -->
<div v-if="['Lowpass (低通)', 'Highpass (高通)'].includes(filterType)">
<label class="text-sm text-slate-600 dark:text-gray-400 flex justify-between mb-1"><span>截止頻率
f_c (Hz)</span><span class="role-text-primary">{{
formatK(params.fc) }}</span></label>
<input type="range" v-model.number="params.fc" @input="debouncedApply" :min="1" :max="fs/2"
class="w-full role-range-primary mb-2">
<input type="number" v-model.number="params.fc" @change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base role-focus-primary outline-none text-slate-900 dark:text-gray-100">
<label class="text-sm text-slate-600 dark:text-gray-400 mt-3 mb-2 block">階數 Order</label>
<div class="selector-button-group flex gap-2">
<button v-for="o in [1,2,3]" :key="o" @click="params.order = o; applyFilterDesign()"
:class="params.order === o ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-700 dark:text-gray-400 border-slate-300 dark:border-gray-700'"
class="selector-button flex-1 rounded py-2.5 text-base font-medium border transition-colors">{{ o
}}</button>
</div>
</div>
<!-- Bandpass -->
<div v-if="filterType === 'Bandpass (帶通)'">
<label class="text-sm text-slate-600 dark:text-gray-400 flex justify-between mb-1"><span>下截止頻率
f_low (Hz)</span><span class="role-text-primary">{{
formatK(params.bp_f_low) }}</span></label>
<input type="number" v-model.number="params.bp_f_low" @change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base mb-2 text-slate-900 dark:text-gray-100">
<label class="text-sm text-slate-600 dark:text-gray-400 flex justify-between mb-1"><span>上截止頻率
f_high (Hz)</span><span class="role-text-primary">{{
formatK(params.bp_f_high) }}</span></label>
<input type="number" v-model.number="params.bp_f_high" @change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base mb-2 text-slate-900 dark:text-gray-100">
<label class="text-sm text-slate-600 dark:text-gray-400 mt-2 mb-2 block">階數 Order</label>
<div class="selector-button-group flex gap-2">
<button v-for="o in [1,2,3]" :key="o" @click="params.bp_order = o; applyFilterDesign()"
:class="params.bp_order === o ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-700 dark:text-gray-400 border-slate-300 dark:border-gray-700'"
class="selector-button flex-1 rounded py-1.5 text-sm border transition-colors">{{ o }}</button>
</div>
</div>
<!-- Notch -->
<div v-if="filterType === 'Notch (陷波器)'" class="space-y-3">
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">中心頻率 f_0
(Hz)</label><input type="number" v-model.number="params.notch_f0"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">品質因數
Q</label><input type="number" v-model.number="params.notch_q"
@change="applyFilterDesign" step="0.1"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
</div>
<!-- 1P1Z -->
<div v-if="filterType === '1P1Z (一極一零)'" class="space-y-3">
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">零點頻率 Freq_z
(Hz)</label><input type="number" v-model.number="params.opoz_fz"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">極點頻率 Freq_p
(Hz)</label><input type="number" v-model.number="params.opoz_fp"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
</div>
<!-- 2P1Z -->
<div v-if="filterType === '2P1Z (二極一零)'" class="space-y-3">
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">零點頻率 Freq_z
(Hz)</label><input type="number" v-model.number="params.tp1z_fz"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">極點頻率 1 Freq_p1
(Hz)</label><input type="number" v-model.number="params.tp1z_fp1"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">極點頻率 2 Freq_p2
(Hz)</label><input type="number" v-model.number="params.tp1z_fp2"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
</div>
<!-- 2P2Z -->
<div v-if="filterType === '2P2Z (二極二零)'" class="space-y-3">
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">零點頻率 1 Freq_z1
(Hz)</label><input type="number" v-model.number="params.tptz_fz1"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">零點頻率 2 Freq_z2
(Hz)</label><input type="number" v-model.number="params.tptz_fz2"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">極點頻率 1 Freq_p1
(Hz)</label><input type="number" v-model.number="params.tptz_fp1"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-sm text-slate-600 dark:text-gray-400 mb-2 block">極點頻率 2 Freq_p2
(Hz)</label><input type="number" v-model.number="params.tptz_fp2"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base text-slate-900 dark:text-gray-100">
</div>
</div>
<!-- PID -->
<div v-if="filterType === 'PID 控制器'" class="space-y-3">
<div v-for="k in [{key:'kp',label:'比例增益 Kp'},{key:'ki',label:'積分增益 Ki'},{key:'kd',label:'微分增益 Kd'}]"
:key="k.key">
<label class="text-sm font-medium text-slate-600 dark:text-gray-400 mb-1 block">{{
k.label
}}</label>
<input type="number" v-model.number="params[k.key]" @change="applyFilterDesign"
step="0.0001"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-3 text-base role-focus-primary outline-none text-slate-900 dark:text-gray-100">
</div>
</div>
<!-- SOGI -->
<div v-if="filterType === 'SOGI-Alpha (帶通)' || filterType === 'SOGI-Beta (低通)'"
class="space-y-3">
<div><label class="text-xs text-slate-600 dark:text-gray-400 mb-1 block">中心頻率 f_0
(Hz)</label><input type="number" v-model.number="params.sogi_f0"
@change="applyFilterDesign"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-2 text-sm text-slate-900 dark:text-gray-100">
</div>
<div><label class="text-xs text-slate-600 dark:text-gray-400 mb-1 block">阻尼因數
k</label><input type="number" v-model.number="params.sogi_k"
@change="applyFilterDesign" step="0.001"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded p-2 text-sm text-slate-900 dark:text-gray-100">
</div>
</div>
</div>
</section>
<hr class="border-slate-200 dark:border-gray-800">
<!-- 系統參數 -->
<section>
<h2 class="text-sm font-semibold text-slate-600 dark:text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
系統參數</h2>
<div class="space-y-3">
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 flex justify-between mb-1"><span>取樣頻率
fs
(Hz)</span><span class="role-text-primary">{{ 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 role-focus-primary outline-none transition-all text-slate-900 dark:text-gray-100">
</div>
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 flex justify-between mb-1"><span>系統增益
K (System Gain)</span><span class="role-text-primary">{{ 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 role-focus-primary outline-none transition-all text-slate-900 dark:text-gray-100">
</div>
</div>
</section>
<hr class="border-slate-200 dark:border-gray-800">
<!-- 係數手動設定 -->
<section class="coeff-section">
<h2
class="text-sm font-semibold text-slate-600 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
<span class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 7h16M4 12h16M4 17h16M8 7v10M16 7v10"></path>
</svg>
濾波器係數設定</span>
<div class="flex items-center gap-2">
<button v-if="isManualDirty" @click="clearManualAdjustments"
class="text-[10px] font-bold border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 px-3 py-1 rounded-full transition-colors uppercase mr-1">重設</button>
</div>
</h2>
<div class="mb-3">
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">前饋分子係數 b (b0 ~ b6)</label>
<!-- b 係數標記顯示 -->
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="item in floatingCoeffs.b" :key="item.label"
class="bg-slate-700 dark:bg-gray-300 px-1.5 py-0.5 rounded border border-transparent flex gap-1 items-center shadow-sm">
<span class="text-[9px] text-white dark:text-gray-900 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-white dark:text-gray-900 font-bold">{{ item.val
}}</span>
</div>
</div>
<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 role-focus-primary outline-none font-mono resize-none leading-relaxed role-text-primary"></textarea>
</div>
<!-- b 係數倍率微調 -->
<details :open="activeCoeffAdjustment === 'bScale'"
class="mb-3 bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-lg">
<summary @click.prevent="toggleCoeffAdjustment('bScale')"
class="text-sm text-slate-600 dark:text-gray-400 px-3 py-3 cursor-pointer hover:text-slate-700 dark:hover:text-gray-300">
b 係數倍率微調</summary>
<div class="px-3 pb-3">
<div class="selector-button-group flex items-center gap-2 mb-2 mt-1">
<span class="text-xs text-slate-500 dark:text-gray-500">靈敏度:</span>
<button v-for="s in ['1%','2x','10x']" :key="s" @click="sense_b = s"
:class="sense_b === s ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-600 dark:text-gray-400'"
class="selector-button btn-small px-3 py-1.5 rounded text-xs font-medium border border-slate-300 dark:border-gray-700">{{
s }}</button>
</div>
<template v-for="i in 7" :key="'bs'+i">
<div v-if="baseB[i-1] !== 0">
<label class="text-xs text-slate-500 dark:text-gray-500 flex justify-between mt-1">
<span>b{{ i-1 }} (基準: {{ (baseB[i-1] * (systemGain || 1.0)).toPrecision(6)
}})</span>
<span v-if="outOfRangeB[i-1]" class="role-text-warning font-bold animate-pulse">超出滑桿範圍</span>
</label>
<input type="range" v-model.number="bSliders[i-1]" @input="updateFromControls"
:min="-maxLogB" :max="maxLogB" :step="maxLogB/100"
class="w-full role-range-primary">
</div>
</template>
</div>
</details>
<div class="mb-3">
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">回饋分母係數 a (a0=1, a1 ~
a6)</label>
<!-- a 係數標記顯示 -->
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="item in floatingCoeffs.a" :key="item.label"
class="bg-slate-700 dark:bg-gray-300 px-1.5 py-0.5 rounded border border-transparent flex gap-1 items-center shadow-sm">
<span class="text-[9px] text-white dark:text-gray-900 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-white dark:text-gray-900 font-bold">{{ item.val
}}</span>
</div>
</div>
<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 role-focus-primary outline-none font-mono resize-none leading-relaxed role-text-primary"></textarea>
</div>
<!-- a 係數倍率微調 -->
<details :open="activeCoeffAdjustment === 'aScale'"
class="bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-lg">
<summary @click.prevent="toggleCoeffAdjustment('aScale')"
class="text-sm text-slate-600 dark:text-gray-400 px-3 py-3 cursor-pointer hover:text-slate-700 dark:hover:text-gray-300">
a 係數倍率微調</summary>
<div class="px-3 pb-3">
<div class="selector-button-group flex items-center gap-2 mb-2 mt-1">
<span class="text-xs text-slate-500 dark:text-gray-500">靈敏度:</span>
<button v-for="s in ['1%','2x','10x']" :key="s" @click="sense_a = s"
:class="sense_a === s ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-600 dark:text-gray-400'"
class="selector-button px-3 py-1.5 rounded text-xs font-medium border border-slate-300 dark:border-gray-700">{{
s }}</button>
</div>
<template v-for="i in 6" :key="'as'+i">
<div v-if="baseA[i] !== 0">
<label class="text-xs text-slate-500 dark:text-gray-500 flex justify-between mt-1">
<span>a{{ i }} (基準: {{ baseA[i].toPrecision(6) }})</span>
<span v-if="outOfRangeA[i]" class="role-text-warning font-bold animate-pulse">超出滑桿範圍</span>
</label>
<input type="range" v-model.number="aSliders[i]" @input="updateFromControls"
:min="-maxLogA" :max="maxLogA" :step="maxLogA/100"
class="w-full role-range-primary">
</div>
</template>
</div>
</details>
<!-- 進階 a1/a2 微調 -->
<details :open="activeCoeffAdjustment === 'a1a2'"
class="mt-3 bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-lg">
<summary @click.prevent="toggleCoeffAdjustment('a1a2')"
class="text-sm text-slate-600 dark:text-gray-400 px-3 py-3 cursor-pointer hover:text-slate-700 dark:hover:text-gray-300">
a1/a2 步階微調</summary>
<div class="px-3 pb-3">
<div class="mb-4">
<div class="grid grid-cols-[3.25rem_1fr_3.25rem] gap-2 mb-3">
<button type="button" @click="adjustAFineStep(-1)" title="調粗"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-sm font-bold shadow-sm">
</button>
<div class="min-w-0 rounded-lg border role-border-primary-soft role-bg-primary-soft px-3 h-[42px] flex flex-col justify-center text-center">
<span class="block text-[9px] role-text-primary-muted role-text-primary font-bold">微調量</span>
<span class="block truncate font-mono text-sm font-bold role-text-primary leading-none mt-0.5" :title="formatAFineDelta()">
+/-{{ formatAFineDelta() }}
</span>
</div>
<button type="button" @click="adjustAFineStep(1)" title="調細"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-sm font-bold shadow-sm">
</button>
</div>
<div class="text-[11px] font-semibold text-slate-500 dark:text-gray-500 mb-2">
{{ isTouchInput ? '觸控拖曳微調' : '直接輸入 / shift + 滾輪微調' }}
</div>
<div class="grid grid-cols-2 gap-2">
<article v-if="!isTouchInput" class="block">
<span class="text-[9px] text-slate-500 block font-bold mb-1">δ</span>
<div class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startA1A2Repeat('delta', -1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="formatFineCoeff(currentDelta)" :step="aFineStep"
@change="setAFineDirect('delta', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleAFineWheel('delta', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-xs font-mono text-center role-text-primary">
<button type="button" @pointerdown.prevent="startA1A2Repeat('delta', 1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
</article>
<button v-else type="button" @pointerdown.prevent="startAFineDrag('delta', $event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-[9px] text-slate-500 block font-bold mb-1">δ</span>
<span class="block max-w-full truncate text-base font-mono font-bold role-text-primary" :title="formatFineCoeff(currentDelta)">{{ formatFineCoeff(currentDelta) }}</span>
</button>
<article v-if="!isTouchInput" class="block">
<span class="text-[9px] text-slate-500 block font-bold mb-1">r</span>
<div class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startA1A2Repeat('r', -1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="formatFineCoeff(currentR)" :step="aFineStep"
@change="setAFineDirect('r', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleAFineWheel('r', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-xs font-mono text-center role-text-primary">
<button type="button" @pointerdown.prevent="startA1A2Repeat('r', 1)" @pointerup="stopA1A2Repeat" @pointerleave="stopA1A2Repeat" @pointercancel="stopA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
</article>
<button v-else type="button" @pointerdown.prevent="startAFineDrag('r', $event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-slate-400 active:ring-2 active:ring-slate-400/20">
<span class="text-[9px] text-slate-500 block font-bold mb-1">r</span>
<span class="block max-w-full truncate text-base font-mono font-bold role-text-primary" :title="formatFineCoeff(currentR)">{{ formatFineCoeff(currentR) }}</span>
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-2 mb-4">
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">a1 = -1 - r</span>
<span class="block max-w-full truncate text-sm font-mono font-bold role-text-primary-muted" :title="formatFineCoeff(currentA1)">{{ formatFineCoeff(currentA1) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">a2 = r + δ</span>
<span class="block max-w-full truncate text-sm font-mono font-bold role-text-primary-muted" :title="formatFineCoeff(currentA2)">{{ formatFineCoeff(currentA2) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">差值 a1-a2</span>
<span class="block max-w-full truncate text-xs font-mono text-slate-700 dark:text-gray-300" :title="formatFineCoeff(currentA1A2Diff)">{{ formatFineCoeff(currentA1A2Diff) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">總和 a1+a2</span>
<span class="block max-w-full truncate text-xs font-mono text-slate-700 dark:text-gray-300" :title="formatFineCoeff(currentA1A2Sum)">{{ formatFineCoeff(currentA1A2Sum) }}</span>
</div>
</div>
<div class="role-bg-warning-soft border role-border-warning-soft rounded-lg p-3">
<p class="text-[10px] role-text-warning leading-relaxed">
<span class="font-bold">運算說明</span><br>
δ = a1 + a2 + 1r = -1 - a1更改數值會自動更新圖表桌機可用 +/- 或 shift + 滾輪,觸控時按住數值面板上下拖曳。
</p>
</div>
</div>
</details>
</section>
<hr class="border-slate-200 dark:border-gray-800">
<!-- 定點數轉換 -->
<section class="fixed-section">
<h2
class="text-sm font-semibold text-slate-600 dark:text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 7h6M9 12h6M9 17h3M5 5a2 2 0 012-2h7l5 5v11a2 2 0 01-2 2H7a2 2 0 01-2-2V5z"></path>
</svg>
<span>定點數係數轉換</span>
<span v-if="shiftBitsB > 0"
class="text-xs role-bg-primary-soft role-text-primary px-2 py-1 rounded">b Q{{
shiftBitsB }}</span>
<span v-if="shiftBitsA > 0"
class="text-xs role-bg-primary-soft role-text-primary px-2 py-1 rounded">a Q{{
shiftBitsA }}</span>
</div>
<div class="flex items-center gap-2">
<button v-if="isFixedPointDirty" @click="resetFixedPointSettings"
class="text-[10px] font-bold border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 px-3 py-1 rounded-full transition-colors uppercase mr-1">重設</button>
</div>
</h2>
<div class="mb-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">b 左移位元數 (Shift Bits)</label>
<div v-if="!isTouchInput" class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startShiftBitRepeat('b', -1)" @pointerup="stopShiftBitRepeat" @pointerleave="stopShiftBitRepeat" @pointercancel="stopShiftBitRepeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="shiftBitsB" min="0"
@change="setShiftBits('b', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleShiftBitWheel('b', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-center text-base role-focus-primary outline-none transition-all text-slate-900 dark:text-gray-100">
<button type="button" @pointerdown.prevent="startShiftBitRepeat('b', 1)" @pointerup="stopShiftBitRepeat" @pointerleave="stopShiftBitRepeat" @pointercancel="stopShiftBitRepeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
<button v-else type="button" @pointerdown.prevent="startShiftBitDrag('b', $event)"
class="touch-none select-none block w-24 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-lg font-mono font-bold role-text-primary">{{ shiftBitsB }}</span>
</button>
<p class="text-[10px] text-slate-500 mt-1">b scaling: 2^{{ shiftBitsB }} ({{
Math.pow(2, shiftBitsB) }})</p>
</div>
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">a 左移位元數 (Shift Bits)</label>
<div v-if="!isTouchInput" class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startShiftBitRepeat('a', -1)" @pointerup="stopShiftBitRepeat" @pointerleave="stopShiftBitRepeat" @pointercancel="stopShiftBitRepeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="shiftBitsA" min="0"
@change="setShiftBits('a', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleShiftBitWheel('a', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-center text-base role-focus-primary outline-none transition-all text-slate-900 dark:text-gray-100">
<button type="button" @pointerdown.prevent="startShiftBitRepeat('a', 1)" @pointerup="stopShiftBitRepeat" @pointerleave="stopShiftBitRepeat" @pointercancel="stopShiftBitRepeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
<button v-else type="button" @pointerdown.prevent="startShiftBitDrag('a', $event)"
class="touch-none select-none block w-24 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-lg font-mono font-bold role-text-primary">{{ shiftBitsA }}</span>
</button>
<p class="text-[10px] text-slate-500 mt-1">a scaling: 2^{{ shiftBitsA }} ({{
Math.pow(2, shiftBitsA) }})</p>
</div>
</div>
<div class="space-y-4">
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">定點分子係數 b
(Integers)</label>
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="(item, idx) in fixedPointCoeffs.b" :key="item.label"
:class="item.isOverridden ? 'border-amber-400 dark:border-amber-500 ring-1 ring-amber-400 dark:ring-amber-500' : 'border-transparent'"
class="bg-slate-700 dark:bg-gray-300 px-1.5 py-0.5 rounded border flex items-center gap-1 transition-all shadow-sm">
<span class="text-[9px] text-white dark:text-gray-900 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-white dark:text-gray-900 font-bold"
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
</div>
</div>
<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 role-focus-primary outline-none font-mono resize-none leading-relaxed role-text-primary"></textarea>
</div>
<div>
<label class="text-sm text-slate-600 dark:text-gray-400 mb-1 block">定點分母係數 a
(Integers)</label>
<div class="flex flex-wrap gap-1 mb-2">
<div v-for="(item, idx) in fixedPointCoeffs.a" :key="item.label"
:class="item.isOverridden ? 'border-amber-400 dark:border-amber-500 ring-1 ring-amber-400 dark:ring-amber-500' : 'border-transparent'"
class="bg-slate-700 dark:bg-gray-300 px-1.5 py-0.5 rounded border flex items-center gap-1 transition-all shadow-sm">
<span class="text-[9px] text-white dark:text-gray-900 font-bold">{{ item.label }}:</span>
<span class="text-[10px] font-mono text-white dark:text-gray-900 font-bold"
:class="{ 'italic': !item.isOverridden }">{{ item.val }}</span>
</div>
</div>
<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 role-focus-primary outline-none font-mono resize-none leading-relaxed role-text-primary"></textarea>
</div>
<details class="mt-3 bg-slate-50 dark:bg-gray-900 border border-slate-200 dark:border-gray-800 rounded-lg">
<summary class="text-sm text-slate-600 dark:text-gray-400 px-3 py-3 cursor-pointer hover:text-slate-700 dark:hover:text-gray-300">
a1/a2 步階微調</summary>
<div class="px-3 pb-3">
<div class="mb-4">
<div class="grid grid-cols-[3.25rem_1fr_3.25rem] gap-2 mb-3">
<button type="button" @click="adjustFixedAFineStep(-1)" title="調粗"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-sm font-bold shadow-sm"></button>
<div class="min-w-0 rounded-lg border role-border-primary-soft role-bg-primary-soft px-3 h-[42px] flex flex-col justify-center text-center">
<span class="block text-[9px] role-text-primary-muted role-text-primary font-bold">微調量</span>
<span class="block truncate font-mono text-sm font-bold role-text-primary leading-none mt-0.5" :title="formatFixedAFineDelta()">+/-{{ formatFixedAFineDelta() }}</span>
</div>
<button type="button" @click="adjustFixedAFineStep(1)" title="調細"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-sm font-bold shadow-sm"></button>
</div>
<div class="text-[11px] font-semibold text-slate-500 dark:text-gray-500 mb-2">
{{ isTouchInput ? '觸控拖曳微調' : '直接輸入 / shift + 滾輪微調' }}
</div>
<div class="grid grid-cols-2 gap-2">
<article v-if="!isTouchInput" class="block">
<span class="text-[9px] text-slate-500 block font-bold mb-1">δ</span>
<div class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startFixedA1A2Repeat('delta', -1)" @pointerup="stopFixedA1A2Repeat" @pointerleave="stopFixedA1A2Repeat" @pointercancel="stopFixedA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="formatFixedFineCoeff(fixedCurrentDelta)" :step="fixedAFineStep"
@change="setFixedAFineDirect('delta', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleFixedAFineWheel('delta', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-xs font-mono text-center role-text-primary">
<button type="button" @pointerdown.prevent="startFixedA1A2Repeat('delta', 1)" @pointerup="stopFixedA1A2Repeat" @pointerleave="stopFixedA1A2Repeat" @pointercancel="stopFixedA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
</article>
<button v-else type="button" @pointerdown.prevent="startFixedAFineDrag('delta', $event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-[9px] text-slate-500 block font-bold mb-1">δ</span>
<span class="block max-w-full truncate text-base font-mono font-bold role-text-primary" :title="formatFixedFineCoeff(fixedCurrentDelta)">{{ formatFixedFineCoeff(fixedCurrentDelta) }}</span>
</button>
<article v-if="!isTouchInput" class="block">
<span class="text-[9px] text-slate-500 block font-bold mb-1">r</span>
<div class="grid grid-cols-[2rem_1fr_2rem] gap-1">
<button type="button" @pointerdown.prevent="startFixedA1A2Repeat('r', -1)" @pointerup="stopFixedA1A2Repeat" @pointerleave="stopFixedA1A2Repeat" @pointercancel="stopFixedA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="formatFixedFineCoeff(fixedCurrentR)" :step="fixedAFineStep"
@change="setFixedAFineDirect('r', $event.target.value)" @keydown="onlyNumberKey"
@wheel="handleFixedAFineWheel('r', $event)"
class="min-w-0 w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-xs font-mono text-center role-text-primary">
<button type="button" @pointerdown.prevent="startFixedA1A2Repeat('r', 1)" @pointerup="stopFixedA1A2Repeat" @pointerleave="stopFixedA1A2Repeat" @pointercancel="stopFixedA1A2Repeat"
class="stepper-button w-full h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
</article>
<button v-else type="button" @pointerdown.prevent="startFixedAFineDrag('r', $event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center active:border-slate-400 active:ring-2 active:ring-slate-400/20">
<span class="text-[9px] text-slate-500 block font-bold mb-1">r</span>
<span class="block max-w-full truncate text-base font-mono font-bold role-text-primary" :title="formatFixedFineCoeff(fixedCurrentR)">{{ formatFixedFineCoeff(fixedCurrentR) }}</span>
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-2 mb-4">
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">a1 = -a0 - r</span>
<span class="block max-w-full truncate text-sm font-mono font-bold role-text-primary-muted" :title="formatFixedFineCoeff(fixedCurrentA1)">{{ formatFixedFineCoeff(fixedCurrentA1) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">a2 = r + δ</span>
<span class="block max-w-full truncate text-sm font-mono font-bold role-text-primary-muted" :title="formatFixedFineCoeff(fixedCurrentA2)">{{ formatFixedFineCoeff(fixedCurrentA2) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">差值 a1-a2</span>
<span class="block max-w-full truncate text-xs font-mono text-slate-700 dark:text-gray-300" :title="formatFixedFineCoeff(fixedCurrentA1A2Diff)">{{ formatFixedFineCoeff(fixedCurrentA1A2Diff) }}</span>
</div>
<div class="min-w-0 bg-white dark:bg-gray-800 border border-slate-100 dark:border-gray-700 rounded-lg p-2 text-center shadow-sm">
<span class="text-[9px] text-slate-500 block font-bold mb-1">總和 a1+a2</span>
<span class="block max-w-full truncate text-xs font-mono text-slate-700 dark:text-gray-300" :title="formatFixedFineCoeff(fixedCurrentA1A2Sum)">{{ formatFixedFineCoeff(fixedCurrentA1A2Sum) }}</span>
</div>
</div>
<div class="role-bg-warning-soft border role-border-warning-soft rounded-lg p-3">
<p class="text-[10px] role-text-warning leading-relaxed">
<span class="font-bold">運算說明</span><br>
a0 = 2^{{ shiftBitsA }}a1 = -a0 - ra2 = r + δ
</p>
</div>
</div>
</details>
</div>
<p class="text-[10px] text-slate-500 dark:text-gray-500 mt-3 italic">b 係數乘以 2^{{ shiftBitsB }}a
係數乘以 2^{{ shiftBitsA }} 後四捨五入可用於定點數 DSP 實作</p>
</section>
</aside>
<!-- 右側主視窗 (RWD: 手機版=圖表頁籤全螢幕桌面版=右側主區) -->
<main :class="mobileTab === 'chart' ? 'flex' : 'hidden'"
class="lg:flex flex-1 flex-col overflow-y-auto bg-slate-50 dark:bg-[#0f0f0f] lg:p-6 gap-0 lg:gap-6 custom-scrollbar relative transition-colors duration-300">
<div v-if="globalError"
class="absolute top-4 right-6 role-bg-error-soft border role-border-error role-text-error role-text-on-fill px-4 py-3 rounded-lg shadow-lg text-base z-50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ globalError }}
<button @click="globalError = null"
class="ml-2 role-hover-error">&times;</button>
</div>
<!-- Bode Plot -->
<div
class="chart-panel bg-white dark:bg-dark lg:rounded-xl border-b lg:border border-slate-100 lg:border-slate-200 dark:border-gray-800 lg:dark:border-gray-800 lg:shadow-md p-0 lg:p-1 flex-shrink-0 relative transition-colors duration-300">
<div
class="hidden lg:block absolute inset-0 bg-gradient-to-br role-surface-gradient pointer-events-none">
</div>
<div
class="chart-panel-header px-5 py-4 border-b border-slate-100 dark:border-gray-800 flex justify-between items-center bg-white dark:bg-dark backdrop-blur-sm z-10 relative">
<h3 class="font-bold text-slate-800 dark:text-gray-200 tracking-wide flex items-center gap-2">
<svg class="w-5 h-5 role-text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 19V5m0 14h16M7 16l3-4 3 2 4-7"></path>
</svg>
頻率響應 (Frequency Response)</h3>
<div v-if="loadingBode"
class="text-sm role-text-primary role-bg-primary-soft px-3 py-1.5 rounded border role-border-primary-soft flex items-center gap-2">
<span class="w-2 h-2 rounded-full role-dot-primary animate-ping"></span>
重新計算中...
</div>
</div>
<!-- 只有圖表繪圖區可水平滾動卡片外框不動 -->
<div class="chart-panel-body overflow-x-auto bg-white dark:bg-dark">
<div id="bodePlot" class="min-w-[580px] w-full h-[800px] z-10 relative bg-white dark:bg-dark">
</div>
</div>
</div>
<!-- 時域分析 -->
<div
class="chart-panel bg-white dark:bg-dark lg:rounded-xl border-b lg:border border-slate-100 lg:border-slate-200 dark:border-gray-100 lg:dark:border-gray-800 lg:shadow-md p-0 lg:p-1 flex-shrink-0 relative transition-colors duration-300">
<div
class="chart-panel-header px-5 py-4 border-b border-slate-100 dark:border-gray-800 bg-white dark:bg-dark backdrop-blur-sm z-10 relative">
<h3 class="font-bold text-slate-800 dark:text-gray-200 tracking-wide flex items-center gap-2">
<svg class="w-5 h-5 role-text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 12h4l2-6 4 12 2-6h4"></path>
</svg>
時域訊號探討</h3>
</div>
<div class="p-6 z-10 relative bg-white dark:bg-dark transition-colors duration-300">
<p class="text-base text-slate-600 dark:text-gray-400 mb-4">請上傳包含時域訊號的 CSV 檔案系統會使用上述設定的差分濾波器
</p>
<div
class="flex items-center flex-wrap gap-4 mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg border border-slate-100 dark:border-gray-700 shadow-sm">
<label
class="cursor-pointer role-bg-primary role-hover-primary role-text-on-fill px-5 py-3 rounded-lg text-base font-semibold transition-all shadow-md role-shadow-primary flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg>
上傳輸入訊號 (CSV)
<input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload"
class="hidden">
</label>
<span v-if="csvFile" class="text-base role-text-primary font-mono">{{
csvFile.name }}</span>
<span v-if="csvInfo"
class="text-sm text-slate-600 dark:text-gray-400 font-mono">
{{ csvInfo.rows }} / {{ csvInfo.columns }}
</span>
<span v-if="csvParseError"
class="text-sm role-text-error font-semibold">
{{ csvParseError }}
</span>
<div v-if="csvColumns.length > 0"
class="h-8 border-l border-slate-200 dark:border-gray-700 mx-2"></div>
<div v-if="csvColumns.length > 0" class="flex items-center gap-2">
<span class="text-sm text-slate-500">訊號欄位:</span>
<select v-model="selectedColumn"
class="bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-base role-focus-primary outline-none text-slate-900 dark:text-gray-100">
<option v-for="(col, idx) in csvColumns" :key="idx" :value="idx">{{ col }}</option>
</select>
</div>
<button v-if="csvFile" @click="processFilter"
class="role-bg-primary role-hover-primary role-text-on-fill px-6 py-3 rounded-lg text-base font-semibold transition-all shadow-md role-shadow-primary disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="loadingFilter || csvColumns.length === 0">
{{ loadingFilter ? '處理中...' : '執行當前濾波器' }}
</button>
<button v-if="filterDone" @click="downloadCsv"
class="ml-auto bg-slate-700 hover:bg-slate-600 role-text-on-fill px-5 py-3 rounded-lg text-base font-semibold transition-all flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
匯出結果
</button>
</div>
<!-- V2: 定點數時域設定 -->
<div
class="mb-6 p-4 bg-slate-50 dark:bg-gray-900/50 rounded-lg border border-slate-100 dark:border-gray-800">
<h4
class="text-sm font-semibold text-slate-600 dark:text-gray-400 mb-4 uppercase tracking-tight flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 7h6M9 12h6M9 17h3M5 5a2 2 0 012-2h7l5 5v11a2 2 0 01-2 2H7a2 2 0 01-2-2V5z">
</path>
</svg>
定點數模擬設定 (Fixed-Point Simulation Settings)
</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-6">
<div>
<label
class="text-sm text-slate-600 dark:text-gray-400 mb-2 block font-bold">輸入訊號量化
(Q_in): 2^{{ shiftBitsIn }}</label>
<div v-if="!isTouchInput"
class="grid grid-cols-[2.5rem_1fr_2.5rem] gap-2 items-center">
<button type="button" @pointerdown.prevent="startShiftInRepeat(-1)"
@pointerup="stopShiftInRepeat" @pointerleave="stopShiftInRepeat"
@pointercancel="stopShiftInRepeat"
class="stepper-button h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="shiftBitsIn"
@change="setShiftBitsIn($event.target.value)"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-center font-mono text-sm role-text-primary outline-none">
<button type="button" @pointerdown.prevent="startShiftInRepeat(1)"
@pointerup="stopShiftInRepeat" @pointerleave="stopShiftInRepeat"
@pointercancel="stopShiftInRepeat"
class="stepper-button h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
<button v-else type="button" @pointerdown.prevent="startShiftInDrag($event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-base font-mono font-bold role-text-primary">{{ shiftBitsIn }}
bits</span>
</button>
</div>
<div>
<label
class="text-sm text-slate-600 dark:text-gray-400 mb-2 block font-bold">輸出訊號量化
(Q_out): 2^{{ shiftBitsOut }}</label>
<div v-if="!isTouchInput"
class="grid grid-cols-[2.5rem_1fr_2.5rem] gap-2 items-center">
<button type="button" @pointerdown.prevent="startShiftOutRepeat(-1)"
@pointerup="stopShiftOutRepeat" @pointerleave="stopShiftOutRepeat"
@pointercancel="stopShiftOutRepeat"
class="stepper-button h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">-</button>
<input type="number" :value="shiftBitsOut"
@change="setShiftBitsOut($event.target.value)"
class="w-full bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg px-2 h-[42px] text-center font-mono text-sm role-text-primary outline-none">
<button type="button" @pointerdown.prevent="startShiftOutRepeat(1)"
@pointerup="stopShiftOutRepeat" @pointerleave="stopShiftOutRepeat"
@pointercancel="stopShiftOutRepeat"
class="stepper-button h-[42px] flex items-center justify-center rounded-full border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-slate-700 dark:text-gray-300 hover:bg-slate-100 dark:hover:bg-gray-700 transition-all duration-150 active:scale-90 text-lg font-bold shadow-sm">+</button>
</div>
<button v-else type="button" @pointerdown.prevent="startShiftOutDrag($event)"
class="touch-none select-none w-28 mx-auto bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700 rounded-lg p-3 text-center role-active-border-primary active:ring-2 role-active-ring-primary">
<span class="text-base font-mono font-bold role-text-primary">{{ shiftBitsOut }}
bits</span>
</button>
</div>
<div>
<label
class="text-sm text-slate-600 dark:text-gray-400 mb-2 block font-bold">位移捨去模式
(Truncation)</label>
<div class="selector-button-group flex gap-2 h-[42px]">
<button type="button" @click="setUseRound(false)"
:class="!useRound ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-700 dark:text-gray-400 border-slate-300 dark:border-gray-700'"
class="selector-button flex-1 rounded py-2 text-sm font-medium border transition-colors">
Floor (SRA)
</button>
<button type="button" @click="setUseRound(true)"
:class="useRound ? 'primary-action role-bg-primary role-text-on-fill role-border-primary' : 'bg-slate-200 dark:bg-gray-800 text-slate-700 dark:text-gray-400 border-slate-300 dark:border-gray-700'"
class="selector-button flex-1 rounded py-2 text-sm font-medium border transition-colors">
Round (+0.5)
</button>
</div>
</div>
</div>
</div>
<!-- 波形比較圖 -->
<h4 v-if="filterDone" class="text-base font-bold text-slate-800 dark:text-gray-200 mb-3">波形比較圖
</h4>
<!-- 只有圖表繪圖區可水平滾動卡片外框不動 -->
<div class="overflow-x-auto bg-white dark:bg-dark">
<div id="timePlot" class="min-w-[580px] w-full h-[400px] bg-white dark:bg-dark"
v-show="filterDone"></div>
</div>
<div v-if="!filterDone && !loadingFilter && csvPreview.length === 0"
class="h-[300px] flex flex-col items-center justify-center text-slate-500 dark:text-gray-500 border-2 border-dashed border-slate-200 dark:border-gray-800 rounded-xl bg-slate-50/50 dark:bg-gray-900/30">
<svg class="w-12 h-12 mb-3 opacity-50" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
</path>
</svg>
<p>請上傳包含時間序列的 CSV 檔案進行測試</p>
</div>
</div>
</div>
</main>
</div>
<!-- 全域置底版權宣告 -->
<footer
class="bg-white dark:bg-dark border-t border-slate-200 dark:border-gray-800 py-2 text-center z-20 transition-colors duration-300">
<p class="text-xs text-slate-500 dark:text-gray-600">© 2026 喆富創新科技股份有限公司. All rights reserved.</p>
</footer>
</div>
</template>
<script src="./app-options.js"></script>