fix(iot): 完善 rule data 的迁移

pull/345/head
YunaiV 2026-05-20 08:45:51 +08:00
parent e7a61ce150
commit ec796b8336
22 changed files with 1040 additions and 735 deletions

View File

@ -1,103 +0,0 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleProductList } from '#/api/iot/product/product';
import { getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '规则名称',
component: 'Input',
componentProps: {
placeholder: '请输入规则名称',
allowClear: true,
},
},
{
fieldName: 'productId',
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
allowClear: true,
},
},
{
fieldName: 'status',
label: '规则状态',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
placeholder: '请选择状态',
allowClear: true,
},
},
{
fieldName: 'createTime',
label: '创建时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
field: 'id',
title: '规则编号',
minWidth: 80,
},
{
field: 'name',
title: '规则名称',
minWidth: 150,
},
{
field: 'description',
title: '规则描述',
minWidth: 200,
},
{
field: 'status',
title: '规则状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
// TODO @haohao这里是【数据源】【数据目的】
{
field: 'sinkCount',
title: '数据流转数',
minWidth: 100,
},
{
field: 'createTime',
title: '创建时间',
minWidth: 180,
formatter: 'formatDateTime',
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@ -1,128 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
// TODO @haohao tabapps/web-antd/src/views/ai/chat/manager import { ref } from 'vue';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import DataRuleList from './rule/index.vue';
import { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule'; import DataSinkList from './sink/index.vue';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; // TODO DONE @AI"/** IoT / */"线
import DataRuleForm from './rule/data-rule-form.vue'; const activeTabName = ref('rule');
/** IoT 数据流转规则列表 */
defineOptions({ name: 'IoTDataRule' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DataRuleForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建规则 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 编辑规则 */
function handleEdit(row: any) {
formModalApi.setData({ type: 'update', id: row.id }).open();
}
/** 删除规则 */
async function handleDelete(row: any) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
await deleteDataRule(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getDataRulePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions,
});
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<FormModal @success="handleRefresh" /> <Tabs v-model:active-key="activeTabName">
<Grid table-title=""> <Tabs.TabPane key="rule" tab="规则">
<template #toolbar-tools> <DataRuleList />
<TableAction </Tabs.TabPane>
:actions="[ <Tabs.TabPane key="sink" tab="目的">
{ <DataSinkList />
label: $t('ui.actionTitle.create', ['规则']), </Tabs.TabPane>
type: 'primary', </Tabs>
icon: ACTION_ICON.ADD,
auth: ['iot:data-rule:create'],
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['iot:data-rule:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['iot:data-rule:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page> </Page>
</template> </template>

View File

@ -1,9 +1,11 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
@ -44,11 +46,11 @@ export function useGridFormSchema(): VbenFormSchema[] {
export function useRuleFormSchema(): VbenFormSchema[] { export function useRuleFormSchema(): VbenFormSchema[] {
return [ return [
{ {
fieldName: 'id',
component: 'Input', component: 'Input',
fieldName: 'id',
dependencies: { dependencies: {
show: false, triggerFields: [''],
triggerFields: ['id'], show: () => false,
}, },
}, },
{ {
@ -82,12 +84,14 @@ export function useRuleFormSchema(): VbenFormSchema[] {
{ {
fieldName: 'sinkIds', fieldName: 'sinkIds',
label: '数据目的', label: '数据目的',
component: 'Select', component: 'ApiSelect',
componentProps: { componentProps: {
placeholder: '请选择数据目的', api: getDataSinkSimpleList,
labelField: 'name',
valueField: 'id',
mode: 'multiple', mode: 'multiple',
allowClear: true, allowClear: true,
options: [], placeholder: '请选择数据目的',
}, },
rules: 'required', rules: 'required',
}, },
@ -95,7 +99,7 @@ export function useRuleFormSchema(): VbenFormSchema[] {
} }
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(): VxeTableGridOptions<DataRuleApi.DataRule>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 }, { type: 'checkbox', width: 40 },
{ {
@ -126,13 +130,13 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'sourceConfigs', field: 'sourceConfigs',
title: '数据源', title: '数据源',
minWidth: 100, minWidth: 100,
formatter: ({ cellValue }: any) => `${cellValue?.length || 0}`, formatter: ({ cellValue }) => `${cellValue?.length || 0}`,
}, },
{ {
field: 'sinkIds', field: 'sinkIds',
title: '数据目的', title: '数据目的',
minWidth: 100, minWidth: 100,
formatter: ({ cellValue }: any) => `${cellValue?.length || 0}`, formatter: ({ cellValue }) => `${cellValue?.length || 0}`,
}, },
{ {
field: 'createTime', field: 'createTime',

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
@ -10,11 +11,10 @@ import { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import DataRuleForm from './data-rule-form.vue'; import Form from './modules/form.vue';
/** IoT 数据流转规则列表 */
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DataRuleForm, connectedComponent: Form,
destroyOnClose: true, destroyOnClose: true,
}); });
@ -29,18 +29,18 @@ function handleCreate() {
} }
/** 编辑规则 */ /** 编辑规则 */
function handleEdit(row: any) { function handleEdit(row: DataRuleApi.DataRule) {
formModalApi.setData({ id: row.id }).open(); formModalApi.setData({ id: row.id }).open();
} }
/** 删除规则 */ /** 删除规则 */
async function handleDelete(row: any) { async function handleDelete(row: DataRuleApi.DataRule) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteDataRule(row.id); await deleteDataRule(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh(); handleRefresh();
} finally { } finally {
@ -75,7 +75,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions, } as VxeTableGridOptions<DataRuleApi.DataRule>,
}); });
</script> </script>

View File

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@ -11,17 +13,14 @@ import {
getDataRule, getDataRule,
updateDataRule, updateDataRule,
} from '#/api/iot/rule/data/rule'; } from '#/api/iot/rule/data/rule';
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales'; import { $t } from '#/locales';
import SourceConfigForm from './components/source-config-form.vue'; import { useRuleFormSchema } from '../data';
import { useRuleFormSchema } from './data'; import SourceConfigForm from './source-config-form.vue';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<any>(); const formData = ref<DataRuleApi.DataRule>();
const sourceConfigRef = ref(); const sourceConfigRef = ref<InstanceType<typeof SourceConfigForm>>();
// TODO @haohao modules
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['数据规则']) ? $t('ui.actionTitle.edit', ['数据规则'])
@ -41,22 +40,18 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false, showDefaultActions: false,
}); });
// TODO @haohao
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
if (!valid) { if (!valid) {
return; return;
} }
// //
await sourceConfigRef.value?.validate(); await sourceConfigRef.value?.validate();
modalApi.lock(); modalApi.lock();
// //
const data = (await formApi.getValues()) as any; const data = (await formApi.getValues()) as DataRuleApi.DataRule;
data.sourceConfigs = sourceConfigRef.value?.getData() || []; data.sourceConfigs = sourceConfigRef.value?.getData() || [];
try { try {
await (formData.value?.id ? updateDataRule(data) : createDataRule(data)); await (formData.value?.id ? updateDataRule(data) : createDataRule(data));
// //
@ -74,22 +69,7 @@ const [Modal, modalApi] = useVbenModal({
return; return;
} }
// //
const data = modalApi.getData<any>(); const data = modalApi.getData<DataRuleApi.DataRule>();
//
const sinkList = await getDataSinkSimpleList();
formApi.updateSchema([
{
fieldName: 'sinkIds',
componentProps: {
options: sinkList.map((item: any) => ({
label: item.name,
value: item.id,
})),
},
},
]);
if (!data || !data.id) { if (!data || !data.id) {
return; return;
} }

View File

@ -1,4 +1,4 @@
<script setup lang="ts"> <script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { IotDeviceMessageMethodEnum } from '@vben/constants'; import { IotDeviceMessageMethodEnum } from '@vben/constants';
@ -190,7 +190,7 @@ const columns = [
{ {
title: '操作', title: '操作',
width: 80, width: 80,
fixed: 'right', fixed: 'right' as const,
}, },
]; ];
@ -199,8 +199,6 @@ defineExpose({ validate, getData, setData });
<template> <template>
<Form ref="formRef" :model="{ data: formData }"> <Form ref="formRef" :model="{ data: formData }">
<!-- TODO @haohao貌似有告警 -->
<!-- TODO @haohao是不是搞成 web-antd/src/views/erp/finance/receipt/modules/item-form.vue 的做法通过 Grid apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue目的后续 ele 通用性更好 -->
<Table <Table
:columns="columns" :columns="columns"
:data-source="formData" :data-source="formData"

View File

@ -64,14 +64,14 @@ watch(
<template> <template>
<div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full"> <div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full">
<Input v-model="item.key" class="mr-2" placeholder="键" /> <Input v-model:value="item.key" class="mr-2" placeholder="键" />
<Input v-model="item.value" placeholder="值" /> <Input v-model:value="item.value" placeholder="值" />
<Button class="ml-2" text danger @click="removeItem(index)"> <Button class="ml-2" type="link" danger @click="removeItem(index)">
<IconifyIcon icon="ant-design:delete-outlined" /> <IconifyIcon icon="ant-design:delete-outlined" />
删除 删除
</Button> </Button>
</div> </div>
<Button text type="primary" @click="addItem"> <Button type="link" @click="addItem">
<IconifyIcon icon="ant-design:plus-outlined" /> <IconifyIcon icon="ant-design:plus-outlined" />
{{ addButtonText }} {{ addButtonText }}
</Button> </Button>

View File

@ -0,0 +1,125 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { isEmpty } from '@vben/utils';
import { useClipboard, useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const TABLE_SQL = `CREATE TABLE iot_device_message_sink (
id VARCHAR(64) NOT NULL COMMENT '消息ID',
device_id BIGINT NOT NULL COMMENT '设备编号',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
method VARCHAR(128) COMMENT '请求方法',
report_time DATETIME COMMENT '上报时间',
data TEXT COMMENT '完整消息JSON',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id) USING BTREE,
INDEX idx_create_time (create_time ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'IoT 设备消息流转目标表';`;
const config = useVModel(props, 'modelValue', emit);
const showSqlTip = ref(false);
const copied = ref(false);
const { copy } = useClipboard();
async function handleCopySql() {
await copy(TABLE_SQL);
copied.value = true;
message.success('建表 SQL 已复制到剪贴板');
setTimeout(() => (copied.value = false), 2000);
}
onMounted(() => {
if (!isEmpty(config.value)) {
return;
}
config.value = {
type: `${IotDataSinkTypeEnum.DATABASE}`,
jdbcUrl: '',
username: '',
password: '',
tableName: 'iot_device_message_sink',
};
});
</script>
<template>
<Form.Item
:name="['config', 'jdbcUrl']"
:rules="[{ required: true, message: 'JDBC 连接地址不能为空', trigger: 'blur' }]"
label="JDBC 地址"
>
<Input
v-model:value="config.jdbcUrl"
placeholder="请输入 JDBC 连接地址jdbc:mysql://localhost:3306/iot_data"
/>
</Form.Item>
<Form.Item
:name="['config', 'username']"
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
label="用户名"
>
<Input v-model:value="config.username" placeholder="请输入数据库用户名" />
</Form.Item>
<Form.Item
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password
v-model:value="config.password"
placeholder="请输入数据库密码"
/>
</Form.Item>
<Form.Item
:name="['config', 'tableName']"
:rules="[{ required: true, message: '目标表名不能为空', trigger: 'blur' }]"
label="目标表名"
>
<div class="flex items-center gap-3">
<Input
v-model:value="config.tableName"
placeholder="目标表名"
class="w-[240px]"
/>
<Button type="link" @click="showSqlTip = !showSqlTip">
<IconifyIcon
:icon="showSqlTip ? 'lucide:chevron-up' : 'lucide:file-text'"
class="mr-1"
/>
{{ showSqlTip ? '收起表结构提示' : '查看表结构提示' }}
</Button>
</div>
</Form.Item>
<div
v-if="showSqlTip"
class="mt-2 overflow-hidden rounded border border-gray-200 dark:border-gray-700"
>
<div
class="flex items-center justify-between bg-gray-100 px-3 py-2 dark:bg-gray-800"
>
<span class="text-xs text-gray-600 dark:text-gray-300">
目标数据库需包含以下结构的表才能正常接收数据流转的消息
</span>
<Button size="small" @click="handleCopySql">
<IconifyIcon
:icon="copied ? 'lucide:check' : 'lucide:copy'"
class="mr-1"
/>
{{ copied ? '已复制' : '复制 SQL' }}
</Button>
</div>
<pre
class="m-0 overflow-x-auto bg-gray-50 p-3 font-mono text-[12px] leading-normal text-gray-800 dark:bg-gray-900 dark:text-gray-200"
><code>{{ TABLE_SQL }}</code></pre>
</div>
</template>

View File

@ -1,53 +1,46 @@
<!--suppress HttpUrlsUsage -->
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input, Select } from 'ant-design-vue'; import { Form, Input, Select } from 'ant-design-vue';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
import KeyValueEditor from './components/key-value-editor.vue'; import KeyValueEditor from './components/key-value-editor.vue';
defineOptions({ name: 'HttpConfigForm' }); const props = defineProps<{ modelValue: any }>();
const props = defineProps<{
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
// noinspection HttpUrlsUsage const urlPrefix = ref<'http://' | 'https://'>('http://');
/** URL处理 */
const urlPrefix = ref('http://');
const urlPath = ref(''); const urlPath = ref('');
const fullUrl = computed(() => { const fullUrl = computed(() =>
return urlPath.value ? urlPrefix.value + urlPath.value : ''; urlPath.value ? urlPrefix.value + urlPath.value : '',
}); );
/** 监听 URL 变化 */
watch([urlPrefix, urlPath], () => { watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value; config.value.url = fullUrl.value;
}); });
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
// URL if (config.value.url?.startsWith('https://')) {
if (config.value.url) { urlPrefix.value = 'https://';
if (config.value.url.startsWith('https://')) { urlPath.value = config.value.url.slice(8);
urlPrefix.value = 'https://'; } else if (config.value.url?.startsWith('http://')) {
urlPath.value = config.value.url.slice(8); urlPrefix.value = 'http://';
} else if (config.value.url.startsWith('http://')) { urlPath.value = config.value.url.slice(7);
urlPrefix.value = 'http://'; } else {
urlPath.value = config.value.url.slice(7); urlPath.value = config.value.url ?? '';
} else {
urlPath.value = config.value.url;
}
} }
return; return;
} }
config.value = { config.value = {
type: `${IotDataSinkTypeEnum.HTTP}`,
url: '', url: '',
method: 'POST', method: 'POST',
headers: {}, headers: {},
@ -55,51 +48,46 @@ onMounted(() => {
body: '', body: '',
}; };
}); });
const methodOptions = [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
];
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="请求地址" required> :name="['config', 'url']"
<Input v-model:value="urlPath" placeholder="请输入请求地址"> :rules="[{ required: true, message: '请求地址不能为空', trigger: 'blur' }]"
<template #addonBefore> label="请求地址"
<Select >
v-model:value="urlPrefix" <Input v-model:value="urlPath" placeholder="请输入请求地址">
placeholder="Select" <template #addonBefore>
style="width: 115px" <Select v-model:value="urlPrefix" class="w-[100px]">
:options="[ <Select.Option value="http://">http://</Select.Option>
{ label: 'http://', value: 'http://' }, <Select.Option value="https://">https://</Select.Option>
{ label: 'https://', value: 'https://' }, </Select>
]" </template>
/> </Input>
</template> </Form.Item>
</Input> <Form.Item
</FormItem> :name="['config', 'method']"
<FormItem label="请求方法" required> :rules="[{ required: true, message: '请求方法不能为空', trigger: 'change' }]"
<Select label="请求方法"
v-model:value="config.method" >
placeholder="请选择请求方法" <Select v-model:value="config.method" placeholder="请选择请求方法">
:options="methodOptions" <Select.Option value="GET">GET</Select.Option>
/> <Select.Option value="POST">POST</Select.Option>
</FormItem> <Select.Option value="PUT">PUT</Select.Option>
<FormItem label="请求头"> <Select.Option value="DELETE">DELETE</Select.Option>
<KeyValueEditor v-model="config.headers" add-button-text="" /> </Select>
</FormItem> </Form.Item>
<FormItem label="请求参数"> <Form.Item label="请求头">
<KeyValueEditor v-model="config.query" add-button-text="" /> <KeyValueEditor v-model="config.headers" add-button-text="" />
</FormItem> </Form.Item>
<FormItem label="请求体"> <Form.Item label="请求参数">
<Input.TextArea <KeyValueEditor v-model="config.query" add-button-text="" />
v-model:value="config.body" </Form.Item>
placeholder="请输入内容" <Form.Item label="请求体">
:rows="3" <Input.TextArea
/> v-model:value="config.body"
</FormItem> placeholder="请输入内容"
</div> :rows="4"
/>
</Form.Item>
</template> </template>

View File

@ -1,6 +1,9 @@
export { default as DatabaseConfigForm } from './database-config-form.vue';
export { default as HttpConfigForm } from './http-config-form.vue'; export { default as HttpConfigForm } from './http-config-form.vue';
export { default as KafkaMqConfigForm } from './kafka-mq-config-form.vue'; export { default as KafkaMqConfigForm } from './kafka-mq-config-form.vue';
export { default as MqttConfigForm } from './mqtt-config-form.vue'; export { default as MqttConfigForm } from './mqtt-config-form.vue';
export { default as RabbitMqConfigForm } from './rabbit-mq-config-form.vue'; export { default as RabbitMqConfigForm } from './rabbit-mq-config-form.vue';
export { default as RedisStreamConfigForm } from './redis-stream-config-form.vue'; export { default as RedisStreamConfigForm } from './redis-stream-config-form.vue';
export { default as RocketMqConfigForm } from './rocket-mq-config-form.vue'; export { default as RocketMqConfigForm } from './rocket-mq-config-form.vue';
export { default as TcpConfigForm } from './tcp-config-form.vue';
export { default as WebSocketConfigForm } from './websocket-config-form.vue';

View File

@ -1,25 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input, Switch } from 'ant-design-vue'; import { Form, Input, Switch } from 'ant-design-vue';
defineOptions({ name: 'KafkaMQConfigForm' }); import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ const props = defineProps<{ modelValue: any }>();
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
return; return;
} }
config.value = { config.value = {
type: `${IotDataSinkTypeEnum.KAFKA}`,
bootstrapServers: '', bootstrapServers: '',
username: '', username: '',
password: '', password: '',
@ -30,27 +29,38 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="服务地址" required> :name="['config', 'bootstrapServers']"
<Input :rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
v-model:value="config.bootstrapServers" label="服务地址"
placeholder="请输入服务地址localhost:9092" >
/> <Input
</FormItem> v-model:value="config.bootstrapServers"
<FormItem label="用户名"> placeholder="请输入服务地址localhost:9092"
<Input v-model:value="config.username" placeholder="请输入用户名" /> />
</FormItem> </Form.Item>
<FormItem label="密码"> <Form.Item
<Input.Password :name="['config', 'username']"
v-model:value="config.password" :rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
placeholder="请输入密码" label="用户名"
/> >
</FormItem> <Input v-model:value="config.username" placeholder="请输入用户名" />
<FormItem label="启用 SSL" required> </Form.Item>
<Switch v-model:checked="config.ssl" /> <Form.Item
</FormItem> :name="['config', 'password']"
<FormItem label="主题" required> :rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
<Input v-model:value="config.topic" placeholder="请输入主题" /> label="密码"
</FormItem> >
</div> <Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item :name="['config', 'ssl']" label="启用 SSL">
<Switch v-model:checked="config.ssl" />
</Form.Item>
<Form.Item
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</template> </template>

View File

@ -1,25 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input } from 'ant-design-vue'; import { Form, Input } from 'ant-design-vue';
defineOptions({ name: 'MqttConfigForm' }); import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ const props = defineProps<{ modelValue: any }>();
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
return; return;
} }
config.value = { config.value = {
type: `${IotDataSinkTypeEnum.MQTT}`,
url: '', url: '',
username: '', username: '',
password: '', password: '',
@ -30,27 +29,42 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="服务地址" required> :name="['config', 'url']"
<Input :rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
v-model:value="config.url" label="服务地址"
placeholder="请输入MQTT服务地址mqtt://localhost:1883" >
/> <Input
</FormItem> v-model:value="config.url"
<FormItem label="用户名" required> placeholder="请输入 MQTT 服务地址mqtt://localhost:1883"
<Input v-model:value="config.username" placeholder="请输入用户名" /> />
</FormItem> </Form.Item>
<FormItem label="密码" required> <Form.Item
<Input.Password :name="['config', 'username']"
v-model:value="config.password" :rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
placeholder="请输入密码" label="用户名"
/> >
</FormItem> <Input v-model:value="config.username" placeholder="请输入用户名" />
<FormItem label="客户端ID" required> </Form.Item>
<Input v-model:value="config.clientId" placeholder="请输入客户端ID" /> <Form.Item
</FormItem> :name="['config', 'password']"
<FormItem label="主题" required> :rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
<Input v-model:value="config.topic" placeholder="请输入主题" /> label="密码"
</FormItem> >
</div> <Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
:name="['config', 'clientId']"
:rules="[{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }]"
label="客户端 ID"
>
<Input v-model:value="config.clientId" placeholder="请输入客户端 ID" />
</Form.Item>
<Form.Item
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</template> </template>

View File

@ -1,30 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input, InputNumber } from 'ant-design-vue'; import { Form, Input, InputNumber } from 'ant-design-vue';
defineOptions({ name: 'RabbitMQConfigForm' }); import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ const props = defineProps<{ modelValue: any }>();
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
return; return;
} }
config.value = { config.value = {
type: `${IotDataSinkTypeEnum.RABBITMQ}`,
host: '', host: '',
port: 5672, port: 5672,
virtualHost: '/',
username: '', username: '',
password: '', password: '',
virtualHost: '/',
exchange: '', exchange: '',
routingKey: '', routingKey: '',
queue: '', queue: '',
@ -33,45 +32,75 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="主机地址" required> :name="['config', 'host']"
<Input :rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
v-model:value="config.host" label="主机地址"
placeholder="请输入主机地址localhost" >
/> <Input v-model:value="config.host" placeholder="请输入主机地址localhost" />
</FormItem> </Form.Item>
<FormItem label="端口" required> <Form.Item
<InputNumber :name="['config', 'port']"
v-model:value="config.port" :rules="[
:min="1" { required: true, message: '端口不能为空', trigger: 'blur' },
:max="65535" {
placeholder="请输入端口5672" type: 'number',
class="w-full" min: 1,
/> max: 65_535,
</FormItem> message: '端口号范围 1-65535',
<FormItem label="用户名" required> trigger: 'blur',
<Input v-model:value="config.username" placeholder="请输入用户名" /> },
</FormItem> ]"
<FormItem label="密码" required> label="端口"
<Input.Password >
v-model:value="config.password" <InputNumber
placeholder="请输入密码" v-model:value="config.port"
/> :max="65535"
</FormItem> :min="1"
<FormItem label="虚拟主机" required> placeholder="请输入端口"
<Input class="w-full"
v-model:value="config.virtualHost" />
placeholder="请输入虚拟主机,如:/" </Form.Item>
/> <Form.Item
</FormItem> :name="['config', 'virtualHost']"
<FormItem label="交换机" required> :rules="[{ required: true, message: '虚拟主机不能为空', trigger: 'blur' }]"
<Input v-model:value="config.exchange" placeholder="请输入交换机名称" /> label="虚拟主机"
</FormItem> >
<FormItem label="路由键" required> <Input v-model:value="config.virtualHost" placeholder="请输入虚拟主机" />
<Input v-model:value="config.routingKey" placeholder="请输入路由键" /> </Form.Item>
</FormItem> <Form.Item
<FormItem label="队列" required> :name="['config', 'username']"
<Input v-model:value="config.queue" placeholder="请输入队列名称" /> :rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
</FormItem> label="用户名"
</div> >
<Input v-model:value="config.username" placeholder="请输入用户名" />
</Form.Item>
<Form.Item
:name="['config', 'password']"
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
label="密码"
>
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
:name="['config', 'exchange']"
:rules="[{ required: true, message: '交换机不能为空', trigger: 'blur' }]"
label="交换机"
>
<Input v-model:value="config.exchange" placeholder="请输入交换机" />
</Form.Item>
<Form.Item
:name="['config', 'routingKey']"
:rules="[{ required: true, message: '路由键不能为空', trigger: 'blur' }]"
label="路由键"
>
<Input v-model:value="config.routingKey" placeholder="请输入路由键" />
</Form.Item>
<Form.Item
:name="['config', 'queue']"
:rules="[{ required: true, message: '队列不能为空', trigger: 'blur' }]"
label="队列"
>
<Input v-model:value="config.queue" placeholder="请输入队列" />
</Form.Item>
</template> </template>

View File

@ -1,58 +1,92 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input, InputNumber } from 'ant-design-vue'; import { Form, Input, InputNumber } from 'ant-design-vue';
defineOptions({ name: 'RedisStreamConfigForm' }); import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ const props = defineProps<{ modelValue: any }>();
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
return; return;
} }
config.value = { config.value = {
url: '', type: `${IotDataSinkTypeEnum.REDIS_STREAM}`,
host: '',
port: 6379,
password: '', password: '',
database: 0, database: 0,
streamKey: '', topic: '',
}; };
}); });
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="服务地址" required> :name="['config', 'host']"
<Input :rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
v-model:value="config.url" label="主机地址"
placeholder="请输入Redis服务地址redis://127.0.0.1:6379" >
/> <Input v-model:value="config.host" placeholder="请输入主机地址localhost" />
</FormItem> </Form.Item>
<FormItem label="密码"> <Form.Item
<Input.Password :name="['config', 'port']"
v-model:value="config.password" :rules="[
placeholder="请输入密码" { required: true, message: '端口不能为空', trigger: 'blur' },
/> {
</FormItem> type: 'number',
<FormItem label="数据库索引" required> min: 1,
<InputNumber max: 65_535,
v-model:value="config.database" message: '端口号范围 1-65535',
:min="0" trigger: 'blur',
:max="15" },
placeholder="请输入数据库索引" ]"
class="w-full" label="端口"
/> >
</FormItem> <InputNumber
<FormItem label="Stream Key" required> v-model:value="config.port"
<Input v-model:value="config.streamKey" placeholder="请输入Stream Key" /> :max="65535"
</FormItem> :min="1"
</div> placeholder="请输入端口"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'password']" label="密码">
<Input.Password v-model:value="config.password" placeholder="请输入密码" />
</Form.Item>
<Form.Item
:name="['config', 'database']"
:rules="[
{ required: true, message: '数据库索引不能为空', trigger: 'blur' },
{
type: 'number',
min: 0,
message: '数据库索引必须是非负整数',
trigger: 'blur',
},
]"
label="数据库"
>
<InputNumber
v-model:value="config.database"
:max="15"
:min="0"
placeholder="请输入数据库索引"
class="w-full"
/>
</Form.Item>
<Form.Item
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
</template> </template>

