|
@@ -1,103 +1,89 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref } from 'vue'
|
|
|
|
|
-import { Link, Unlink, UndoAlt } from './icons'
|
|
|
|
|
-import type { Direction, NumberParamKey, SpeakerParams } from '../../lib/speaker/types'
|
|
|
|
|
-import { DEG } from '../../lib/speaker/calc'
|
|
|
|
|
|
|
+import { computed, ref, Ref } from 'vue'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
import { devicesConfig } from '../../config'
|
|
import { devicesConfig } from '../../config'
|
|
|
-const { t } = useI18n()
|
|
|
|
|
|
|
+import type { DeviceSpec } 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 }>
|
|
|
|
|
+}
|
|
|
type NumberField = {
|
|
type NumberField = {
|
|
|
kind: 'number'
|
|
kind: 'number'
|
|
|
key: NumberParamKey
|
|
key: NumberParamKey
|
|
|
symbol: string
|
|
symbol: string
|
|
|
labelKey: string
|
|
labelKey: string
|
|
|
unit: string
|
|
unit: string
|
|
|
- disabled: boolean | (() => boolean)
|
|
|
|
|
step: number
|
|
step: number
|
|
|
|
|
+ value: Ref<number> | number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type DirectionField = { kind: 'direction' }
|
|
|
|
|
-
|
|
|
|
|
-type FieldConfig = NumberField | DirectionField
|
|
|
|
|
-const props = defineProps<{
|
|
|
|
|
- params: SpeakerParams
|
|
|
|
|
- linked: boolean
|
|
|
|
|
-}>()
|
|
|
|
|
-
|
|
|
|
|
|
|
+type FieldConfig = NumberField
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
|
- (e: 'update', field: NumberParamKey, value: number): void
|
|
|
|
|
- (e: 'toggle-link'): void
|
|
|
|
|
- (e: 'set-dir', dir: Direction): void
|
|
|
|
|
- (e: 'reset'): void
|
|
|
|
|
- (e: 'apply-device', hCov: number, vCov: number): void
|
|
|
|
|
|
|
+ (e: 'apply-spec', spec: DeviceSpec): void
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
-// ---------- 设备型号下拉:选择后自动应用水平/垂直指向角 ----------
|
|
|
|
|
-
|
|
|
|
|
const modelOptions = devicesConfig.map((d) => ({
|
|
const modelOptions = devicesConfig.map((d) => ({
|
|
|
- label: d.model == 'custom' ? t('params.custom') : d.model,
|
|
|
|
|
- value: d.model
|
|
|
|
|
|
|
+ label: d.model === 'custom' ? t('params.custom') : d.model,
|
|
|
|
|
+ value: d.model,
|
|
|
|
|
+ horizontalAngle: d.horizontalAngle,
|
|
|
|
|
+ verticalAngle: d.verticalAngle,
|
|
|
|
|
+ splMax: d.splMax
|
|
|
}))
|
|
}))
|
|
|
|
|
+
|
|
|
const selectedModel = ref<string | null>('custom')
|
|
const selectedModel = ref<string | null>('custom')
|
|
|
|
|
|
|
|
-const onSelectModel = (model: string): void => {
|
|
|
|
|
- selectedModel.value = model
|
|
|
|
|
- const device = devicesConfig.find((d) => d.model === model)
|
|
|
|
|
- if (device && model != 'custom') {
|
|
|
|
|
- emit('apply-device', device.horizontalAngle!, device.verticalAngle!)
|
|
|
|
|
|
|
+const isExtension = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const powerMode = ref<string>('')
|
|
|
|
|
+
|
|
|
|
|
+const currentSplMaxConfig = ref<SplMaxConfig>({ standalone: [], extension: [] })
|
|
|
|
|
+const splMax = ref<number | null>(null)
|
|
|
|
|
+const hCov = ref<number | null>(null)
|
|
|
|
|
+const vCov = ref<number | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+const powerModeOptions = computed(() => {
|
|
|
|
|
+ return isExtension.value
|
|
|
|
|
+ ? currentSplMaxConfig.value.extension
|
|
|
|
|
+ : currentSplMaxConfig.value.standalone
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const apply = (): void => {
|
|
|
|
|
+ const device = devicesConfig.find((d) => d.model === selectedModel.value)
|
|
|
|
|
+ const isCustom = !device || device.model === 'custom'
|
|
|
|
|
+ hCov.value = isCustom ? hCov.value : device.horizontalAngle
|
|
|
|
|
+ vCov.value = isCustom ? vCov.value : device.verticalAngle
|
|
|
|
|
+ const data = {
|
|
|
|
|
+ hCov: hCov.value,
|
|
|
|
|
+ vCov: vCov.value,
|
|
|
|
|
+ splMax: splMax.value
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-const onReset = (): void => {
|
|
|
|
|
- selectedModel.value = null
|
|
|
|
|
- emit('reset')
|
|
|
|
|
|
|
+ emit('apply-spec', data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const flashKeys = ref<Array<NumberParamKey>>([])
|
|
|
|
|
-
|
|
|
|
|
-const flash = (key: NumberParamKey): void => {
|
|
|
|
|
- // 先移除再加,保证连续触发也能重放动画
|
|
|
|
|
- flashKeys.value = flashKeys.value.filter((k) => k !== key)
|
|
|
|
|
- void requestAnimationFrame(() => {
|
|
|
|
|
- flashKeys.value = [...flashKeys.value, key]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+const onSelectModel = (model: string, option): void => {
|
|
|
|
|
+ console.log('onSelectModel', model, option)
|
|
|
|
|
+ currentSplMaxConfig.value = option.splMax
|
|
|
|
|
+ selectedModel.value = model
|
|
|
|
|
+ isExtension.value = false
|
|
|
|
|
+ powerMode.value = ''
|
|
|
|
|
+ apply()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const syncD3From = (z: number, angle: number): void => {
|
|
|
|
|
- if (angle > 0.05 && z > 0) {
|
|
|
|
|
- const d3 = Number((z / Math.tan(angle * DEG)).toFixed(2))
|
|
|
|
|
- emit('update', 'd3', d3)
|
|
|
|
|
- flash('d3')
|
|
|
|
|
- } else if (angle >= 89.95) {
|
|
|
|
|
- emit('update', 'd3', 0)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const onToggleExtension = (value: boolean): void => {
|
|
|
|
|
+ isExtension.value = value
|
|
|
|
|
+ powerMode.value = ''
|
|
|
|
|
+ splMax.value = powerModeOptions.value[0].value
|
|
|
|
|
+ apply()
|
|
|
}
|
|
}
|
|
|
-const syncAFrom = (z: number, d3: number): void => {
|
|
|
|
|
- if (d3 > 0.001 && z > 0) {
|
|
|
|
|
- const angle = Number((Math.atan(z / d3) / DEG).toFixed(2))
|
|
|
|
|
- emit('update', 'angle', angle)
|
|
|
|
|
- flash('angle')
|
|
|
|
|
- } else if (d3 <= 0.001) {
|
|
|
|
|
- emit('update', 'angle', 90)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const onUpdate = (field: NumberParamKey, value: number | null): void => {
|
|
|
|
|
- if (value === null) return
|
|
|
|
|
- emit('update', field, value)
|
|
|
|
|
|
|
|
|
|
- if (!props.linked) return
|
|
|
|
|
-
|
|
|
|
|
- if (field === 'z' || field === 'angle') {
|
|
|
|
|
- const z = field === 'z' ? value : props.params.z
|
|
|
|
|
- const angle = field === 'angle' ? value : props.params.angle
|
|
|
|
|
- syncD3From(z, angle)
|
|
|
|
|
- } else if (field === 'd3') {
|
|
|
|
|
- syncAFrom(props.params.z, value)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const onSelectPowerMode = (mode: string): void => {
|
|
|
|
|
+ powerMode.value = mode
|
|
|
|
|
+ apply()
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
const fields: FieldConfig[] = [
|
|
const fields: FieldConfig[] = [
|
|
|
{
|
|
{
|
|
|
kind: 'number',
|
|
kind: 'number',
|
|
@@ -105,17 +91,8 @@ const fields: FieldConfig[] = [
|
|
|
symbol: 'SPLₘₐₓ',
|
|
symbol: 'SPLₘₐₓ',
|
|
|
labelKey: 'params.splMax',
|
|
labelKey: 'params.splMax',
|
|
|
unit: 'dB SPL',
|
|
unit: 'dB SPL',
|
|
|
- step: 0.1,
|
|
|
|
|
- disabled: false
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- kind: 'number',
|
|
|
|
|
- key: 'z',
|
|
|
|
|
- symbol: 'Z',
|
|
|
|
|
- labelKey: 'params.z',
|
|
|
|
|
- unit: 'm',
|
|
|
|
|
- step: 0.01,
|
|
|
|
|
- disabled: false
|
|
|
|
|
|
|
+ value: splMax,
|
|
|
|
|
+ step: 0.1
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
kind: 'number',
|
|
kind: 'number',
|
|
@@ -123,8 +100,8 @@ const fields: FieldConfig[] = [
|
|
|
symbol: 'H',
|
|
symbol: 'H',
|
|
|
labelKey: 'params.hCov',
|
|
labelKey: 'params.hCov',
|
|
|
unit: '°',
|
|
unit: '°',
|
|
|
- step: 1,
|
|
|
|
|
- disabled: () => selectedModel.value != 'custom'
|
|
|
|
|
|
|
+ value: hCov,
|
|
|
|
|
+ step: 1
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
kind: 'number',
|
|
kind: 'number',
|
|
@@ -132,131 +109,66 @@ const fields: FieldConfig[] = [
|
|
|
symbol: 'V',
|
|
symbol: 'V',
|
|
|
labelKey: 'params.vCov',
|
|
labelKey: 'params.vCov',
|
|
|
unit: '°',
|
|
unit: '°',
|
|
|
- step: 1,
|
|
|
|
|
- disabled: () => selectedModel.value != 'custom'
|
|
|
|
|
- },
|
|
|
|
|
- { kind: 'direction' },
|
|
|
|
|
- {
|
|
|
|
|
- kind: 'number',
|
|
|
|
|
- key: 'angle',
|
|
|
|
|
- symbol: 'A',
|
|
|
|
|
- labelKey: 'params.angle',
|
|
|
|
|
- unit: '°',
|
|
|
|
|
- step: 0.01,
|
|
|
|
|
- disabled: false
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- kind: 'number',
|
|
|
|
|
- key: 'd1',
|
|
|
|
|
- symbol: 'D1',
|
|
|
|
|
- labelKey: 'params.d1',
|
|
|
|
|
- unit: 'm',
|
|
|
|
|
- step: 0.01,
|
|
|
|
|
- disabled: false
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- kind: 'number',
|
|
|
|
|
- key: 'd2',
|
|
|
|
|
- symbol: 'D2',
|
|
|
|
|
- labelKey: 'params.d2',
|
|
|
|
|
- unit: 'm',
|
|
|
|
|
- step: 0.01,
|
|
|
|
|
- disabled: false
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- kind: 'number',
|
|
|
|
|
- key: 'd3',
|
|
|
|
|
- symbol: 'D3',
|
|
|
|
|
- labelKey: 'params.d3',
|
|
|
|
|
- unit: 'm',
|
|
|
|
|
- step: 0.01,
|
|
|
|
|
- disabled: false
|
|
|
|
|
|
|
+ value: vCov,
|
|
|
|
|
+ step: 1
|
|
|
}
|
|
}
|
|
|
]
|
|
]
|
|
|
-
|
|
|
|
|
-const isFlashing = (key: NumberParamKey): boolean => {
|
|
|
|
|
- return flashKeys.value.includes(key)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const getFieldValue = (field: NumberField): number => {
|
|
|
|
|
- return props.params[field.key] as number
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** 求值字段的 disabled:支持布尔值或返回布尔的函数(响应式) */
|
|
|
|
|
-const isFieldDisabled = (field: NumberField): boolean => {
|
|
|
|
|
- return typeof field.disabled === 'function' ? field.disabled() : field.disabled
|
|
|
|
|
-}
|
|
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
<div class="params-body">
|
|
<div class="params-body">
|
|
|
- <!-- 设备型号选择:自动填充水平/垂直指向角 -->
|
|
|
|
|
<div class="field model-field">
|
|
<div class="field model-field">
|
|
|
<div class="field-label">
|
|
<div class="field-label">
|
|
|
<span class="sym">⌘</span>
|
|
<span class="sym">⌘</span>
|
|
|
{{ $t('params.model') }}
|
|
{{ $t('params.model') }}
|
|
|
</div>
|
|
</div>
|
|
|
- <NSelect
|
|
|
|
|
|
|
+ <n-select
|
|
|
:value="selectedModel"
|
|
:value="selectedModel"
|
|
|
:options="modelOptions"
|
|
:options="modelOptions"
|
|
|
:placeholder="$t('params.modelPlaceholder')"
|
|
:placeholder="$t('params.modelPlaceholder')"
|
|
|
filterable
|
|
filterable
|
|
|
clearable
|
|
clearable
|
|
|
- @update:value="(v) => v && onSelectModel(v)"
|
|
|
|
|
|
|
+ @update:value="onSelectModel"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <n-flex>
|
|
|
|
|
+ <div class="field model-field" v-if="selectedModel && selectedModel !== 'custom'">
|
|
|
|
|
+ <div class="field-label">
|
|
|
|
|
+ <span class="sym">🔊</span>
|
|
|
|
|
+ {{ $t('params.hasSubwoofer') }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <NCheckbox :model-value="isExtension" @update:checked="onToggleExtension">
|
|
|
|
|
+ {{ $t('params.hasSubwooferOn') }}
|
|
|
|
|
+ </NCheckbox>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
|
+ <div v-if="powerModeOptions.length > 0" class="field model-field">
|
|
|
|
|
+ <div class="field-label">
|
|
|
|
|
+ <span class="sym">⚡</span>
|
|
|
|
|
+ {{ $t('params.powerMode') }}
|
|
|
|
|
+ <div class="spl-mode-hint">{{ $t('params.splMaxAuto') }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <n-radio-group
|
|
|
|
|
+ v-model:value="splMax"
|
|
|
|
|
+ :options="powerModeOptions"
|
|
|
|
|
+ @update:value="onSelectPowerMode"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </n-flex>
|
|
|
<NGrid cols="3" responsive="screen" :x-gap="14" :y-gap="14" item-responsive>
|
|
<NGrid cols="3" responsive="screen" :x-gap="14" :y-gap="14" item-responsive>
|
|
|
<template v-for="field in fields" :key="field.kind === 'number' ? field.key : 'dir'">
|
|
<template v-for="field in fields" :key="field.kind === 'number' ? field.key : 'dir'">
|
|
|
- <!-- 方向选择 -->
|
|
|
|
|
- <NGi v-if="field.kind === 'direction'" span="3 m:1">
|
|
|
|
|
|
|
+ <NGi span="3 s:1 m:1">
|
|
|
<div class="field">
|
|
<div class="field">
|
|
|
- <div class="field-label">
|
|
|
|
|
- <span class="sym">H/V</span>
|
|
|
|
|
- {{ $t('params.dir') }}
|
|
|
|
|
- </div>
|
|
|
|
|
- <NRadioGroup
|
|
|
|
|
- :value="params.dir"
|
|
|
|
|
- size="medium"
|
|
|
|
|
- @update:value="(v) => emit('set-dir', v as Direction)"
|
|
|
|
|
- >
|
|
|
|
|
- <NRadioButton value="H">{{ $t('params.dirH') }}</NRadioButton>
|
|
|
|
|
- <NRadioButton value="V">{{ $t('params.dirV') }}</NRadioButton>
|
|
|
|
|
- </NRadioGroup>
|
|
|
|
|
- </div>
|
|
|
|
|
- </NGi>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 数值输入 -->
|
|
|
|
|
- <NGi v-else span="3 s:1 m:1">
|
|
|
|
|
- <div class="field" :class="{ flash: isFlashing(field.key) }">
|
|
|
|
|
<div class="field-label">
|
|
<div class="field-label">
|
|
|
<span class="sym">{{ field.symbol }}</span>
|
|
<span class="sym">{{ field.symbol }}</span>
|
|
|
{{ $t(field.labelKey) }}
|
|
{{ $t(field.labelKey) }}
|
|
|
- <NTooltip v-if="field.key === 'd3'" trigger="hover">
|
|
|
|
|
- <template #trigger>
|
|
|
|
|
- <NButton
|
|
|
|
|
- quaternary
|
|
|
|
|
- circle
|
|
|
|
|
- size="tiny"
|
|
|
|
|
- class="link-btn"
|
|
|
|
|
- :class="{ off: !linked }"
|
|
|
|
|
- @click="emit('toggle-link')"
|
|
|
|
|
- >
|
|
|
|
|
- <template #icon>
|
|
|
|
|
- <NIcon :component="linked ? Link : Unlink" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </NButton>
|
|
|
|
|
- </template>
|
|
|
|
|
- {{ linked ? $t('params.linkOn') : $t('params.linkOff') }}
|
|
|
|
|
- </NTooltip>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
<NInputNumber
|
|
<NInputNumber
|
|
|
- :value="getFieldValue(field)"
|
|
|
|
|
|
|
+ :value="field.value"
|
|
|
:step="field.step"
|
|
:step="field.step"
|
|
|
:show-button="false"
|
|
:show-button="false"
|
|
|
- :disabled="isFieldDisabled(field)"
|
|
|
|
|
|
|
+ :disabled="selectedModel !== 'custom'"
|
|
|
class="num-input"
|
|
class="num-input"
|
|
|
- @update:value="(v) => onUpdate(field.key, v)"
|
|
|
|
|
>
|
|
>
|
|
|
<template #suffix>{{ field.unit }}</template>
|
|
<template #suffix>{{ field.unit }}</template>
|
|
|
</NInputNumber>
|
|
</NInputNumber>
|
|
@@ -264,14 +176,5 @@ const isFieldDisabled = (field: NumberField): boolean => {
|
|
|
</NGi>
|
|
</NGi>
|
|
|
</template>
|
|
</template>
|
|
|
</NGrid>
|
|
</NGrid>
|
|
|
-
|
|
|
|
|
- <NSpace justify="end" class="reset-row">
|
|
|
|
|
- <NButton size="small" quaternary @click="onReset">
|
|
|
|
|
- <template #icon>
|
|
|
|
|
- <NIcon :component="UndoAlt" :size="12" />
|
|
|
|
|
- </template>
|
|
|
|
|
- {{ $t('params.reset') }}
|
|
|
|
|
- </NButton>
|
|
|
|
|
- </NSpace>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|