caoziwu 2 هفته پیش
والد
کامیت
50c9301e43

+ 0 - 19
src/main/index.ts

@@ -4,7 +4,6 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
 import icon from '../../resources/icon.png?asset'
 
 function createWindow(): void {
-  // Create the browser window.
   const mainWindow = new BrowserWindow({
     width: 1120,
     height: 860,
@@ -28,8 +27,6 @@ function createWindow(): void {
     return { action: 'deny' }
   })
 
-  // HMR for renderer base on electron-vite cli.
-  // Load the remote URL for development or the local html file for production.
   if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
     mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
   } else {
@@ -37,40 +34,24 @@ function createWindow(): void {
   }
 }
 
-// This method will be called when Electron has finished
-// initialization and is ready to create browser windows.
-// Some APIs can only be used after this event occurs.
 app.whenReady().then(() => {
-  // Set app user model id for windows
   electronApp.setAppUserModelId('com.electron')
 
-  // Default open or close DevTools by F12 in development
-  // and ignore CommandOrControl + R in production.
-  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
   app.on('browser-window-created', (_, window) => {
     optimizer.watchWindowShortcuts(window)
   })
 
-  // IPC test
   ipcMain.on('ping', () => console.log('pong'))
 
   createWindow()
 
   app.on('activate', function () {
-    // On macOS it's common to re-create a window in the app when the
-    // dock icon is clicked and there are no other windows open.
     if (BrowserWindow.getAllWindows().length === 0) createWindow()
   })
 })
 
-// Quit when all windows are closed, except on macOS. There, it's common
-// for applications and their menu bar to stay active until the user quits
-// explicitly with Cmd + Q.
 app.on('window-all-closed', () => {
   if (process.platform !== 'darwin') {
     app.quit()
   }
 })
-
-// In this file you can include the rest of your app's specific main process
-// code. You can also put them in separate files and require them here.

+ 3 - 0
src/renderer/src/assets/simulator.less

@@ -66,6 +66,9 @@
 .panel-card {
   margin-bottom: 18px;
 }
+.reslut-panel {
+  padding: 1rem;
+}
 
 /* 图表卡片:Tab 内容区内边距,避免图表贴边 */
 .chart-card :deep(.n-tabs-nav) {

+ 62 - 17
src/renderer/src/components/simulator/ParamsPanel.vue

@@ -1,15 +1,25 @@
 <script setup lang="ts">
-import { computed, ref, Ref } from 'vue'
+import { computed, ref, Ref, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { devicesConfig } from '../../config'
-import type { DeviceSpec } from '../../lib/speaker/types'
+import type { DeviceSpec, DevicePanelState } from '../../lib/speaker/types'
 import type { NumberParamKey } from '../../lib/speaker/types'
 
 const { t } = useI18n()
-interface SplMaxConfig {
-  standalone: Array<{ label: string; value: number }>
-  extension: Array<{ label: string; value: number }>
-}
+
+const props = defineProps({
+  state: {
+    type: Object,
+    default: () => ({
+      model: 'custom',
+      isExtension: false,
+      powerMode: '',
+      splMax: 0,
+      hCov: 0,
+      vCov: 0
+    })
+  }
+})
 type NumberField = {
   kind: 'number'
   key: NumberParamKey
@@ -23,6 +33,7 @@ type NumberField = {
 type FieldConfig = NumberField
 const emit = defineEmits<{
   (e: 'apply-spec', spec: DeviceSpec): void
+  (e: 'update:state', state: DevicePanelState): void
 }>()
 
 const modelOptions = devicesConfig.map((d) => ({
@@ -35,16 +46,19 @@ const modelOptions = devicesConfig.map((d) => ({
 
 const selectedModel = ref<string | null>('custom')
 
-const isExtension = ref(false)
+const isExtension = ref(props.state.isExtension || false)
 
 const powerMode = ref<string>('')
 
-const currentSplMaxConfig = ref<SplMaxConfig>({ standalone: [], extension: [] })
+const currentSplMaxConfig = computed(() => {
+  return devicesConfig.find((d) => d.model === selectedModel.value).splMax
+})
 const splMax = ref<number | null>(null)
 const hCov = ref<number | null>(null)
 const vCov = ref<number | null>(null)
 
 const powerModeOptions = computed(() => {
+  if (currentSplMaxConfig.value.extension.length == 0) return currentSplMaxConfig.value.standalone
   return isExtension.value
     ? currentSplMaxConfig.value.extension
     : currentSplMaxConfig.value.standalone
@@ -55,35 +69,60 @@ const apply = (): void => {
   const isCustom = !device || device.model === 'custom'
   hCov.value = isCustom ? hCov.value : device.horizontalAngle
   vCov.value = isCustom ? vCov.value : device.verticalAngle
+  const deviceState = {
+    model: selectedModel.value,
+    isExtension: isExtension.value,
+    powerMode: powerMode.value,
+    splMax: splMax.value,
+    hCov: hCov.value,
+    vCov: vCov.value
+  }
   const data = {
     hCov: hCov.value,
     vCov: vCov.value,
     splMax: splMax.value
   }
 
+  emit('update:state', deviceState)
   emit('apply-spec', data)
 }
 
-const onSelectModel = (model: string, option): void => {
-  console.log('onSelectModel', model, option)
-  currentSplMaxConfig.value = option.splMax
+const onSelectModel = (model: string): void => {
   selectedModel.value = model
   isExtension.value = false
-  powerMode.value = ''
+  powerMode.value = powerModeOptions.value[0].label
+  splMax.value = powerModeOptions.value[0].value
   apply()
 }
 
 const onToggleExtension = (value: boolean): void => {
   isExtension.value = value
-  powerMode.value = ''
+  powerMode.value = powerModeOptions.value[0].label
   splMax.value = powerModeOptions.value[0].value
   apply()
 }
 
 const onSelectPowerMode = (mode: string): void => {
-  powerMode.value = mode
+  powerModeOptions.value.forEach((item) => {
+    if (item.label == mode) splMax.value = item.value
+  })
   apply()
 }
+watch(
+  () => props.state,
+  (newVal) => {
+    selectedModel.value = newVal.model
+    isExtension.value = newVal.isExtension
+    powerMode.value = newVal.powerMode
+    splMax.value = newVal.splMax
+    hCov.value = newVal.hCov
+    vCov.value = newVal.vCov
+    console.log(isExtension.value, props.state)
+  },
+  {
+    immediate: true
+  }
+)
 const fields: FieldConfig[] = [
   {
     kind: 'number',
@@ -132,12 +171,17 @@ const fields: FieldConfig[] = [
       />
     </div>
     <n-flex>
-      <div class="field model-field" v-if="selectedModel && selectedModel !== 'custom'">
+      <div
+        class="field model-field"
+        v-if="
+          selectedModel && selectedModel !== 'custom' && currentSplMaxConfig.extension.length > 0
+        "
+      >
         <div class="field-label">
           <span class="sym">🔊</span>
           {{ $t('params.hasSubwoofer') }}
         </div>
-        <NCheckbox :model-value="isExtension" @update:checked="onToggleExtension">
+        <NCheckbox v-model:checked="isExtension" @update:checked="onToggleExtension">
           {{ $t('params.hasSubwooferOn') }}
         </NCheckbox>
       </div>
@@ -149,8 +193,9 @@ const fields: FieldConfig[] = [
           <div class="spl-mode-hint">{{ $t('params.splMaxAuto') }}</div>
         </div>
         <n-radio-group
-          v-model:value="splMax"
+          v-model:value="powerMode"
           :options="powerModeOptions"
+          value-field="label"
           @update:value="onSelectPowerMode"
         />
       </div>

+ 2 - 2
src/renderer/src/components/simulator/ResultPanel.vue

@@ -57,7 +57,7 @@ const assessType = computed<'success' | 'info' | 'warning' | 'error'>(() =>
 </script>
 
 <template>
-  <NCard :title="$t('result.panel')" size="small" class="panel-card">
+  <div class="reslut-panel">
     <NGrid cols="5" responsive="screen" :x-gap="10" item-responsive>
       <NGi span="5 s:3 m:1">
         <NStatistic
@@ -117,5 +117,5 @@ const assessType = computed<'success' | 'info' | 'warning' | 'error'>(() =>
         {{ assessText }}
       </NAlert>
     </div>
-  </NCard>
+  </div>
 </template>

+ 24 - 2
src/renderer/src/components/simulator/SpeakerSimulator.vue

@@ -4,6 +4,7 @@ import { SlidersH } from './icons'
 import type {
   Assessment,
   CalcResult,
+  DevicePanelState,
   DeviceSpec,
   Direction,
   NumberParamKey,
@@ -37,6 +38,19 @@ const result = ref<CalcResult | null>(null)
 const assessment = ref<Assessment | null>(null)
 const showParams = ref(false)
 
+const deviceState = reactive<DevicePanelState>({
+  model: 'custom',
+  isExtension: false,
+  powerMode: '',
+  splMax: params.splMax,
+  hCov: params.hCov,
+  vCov: params.vCov
+})
+
+const patchDeviceState = (patch: Partial<DevicePanelState>): void => {
+  Object.assign(deviceState, patch)
+}
+
 const runCalc = (): void => {
   const r = calculate(params)
   if (!r) {
@@ -113,7 +127,7 @@ runCalc()
         @set-dir="setDir"
       />
     </NCard>
-    <ResultPanel :result="result" :assessment="assessment" />
+    <!-- <ResultPanel :result="result" :assessment="assessment" /> -->
 
     <NCard size="small" class="panel-card chart-card">
       <NTabs type="line" animated>
@@ -123,6 +137,9 @@ runCalc()
         <NTabPane name="chart" :tab="$t('chart.panel')">
           <SPLChartPanel :params="params" :result="result" />
         </NTabPane>
+        <NTabPane name="result" :tab="$t('result.panel')">
+          <ResultPanel :result="result" :assessment="assessment" />
+        </NTabPane>
       </NTabs>
     </NCard>
 
@@ -134,7 +151,12 @@ runCalc()
       @submit="showParams = false"
       @close="showParams = false"
     >
-      <ParamsPanel @apply-spec="applySpec" @reset="reset" />
+      <ParamsPanel
+        :state="deviceState"
+        @update:state="patchDeviceState"
+        @apply-spec="applySpec"
+        @reset="reset"
+      />
     </BaseModel>
   </div>
 </template>

+ 48 - 68
src/renderer/src/config/index.ts

@@ -4,124 +4,104 @@ export const devicesConfig = [
     horizontalAngle: 0,
     verticalAngle: 0,
     splMax: {
-      standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
-      ],
-      extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
-      ]
+      standalone: [],
+      extension: []
     }
   },
   {
-    model: 'IPS-M1-D2W',
-    horizontalAngle: 140,
-    verticalAngle: 140,
+    model: 'IPS-M1-BW',
+    horizontalAngle: 60,
+    verticalAngle: 60,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 101 },
+        { label: 'PoE+', value: 101 },
+        { label: 'PoE', value: 99 }
       ],
-      extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
-      ]
+      extension: [] // 无扩展
     }
   },
   {
     model: 'IPS-M1-D1W',
-    horizontalAngle: 120,
-    verticalAngle: 120,
+    horizontalAngle: 60,
+    verticalAngle: 60,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 101 },
+        { label: 'PoE+', value: 101 },
+        { label: 'PoE', value: 98 }
       ],
-      extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
-      ]
+      extension: [] // 无扩展
     }
   },
   {
-    model: 'IPS-M1-BW',
-    horizontalAngle: 120,
-    verticalAngle: 120,
+    model: 'IPS-M1-D2W',
+    horizontalAngle: 70,
+    verticalAngle: 70,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 98 },
+        { label: 'PoE+', value: 96 },
+        { label: 'PoE', value: 91 }
       ],
-      extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
-      ]
+      extension: [] // 无扩展
     }
   },
   {
     model: 'IPS-C1',
-    horizontalAngle: 120,
-    verticalAngle: 120,
+    horizontalAngle: 60,
+    verticalAngle: 60,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 97 },
+        { label: 'PoE+', value: 97 },
+        { label: 'PoE', value: 95 }
       ],
       extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
+        { label: 'DC', value: 2 * 97 },
+        { label: 'PoE+', value: 2 * 95 },
+        { label: 'PoE', value: 2 * 92 }
       ]
     }
   },
   {
     model: 'IPS-W1',
-    horizontalAngle: 120,
-    verticalAngle: 140,
+    horizontalAngle: 60,
+    verticalAngle: 70,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 95 },
+        { label: 'PoE+', value: 95 },
+        { label: 'PoE', value: 93 }
       ],
       extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
+        { label: 'DC', value: 2 * 95 },
+        { label: 'PoE+', value: 2 * 93 },
+        { label: 'PoE', value: 2 * 90 }
       ]
     }
   },
   {
     model: 'IPS-W1-R1',
-    horizontalAngle: 120,
-    verticalAngle: 140,
+    horizontalAngle: 60,
+    verticalAngle: 70,
     splMax: {
       standalone: [
-        { label: 'DC', value: 118 },
-        { label: 'PoE+', value: 117 },
-        { label: 'PoE', value: 115 }
+        { label: 'DC', value: 95 },
+        { label: 'PoE+', value: 95 },
+        { label: 'PoE', value: 93 }
       ],
       extension: [
-        { label: 'DC', value: 2 * 118 },
-        { label: 'PoE+', value: 2 * 114 },
-        { label: 'PoE', value: 2 * 112 }
+        { label: 'DC', value: 2 * 95 },
+        { label: 'PoE+', value: 2 * 93 },
+        { label: 'PoE', value: 2 * 90 }
       ]
     }
   },
   {
     model: 'IPS-H1',
-    horizontalAngle: 120,
-    verticalAngle: 120,
+    horizontalAngle: 60,
+    verticalAngle: 60,
     splMax: {
       standalone: [
         { label: 'DC', value: 118 },

+ 21 - 0
src/renderer/src/lib/speaker/types.ts

@@ -76,3 +76,24 @@ export interface DeviceSpec {
   /** 最大声压级 dB SPL */
   splMax: number | null
 }
+
+/**
+ * 设备选型弹框的表单状态
+ *
+ * 该状态由 SpeakerSimulator 持有(提升到父组件),
+ * 弹框关闭再打开时不会重置为默认值。
+ */
+export interface DevicePanelState {
+  /** 选中的设备型号,null 表示未选择 */
+  model: string | null
+  /** 是否配备子喇叭(扩展单元) */
+  isExtension: boolean
+  /** 选中的供电模式标签 */
+  powerMode: string
+  /** 最大声压级 dB SPL */
+  splMax: number | null
+  /** 水平指向角 (-6 dB) ° */
+  hCov: number | null
+  /** 垂直指向角 (-6 dB) ° */
+  vCov: number | null
+}

+ 2 - 2
src/renderer/src/locales/lang/en_US.json

@@ -51,8 +51,8 @@
     "linkOn": "Click to unlink A and D3",
     "linkOff": "Click to link A and D3",
     "custom":"Custom",
-    "hasSubwoofer": "Has Subwoofer",
-    "hasSubwooferOn": "Equipped with subwoofer (extension unit)",
+    "hasSubwoofer": "With Extension Speaker ",
+    "hasSubwooferOn": "Equipped with Extension Speaker ",
     "powerMode": "Power Mode",
     "splMaxAuto": "Max SPL will be auto-filled by selected mode",
     "tuner": "Live Parameter Tuning",

+ 2 - 2
src/renderer/src/locales/lang/zh_CN.json

@@ -51,8 +51,8 @@
     "linkOn": "点击解除 A 与 D3 关联",
     "linkOff": "点击关联 A 与 D3",
     "custom":"自定义",
-    "hasSubwoofer": "是否有子喇叭",
-    "hasSubwooferOn": "配备子喇叭(扩展单元)",
+    "hasSubwoofer": "是否有扩展音箱",
+    "hasSubwooferOn": "配备扩展音箱",
     "powerMode": "供电模式",
     "splMaxAuto": "最大声压级将按所选模式自动填充",
     "tuner": "实时参数调节",