View File

@ -1,25 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { FormItem, Input } from 'ant-design-vue'; import { Form, Input } from 'ant-design-vue';
defineOptions({ name: 'RocketMQConfigForm' }); import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ const props = defineProps<{ modelValue: any }>();
modelValue: any;
}>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
return; return;
} }
config.value = { config.value = {
type: `${IotDataSinkTypeEnum.ROCKETMQ}`,
nameServer: '', nameServer: '',
accessKey: '', accessKey: '',
secretKey: '', secretKey: '',
@ -31,30 +30,48 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="space-y-4"> <Form.Item
<FormItem label="NameServer" required> :name="['config', 'nameServer']"
<Input :rules="[{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }]"
v-model:value="config.nameServer" label="NameServer"
placeholder="请输入 NameServer 地址127.0.0.1:9876" >
/> <Input
</FormItem> v-model:value="config.nameServer"
<FormItem label="AccessKey" required> placeholder="请输入 NameServer 地址127.0.0.1:9876"
<Input v-model:value="config.accessKey" placeholder="请输入 AccessKey" /> />
</FormItem> </Form.Item>
<FormItem label="SecretKey" required> <Form.Item
<Input.Password :name="['config', 'accessKey']"
v-model:value="config.secretKey" :rules="[{ required: true, message: 'AccessKey 不能为空', trigger: 'blur' }]"
placeholder="请输入 SecretKey" label="AccessKey"
/> >
</FormItem> <Input v-model:value="config.accessKey" placeholder="请输入 AccessKey" />
<FormItem label="消费组" required> </Form.Item>
<Input v-model:value="config.group" placeholder="请输入消费组" /> <Form.Item
</FormItem> :name="['config', 'secretKey']"
<FormItem label="主题" required> :rules="[{ required: true, message: 'SecretKey 不能为空', trigger: 'blur' }]"
<Input v-model:value="config.topic" placeholder="请输入主题" /> label="SecretKey"
</FormItem> >
<FormItem label="标签"> <Input.Password
<Input v-model:value="config.tags" placeholder="请输入标签" /> v-model:value="config.secretKey"
</FormItem> placeholder="请输入 SecretKey"
</div> />
</Form.Item>
<Form.Item
:name="['config', 'group']"
:rules="[{ required: true, message: '消费组不能为空', trigger: 'blur' }]"
label="消费组"
>
<Input v-model:value="config.group" placeholder="请输入消费组" />
</Form.Item>
<Form.Item
:name="['config', 'topic']"
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
label="主题"
>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</Form.Item>
<Form.Item :name="['config', 'tags']" label="标签">
<Input v-model:value="config.tags" placeholder="请输入标签" />
</Form.Item>
</template> </template>

View File

@ -0,0 +1,137 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber, Select, Switch } from 'ant-design-vue';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit);
onMounted(() => {
if (!isEmpty(config.value)) {
return;
}
config.value = {
type: `${IotDataSinkTypeEnum.TCP}`,
host: '',
port: 8080,
connectTimeoutMs: 5000,
readTimeoutMs: 10_000,
ssl: false,
sslCertPath: '',
dataFormat: 'JSON',
heartbeatIntervalMs: 30_000,
reconnectIntervalMs: 5000,
maxReconnectAttempts: 3,
};
});
</script>
<template>
<Form.Item
:name="['config', 'host']"
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
label="服务器地址"
>
<Input
v-model:value="config.host"
placeholder="请输入 TCP 服务器地址localhost"
/>
</Form.Item>
<Form.Item
:name="['config', 'port']"
:rules="[
{ required: true, message: '端口不能为空', trigger: 'blur' },
{
type: 'number',
min: 1,
max: 65_535,
message: '端口号范围 1-65535',
trigger: 'blur',
},
]"
label="端口"
>
<InputNumber
v-model:value="config.port"
:max="65535"
:min="1"
placeholder="请输入端口"
class="w-full"
/>
</Form.Item>
<Form.Item
:name="['config', 'connectTimeoutMs']"
:rules="[{ required: true, message: '连接超时时间不能为空', trigger: 'blur' }]"
label="连接超时(ms)"
>
<InputNumber
v-model:value="config.connectTimeoutMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item
:name="['config', 'readTimeoutMs']"
:rules="[{ required: true, message: '读取超时时间不能为空', trigger: 'blur' }]"
label="读取超时(ms)"
>
<InputNumber
v-model:value="config.readTimeoutMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'ssl']" label="启用 SSL">
<Switch v-model:checked="config.ssl" />
</Form.Item>
<Form.Item
v-if="config.ssl"
:name="['config', 'sslCertPath']"
label="SSL 证书路径"
>
<Input v-model:value="config.sslCertPath" placeholder="请输入 SSL 证书路径" />
</Form.Item>
<Form.Item
:name="['config', 'dataFormat']"
:rules="[{ required: true, message: '数据格式不能为空', trigger: 'change' }]"
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<Select.Option value="JSON">JSON</Select.Option>
<Select.Option value="BINARY">BINARY</Select.Option>
</Select>
</Form.Item>
<Form.Item :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
<InputNumber
v-model:value="config.heartbeatIntervalMs"
:min="0"
:step="1000"
placeholder="0 表示不启用心跳"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
<InputNumber
v-model:value="config.reconnectIntervalMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
<InputNumber
v-model:value="config.maxReconnectAttempts"
:min="0"
class="w-full"
/>
</Form.Item>
</template>

View File

@ -0,0 +1,151 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, InputNumber, Select, Switch } from 'ant-design-vue';
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit);
onMounted(() => {
if (!isEmpty(config.value)) {
return;
}
config.value = {
type: `${IotDataSinkTypeEnum.WEBSOCKET}`,
serverUrl: '',
connectTimeoutMs: 5000,
sendTimeoutMs: 10_000,
heartbeatIntervalMs: 30_000,
heartbeatMessage: '{"type":"heartbeat"}',
subprotocols: '',
customHeaders: '',
verifySslCert: true,
dataFormat: 'JSON',
reconnectIntervalMs: 5000,
maxReconnectAttempts: 3,
enableCompression: false,
sendRetryCount: 1,
sendRetryIntervalMs: 1000,
};
});
</script>
<template>
<Form.Item
:name="['config', 'serverUrl']"
:rules="[
{ required: true, message: 'WebSocket 服务器地址不能为空', trigger: 'blur' },
]"
label="服务器地址"
>
<Input
v-model:value="config.serverUrl"
placeholder="请输入 WebSocket 地址ws://localhost:8080/ws"
/>
</Form.Item>
<Form.Item
:name="['config', 'connectTimeoutMs']"
:rules="[{ required: true, message: '连接超时时间不能为空', trigger: 'blur' }]"
label="连接超时(ms)"
>
<InputNumber
v-model:value="config.connectTimeoutMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item
:name="['config', 'sendTimeoutMs']"
:rules="[{ required: true, message: '发送超时时间不能为空', trigger: 'blur' }]"
label="发送超时(ms)"
>
<InputNumber
v-model:value="config.sendTimeoutMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'heartbeatIntervalMs']" label="心跳间隔(ms)">
<InputNumber
v-model:value="config.heartbeatIntervalMs"
:min="0"
:step="1000"
placeholder="0 表示不启用心跳"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'heartbeatMessage']" label="心跳消息">
<Input
v-model:value="config.heartbeatMessage"
placeholder="请输入心跳消息内容JSON 格式)"
/>
</Form.Item>
<Form.Item :name="['config', 'subprotocols']" label="子协议">
<Input
v-model:value="config.subprotocols"
placeholder="请输入子协议列表,多个用逗号分隔"
/>
</Form.Item>
<Form.Item :name="['config', 'customHeaders']" label="自定义请求头">
<Input.TextArea
v-model:value="config.customHeaders"
placeholder="请输入自定义请求头JSON 格式)"
:rows="3"
/>
</Form.Item>
<Form.Item :name="['config', 'verifySslCert']" label="验证 SSL 证书">
<Switch v-model:checked="config.verifySslCert" />
</Form.Item>
<Form.Item
:name="['config', 'dataFormat']"
:rules="[{ required: true, message: '数据格式不能为空', trigger: 'change' }]"
label="数据格式"
>
<Select v-model:value="config.dataFormat" placeholder="请选择数据格式">
<Select.Option value="JSON">JSON</Select.Option>
<Select.Option value="TEXT">TEXT</Select.Option>
</Select>
</Form.Item>
<Form.Item :name="['config', 'reconnectIntervalMs']" label="重连间隔(ms)">
<InputNumber
v-model:value="config.reconnectIntervalMs"
:min="1000"
:step="1000"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'maxReconnectAttempts']" label="最大重连次数">
<InputNumber
v-model:value="config.maxReconnectAttempts"
:min="0"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'enableCompression']" label="启用压缩">
<Switch v-model:checked="config.enableCompression" />
</Form.Item>
<Form.Item :name="['config', 'sendRetryCount']" label="发送重试次数">
<InputNumber
v-model:value="config.sendRetryCount"
:min="0"
class="w-full"
/>
</Form.Item>
<Form.Item :name="['config', 'sendRetryIntervalMs']" label="重试间隔(ms)">
<InputNumber
v-model:value="config.sendRetryIntervalMs"
:min="100"
:step="500"
class="w-full"
/>
</Form.Item>
</template>

