feat: 迁移信号发生器前端至底栏
This commit is contained in:
parent
cbb83d3dcd
commit
3c52110a2f
|
@ -67,6 +67,17 @@
|
|||
<Hand class="icon" />
|
||||
嵌入式逻辑分析仪
|
||||
</label>
|
||||
<label class="tab">
|
||||
<input
|
||||
type="radio"
|
||||
name="function-bar"
|
||||
id="7"
|
||||
:checked="checkID === 7"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<Signature class="icon" />
|
||||
信号发生器
|
||||
</label>
|
||||
<!-- 全屏按钮 -->
|
||||
<button
|
||||
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
|
||||
|
@ -93,7 +104,10 @@
|
|||
<LogicAnalyzerView />
|
||||
</div>
|
||||
<div v-else-if="checkID === 6" class="h-full overflow-y-auto">
|
||||
<Debugger />
|
||||
<DebuggerView />
|
||||
</div>
|
||||
<div v-else-if="checkID === 7" class="h-full overflow-y-auto">
|
||||
<DDSCtrlView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,15 +123,17 @@ import {
|
|||
Binary,
|
||||
Hand,
|
||||
Monitor,
|
||||
Signature,
|
||||
} from "lucide-vue-next";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
||||
import HdmiVideoStreamView from "@/views/Project/HdmiVideoStream.vue";
|
||||
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
||||
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
|
||||
import DebuggerView from "./Debugger.vue";
|
||||
import DDSCtrlView from "./DDSCtrl.vue";
|
||||
import { isNull, toNumber } from "lodash";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import Debugger from "./Debugger.vue";
|
||||
import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer";
|
||||
import { useProvideOscilloscope } from "@/components/Oscilloscope/OscilloscopeManager";
|
||||
|
||||
|
|
|
@ -0,0 +1,884 @@
|
|||
<template>
|
||||
<div
|
||||
class="dds-controller min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 dark:from-slate-900 dark:to-blue-900 p-4"
|
||||
>
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<!-- 左侧: 波形显示和自定义波形区域 (xl屏幕时占2列) -->
|
||||
<div class="xl:col-span-2 space-y-6">
|
||||
<!-- 波形显示区 -->
|
||||
<div
|
||||
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-xl border border-slate-200/50 dark:border-slate-700/50 hover:shadow-2xl transition-all duration-300"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div
|
||||
class="p-2 rounded-lg bg-gradient-to-r from-green-400 to-green-600"
|
||||
>
|
||||
<Signature class="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-200">
|
||||
实时波形显示
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 波形显示容器 -->
|
||||
<div
|
||||
ref="waveformContainer"
|
||||
class="relative bg-slate-900 rounded-xl p-4 border-2 border-slate-700 overflow-hidden group"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
></div>
|
||||
<svg
|
||||
:width="svgWidth"
|
||||
:height="svgHeight"
|
||||
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
|
||||
class="w-full h-auto transition-all duration-500 ease-in-out"
|
||||
style="min-height: 300px"
|
||||
>
|
||||
<!-- 背景网格 -->
|
||||
<defs>
|
||||
<pattern
|
||||
id="grid"
|
||||
width="20"
|
||||
height="20"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path
|
||||
d="M 20 0 L 0 0 0 20"
|
||||
fill="none"
|
||||
stroke="#334155"
|
||||
stroke-width="0.5"
|
||||
opacity="0.3"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect :width="svgWidth" :height="svgHeight" fill="url(#grid)" />
|
||||
|
||||
<!-- 波形路径 -->
|
||||
<path
|
||||
:d="currentWaveformPath"
|
||||
stroke="url(#waveGradient)"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
class="animate-pulse"
|
||||
filter="url(#glow)"
|
||||
/>
|
||||
|
||||
<!-- 渐变定义 -->
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="waveGradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="0%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
style="stop-color: #10b981; stop-opacity: 1"
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
style="stop-color: #06d6a0; stop-opacity: 1"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
style="stop-color: #0891b2; stop-opacity: 1"
|
||||
/>
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 信息显示 -->
|
||||
<text
|
||||
x="20"
|
||||
y="30"
|
||||
fill="#10b981"
|
||||
font-size="16"
|
||||
font-weight="bold"
|
||||
class="drop-shadow-sm"
|
||||
>
|
||||
{{ displayFrequency }}
|
||||
</text>
|
||||
<text
|
||||
:x="svgWidth - 80"
|
||||
y="30"
|
||||
fill="#10b981"
|
||||
font-size="16"
|
||||
font-weight="bold"
|
||||
class="drop-shadow-sm"
|
||||
>
|
||||
φ: {{ state.phase }}°
|
||||
</text>
|
||||
<text
|
||||
:x="svgWidth / 2"
|
||||
:y="svgHeight - 10"
|
||||
fill="#10b981"
|
||||
font-size="16"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
class="drop-shadow-sm"
|
||||
>
|
||||
{{ displayTimebase }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义波形区域 (xl屏幕时在波形显示下方) -->
|
||||
<div
|
||||
class="card hidden xl:block bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg border border-slate-200/50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<h3
|
||||
class="font-bold text-xl text-slate-800 dark:text-slate-200 mb-4 flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
class="p-2 rounded-lg bg-gradient-to-r from-purple-400 to-purple-600"
|
||||
>
|
||||
<CodeIcon class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
自定义波形函数
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label
|
||||
class="text-sm font-medium text-slate-700 dark:text-slate-300 min-w-fit"
|
||||
>函数表达式:</label
|
||||
>
|
||||
<input
|
||||
v-model="state.customExpr"
|
||||
class="input input-bordered flex-1 transition-all duration-200 focus:shadow-md focus:scale-[1.02]"
|
||||
placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]"
|
||||
@keyup.enter="applyCustomWaveform"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary font-bold hover:shadow-lg transition-all duration-300 transform hover:scale-105"
|
||||
@click="applyCustomWaveform"
|
||||
>
|
||||
<PlayIcon class="w-4 h-4 mr-2" />
|
||||
应用
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
示例函数:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t)')"
|
||||
>
|
||||
正弦波
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t)^3')"
|
||||
>
|
||||
立方正弦
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="
|
||||
applyExampleFunction(
|
||||
'((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
|
||||
)
|
||||
"
|
||||
>
|
||||
心形函数
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t) + 0.3*sin(3*t)')"
|
||||
>
|
||||
谐波叠加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧控制面板 (xl屏幕时占1列) -->
|
||||
<div class="space-y-6">
|
||||
<!-- 自动应用开关 -->
|
||||
<div
|
||||
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg border border-slate-200/50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="card-body p-4 gap-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Settings class="w-5 h-5 text-blue-600" />
|
||||
<span class="font-semibold text-slate-800 dark:text-slate-200"
|
||||
>自动应用设置</span
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary scale-125"
|
||||
v-model="state.autoApply"
|
||||
id="auto-apply-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 应用按钮 -->
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="btn btn-primary btn-lg w-full shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105"
|
||||
:disabled="state.isApplying"
|
||||
@click="applyWaveSettings"
|
||||
>
|
||||
<span v-if="state.isApplying" class="flex items-center gap-3">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
正在应用设置...
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-3">
|
||||
<Zap class="w-5 h-5" />
|
||||
应用输出波形
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 波形选择 -->
|
||||
<div
|
||||
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg border border-slate-200/50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<h3
|
||||
class="font-bold text-lg text-slate-800 dark:text-slate-200 mb-4"
|
||||
>
|
||||
波形类型
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 xl:grid-cols-2 gap-2">
|
||||
<div
|
||||
v-for="(wave, index) in waveforms"
|
||||
:key="`wave-${index}`"
|
||||
:class="[
|
||||
'btn transition-all duration-300 transform hover:scale-105',
|
||||
state.waveformIndex === index
|
||||
? 'btn-primary shadow-lg shadow-blue-500/25'
|
||||
: 'btn-outline btn-primary hover:shadow-md',
|
||||
]"
|
||||
@click="selectWaveform(index)"
|
||||
>
|
||||
{{ wave.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数控制 -->
|
||||
<div
|
||||
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg border border-slate-200/50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<div class="card-title flex flex-row items-center justify-between">
|
||||
<h3 class="font-bold text-lg text-slate-800 dark:text-slate-200">
|
||||
信号参数
|
||||
</h3>
|
||||
<button
|
||||
@click="resetConfiguration"
|
||||
class="w-8 h-8 bg-transparent text-red-600 text-sm border border-red-200 rounded-md py-2 px-2.5 transition duration-300 ease ring ring-transparent hover:ring-red-600/10 focus:ring-red-600/10 hover:border-red-600 shadow-sm focus:shadow flex items-center justify-center"
|
||||
type="button"
|
||||
title="重置配置"
|
||||
>
|
||||
<RefreshCcw />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 时基控制 -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>时基</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="decreaseTimebase"
|
||||
>
|
||||
<Minus class="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
v-model="state.timebaseInput"
|
||||
@blur="applyTimebaseInput"
|
||||
@keyup.enter="applyTimebaseInput"
|
||||
class="input input-bordered flex-1 text-center transition-all duration-200 focus:shadow-md focus:scale-105"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="increaseTimebase"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 频率控制 -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>频率</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="decreaseFrequency"
|
||||
>
|
||||
<Minus class="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
v-model="state.frequencyInput"
|
||||
@blur="applyFrequencyInput"
|
||||
@keyup.enter="applyFrequencyInput"
|
||||
class="input input-bordered flex-1 text-center transition-all duration-200 focus:shadow-md focus:scale-105"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="increaseFrequency"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 相位控制 -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>相位</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="decreasePhase"
|
||||
>
|
||||
<Minus class="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
v-model="state.phaseInput"
|
||||
@blur="applyPhaseInput"
|
||||
@keyup.enter="applyPhaseInput"
|
||||
class="input input-bordered flex-1 text-center transition-all duration-200 focus:shadow-md focus:scale-105"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-outline btn-primary hover:shadow-md transition-all duration-200"
|
||||
@click="increasePhase"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 小屏幕时自定义波形区域移到最后 -->
|
||||
<div class="xl:hidden">
|
||||
<div
|
||||
class="card bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm shadow-lg border border-slate-200/50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="card-body p-6">
|
||||
<h3
|
||||
class="font-bold text-xl text-slate-800 dark:text-slate-200 mb-4 flex items-center gap-3"
|
||||
>
|
||||
<div
|
||||
class="p-2 rounded-lg bg-gradient-to-r from-purple-400 to-purple-600"
|
||||
>
|
||||
<CodeIcon class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
自定义波形函数
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-start sm:items-center gap-3"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium text-slate-700 dark:text-slate-300 min-w-fit"
|
||||
>函数表达式:</label
|
||||
>
|
||||
<input
|
||||
v-model="state.customExpr"
|
||||
class="input input-bordered flex-1 transition-all duration-200 focus:shadow-md focus:scale-[1.02]"
|
||||
placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]"
|
||||
@keyup.enter="applyCustomWaveform"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary font-bold hover:shadow-lg transition-all duration-300 transform hover:scale-105 w-full sm:w-auto"
|
||||
@click="applyCustomWaveform"
|
||||
>
|
||||
<PlayIcon class="w-4 h-4 mr-2" />
|
||||
应用
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
示例函数:
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:flex sm:flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t)')"
|
||||
>
|
||||
正弦波
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t)^3')"
|
||||
>
|
||||
立方正弦
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="
|
||||
applyExampleFunction(
|
||||
'((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
|
||||
)
|
||||
"
|
||||
>
|
||||
心形函数
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-secondary hover:shadow-md transition-all duration-200 transform hover:scale-105"
|
||||
@click="applyExampleFunction('sin(t) + 0.3*sin(3*t)')"
|
||||
>
|
||||
谐波叠加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { toInteger } from "lodash";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { DDSClient } from "@/APIClient";
|
||||
import { compile, type EvalFunction } from "mathjs";
|
||||
import {
|
||||
Settings,
|
||||
Signature,
|
||||
Plus,
|
||||
Minus,
|
||||
Zap,
|
||||
Play as PlayIcon,
|
||||
Code as CodeIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
// 新增:重置DDS参数
|
||||
function resetConfiguration() {
|
||||
state.value.frequency = 1000;
|
||||
state.value.phase = 0;
|
||||
state.value.timebase = 1;
|
||||
state.value.waveformIndex = 0;
|
||||
state.value.customExpr = "";
|
||||
state.value.frequencyInput = "1.00 kHz";
|
||||
state.value.phaseInput = "0";
|
||||
state.value.timebaseInput = "1.00 s/div";
|
||||
// 清除自定义波形
|
||||
customWaveformFunction.value = null;
|
||||
// 应用重置后的设置
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
// 状态变量
|
||||
const dds = AuthManager.createClient(DDSClient);
|
||||
const eqps = useEquipments();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
// 响应式SVG宽高与容器引用
|
||||
const waveformContainer = ref<HTMLElement | null>(null);
|
||||
const svgWidth = ref(400);
|
||||
const svgHeight = ref(300);
|
||||
|
||||
function updateSvgWidth() {
|
||||
if (waveformContainer.value) {
|
||||
svgWidth.value = waveformContainer.value.offsetWidth || 400;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateSvgWidth();
|
||||
window.addEventListener("resize", updateSvgWidth);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", updateSvgWidth);
|
||||
});
|
||||
|
||||
const state = ref({
|
||||
frequency: 1000,
|
||||
phase: 0,
|
||||
timebase: 1,
|
||||
waveformIndex: 0,
|
||||
customExpr: "",
|
||||
isApplying: false,
|
||||
frequencyInput: "1.00 kHz",
|
||||
phaseInput: "0",
|
||||
timebaseInput: "1.00 s/div",
|
||||
autoApply: false,
|
||||
});
|
||||
|
||||
const waveforms = [
|
||||
{
|
||||
name: "正弦波",
|
||||
type: "sine",
|
||||
fn: (x: number, width: number, height: number, phaseRad: number) =>
|
||||
(height / 2) * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad),
|
||||
},
|
||||
{
|
||||
name: "方波",
|
||||
type: "square",
|
||||
fn: (x: number, width: number, height: number, phaseRad: number) => {
|
||||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||||
return normX < 0.5 ? height / 4 : -height / 4;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "三角波",
|
||||
type: "triangle",
|
||||
fn: (x: number, width: number, height: number, phaseRad: number) => {
|
||||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||||
return height / 2 - height * Math.abs(2 * normX - 1);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "锯齿波",
|
||||
type: "sawtooth",
|
||||
fn: (x: number, width: number, height: number, phaseRad: number) => {
|
||||
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
|
||||
return height / 2 - (height / 2) * (2 * normX);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "自定义",
|
||||
type: "custom",
|
||||
fn: (x: number, width: number, height: number, phaseRad: number) => {
|
||||
if (customWaveformFunction.value) {
|
||||
try {
|
||||
const t = 2 * Math.PI * (x / width) * 2 + phaseRad;
|
||||
// 归一化x到[-1,1],便于自定义表达式使用
|
||||
const xn = (x / width) * 2 - 1;
|
||||
const scope = { t, x, xn, width, height, phaseRad, PI: Math.PI };
|
||||
const y = customWaveformFunction.value.evaluate(scope);
|
||||
if (typeof y === "number" && isFinite(y)) {
|
||||
return y * (height / 2);
|
||||
}
|
||||
return 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 自定义表达式函数引用(mathjs EvalFunction)
|
||||
const customWaveformFunction = ref<EvalFunction | null>(null);
|
||||
|
||||
function formatFrequency(freq: number): string {
|
||||
if (freq >= 1000000) {
|
||||
return `${(freq / 1000000).toFixed(2)} MHz`;
|
||||
} else if (freq >= 1000) {
|
||||
return `${(freq / 1000).toFixed(2)} kHz`;
|
||||
} else {
|
||||
return `${freq.toFixed(2)} Hz`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFrequency(str: string): number {
|
||||
let value = parseFloat(str);
|
||||
if (str.includes("MHz")) value *= 1e6;
|
||||
else if (str.includes("kHz")) value *= 1e3;
|
||||
else if (str.includes("Hz")) value *= 1;
|
||||
return isNaN(value) ? 1000 : value;
|
||||
}
|
||||
|
||||
function formatTimebase(tb: number): string {
|
||||
if (tb < 0.001) {
|
||||
return `${(tb * 1e6).toFixed(0)} μs/div`;
|
||||
} else if (tb < 1) {
|
||||
return `${(tb * 1000).toFixed(0)} ms/div`;
|
||||
} else {
|
||||
return `${tb.toFixed(2)} s/div`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseTimebase(str: string): number {
|
||||
let value = parseFloat(str);
|
||||
if (str.includes("μs")) value /= 1e6;
|
||||
else if (str.includes("ms")) value /= 1e3;
|
||||
else if (str.includes("s")) value /= 1;
|
||||
return isNaN(value) ? 1 : value;
|
||||
}
|
||||
|
||||
const displayTimebase = computed(() => formatTimebase(state.value.timebase));
|
||||
const displayFrequency = computed(() => formatFrequency(state.value.frequency));
|
||||
|
||||
// 生成波形SVG路径
|
||||
const currentWaveformPath = computed(() => {
|
||||
const width = svgWidth.value;
|
||||
const height = svgHeight.value - 60;
|
||||
const xOffset = 0;
|
||||
const yOffset = 40;
|
||||
const currentWaveform = waveforms[state.value.waveformIndex];
|
||||
const phaseRadians = (state.value.phase * Math.PI) / 180;
|
||||
const freqLog = Math.log10(state.value.frequency) - 2;
|
||||
const frequencyFactor = Math.max(0.1, Math.min(10, freqLog));
|
||||
const timebaseFactor = 1 / state.value.timebase;
|
||||
const scaleFactor = timebaseFactor * frequencyFactor;
|
||||
|
||||
let path = `M${xOffset},${yOffset + height / 2}`;
|
||||
const waveFunction = currentWaveform.fn;
|
||||
|
||||
for (let x = 0; x <= width; x++) {
|
||||
const scaledX = x * scaleFactor;
|
||||
const y = waveFunction(scaledX, width, height, phaseRadians);
|
||||
path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
|
||||
}
|
||||
return path;
|
||||
});
|
||||
|
||||
// 只允许number类型key
|
||||
type NumberStateKey = "frequency" | "phase" | "timebase";
|
||||
|
||||
// 通用增减函数(类型安全)
|
||||
function adjustValue(
|
||||
key: NumberStateKey,
|
||||
delta: number,
|
||||
steps: number[],
|
||||
min: number,
|
||||
max: number,
|
||||
) {
|
||||
let v = state.value[key];
|
||||
if (typeof v !== "number") return;
|
||||
let step = steps.find((s) => Math.abs(v) < s * 10) || steps[steps.length - 1];
|
||||
v += delta * step;
|
||||
v = Math.max(min, Math.min(max, v));
|
||||
state.value[key] = parseFloat(v.toFixed(2));
|
||||
if (key === "frequency") {
|
||||
state.value.frequencyInput = formatFrequency(state.value.frequency);
|
||||
}
|
||||
if (key === "phase") {
|
||||
state.value.phaseInput = state.value.phase.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function increaseTimebase() {
|
||||
adjustValue("timebase", 1, [0.001, 0.01, 0.1, 0.5], 0.001, 5);
|
||||
state.value.timebaseInput = formatTimebase(state.value.timebase);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function decreaseTimebase() {
|
||||
adjustValue("timebase", -1, [0.001, 0.01, 0.1, 0.5], 0.001, 5);
|
||||
state.value.timebaseInput = formatTimebase(state.value.timebase);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function applyTimebaseInput() {
|
||||
const value = parseTimebase(state.value.timebaseInput);
|
||||
state.value.timebase = Math.min(Math.max(value, 0.001), 5);
|
||||
state.value.timebaseInput = formatTimebase(state.value.timebase);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function selectWaveform(index: number) {
|
||||
state.value.waveformIndex = index;
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function increaseFrequency() {
|
||||
adjustValue("frequency", 1, [0.1, 1, 10, 100, 1000, 10000], 0.1, 10000000);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function decreaseFrequency() {
|
||||
adjustValue("frequency", -1, [0.1, 1, 10, 100, 1000, 10000], 0.1, 10000000);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function applyFrequencyInput() {
|
||||
const value = parseFrequency(state.value.frequencyInput);
|
||||
state.value.frequency = Math.min(Math.max(value, 0.1), 10000000);
|
||||
state.value.frequencyInput = formatFrequency(state.value.frequency);
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function increasePhase() {
|
||||
adjustValue("phase", 15, [1], 0, 359.99);
|
||||
if (state.value.phase >= 360) state.value.phase -= 360;
|
||||
state.value.phaseInput = state.value.phase.toString();
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function decreasePhase() {
|
||||
adjustValue("phase", -15, [1], 0, 359.99);
|
||||
if (state.value.phase < 0) state.value.phase += 360;
|
||||
state.value.phaseInput = state.value.phase.toString();
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
}
|
||||
|
||||
function applyPhaseInput() {
|
||||
let value = parseFloat(state.value.phaseInput);
|
||||
if (!isNaN(value)) {
|
||||
while (value >= 360) value -= 360;
|
||||
while (value < 0) value += 360;
|
||||
state.value.phase = value;
|
||||
state.value.phaseInput = state.value.phase.toString();
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
} else {
|
||||
state.value.phaseInput = state.value.phase.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义波形表达式
|
||||
function applyCustomWaveform() {
|
||||
if (!state.value.customExpr) {
|
||||
customWaveformFunction.value = null;
|
||||
return;
|
||||
}
|
||||
customWaveformFunction.value = null;
|
||||
try {
|
||||
const expr = state.value.customExpr;
|
||||
const compiled = compile(expr);
|
||||
customWaveformFunction.value = compiled;
|
||||
state.value.waveformIndex = waveforms.findIndex((w) => w.type === "custom");
|
||||
if (state.value.autoApply) applyWaveSettings();
|
||||
} catch (e) {
|
||||
dialog.error("表达式无效");
|
||||
customWaveformFunction.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function applyExampleFunction(expr: string) {
|
||||
state.value.customExpr = expr;
|
||||
applyCustomWaveform();
|
||||
}
|
||||
|
||||
// 应用输出(集中设备操作和错误处理)
|
||||
async function applyWaveSettings() {
|
||||
try {
|
||||
state.value.isApplying = true;
|
||||
const idx = state.value.waveformIndex;
|
||||
const freq = state.value.frequency;
|
||||
const ph = state.value.phase;
|
||||
let ok = true;
|
||||
const ret1 = await dds.setWaveNum(eqps.boardAddr, eqps.boardPort, 0, idx);
|
||||
if (!ret1) ok = false;
|
||||
const ret2 = await dds.setFreq(
|
||||
eqps.boardAddr,
|
||||
eqps.boardPort,
|
||||
0,
|
||||
idx,
|
||||
Math.round((freq * Math.pow(2, 32 - 20)) / 10),
|
||||
);
|
||||
if (!ret2) ok = false;
|
||||
const ret3 = await dds.setPhase(
|
||||
eqps.boardAddr,
|
||||
eqps.boardPort,
|
||||
0,
|
||||
idx,
|
||||
Math.round((ph * 4096) / 360),
|
||||
);
|
||||
if (!ret3) ok = false;
|
||||
if (!ok) dialog.error("应用失败");
|
||||
} catch (e) {
|
||||
dialog.error("应用失败");
|
||||
console.error(e);
|
||||
} finally {
|
||||
state.value.isApplying = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.dds-controller {
|
||||
animation: fadeIn 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 自定义动画 */
|
||||
@keyframes pulse-gentle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse-gentle 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* 渐变文字效果 */
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 毛玻璃效果增强 */
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue