fix: iot 补齐 vue3 源缺失的表单校验(P1)

- 物模型功能定义「数据类型」字段补 required 必填校验
- 产品选择器(ProductSelect)补搜索能力:antd 加 show-search + option-filter-prop ,ele 加 filterable
- 设备表单 onConfirm 增加 advancedFormApi.validate() 调用,
  否则高级表单(含经纬度等字段)的 schema rules 不会触发
- 设备经纬度增加「成对填写」跨字段校验:仅填一项时给 warning 提示,
  与 vue3 源 DeviceForm.vue 行为对齐
pull/345/head
YunaiV 2026-05-21 14:43:06 +08:00
parent d207e3b82c
commit 58f2e23654
12 changed files with 113 additions and 28 deletions

View File

@ -75,7 +75,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '是否处理', label: '是否处理',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING), options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
placeholder: '请选择是否处理', placeholder: '请选择是否处理',
allowClear: true, allowClear: true,
}, },

View File

@ -118,6 +118,26 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) { if (!valid) {
return; return;
} }
//
if (advancedFormApi.isMounted) {
const { valid: advancedValid } = await advancedFormApi.validate();
if (!advancedValid) {
return;
}
const advValues = await advancedFormApi.getValues();
const hasLongitude =
advValues.longitude !== undefined &&
advValues.longitude !== null &&
advValues.longitude !== '';
const hasLatitude =
advValues.latitude !== undefined &&
advValues.latitude !== null &&
advValues.latitude !== '';
if (hasLongitude !== hasLatitude) {
message.warning(hasLongitude ? '请同时填写设备纬度' : '请同时填写设备经度');
return;
}
}
modalApi.lock(); modalApi.lock();
// //
const basicValues = await formApi.getValues(); const basicValues = await formApi.getValues();

View File

@ -15,7 +15,7 @@ export function getMessageTrendChartOptions(
}, },
}, },
legend: { legend: {
data: ['上行消息', '下行消息'], data: ['上行消息', '下行消息'],
top: '5%', top: '5%',
}, },
grid: { grid: {
@ -40,11 +40,21 @@ export function getMessageTrendChartOptions(
], ],
series: [ series: [
{ {
name: '上行消息', name: '上行消息',
type: 'line', type: 'line',
smooth: true, smooth: true,
areaStyle: { areaStyle: {
opacity: 0.3, color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
{ offset: 1, color: 'rgba(24, 144, 255, 0)' },
],
},
}, },
emphasis: { emphasis: {
focus: 'series', focus: 'series',
@ -55,11 +65,21 @@ export function getMessageTrendChartOptions(
}, },
}, },
{ {
name: '下行消息', name: '下行消息',
type: 'line', type: 'line',
smooth: true, smooth: true,
areaStyle: { areaStyle: {
opacity: 0.3, color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
{ offset: 1, color: 'rgba(82, 196, 26, 0)' },
],
},
}, },
emphasis: { emphasis: {
focus: 'series', focus: 'series',
@ -84,8 +104,8 @@ export function getDeviceStateGaugeChartOptions(
series: [ series: [
{ {
type: 'gauge', type: 'gauge',
startAngle: 225, startAngle: 360,
endAngle: -45, endAngle: 0,
min: 0, min: 0,
max, max,
center: ['50%', '50%'], center: ['50%', '50%'],

View File

@ -3,7 +3,7 @@ import type { Dayjs } from 'dayjs';
import type { IotStatisticsApi } from '#/api/iot/statistics'; import type { IotStatisticsApi } from '#/api/iot/statistics';
import { computed, nextTick, onMounted, reactive, ref } from 'vue'; import { computed, nextTick, reactive, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
@ -123,10 +123,6 @@ async function renderChartWhenReady() {
initChart(); initChart();
} }
/** 组件挂载时查询数据 */
onMounted(() => {
fetchMessageData();
});
</script> </script>
<template> <template>

View File

@ -52,6 +52,8 @@ onMounted(() => {
placeholder="请选择产品" placeholder="请选择产品"
allow-clear allow-clear
class="w-full" class="w-full"
option-filter-prop="label"
show-search
@change="handleChange" @change="handleChange"
/> />
</template> </template>

View File

@ -102,7 +102,11 @@ if (!props.isStructDataSpecs && !props.isParams) {
</script> </script>
<template> <template>
<Form.Item label="数据类型"> <Form.Item
:name="['property', 'dataType']"
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
label="数据类型"
>
<Select <Select
v-model:value="property.dataType" v-model:value="property.dataType"
placeholder="请选择数据类型" placeholder="请选择数据类型"

View File

@ -75,7 +75,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '是否处理', label: '是否处理',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING), options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
placeholder: '请选择是否处理', placeholder: '请选择是否处理',
clearable: true, clearable: true,
}, },

View File

@ -123,6 +123,28 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) { if (!valid) {
return; return;
} }
//
if (advancedFormApi.isMounted) {
const { valid: advancedValid } = await advancedFormApi.validate();
if (!advancedValid) {
return;
}
const advValues = await advancedFormApi.getValues();
const hasLongitude =
advValues.longitude !== undefined &&
advValues.longitude !== null &&
advValues.longitude !== '';
const hasLatitude =
advValues.latitude !== undefined &&
advValues.latitude !== null &&
advValues.latitude !== '';
if (hasLongitude !== hasLatitude) {
ElMessage.warning(
hasLongitude ? '请同时填写设备纬度' : '请同时填写设备经度',
);
return;
}
}
modalApi.lock(); modalApi.lock();
// //
const basicValues = await formApi.getValues(); const basicValues = await formApi.getValues();

View File

@ -15,7 +15,7 @@ export function getMessageTrendChartOptions(
}, },
}, },
legend: { legend: {
data: ['上行消息', '下行消息'], data: ['上行消息', '下行消息'],
top: '5%', top: '5%',
}, },
grid: { grid: {
@ -40,11 +40,21 @@ export function getMessageTrendChartOptions(
], ],
series: [ series: [
{ {
name: '上行消息', name: '上行消息',
type: 'line', type: 'line',
smooth: true, smooth: true,
areaStyle: { areaStyle: {
opacity: 0.3, color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
{ offset: 1, color: 'rgba(24, 144, 255, 0)' },
],
},
}, },
emphasis: { emphasis: {
focus: 'series', focus: 'series',
@ -55,11 +65,21 @@ export function getMessageTrendChartOptions(
}, },
}, },
{ {
name: '下行消息', name: '下行消息',
type: 'line', type: 'line',
smooth: true, smooth: true,
areaStyle: { areaStyle: {
opacity: 0.3, color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
{ offset: 1, color: 'rgba(82, 196, 26, 0)' },
],
},
}, },
emphasis: { emphasis: {
focus: 'series', focus: 'series',
@ -84,8 +104,8 @@ export function getDeviceStateGaugeChartOptions(
series: [ series: [
{ {
type: 'gauge', type: 'gauge',
startAngle: 225, startAngle: 360,
endAngle: -45, endAngle: 0,
min: 0, min: 0,
max, max,
center: ['50%', '50%'], center: ['50%', '50%'],

View File

@ -3,7 +3,7 @@ import type { Dayjs } from 'dayjs';
import type { IotStatisticsApi } from '#/api/iot/statistics'; import type { IotStatisticsApi } from '#/api/iot/statistics';
import { computed, nextTick, onMounted, reactive, ref } from 'vue'; import { computed, nextTick, reactive, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
@ -123,10 +123,6 @@ async function renderChartWhenReady() {
initChart(); initChart();
} }
/** 组件挂载时查询数据 */
onMounted(() => {
fetchMessageData();
});
</script> </script>
<template> <template>

View File

@ -50,6 +50,7 @@ onMounted(() => {
:loading="loading" :loading="loading"
placeholder="请选择产品" placeholder="请选择产品"
clearable clearable
filterable
class="w-full" class="w-full"
@change="handleChange" @change="handleChange"
> >

View File

@ -109,7 +109,11 @@ if (!props.isStructDataSpecs && !props.isParams) {
</script> </script>
<template> <template>
<ElFormItem label="数据类型"> <ElFormItem
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
label="数据类型"
prop="property.dataType"
>
<ElSelect <ElSelect
v-model="property.dataType" v-model="property.dataType"
placeholder="请选择数据类型" placeholder="请选择数据类型"