View File

@ -1,149 +0,0 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createDataSink,
getDataSink,
updateDataSink,
} from '#/api/iot/rule/data/sink';
import { $t } from '#/locales';
import {
HttpConfigForm,
KafkaMqConfigForm,
MqttConfigForm,
RabbitMqConfigForm,
RedisStreamConfigForm,
RocketMqConfigForm,
} from './config';
import { useSinkFormSchema } from './data';
const emit = defineEmits(['success']);
const IotDataSinkTypeEnum = {
HTTP: 1,
MQTT: 2,
ROCKETMQ: 3,
KAFKA: 4,
RABBITMQ: 5,
REDIS_STREAM: 6,
} as const;
const formData = ref<any>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['数据目的'])
: $t('ui.actionTitle.create', ['数据目的']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},
layout: 'horizontal',
schema: useSinkFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data = (await formApi.getValues()) as any;
data.config = formData.value.config;
try {
await (formData.value?.id ? updateDataSink(data) : createDataSink(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
//
const data = modalApi.getData<any>();
if (!data || !data.id) {
formData.value = {
type: IotDataSinkTypeEnum.HTTP,
status: 0,
config: {},
};
await formApi.setValues(formData.value);
return;
}
modalApi.lock();
try {
formData.value = await getDataSink(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
//
watch(
() => formApi.getValues().then((values) => values.type),
(newType) => {
if (formData.value && newType !== formData.value.type) {
formData.value.config = {};
}
},
);
</script>
<template>
<Modal class="w-3/5" :title="getTitle">
<Form class="mx-4" />
<div v-if="formData" class="mx-4 mt-4">
<div class="mb-2 text-sm font-medium">配置信息</div>
<!-- TODO @haohao下面的 form看看有没办法搞成 form schema 方便后续 ele 的迁移 -->
<HttpConfigForm
v-if="IotDataSinkTypeEnum.HTTP === formData.type"
v-model="formData.config"
/>
<MqttConfigForm
v-if="IotDataSinkTypeEnum.MQTT === formData.type"
v-model="formData.config"
/>
<RocketMqConfigForm
v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
v-model="formData.config"
/>
<KafkaMqConfigForm
v-if="IotDataSinkTypeEnum.KAFKA === formData.type"
v-model="formData.config"
/>
<RabbitMqConfigForm
v-if="IotDataSinkTypeEnum.RABBITMQ === formData.type"
v-model="formData.config"
/>
<RedisStreamConfigForm
v-if="IotDataSinkTypeEnum.REDIS_STREAM === formData.type"
v-model="formData.config"
/>
</div>
</Modal>
</template>

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DataSinkApi } from '#/api/iot/rule/data/sink';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
@ -50,60 +51,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
]; ];
} }
/** 目的表单 Schema */
export function useSinkFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
show: false,
triggerFields: ['id'],
},
},
{
fieldName: 'name',
label: '目的名称',
component: 'Input',
componentProps: {
placeholder: '请输入目的名称',
},
rules: 'required',
},
{
fieldName: 'description',
label: '目的描述',
component: 'Textarea',
componentProps: {
placeholder: '请输入目的描述',
rows: 3,
},
},
{
fieldName: 'type',
label: '目的类型',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, 'number'),
placeholder: '请选择目的类型',
},
rules: 'required',
},
{
fieldName: 'status',
label: '目的状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
defaultValue: 0,
rules: 'required',
},
];
}
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(): VxeTableGridOptions<DataSinkApi.DataSink>['columns'] {
return [ return [
{ type: 'checkbox', width: 40 }, { type: 'checkbox', width: 40 },
{ {

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DataSinkApi } from '#/api/iot/rule/data/sink';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
@ -10,15 +11,10 @@ import { deleteDataSink, getDataSinkPage } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import DataSinkForm from './data-sink-form.vue'; import Form from './modules/form.vue';
// TODO @haohao
/** IoT 数据流转目的列表 */
defineOptions({ name: 'IotDataSink' });
const [FormModal, formModalApi] = useVbenModal({ const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DataSinkForm, connectedComponent: Form,
destroyOnClose: true, destroyOnClose: true,
}); });
@ -29,22 +25,22 @@ function handleRefresh() {
/** 创建数据目的 */ /** 创建数据目的 */
function handleCreate() { function handleCreate() {
formModalApi.setData({ type: 'create' }).open(); formModalApi.setData(null).open();
} }
/** 编辑数据目的 */ /** 编辑数据目的 */
function handleEdit(row: any) { function handleEdit(row: DataSinkApi.DataSink) {
formModalApi.setData({ type: 'update', id: row.id }).open(); formModalApi.setData({ id: row.id }).open();
} }
/** 删除数据目的 */ /** 删除数据目的 */
async function handleDelete(row: any) { async function handleDelete(row: DataSinkApi.DataSink) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]), content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0, duration: 0,
}); });
try { try {
await deleteDataSink(row.id); await deleteDataSink(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name])); message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh(); handleRefresh();
} finally { } finally {
@ -79,7 +75,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true, refresh: true,
search: true, search: true,
}, },
} as VxeTableGridOptions, } as VxeTableGridOptions<DataSinkApi.DataSink>,
}); });
</script> </script>

View File

@ -0,0 +1,185 @@
<script lang="ts" setup>
import type { DataSinkApi } from '#/api/iot/rule/data/sink';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Form, Input, message, Radio, Select } from 'ant-design-vue';
import {
createDataSink,
getDataSink,
IotDataSinkTypeEnum,
updateDataSink,
} from '#/api/iot/rule/data/sink';
import { $t } from '#/locales';
import {
DatabaseConfigForm,
HttpConfigForm,
KafkaMqConfigForm,
MqttConfigForm,
RabbitMqConfigForm,
RedisStreamConfigForm,
RocketMqConfigForm,
TcpConfigForm,
WebSocketConfigForm,
} from '../config';
const emit = defineEmits(['success']);
const formRef = ref();
const formData = ref<DataSinkApi.DataSink>(buildEmptyFormData());
const getTitle = computed(() =>
formData.value.id
? $t('ui.actionTitle.edit', ['数据目的'])
: $t('ui.actionTitle.create', ['数据目的']),
);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await formRef.value?.validate();
} catch {
return;
}
modalApi.lock();
try {
const data = { ...formData.value };
await (data.id ? updateDataSink(data) : createDataSink(data));
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
formData.value = buildEmptyFormData();
formRef.value?.clearValidate?.();
const data = modalApi.getData<{ id?: number }>();
if (!data?.id) {
return;
}
modalApi.lock();
try {
formData.value = await getDataSink(data.id);
} finally {
modalApi.unlock();
}
},
});
/** 构造空白表单数据 */
function buildEmptyFormData(): DataSinkApi.DataSink {
return {
status: CommonStatusEnum.ENABLE,
type: IotDataSinkTypeEnum.HTTP,
config: {} as any,
};
}
/** 类型切换时清空配置,子组件 onMounted 会按新类型重新初始化 */
function handleTypeChange(type: number) {
formData.value.type = type;
formData.value.config = {} as any;
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form
ref="formRef"
:label-col="{ span: 5 }"
:model="formData"
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
:rules="[{ required: true, message: '目的名称不能为空', trigger: 'blur' }]"
label="目的名称"
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入目的名称" />
</Form.Item>
<Form.Item label="目的描述" name="description">
<Input.TextArea
v-model:value="formData.description"
placeholder="请输入目的描述"
:rows="3"
/>
</Form.Item>
<Form.Item
:rules="[{ required: true, message: '目的类型不能为空', trigger: 'change' }]"
label="目的类型"
name="type"
>
<Select
:value="formData.type"
:options="getDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, 'number') as any"
placeholder="请选择目的类型"
@change="(value: any) => handleTypeChange(value as number)"
/>
</Form.Item>
<!-- 配置项按目的类型分支 -->
<HttpConfigForm
v-if="formData.type === IotDataSinkTypeEnum.HTTP"
v-model="formData.config"
/>
<TcpConfigForm
v-if="formData.type === IotDataSinkTypeEnum.TCP"
v-model="formData.config"
/>
<WebSocketConfigForm
v-if="formData.type === IotDataSinkTypeEnum.WEBSOCKET"
v-model="formData.config"
/>
<MqttConfigForm
v-if="formData.type === IotDataSinkTypeEnum.MQTT"
v-model="formData.config"
/>
<DatabaseConfigForm
v-if="formData.type === IotDataSinkTypeEnum.DATABASE"
v-model="formData.config"
/>
<RocketMqConfigForm
v-if="formData.type === IotDataSinkTypeEnum.ROCKETMQ"
v-model="formData.config"
/>
<KafkaMqConfigForm
v-if="formData.type === IotDataSinkTypeEnum.KAFKA"
v-model="formData.config"
/>
<RabbitMqConfigForm
v-if="formData.type === IotDataSinkTypeEnum.RABBITMQ"
v-model="formData.config"
/>
<RedisStreamConfigForm
v-if="formData.type === IotDataSinkTypeEnum.REDIS_STREAM"
v-model="formData.config"
/>
<Form.Item
:rules="[{ required: true, message: '目的状态不能为空', trigger: 'change' }]"
label="目的状态"
name="status"
>
<Radio.Group v-model:value="formData.status">
<Radio
v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS, 'number')"
:key="String(dict.value)"
:value="dict.value"
>
{{ dict.label }}
</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
</template>

