feat: 迁移信号发生器前端至底栏
This commit is contained in:
parent
cbb83d3dcd
commit
3c52110a2f
|
@ -67,6 +67,17 @@
|
||||||
<Hand class="icon" />
|
<Hand class="icon" />
|
||||||
嵌入式逻辑分析仪
|
嵌入式逻辑分析仪
|
||||||
</label>
|
</label>
|
||||||
|
<label class="tab">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="function-bar"
|
||||||
|
id="7"
|
||||||
|
:checked="checkID === 7"
|
||||||
|
@change="handleTabChange"
|
||||||
|
/>
|
||||||
|
<Signature class="icon" />
|
||||||
|
信号发生器
|
||||||
|
</label>
|
||||||
<!-- 全屏按钮 -->
|
<!-- 全屏按钮 -->
|
||||||
<button
|
<button
|
||||||
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
|
class="fullscreen-btn ml-auto btn btn-ghost btn-sm"
|
||||||
|
@ -93,7 +104,10 @@
|
||||||
<LogicAnalyzerView />
|
<LogicAnalyzerView />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="checkID === 6" class="h-full overflow-y-auto">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,15 +123,17 @@ import {
|
||||||
Binary,
|
Binary,
|
||||||
Hand,
|
Hand,
|
||||||
Monitor,
|
Monitor,
|
||||||
|
Signature,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
import VideoStreamView from "@/views/Project/VideoStream.vue";
|
||||||
import HdmiVideoStreamView from "@/views/Project/HdmiVideoStream.vue";
|
import HdmiVideoStreamView from "@/views/Project/HdmiVideoStream.vue";
|
||||||
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
import OscilloscopeView from "@/views/Project/Oscilloscope.vue";
|
||||||
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
|
import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue";
|
||||||
|
import DebuggerView from "./Debugger.vue";
|
||||||
|
import DDSCtrlView from "./DDSCtrl.vue";
|
||||||
import { isNull, toNumber } from "lodash";
|
import { isNull, toNumber } from "lodash";
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import Debugger from "./Debugger.vue";
|
|
||||||
import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer";
|
import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer";
|
||||||
import { useProvideOscilloscope } from "@/components/Oscilloscope/OscilloscopeManager";
|
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