perf:【IoT 物联网】场景联动触发器优化

pull/802/head
puhui999 2025-08-01 17:18:23 +08:00
parent 081603788a
commit a5d458b96d
16 changed files with 122 additions and 435 deletions

View File

@ -190,22 +190,20 @@ const handleValidate = (result: { valid: boolean; message: string }) => {
emit('validate', result)
}
const handleProductChange = (productId: number) => {
const handleProductChange = (_: number) => {
//
condition.value.productId = productId
condition.value.deviceId = undefined
condition.value.identifier = ''
updateValidationResult()
}
const handleDeviceChange = (deviceId: number) => {
const handleDeviceChange = (_: number) => {
//
condition.value.deviceId = deviceId
condition.value.identifier = ''
updateValidationResult()
}
const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
debugger
console.log(propertyInfo)
propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config

View File

@ -1,253 +0,0 @@
<!-- 条件组配置组件 -->
<template>
<div class="p-16px">
<!-- 条件组说明 -->
<div
v-if="group.conditions && group.conditions.length > 1"
class="mb-12px flex items-center justify-center"
>
<div
class="flex items-center gap-6px px-10px py-4px bg-green-50 border border-green-200 rounded-full text-11px text-green-600"
>
<Icon icon="ep:info-filled" />
<span>组内所有条件必须同时满足且关系</span>
</div>
</div>
<div class="space-y-12px">
<!-- 条件列表 -->
<div v-if="group.conditions && group.conditions.length > 0" class="space-y-12px">
<div
v-for="(condition, index) in group.conditions"
:key="`condition-${index}`"
class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-light)] shadow-sm hover:shadow-md transition-shadow"
>
<div class="flex items-center justify-between mb-12px">
<div class="flex items-center gap-8px">
<div class="flex items-center gap-6px">
<div
class="w-18px h-18px bg-green-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
>
{{ index + 1 }}
</div>
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件</span>
</div>
<el-tag size="small" type="primary">
{{ getConditionTypeName(condition.type) }}
</el-tag>
</div>
<el-button
type="danger"
size="small"
text
@click="removeCondition(index)"
v-if="group.conditions!.length > 1"
>
<Icon icon="ep:delete" />
删除
</el-button>
</div>
<div class="p-12px bg-[var(--el-fill-color-blank)] rounded-4px">
<ConditionConfig
:model-value="condition"
@update:model-value="(value) => updateCondition(index, value)"
:trigger-type="triggerType"
:product-id="productId"
:device-id="deviceId"
@validate="(result) => handleConditionValidate(index, result)"
/>
</div>
<!-- 条件间的"且"连接符 -->
<div
v-if="index < group.conditions!.length - 1"
class="flex items-center justify-center py-8px"
>
<div class="flex items-center gap-6px">
<!-- 连接线 -->
<div class="w-24px h-1px bg-green-300"></div>
<!-- 且标签 -->
<div class="px-12px py-4px bg-green-100 border-2 border-green-300 rounded-full">
<span class="text-12px font-600 text-green-600"></span>
</div>
<!-- 连接线 -->
<div class="w-24px h-1px bg-green-300"></div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="py-40px text-center">
<el-empty description="暂无条件配置" :image-size="80">
<template #description>
<div class="space-y-8px">
<p class="text-[var(--el-text-color-secondary)]">暂无条件配置</p>
<p class="text-12px text-[var(--el-text-color-placeholder)]">
条件组需要至少包含一个条件才能生效
</p>
</div>
</template>
</el-empty>
</div>
<!-- 添加条件按钮 -->
<div
v-if="
group.conditions && group.conditions.length > 0 && group.conditions.length < maxConditions
"
class="text-center py-16px"
>
<el-button type="primary" plain @click="addCondition">
<Icon icon="ep:plus" />
继续添加条件
</el-button>
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
最多可添加 {{ maxConditions }} 个条件
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ConditionConfig from './ConditionConfig.vue'
import { ConditionFormData, ConditionGroupFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
/** 条件组配置组件 */
defineOptions({ name: 'ConditionGroupConfig' })
interface Props {
modelValue: ConditionGroupFormData
triggerType: number
productId?: number
deviceId?: number
maxConditions?: number
}
interface Emits {
(e: 'update:modelValue', value: ConditionGroupFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const group = useVModel(props, 'modelValue', emit)
//
const maxConditions = computed(() => props.maxConditions || 3)
//
const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
const validationMessage = ref('')
const isValid = ref(true)
//
const conditionTypeNames = {
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性条件',
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: '事件条件',
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务条件'
}
//
const getConditionTypeName = (type: number) => {
return conditionTypeNames[type] || '未知条件'
}
//
const updateCondition = (index: number, condition: ConditionFormData) => {
if (group.value.conditions) {
group.value.conditions[index] = condition
}
}
const addCondition = () => {
if (!group.value.conditions) {
group.value.conditions = []
}
if (group.value.conditions.length >= maxConditions.value) {
return
}
const newCondition: ConditionFormData = {
type: 2, //
productId: props.productId || 0,
deviceId: props.deviceId || 0,
identifier: '',
operator: '=',
param: ''
}
group.value.conditions.push(newCondition)
}
const removeCondition = (index: number) => {
if (group.value.conditions) {
group.value.conditions.splice(index, 1)
delete conditionValidations.value[index]
//
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
Object.keys(conditionValidations.value).forEach((key) => {
const numKey = parseInt(key)
if (numKey > index) {
newValidations[numKey - 1] = conditionValidations.value[numKey]
} else if (numKey < index) {
newValidations[numKey] = conditionValidations.value[numKey]
}
})
conditionValidations.value = newValidations
updateValidationResult()
}
}
const handleConditionValidate = (index: number, result: { valid: boolean; message: string }) => {
conditionValidations.value[index] = result
updateValidationResult()
}
const updateValidationResult = () => {
if (!group.value.conditions || group.value.conditions.length === 0) {
isValid.value = false
validationMessage.value = '请至少添加一个条件'
emit('validate', { valid: false, message: validationMessage.value })
return
}
const validations = Object.values(conditionValidations.value)
const allValid = validations.every((v) => v.valid)
if (allValid) {
isValid.value = true
validationMessage.value = '条件组配置验证通过'
} else {
isValid.value = false
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
validationMessage.value = `条件配置错误: ${errorMessages.join('; ')}`
}
emit('validate', { valid: isValid.value, message: validationMessage.value })
}
//
watch(
() => group.value.conditions?.length,
() => {
updateValidationResult()
}
)
//
onMounted(() => {
if (!group.value.conditions || group.value.conditions.length === 0) {
addCondition()
}
})
</script>

View File

@ -137,6 +137,7 @@ const props = defineProps<{
modelValue: ConditionGroupContainerFormData
triggerType: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: ConditionGroupContainerFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void

View File

@ -102,18 +102,14 @@ import {
/** 当前时间条件配置组件 */
defineOptions({ name: 'CurrentTimeConditionConfig' })
interface Props {
const props = defineProps<{
modelValue: ConditionFormData
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const condition = useVModel(props, 'modelValue', emit)

View File

@ -27,9 +27,13 @@
<template #default>
<div class="space-y-8px">
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例</p>
<pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "temperature": 25, "power": true }</code></pre>
<pre
class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
><code>{ "temperature": 25, "power": true }</code></pre>
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例</p>
<pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
<pre
class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
</div>
</template>
</el-alert>
@ -56,17 +60,14 @@ import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
/** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' })
interface Props {
const props = defineProps<{
modelValue: ActionFormData
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: ActionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const action = useVModel(props, 'modelValue', emit)

View File

@ -26,31 +26,6 @@
<!-- 状态和操作符选择 -->
<el-row :gutter="16">
<!-- 状态选择 -->
<el-col :span="12">
<el-form-item label="设备状态" required>
<el-select
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
placeholder="请选择设备状态"
class="w-full"
>
<el-option
v-for="option in deviceStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center gap-8px">
<Icon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
<el-tag :type="option.tag" size="small">{{ option.description }}</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 操作符选择 -->
<el-col :span="12">
<el-form-item label="操作符" required>
@ -76,35 +51,32 @@
</el-select>
</el-form-item>
</el-col>
<!-- 状态选择 -->
<el-col :span="12">
<el-form-item label="设备状态" required>
<el-select
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
placeholder="请选择设备状态"
class="w-full"
>
<el-option
v-for="option in deviceStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center gap-8px">
<Icon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
<el-tag :type="option.tag" size="small">{{ option.description }}</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 条件预览 -->
<!-- TODO puhui999可以去掉因为表单选择了可以看懂的呀 -->
<div
v-if="conditionPreview"
class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
>
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
</div>
<div class="pl-24px">
<code
class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
>{{ conditionPreview }}</code
>
</div>
</div>
<!-- 验证结果 -->
<div v-if="validationMessage" class="mt-8px">
<el-alert
:title="validationMessage"
:type="isValid ? 'success' : 'error'"
:closable="false"
show-icon
/>
</div>
</div>
</template>
@ -117,17 +89,14 @@ import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
/** 设备状态条件配置组件 */
defineOptions({ name: 'DeviceStatusConditionConfig' })
interface Props {
const props = defineProps<{
modelValue: ConditionFormData
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const condition = useVModel(props, 'modelValue', emit)
@ -169,22 +138,6 @@ const statusOperatorOptions = [
const validationMessage = ref('')
const isValid = ref(true)
//
const conditionPreview = computed(() => {
if (!condition.value.param || !condition.value.operator) {
return ''
}
const statusLabel =
deviceStatusOptions.find((opt) => opt.value === condition.value.param)?.label ||
condition.value.param
const operatorLabel =
statusOperatorOptions.find((opt) => opt.value === condition.value.operator)?.label ||
condition.value.operator
return `设备状态 ${operatorLabel} ${statusLabel}`
})
//
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
condition.value[field] = value

View File

@ -68,14 +68,13 @@ import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/util
/** 设备触发配置组件 */
defineOptions({ name: 'DeviceTriggerConfig' })
// Props Emits
const props = defineProps<{
modelValue: TriggerFormData
}>()
const emit = defineEmits<{
'update:modelValue': [value: TriggerFormData]
validate: [result: { valid: boolean; message: string }]
(e: 'update:modelValue', value: TriggerFormData): void
(e: 'validate', value: { valid: boolean; message: string }): void
}>()
const trigger = useVModel(props, 'modelValue', emit)
@ -126,7 +125,6 @@ const addConditionGroup = () => {
}
//
const handleConditionGroupValidate = () => {
updateValidationResult()
}

View File

@ -51,6 +51,7 @@ defineProps<{
modelValue?: ConditionFormData
triggerType: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value?: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void

View File

@ -104,19 +104,15 @@ import { useVModel } from '@vueuse/core'
/** 主条件内部配置组件 */
defineOptions({ name: 'MainConditionInnerConfig' })
interface Props {
const props = defineProps<{
modelValue: ConditionFormData
triggerType: number
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
//
const condition = useVModel(props, 'modelValue', emit)

View File

@ -98,20 +98,16 @@ import {
/** 子条件组配置组件 */
defineOptions({ name: 'SubConditionGroupConfig' })
interface Props {
const props = defineProps<{
modelValue: SubConditionGroupFormData
triggerType: number
maxConditions?: number
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: SubConditionGroupFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const subGroup = useVModel(props, 'modelValue', emit)

View File

@ -25,18 +25,13 @@ import { Crontab } from '@/components/Crontab'
/** 定时触发配置组件 */
defineOptions({ name: 'TimerTriggerConfig' })
// TODO @puhui999 PropsEmits
interface Props {
const props = defineProps<{
modelValue?: string
}
interface Emits {
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const localValue = useVModel(props, 'modelValue', emit, {
defaultValue: '0 0 12 * * ?'

View File

@ -29,17 +29,14 @@ import { IotRuleSceneTriggerConditionTypeEnum } from '@/api/iot/rule/scene/scene
/** 条件类型选择器组件 */
defineOptions({ name: 'ConditionTypeSelector' })
interface Props {
defineProps<{
modelValue?: number
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
//
const conditionTypeOptions = [

View File

@ -18,9 +18,9 @@
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
device.deviceName
}}</div>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
>{{ device.deviceName }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
</div>
<div class="flex items-center gap-4px">
@ -42,18 +42,15 @@ import { DeviceApi } from '@/api/iot/device/device'
/** 设备选择器组件 */
defineOptions({ name: 'DeviceSelector' })
interface Props {
const props = defineProps<{
modelValue?: number
productId?: number
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value?: number): void
(e: 'change', value?: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
//
const deviceLoading = ref(false)

View File

@ -39,19 +39,15 @@ import { useVModel } from '@vueuse/core'
/** 操作符选择器组件 */
defineOptions({ name: 'OperatorSelector' })
interface Props {
const props = defineProps<{
modelValue?: string
propertyType?: string
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
const localValue = useVModel(props, 'modelValue', emit)

View File

@ -22,13 +22,14 @@
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ product.name }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ product.productKey }}</div>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
>{{ product.name }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]"
>{{ product.productKey }}
</div>
</div>
<!-- TODO @puhui999是不是用字典 -->
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
{{ product.status === 0 ? '正常' : '禁用' }}
</el-tag>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
</div>
</el-option>
</el-select>
@ -70,8 +71,12 @@
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ device.deviceName }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.nickname || '无备注' }}</div>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
>{{ device.deviceName }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]"
>{{ device.nickname || '无备注' }}
</div>
</div>
<el-tag size="small" :type="getDeviceStatusTag(device.state)">
{{ getDeviceStatusText(device.state) }}
@ -84,7 +89,10 @@
</el-row>
<!-- 选择结果展示 -->
<div v-if="localProductId && localDeviceId !== undefined" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div
v-if="localProductId && localDeviceId !== undefined"
class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
>
<div class="flex items-center gap-6px mb-8px">
<Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span>
@ -92,14 +100,22 @@
<div class="flex flex-col gap-6px ml-22px">
<div class="flex items-center gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品</span>
<span class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedProduct?.name }}</span>
<span class="text-12px text-[var(--el-text-color-primary)] font-500">{{
selectedProduct?.name
}}</span>
<el-tag size="small" type="primary">{{ selectedProduct?.productKey }}</el-tag>
</div>
<div class="flex items-center gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备</span>
<span v-if="deviceSelectionMode === 'all'" class="text-12px text-[var(--el-text-color-primary)] font-500"></span>
<span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedDevice?.deviceName }}</span>
<el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> </el-tag>
<span
v-if="deviceSelectionMode === 'all'"
class="text-12px text-[var(--el-text-color-primary)] font-500"
>全部设备</span
>
<span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{
selectedDevice?.deviceName
}}</span>
<el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> </el-tag>
<el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)">
{{ getDeviceStatusText(selectedDevice?.state) }}
</el-tag>
@ -113,6 +129,7 @@
import { useVModel } from '@vueuse/core'
import { ProductApi } from '@/api/iot/product/product'
import { DeviceApi } from '@/api/iot/device/device'
import { DICT_TYPE } from '@/utils/dict'
/** 产品设备选择器组件 */
defineOptions({ name: 'ProductDeviceSelector' })
@ -124,7 +141,9 @@ interface Props {
interface Emits {
(e: 'update:productId', value?: number): void
(e: 'update:deviceId', value?: number): void
(e: 'change', value: { productId?: number; deviceId?: number }): void
}

View File

@ -17,16 +17,14 @@
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
product.name
}}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{
product.productKey
}}</div>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
>{{ product.name }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]"
>{{ product.productKey }}
</div>
</div>
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
{{ product.status === 0 ? '正常' : '禁用' }}
</el-tag>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
</div>
</el-option>
</el-select>
@ -34,21 +32,19 @@
<script setup lang="ts">
import { ProductApi } from '@/api/iot/product/product'
import { DICT_TYPE } from '@/utils/dict'
/** 产品选择器组件 */
defineOptions({ name: 'ProductSelector' })
interface Props {
defineProps<{
modelValue?: number
}
}>()
interface Emits {
const emit = defineEmits<{
(e: 'update:modelValue', value?: number): void
(e: 'change', value?: number): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
}>()
//
const productLoading = ref(false)