View File

@ -13,4 +13,43 @@ export const MesItemOrProductEnum = {
/** MES 自动编码规则 Code 枚举 */ /** MES 自动编码规则 Code 枚举 */
export const MesAutoCodeRuleCode = { export const MesAutoCodeRuleCode = {
MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE', MD_ITEM_TYPE_CODE: 'MD_ITEM_TYPE_CODE',
MD_ITEM_CODE: 'MD_ITEM_CODE',
} as const; } as const;
/** MES 条码格式枚举 */
export enum BarcodeFormatEnum {
QR_CODE = 1,
EAN13 = 2,
CODE39 = 3,
UPC_A = 4,
}
/** 条码格式映射表(枚举值 -> JsBarcode 格式名) */
export const BARCODE_FORMAT_MAP: Record<BarcodeFormatEnum, string> = {
[BarcodeFormatEnum.QR_CODE]: 'QR_CODE',
[BarcodeFormatEnum.EAN13]: 'EAN13',
[BarcodeFormatEnum.CODE39]: 'CODE39',
[BarcodeFormatEnum.UPC_A]: 'UPC_A',
};
/** MES 条码业务类型枚举 */
export enum BarcodeBizTypeEnum {
WAREHOUSE = 102,
LOCATION = 103,
AREA = 104,
PACKAGE = 105,
STOCK = 106,
BATCH = 107,
PROCARD = 300,
WORKORDER = 301,
TRANSORDER = 302,
TASK = 303,
MACHINERY = 400,
TOOL = 500,
ITEM = 600,
VENDOR = 601,
WORKSTATION = 602,
WORKSHOP = 603,
USER = 604,
CLIENT = 605,
}