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>
// TODO @haohao tabapps/web-antd/src/views/ai/chat/manager
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { ref } from 'vue';
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 { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule';
import { $t } from '#/locales';
import DataRuleList from './rule/index.vue';
import DataSinkList from './sink/index.vue';
import { useGridColumns, useGridFormSchema } from './data';
import DataRuleForm from './rule/data-rule-form.vue';
/** 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,
});
// TODO DONE @AI"/** IoT / */"线
const activeTabName = ref('rule');
</script>
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['规则']),
type: 'primary',
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>
<Tabs v-model:active-key="activeTabName">
<Tabs.TabPane key="rule" tab="规则">
<DataRuleList />
</Tabs.TabPane>
<Tabs.TabPane key="sink" tab="目的">
<DataSinkList />
</Tabs.TabPane>
</Tabs>
</Page>
</template>

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
@ -190,7 +190,7 @@ const columns = [
{
title: '操作',
width: 80,
fixed: 'right',
fixed: 'right' as const,
},
];
@ -199,8 +199,6 @@ defineExpose({ validate, getData, setData });
<template>
<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
:columns="columns"
:data-source="formData"

View File

@ -64,14 +64,14 @@ watch(
<template>
<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="item.value" placeholder="值" />
<Button class="ml-2" text danger @click="removeItem(index)">
<Input v-model:value="item.key" class="mr-2" placeholder="键" />
<Input v-model:value="item.value" placeholder="值" />
<Button class="ml-2" type="link" danger @click="removeItem(index)">
<IconifyIcon icon="ant-design:delete-outlined" />
删除
</Button>
</div>
<Button text type="primary" @click="addItem">
<Button type="link" @click="addItem">
<IconifyIcon icon="ant-design:plus-outlined" />
{{ addButtonText }}
</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>
import { computed, onMounted, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils';
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';
defineOptions({ name: 'HttpConfigForm' });
const props = defineProps<{
modelValue: any;
}>();
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any;
const config = useVModel(props, 'modelValue', emit);
// noinspection HttpUrlsUsage
/** URL处理 */
const urlPrefix = ref('http://');
const urlPrefix = ref<'http://' | 'https://'>('http://');
const urlPath = ref('');
const fullUrl = computed(() => {
return urlPath.value ? urlPrefix.value + urlPath.value : '';
});
const fullUrl = computed(() =>
urlPath.value ? urlPrefix.value + urlPath.value : '',
);
/** 监听 URL 变化 */
watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value;
});
/** 组件初始化 */
onMounted(() => {
if (!isEmpty(config.value)) {
// URL
if (config.value.url) {
if (config.value.url.startsWith('https://')) {
urlPrefix.value = 'https://';
urlPath.value = config.value.url.slice(8);
} else if (config.value.url.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = config.value.url.slice(7);
} else {
urlPath.value = config.value.url;
}
if (config.value.url?.startsWith('https://')) {
urlPrefix.value = 'https://';
urlPath.value = config.value.url.slice(8);
} else if (config.value.url?.startsWith('http://')) {
urlPrefix.value = 'http://';
urlPath.value = config.value.url.slice(7);
} else {
urlPath.value = config.value.url ?? '';
}
return;
}
config.value = {
type: `${IotDataSinkTypeEnum.HTTP}`,
url: '',
method: 'POST',
headers: {},
@ -55,51 +48,46 @@ onMounted(() => {
body: '',
};
});
const methodOptions = [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'DELETE', value: 'DELETE' },
];
</script>
<template>
<div class="space-y-4">
<FormItem label="请求地址" required>
<Input v-model:value="urlPath" placeholder="请输入请求地址">
<template #addonBefore>
<Select
v-model:value="urlPrefix"
placeholder="Select"
style="width: 115px"
:options="[
{ label: 'http://', value: 'http://' },
{ label: 'https://', value: 'https://' },
]"
/>
</template>
</Input>
</FormItem>
<FormItem label="请求方法" required>
<Select
v-model:value="config.method"
placeholder="请选择请求方法"
:options="methodOptions"
/>
</FormItem>
<FormItem label="请求头">
<KeyValueEditor v-model="config.headers" add-button-text="" />
</FormItem>
<FormItem label="请求参数">
<KeyValueEditor v-model="config.query" add-button-text="" />
</FormItem>
<FormItem label="请求体">
<Input.TextArea
v-model:value="config.body"
placeholder="请输入内容"
:rows="3"
/>
</FormItem>
</div>
<Form.Item
:name="['config', 'url']"
:rules="[{ required: true, message: '请求地址不能为空', trigger: 'blur' }]"
label="请求地址"
>
<Input v-model:value="urlPath" placeholder="请输入请求地址">
<template #addonBefore>
<Select v-model:value="urlPrefix" class="w-[100px]">
<Select.Option value="http://">http://</Select.Option>
<Select.Option value="https://">https://</Select.Option>
</Select>
</template>
</Input>
</Form.Item>
<Form.Item
:name="['config', 'method']"
:rules="[{ required: true, message: '请求方法不能为空', trigger: 'change' }]"
label="请求方法"
>
<Select v-model:value="config.method" placeholder="请选择请求方法">
<Select.Option value="GET">GET</Select.Option>
<Select.Option value="POST">POST</Select.Option>
<Select.Option value="PUT">PUT</Select.Option>
<Select.Option value="DELETE">DELETE</Select.Option>
</Select>
</Form.Item>
<Form.Item label="请求头">
<KeyValueEditor v-model="config.headers" add-button-text="" />
</Form.Item>
<Form.Item label="请求参数">
<KeyValueEditor v-model="config.query" add-button-text="" />
</Form.Item>
<Form.Item label="请求体">
<Input.TextArea
v-model:value="config.body"
placeholder="请输入内容"
:rows="4"
/>
</Form.Item>
</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 KafkaMqConfigForm } from './kafka-mq-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 RedisStreamConfigForm } from './redis-stream-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>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
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<{
modelValue: any;
}>();
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any;
const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => {
if (!isEmpty(config.value)) {
return;
}
config.value = {
type: `${IotDataSinkTypeEnum.KAFKA}`,
bootstrapServers: '',
username: '',
password: '',
@ -30,27 +29,38 @@ onMounted(() => {
</script>
<template>
<div class="space-y-4">
<FormItem label="服务地址" required>
<Input
v-model:value="config.bootstrapServers"
placeholder="请输入服务地址localhost:9092"
/>
</FormItem>
<FormItem label="用户名">
<Input v-model:value="config.username" placeholder="请输入用户名" />
</FormItem>
<FormItem label="密码">
<Input.Password
v-model:value="config.password"
placeholder="请输入密码"
/>
</FormItem>
<FormItem label="启用 SSL" required>
<Switch v-model:checked="config.ssl" />
</FormItem>
<FormItem label="主题" required>
<Input v-model:value="config.topic" placeholder="请输入主题" />
</FormItem>
</div>
<Form.Item
:name="['config', 'bootstrapServers']"
:rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
label="服务地址"
>
<Input
v-model:value="config.bootstrapServers"
placeholder="请输入服务地址localhost:9092"
/>
</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', '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>

View File

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

View File

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

View File

@ -1,58 +1,92 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { isEmpty } from '@vben/utils';
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<{
modelValue: any;
}>();
const props = defineProps<{ modelValue: any }>();
const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as any;
const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */
onMounted(() => {
if (!isEmpty(config.value)) {
return;
}
config.value = {
url: '',
type: `${IotDataSinkTypeEnum.REDIS_STREAM}`,
host: '',
port: 6379,
password: '',
database: 0,
streamKey: '',
topic: '',
};
});
</script>
<template>
<div class="space-y-4">
<FormItem label="服务地址" required>
<Input
v-model:value="config.url"
placeholder="请输入Redis服务地址redis://127.0.0.1:6379"
/>
</FormItem>
<FormItem label="密码">
<Input.Password
v-model:value="config.password"
placeholder="请输入密码"
/>
</FormItem>
<FormItem label="数据库索引" required>
<InputNumber
v-model:value="config.database"
:min="0"
:max="15"
placeholder="请输入数据库索引"
class="w-full"
/>
</FormItem>
<FormItem label="Stream Key" required>
<Input v-model:value="config.streamKey" placeholder="请输入Stream Key" />
</FormItem>
</div>
<Form.Item
:name="['config', 'host']"
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
label="主机地址"
>
<Input v-model:value="config.host" placeholder="请输入主机地址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', '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>

View File

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

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 { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { DataSinkApi } from '#/api/iot/rule/data/sink';
import { DICT_TYPE } from '@vben/constants';
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 [
{ type: 'checkbox', width: 40 },
{

View File

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