fix(iot): 完善 rule data 的迁移
parent
e7a61ce150
commit
ec796b8336
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,128 +1,26 @@
|
|||
<script lang="ts" setup>
|
||||
// TODO @haohao:应该先有【规则】【目的】两个 tab;然后,在进行管理操作;类似,apps/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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue