Browse Source

feat(release): add pasture,user module

Yi 1 year ago
parent
commit
2d89783f94

+ 36 - 0
src/constants/business.ts

@@ -160,3 +160,39 @@ export const WorkflowIsShowLabels: Record<BackgroundWorkflow.IsShowKey, string>
   1: '是',
   2: '否'
 };
+
+export const MqttAuthIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
+  0: '无效',
+  1: '是',
+  2: '否'
+};
+
+export const MqttUserIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
+  0: '无效',
+  1: '是',
+  2: '否'
+};
+
+export const MqttPastureIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
+  0: '无效',
+  1: '是',
+  2: '否'
+};
+
+export const MqttPastureOptions: Common.OptionWithKey<ApiMqttAuth.IsShowKey>[] = [
+  { value: 1, label: MqttPastureIsShowLabels['1'] },
+  { value: 2, label: MqttPastureIsShowLabels['2'] }
+];
+
+/** 主题动作 */
+export const topicAccessLabel: Record<ApiMqttUser.IsAccessKey, string> = {
+  1: '发布',
+  2: '订阅',
+  3: '发布订阅'
+};
+
+export const TopicAccessOptions: Common.OptionWithKey<ApiMqttUser.IsAccessKey>[] = [
+  { value: 1, label: topicAccessLabel['1'] },
+  { value: 2, label: topicAccessLabel['2'] },
+  { value: 3, label: topicAccessLabel['3'] }
+];

+ 31 - 1
src/router/modules/mqtt.ts

@@ -3,6 +3,36 @@ const mqtt: AuthRoute.Route = {
   path: '/mqtt',
   component: 'basic',
   children: [
+    {
+      name: 'mqtt_pasture',
+      path: '/mqtt/pasture',
+      component: 'self',
+      meta: {
+        title: '牧场管理',
+        requiresAuth: true,
+        icon: 'icon-park-outline:workbench'
+      }
+    },
+    {
+      name: 'mqtt_user',
+      path: '/mqtt/user',
+      component: 'self',
+      meta: {
+        title: '用户管理',
+        requiresAuth: true,
+        icon: 'ic:round-manage-accounts'
+      }
+    },
+    {
+      name: 'mqtt_topic',
+      path: '/mqtt/topic',
+      component: 'self',
+      meta: {
+        title: 'topic配置',
+        requiresAuth: true,
+        icon: 'ps:ping'
+      }
+    },
     {
       name: 'mqtt_authentication',
       path: '/mqtt/authentication',
@@ -10,7 +40,7 @@ const mqtt: AuthRoute.Route = {
       meta: {
         title: '认证鉴权',
         requiresAuth: true,
-        icon: 'icon-park-outline:workbench'
+        icon: 'ic:round-construction'
       }
     }
   ],

+ 43 - 2
src/service/api/mqtt.adapter.ts

@@ -1,7 +1,7 @@
-export function adapterOfMqttAuthList(data: ApiBackground.MqttAuth[] | null): BackgroundMqttAuth.Auth[] {
+export function adapterOfMqttAuthList(data: Mqtt.Auth[] | null): ApiMqttAuth.Auth[] {
   if (!data) return [];
   return data.map(item => {
-    const field: BackgroundMqttAuth.Auth = {
+    const field: ApiMqttAuth.Auth = {
       index: item.id,
       key: item.id,
       ...item
@@ -9,3 +9,44 @@ export function adapterOfMqttAuthList(data: ApiBackground.MqttAuth[] | null): Ba
     return field;
   });
 }
+
+export function adapterOfMqttTopicList(data: Mqtt.Topic[] | null): ApiMqttTopic.Topic[] {
+  if (!data) return [];
+  return data.map(item => {
+    const field: ApiMqttTopic.Topic = {
+      index: item.id,
+      key: item.id,
+      ...item
+    };
+    return field;
+  });
+}
+
+export function adapterOfMqttUserList(data: Mqtt.User[] | null): ApiMqttUser.User[] {
+  if (!data) return [];
+  return data.map(item => {
+    const field: ApiMqttUser.User = {
+      index: item.id,
+      key: item.id,
+      ...item
+    };
+    return field;
+  });
+}
+
+export function adapterOfMqttPastureList(data: Mqtt.Pasture[] | null): ApiMqttPasture.Pasture[] {
+  if (!data) return [];
+  return data.map(item => {
+    const field: ApiMqttPasture.Pasture = {
+      index: item.id,
+      key: item.id,
+      ...item
+    };
+    return field;
+  });
+}
+
+export function adapterOfMqttPastureEnumList(data: Common.OptionWithKey<any>[] | null): Common.OptionWithKey<any>[] {
+  if (!data) return [];
+  return data;
+}

+ 79 - 4
src/service/api/mqtt.ts

@@ -1,10 +1,16 @@
 import { adapter } from '@/utils';
-import { adapterOfMqttAuthList } from '@/service/api/mqtt.adapter';
+import {
+  adapterOfMqttAuthList,
+  adapterOfMqttPastureEnumList,
+  adapterOfMqttPastureList,
+  adapterOfMqttTopicList,
+  adapterOfMqttUserList
+} from '@/service/api/mqtt.adapter';
 import { backgroundRequest } from '../request';
 
 /** 获取用户鉴权列表 */
 export const fetchMqttAuthList = async (page: number, pageSize: number, topic: string) => {
-  const data = await backgroundRequest.post<ApiBackground.MqttAuth[] | null>(
+  const data = await backgroundRequest.post<Mqtt.Auth[] | null>(
     '/mqtt/auth/list',
     { topic },
     { headers: { page, page_size: pageSize } }
@@ -18,11 +24,80 @@ export const mqttAuthDelete = (authId: number) => {
 };
 
 /** 添加用户鉴权列表 */
-export const mqttAuthAdd = (params: ApiBackground.MqttAuth) => {
+export const mqttAuthAdd = (params: Mqtt.Auth) => {
   return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/auth/add', params);
 };
 
 /** 编辑用户鉴权列表 */
-export const mqttAuthEdit = (param: ApiBackground.MqttAuth) => {
+export const mqttAuthEdit = (param: Mqtt.Auth) => {
   return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/auth/edit', param);
 };
+
+/** 获取topic列表 */
+export const fetchMqttTopicList = async (page: number, pageSize: number, topic_name: string) => {
+  const data = await backgroundRequest.post<Mqtt.Topic[] | null>(
+    '/mqtt/topic/list',
+    { topic_name },
+    { headers: { page, page_size: pageSize } }
+  );
+  return adapter(adapterOfMqttTopicList, data);
+};
+
+/** 添加topic */
+export const mqttTopicAdd = (params: Mqtt.Topic) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/topic/add', params);
+};
+
+/** 编辑topic */
+export const mqttTopicEdit = (param: Mqtt.Topic) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/topic/edit', param);
+};
+
+export const mqttTopicDelete = (authId: number) => {
+  return backgroundRequest.delete<ApiBoolean.OK | null>(`/mqtt/topic/delete/${authId}`);
+};
+
+/** 获取用户列表 */
+export const fetchMqttUserList = async (page: number, pageSize: number, name: string) => {
+  const data = await backgroundRequest.post<Mqtt.User[] | null>(
+    '/mqtt/user/list',
+    { name },
+    { headers: { page, page_size: pageSize } }
+  );
+  return adapter(adapterOfMqttUserList, data);
+};
+
+/** 添加User */
+export const mqttUserAdd = (params: Mqtt.User) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/user/add', params);
+};
+
+/** 编辑User */
+export const mqttUserEdit = (param: Mqtt.User) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/user/edit', param);
+};
+
+/** 获取牧场列表 */
+export const fetchMqttPastureList = async (page: number, pageSize: number, name: string) => {
+  const data = await backgroundRequest.post<Mqtt.Pasture[] | null>(
+    '/mqtt/pasture/list',
+    { name },
+    { headers: { page, page_size: pageSize } }
+  );
+  return adapter(adapterOfMqttPastureList, data);
+};
+
+/** 添加牧场 */
+export const mqttPastureAdd = (params: Mqtt.Pasture) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/pasture/add', params);
+};
+
+/** 编辑牧场 */
+export const mqttPastureEdit = (param: Mqtt.Pasture) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/pasture/edit', param);
+};
+
+export const pastureEnumList = async () => {
+  const data = await backgroundRequest.post<Common.OptionWithKey<any>[] | null>('/mqtt/pasture/enum/list');
+  return adapter(adapterOfMqttPastureEnumList, data);
+};

+ 62 - 3
src/typings/business.d.ts

@@ -129,8 +129,25 @@ declare namespace BackgroundWorkflow {
   type IsShowKey = NonNullable<Workflow['is_show']>;
 }
 
-declare namespace BackgroundMqttAuth {
-  interface Auth extends ApiBackground.MqttAuth {
+declare namespace ApiMqttAuth {
+  interface Auth extends Mqtt.Auth {
+    /** 序号 */
+    index: number;
+    /** 表格的key(id) */
+    key: number;
+  }
+
+  /**
+   * 是否启动
+   * 0 无效
+   * 1:是
+   * 2: 否 */
+
+  type IsShowKey = NonNullable<Auth['is_show']>;
+}
+
+declare namespace ApiMqttTopic {
+  interface Topic extends Mqtt.Topic {
     /** 序号 */
     index: number;
     /** 表格的key(id) */
@@ -143,5 +160,47 @@ declare namespace BackgroundMqttAuth {
    * 1:是
    * 2: 否
    */
-  type IsShowKey = NonNullable<Auth['is_show']>;
+  type IsShowKey = NonNullable<Topic['is_show']>;
+}
+
+declare namespace ApiMqttUser {
+  interface User extends Mqtt.User {
+    /** 序号 */
+    index: number;
+    /** 表格的key(id) */
+    key: number;
+  }
+
+  /**
+   * 是否启动
+   * 0 无效
+   * 1:是
+   * 2: 否
+   */
+  type IsShowKey = NonNullable<User['is_show']>;
+  /**
+   * 主题动作
+   * 0 无效
+   * 1 发布
+   * 2 订阅
+   * 3 订阅发布
+   */
+  type IsAccessKey = NonNullable<User['access']>;
+}
+
+declare namespace ApiMqttPasture {
+  interface Pasture extends Mqtt.Pasture {
+    /** 序号 */
+    index: number;
+    /** 表格的key(id) */
+    key: number;
+  }
+
+  /**
+   * 是否启动
+   * 0 无效
+   * 1:是
+   * 2: 否
+   */
+  type IsGroupKey = NonNullable<Pasture['is_group']>;
 }

+ 0 - 14
src/typings/mqtt-auth.d.ts

@@ -1,14 +0,0 @@
-declare namespace ApiBackground {
-  interface MqttAuth {
-    /** id */
-    id: number;
-    /** 字段名 */
-    client_id: string | null;
-    mount_point: string | null;
-    user_name: string | null;
-    publish_acl: string | null;
-    subscribe_acl: string | null;
-    is_show: number | null;
-    remark: string | null;
-  }
-}

+ 54 - 0
src/typings/mqtt.d.ts

@@ -0,0 +1,54 @@
+declare namespace Mqtt {
+  interface Auth {
+    /** id */
+    id: number;
+    /** 字段名 */
+    client_id: string | null;
+    mount_point: string | null;
+    user_name: string | null;
+    publish_acl: string | null;
+    subscribe_acl: string | null;
+    is_show: number | null;
+    remark: string | null;
+  }
+
+  interface User {
+    /** id */
+    id: number;
+    /** 字段名 */
+    user_name: string | null;
+    password: string;
+    client_id: string | null;
+    pasture_id: number | null;
+    topic_id: number | null;
+    is_show: number | null;
+    access: number | null;
+    created_at_format: string | null;
+    updated_at_format: string | null;
+  }
+
+  interface Topic {
+    /** id */
+    id: number;
+    /** 字段名 */
+    topic_name: string | null;
+    topic_template: string | null;
+    created_at_format: string | null;
+    updated_at_format: string | null;
+    is_show: number | null;
+  }
+
+  interface Pasture {
+    /** id */
+    id: number;
+    /** 字段名 */
+    name: string | null;
+    short_name: string | null;
+    address: string | null;
+    is_group: number | null;
+    is_show: number | null;
+    group_id: number | null;
+    created_at_format: string | null;
+    updated_at_format: string | null;
+  }
+}

+ 6 - 0
src/typings/page-route.d.ts

@@ -59,6 +59,9 @@ declare namespace PageRoute {
     | 'management_user'
     | 'mqtt'
     | 'mqtt_authentication'
+    | 'mqtt_pasture'
+    | 'mqtt_topic'
+    | 'mqtt_user'
     | 'multi-menu'
     | 'multi-menu_first'
     | 'multi-menu_first_second-new'
@@ -118,6 +121,9 @@ declare namespace PageRoute {
     | 'management_route'
     | 'management_user'
     | 'mqtt_authentication'
+    | 'mqtt_pasture'
+    | 'mqtt_topic'
+    | 'mqtt_user'
     | 'multi-menu_first_second-new_third'
     | 'multi-menu_first_second'
     | 'plugin_charts_antv'

+ 8 - 0
src/utils/crypto/index.ts

@@ -23,3 +23,11 @@ export function decrypto(cipherText: string) {
   }
   return null;
 }
+
+/**
+ * Md5加密数据
+ * @param data - 数据
+ */
+export function MD5(data: string) {
+  return CryptoJS.MD5(data).toString();
+}

+ 3 - 0
src/views/index.ts

@@ -38,6 +38,9 @@ export const views: Record<
   management_route: () => import('./management/route/index.vue'),
   management_user: () => import('./management/user/index.vue'),
   mqtt_authentication: () => import('./mqtt/authentication/index.vue'),
+  mqtt_pasture: () => import('./mqtt/pasture/index.vue'),
+  mqtt_topic: () => import('./mqtt/topic/index.vue'),
+  mqtt_user: () => import('./mqtt/user/index.vue'),
   'multi-menu_first_second-new_third': () => import('./multi-menu/first/second-new/third/index.vue'),
   'multi-menu_first_second': () => import('./multi-menu/first/second/index.vue'),
   plugin_charts_antv: () => import('./plugin/charts/antv/index.vue'),

+ 2 - 2
src/views/mqtt/authentication/components/table-action-modal.vue

@@ -48,7 +48,7 @@ export interface Props {
    */
   type?: 'add' | 'edit';
   /** 编辑的表格行数据 */
-  editData?: BackgroundMqttAuth.Auth | null;
+  editData?: ApiMqttAuth.Auth | null;
 }
 
 export type ModalType = NonNullable<Props['type']>;
@@ -89,7 +89,7 @@ const title = computed(() => {
 const formRef = ref<HTMLElement & FormInst>();
 
 type FormModel = Pick<
-  ApiBackground.MqttAuth,
+  Mqtt.Auth,
   'id' | 'client_id' | 'mount_point' | 'user_name' | 'publish_acl' | 'subscribe_acl' | 'remark' | 'is_show'
 >;
 

+ 31 - 17
src/views/mqtt/authentication/index.vue

@@ -3,10 +3,10 @@
     <n-card title="认证鉴权" :bordered="false" class="rounded-16px shadow-sm">
       <n-space class="pb-12px" justify="space-between">
         <n-space>
-          <n-button type="primary" @click="handleAddTable">
+          <!--<n-button type="primary" @click="handleAddTable">
             <icon-ic-round-plus class="mr-4px text-20px" />
             新增
-          </n-button>
+          </n-button>-->
         </n-space>
         <n-space align="center" :size="18">
           <n-input-group>
@@ -28,18 +28,19 @@
 <script setup lang="tsx">
 import { reactive, ref } from 'vue';
 import type { Ref } from 'vue';
-import { NButton, NPopconfirm, NSpace } from 'naive-ui';
+import { NButton, NSpace, NTag } from 'naive-ui';
 import type { DataTableColumns, PaginationProps } from 'naive-ui';
+import { MqttAuthIsShowLabels } from '@/constants';
 import { useBoolean, useLoading } from '@/hooks';
-import { fetchMqttAuthList, mqttAuthDelete } from '@/service/api/mqtt';
+import { fetchMqttAuthList } from '@/service/api/mqtt';
 import TableActionModal from '../authentication/components/table-action-modal.vue';
 import type { ModalType } from '../authentication/components/table-action-modal.vue';
 
 const { loading, startLoading, endLoading } = useLoading(false);
-const { bool: visible, setTrue: openModal } = useBoolean();
+const { bool: visible } = useBoolean();
 const topicName = ref('');
-const tableData = ref<BackgroundMqttAuth.Auth[]>([]);
-function setTableData(data: BackgroundMqttAuth.Auth[]) {
+const tableData = ref<ApiMqttAuth.Auth[]>([]);
+function setTableData(data: ApiMqttAuth.Auth[]) {
   tableData.value = data;
 }
 
@@ -56,7 +57,7 @@ async function getTableData() {
   }
 }
 
-const columns: Ref<DataTableColumns<BackgroundMqttAuth.Auth>> = ref([
+const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
   {
     type: 'selection',
     align: 'center'
@@ -92,10 +93,21 @@ const columns: Ref<DataTableColumns<BackgroundMqttAuth.Auth>> = ref([
     align: 'center'
   },
   {
-    key: '是否允许',
-    title: '描述',
-    align: 'center'
-  },
+    key: 'is_show',
+    title: '是否启用',
+    align: 'center',
+    render: row => {
+      if (row.is_show) {
+        const tagTypes: Record<ApiMqttAuth.IsShowKey, NaiveUI.ThemeColor> = {
+          '0': 'error',
+          '1': 'success',
+          '2': 'warning'
+        };
+        return <NTag type={tagTypes[row.is_show]}>{MqttAuthIsShowLabels[row.is_show]}</NTag>;
+      }
+      return <span></span>;
+    }
+  } /*,
   {
     key: 'actions',
     title: '操作',
@@ -119,20 +131,21 @@ const columns: Ref<DataTableColumns<BackgroundMqttAuth.Auth>> = ref([
         </NSpace>
       );
     }
-  }
-]) as Ref<DataTableColumns<BackgroundMqttAuth.Auth>>;
+  } */
+]) as Ref<DataTableColumns<ApiMqttAuth.Auth>>;
 
-const editData = ref<BackgroundMqttAuth.Auth | null>(null);
+const editData = ref<ApiMqttAuth.Auth | null>(null);
 const modalType = ref<ModalType>('add');
-
+/*
 function setModalType(type: ModalType) {
   modalType.value = type;
 }
 
-function setEditData(data: BackgroundMqttAuth.Auth | null) {
+function setEditData(data: ApiMqttAuth.Auth | null) {
   editData.value = data;
 }
 
+
 function handleAddTable() {
   openModal();
   setModalType('add');
@@ -156,6 +169,7 @@ function handleDeleteTable(rowId: number) {
   });
   init();
 }
+*/
 
 const pagination: PaginationProps = reactive({
   page: 1,

+ 176 - 0
src/views/mqtt/pasture/components/table-action-modal.vue

@@ -0,0 +1,176 @@
+<template>
+  <n-modal v-model:show="modalVisible" preset="card" :title="title" class="w-700px">
+    <n-form ref="formRef" label-placement="left" :label-width="100" :model="formModel" :rules="rules">
+      <n-grid :cols="48" :x-gap="18">
+        <n-form-item-grid-item :span="25" label="牧场名称" path="name">
+          <n-input v-model:value="formModel.name" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="25" label="牧场简称" path="short_name">
+          <n-input v-model:value="formModel.short_name" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="25" label="地址" path="address">
+          <n-input v-model:value="formModel.address" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="25" label="是否启用" path="is_show">
+          <n-radio-group v-model:value="formModel.is_show">
+            <n-radio v-for="item in MqttPastureOptions" :key="item.value" :value="item.value">{{ item.label }}</n-radio>
+          </n-radio-group>
+        </n-form-item-grid-item>
+      </n-grid>
+      <n-space class="w-full pt-16px" :size="24" justify="end">
+        <n-button class="w-72px" @click="closeModal">取消</n-button>
+        <n-button class="w-72px" type="primary" @click="handleSubmit">确定</n-button>
+      </n-space>
+    </n-form>
+  </n-modal>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, reactive, watch } from 'vue';
+import type { FormInst, FormItemRule } from 'naive-ui';
+import { MqttPastureOptions } from '@/constants';
+import { createRequiredFormRule } from '@/utils';
+import { mqttPastureAdd, mqttPastureEdit } from '@/service/api/mqtt';
+export interface Props {
+  /** 弹窗可见性 */
+  visible: boolean;
+  /**
+   * 弹窗类型
+   * add: 新增
+   * edit: 编辑
+   */
+  type?: 'add' | 'edit';
+  /** 编辑的表格行数据 */
+  editData?: Mqtt.Pasture | null;
+}
+
+export type ModalType = NonNullable<Props['type']>;
+
+defineOptions({ name: 'TableActionModal' });
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'add',
+  editData: null
+});
+
+interface Emits {
+  (e: 'update:visible', visible: boolean): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const modalVisible = computed({
+  get() {
+    return props.visible;
+  },
+  set(visible) {
+    emit('update:visible', visible);
+  }
+});
+const closeModal = () => {
+  modalVisible.value = false;
+};
+const titles: Record<ModalType, string> = {
+  add: '添加牧场',
+  edit: '编辑牧场'
+};
+
+const title = computed(() => {
+  return titles[props.type];
+});
+
+const formRef = ref<HTMLElement & FormInst>();
+
+type FormModel = Pick<
+  Mqtt.Pasture,
+  | 'id'
+  | 'name'
+  | 'short_name'
+  | 'address'
+  | 'is_show'
+  | 'is_group'
+  | 'group_id'
+  | 'created_at_format'
+  | 'updated_at_format'
+>;
+
+const formModel = reactive<FormModel>(createDefaultFormModel());
+
+const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
+  name: createRequiredFormRule('请输入牧场名称'),
+  short_name: createRequiredFormRule(),
+  address: createRequiredFormRule(),
+  is_show: createRequiredFormRule(),
+  is_group: createRequiredFormRule(),
+  created_at_format: createRequiredFormRule(),
+  updated_at_format: createRequiredFormRule(),
+  group_id: createRequiredFormRule(),
+  id: createRequiredFormRule()
+};
+
+function createDefaultFormModel(): FormModel {
+  return {
+    id: 0,
+    name: '',
+    short_name: '',
+    address: '',
+    is_show: 1,
+    is_group: 0,
+    group_id: 0,
+    created_at_format: '',
+    updated_at_format: ''
+  };
+}
+
+function handleUpdateFormModel(model: Partial<FormModel>) {
+  Object.assign(formModel, model);
+}
+
+function handleUpdateFormModelByModalType() {
+  const handlers: Record<ModalType, () => void> = {
+    add: () => {
+      const defaultFormModel = createDefaultFormModel();
+      handleUpdateFormModel(defaultFormModel);
+    },
+    edit: () => {
+      if (props.editData) {
+        handleUpdateFormModel(props.editData);
+      }
+    }
+  };
+  handlers[props.type]();
+}
+
+async function handleSubmit() {
+  await formRef.value?.validate();
+  if (props.type === 'add') {
+    const data = mqttPastureAdd(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+
+  if (props.type === 'edit') {
+    const data = mqttPastureEdit(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+  closeModal();
+}
+
+watch(
+  () => props.visible,
+  newValue => {
+    if (newValue) {
+      handleUpdateFormModelByModalType();
+    }
+  }
+);
+</script>
+
+<style scoped></style>

+ 176 - 0
src/views/mqtt/pasture/index.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="h-full overflow-hidden">
+    <n-card title="牧场管理" :bordered="false" class="rounded-16px shadow-sm">
+      <n-space class="pb-12px" justify="space-between">
+        <n-space>
+          <n-button type="primary" @click="handleAddTable">
+            <icon-ic-round-plus class="mr-4px text-20px" />
+            新增
+          </n-button>
+        </n-space>
+        <n-space align="center" :size="18">
+          <n-input-group>
+            <n-input v-model:value="PastureName" />
+            <n-button type="primary" @click="handleSearch">搜索</n-button>
+          </n-input-group>
+          <n-button size="small" type="primary" @click="getTableData">
+            <icon-mdi-refresh class="mr-4px text-16px" :class="{ 'animate-spin': loading }" />
+            刷新表格
+          </n-button>
+        </n-space>
+      </n-space>
+      <n-data-table :columns="columns" :data="tableData" :loading="loading" :pagination="pagination" />
+      <table-action-modal v-model:visible="visible" :type="modalType" :edit-data="editData" />
+    </n-card>
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { reactive, ref } from 'vue';
+import type { Ref } from 'vue';
+import { NButton, NSpace, NTag } from 'naive-ui';
+import type { DataTableColumns, PaginationProps } from 'naive-ui';
+import { MqttPastureIsShowLabels } from '@/constants';
+import { useBoolean, useLoading } from '@/hooks';
+import { fetchMqttPastureList } from '@/service/api/mqtt';
+import TableActionModal from '../pasture/components/table-action-modal.vue';
+import type { ModalType } from '../pasture/components/table-action-modal.vue';
+
+const { loading, startLoading, endLoading } = useLoading(false);
+const { bool: visible, setTrue: openModal } = useBoolean();
+const PastureName = ref('');
+const tableData = ref<Mqtt.Pasture[]>([]);
+function setTableData(data: Mqtt.Pasture[]) {
+  tableData.value = data;
+}
+
+async function getTableData() {
+  startLoading();
+  const { data } = await fetchMqttPastureList(1, 10, PastureName.value);
+  if (data) {
+    setTimeout(() => {
+      setTableData(data);
+      endLoading();
+    }, 1000);
+  } else {
+    endLoading();
+  }
+}
+
+const columns: Ref<DataTableColumns<Mqtt.Pasture>> = ref([
+  {
+    type: 'selection',
+    align: 'center'
+  },
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center'
+  },
+  {
+    key: 'name',
+    title: '名称',
+    align: 'center'
+  },
+  {
+    key: 'short_name',
+    title: '简称',
+    align: 'center'
+  },
+  {
+    key: 'address',
+    title: '地址',
+    align: 'center'
+  },
+  {
+    key: 'is_group',
+    title: '是否是集团',
+    align: 'center',
+    render: row => {
+      if (row.is_group) {
+        const tagTypes: Record<ApiMqttPasture.IsGroupKey, NaiveUI.ThemeColor> = {
+          '1': 'success',
+          '2': 'warning'
+        };
+        return <NTag type={tagTypes[row.is_group]}>{MqttPastureIsShowLabels[row.is_group]}</NTag>;
+      }
+      return <span></span>;
+    }
+  },
+  {
+    key: 'created_at_format',
+    title: '创建时间',
+    align: 'center'
+  },
+  {
+    key: 'actions',
+    title: '操作',
+    align: 'center',
+    render: row => {
+      return (
+        <NSpace justify={'center'}>
+          <NButton type="info" size={'small'} onClick={() => handleEditTable(row.id)}>
+            编辑
+          </NButton>
+        </NSpace>
+      );
+    }
+  }
+]) as Ref<DataTableColumns<Mqtt.Pasture>>;
+
+const editData = ref<Mqtt.Pasture | null>(null);
+const modalType = ref<ModalType>('add');
+
+function setModalType(type: ModalType) {
+  modalType.value = type;
+}
+
+function setEditData(data: Mqtt.Pasture | null) {
+  editData.value = data;
+}
+
+function handleAddTable() {
+  openModal();
+  setModalType('add');
+}
+
+function handleEditTable(rowId: number) {
+  const findItem = tableData.value.find(item => item.id === rowId);
+  if (findItem) {
+    setEditData(findItem);
+  }
+  setModalType('edit');
+  openModal();
+}
+
+const pagination: PaginationProps = reactive({
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 15, 20, 25, 30],
+  onChange: (page: number) => {
+    pagination.page = page;
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    pagination.page = 1;
+  }
+});
+
+function handleSearch() {
+  if (!PastureName.value) {
+    window.$message?.warning('请输入牧场名称');
+  } else {
+    startLoading();
+  }
+}
+
+function init() {
+  getTableData();
+}
+
+// 初始化
+init();
+</script>
+
+<style scoped></style>

+ 153 - 0
src/views/mqtt/topic/components/table-action-modal.vue

@@ -0,0 +1,153 @@
+<template>
+  <n-modal v-model:show="modalVisible" preset="card" :title="title" class="w-700px">
+    <n-form ref="formRef" label-placement="left" :label-width="100" :model="formModel" :rules="rules">
+      <n-grid :cols="48" :x-gap="18">
+        <n-form-item-grid-item :span="25" label="topic名称" path="topic_name">
+          <n-input v-model:value="formModel.topic_name" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="25" label="topic模板" path="topic_template">
+          <n-input v-model:value="formModel.topic_template" />
+        </n-form-item-grid-item>
+      </n-grid>
+      <n-space class="w-full pt-16px" :size="24" justify="end">
+        <n-button class="w-72px" @click="closeModal">取消</n-button>
+        <n-button class="w-72px" type="primary" @click="handleSubmit">确定</n-button>
+      </n-space>
+    </n-form>
+  </n-modal>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, reactive, watch } from 'vue';
+import type { FormInst, FormItemRule } from 'naive-ui';
+import { createRequiredFormRule } from '@/utils';
+import { mqttTopicAdd, mqttTopicEdit } from '@/service/api/mqtt';
+export interface Props {
+  /** 弹窗可见性 */
+  visible: boolean;
+  /**
+   * 弹窗类型
+   * add: 新增
+   * edit: 编辑
+   */
+  type?: 'add' | 'edit';
+  /** 编辑的表格行数据 */
+  editData?: Mqtt.Topic | null;
+}
+
+export type ModalType = NonNullable<Props['type']>;
+
+defineOptions({ name: 'TableActionModal' });
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'add',
+  editData: null
+});
+
+interface Emits {
+  (e: 'update:visible', visible: boolean): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const modalVisible = computed({
+  get() {
+    return props.visible;
+  },
+  set(visible) {
+    emit('update:visible', visible);
+  }
+});
+const closeModal = () => {
+  modalVisible.value = false;
+};
+const titles: Record<ModalType, string> = {
+  add: '添加topic模板',
+  edit: '编辑topic模板'
+};
+
+const title = computed(() => {
+  return titles[props.type];
+});
+
+const formRef = ref<HTMLElement & FormInst>();
+
+type FormModel = Pick<
+  Mqtt.Topic,
+  'id' | 'topic_name' | 'topic_template' | 'created_at_format' | 'is_show' | 'updated_at_format'
+>;
+
+const formModel = reactive<FormModel>(createDefaultFormModel());
+
+const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
+  topic_name: createRequiredFormRule('请输入客户端id名称'),
+  topic_template: createRequiredFormRule(),
+  is_show: createRequiredFormRule(),
+  created_at_format: createRequiredFormRule(),
+  updated_at_format: createRequiredFormRule(),
+  id: createRequiredFormRule()
+};
+
+function createDefaultFormModel(): FormModel {
+  return {
+    id: 0,
+    topic_name: '',
+    topic_template: '',
+    created_at_format: '',
+    updated_at_format: '',
+    is_show: 1
+  };
+}
+
+function handleUpdateFormModel(model: Partial<FormModel>) {
+  Object.assign(formModel, model);
+}
+
+function handleUpdateFormModelByModalType() {
+  const handlers: Record<ModalType, () => void> = {
+    add: () => {
+      const defaultFormModel = createDefaultFormModel();
+      handleUpdateFormModel(defaultFormModel);
+    },
+    edit: () => {
+      if (props.editData) {
+        handleUpdateFormModel(props.editData);
+      }
+    }
+  };
+  handlers[props.type]();
+}
+
+async function handleSubmit() {
+  await formRef.value?.validate();
+  if (props.type === 'add') {
+    const data = mqttTopicAdd(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+
+  if (props.type === 'edit') {
+    const data = mqttTopicEdit(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+  closeModal();
+}
+
+watch(
+  () => props.visible,
+  newValue => {
+    if (newValue) {
+      handleUpdateFormModelByModalType();
+    }
+  }
+);
+</script>
+
+<style scoped></style>

+ 184 - 0
src/views/mqtt/topic/index.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="h-full overflow-hidden">
+    <n-card title="消息主题模板" :bordered="false" class="rounded-16px shadow-sm">
+      <n-space class="pb-12px" justify="space-between">
+        <n-space>
+          <n-button type="primary" @click="handleAddTable">
+            <icon-ic-round-plus class="mr-4px text-20px" />
+            新增
+          </n-button>
+        </n-space>
+        <n-space align="center" :size="18">
+          <n-input-group>
+            <n-input v-model:value="topicName" />
+            <n-button type="primary" @click="handleSearch">搜索</n-button>
+          </n-input-group>
+          <n-button size="small" type="primary" @click="getTableData">
+            <icon-mdi-refresh class="mr-4px text-16px" :class="{ 'animate-spin': loading }" />
+            刷新表格
+          </n-button>
+        </n-space>
+      </n-space>
+      <n-data-table :columns="columns" :data="tableData" :loading="loading" :pagination="pagination" />
+      <table-action-modal v-model:visible="visible" :type="modalType" :edit-data="editData" />
+    </n-card>
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { reactive, ref } from 'vue';
+import type { Ref } from 'vue';
+import { NButton, NSpace, NPopconfirm } from 'naive-ui';
+import type { DataTableColumns, PaginationProps } from 'naive-ui';
+import { useBoolean, useLoading } from '@/hooks';
+import { fetchMqttTopicList, mqttTopicDelete } from '@/service/api/mqtt';
+import TableActionModal from '../topic/components/table-action-modal.vue';
+import type { ModalType } from '../topic/components/table-action-modal.vue';
+
+const { loading, startLoading, endLoading } = useLoading(false);
+const { bool: visible, setTrue: openModal } = useBoolean();
+const topicName = ref('');
+const tableData = ref<ApiMqttTopic.Topic[]>([]);
+function setTableData(data: ApiMqttTopic.Topic[]) {
+  tableData.value = data;
+}
+
+async function getTableData() {
+  startLoading();
+  const { data } = await fetchMqttTopicList(1, 10, topicName.value);
+  if (data) {
+    setTimeout(() => {
+      setTableData(data);
+      endLoading();
+    }, 1000);
+  } else {
+    endLoading();
+  }
+}
+
+const columns: Ref<DataTableColumns<ApiMqttTopic.Topic>> = ref([
+  {
+    type: 'selection',
+    align: 'center'
+  },
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center'
+  },
+  {
+    key: 'topic_name',
+    title: 'topic名称',
+    align: 'center'
+  },
+  {
+    key: 'topic_template',
+    title: 'topic模板',
+    align: 'center'
+  },
+  {
+    key: 'created_at_format',
+    title: '创建时间',
+    align: 'center'
+  },
+  {
+    key: 'actions',
+    title: '操作',
+    align: 'center',
+    render: row => {
+      return (
+        <NSpace justify={'center'}>
+          <NButton type="info" size={'small'} onClick={() => handleEditTable(row.id)}>
+            编辑
+          </NButton>
+          <NPopconfirm onPositiveClick={() => handleDeleteTable(row.id)}>
+            {{
+              default: () => '确认删除',
+              trigger: () => (
+                <NButton type="error" size={'small'}>
+                  删除
+                </NButton>
+              )
+            }}
+          </NPopconfirm>
+        </NSpace>
+      );
+    }
+  }
+]) as Ref<DataTableColumns<ApiMqttTopic.Topic>>;
+
+const editData = ref<ApiMqttTopic.Topic | null>(null);
+const modalType = ref<ModalType>('add');
+
+function setModalType(type: ModalType) {
+  modalType.value = type;
+}
+
+function setEditData(data: ApiMqttTopic.Topic | null) {
+  editData.value = data;
+}
+
+function handleAddTable() {
+  openModal();
+  setModalType('add');
+}
+
+function handleEditTable(rowId: number) {
+  const findItem = tableData.value.find(item => item.id === rowId);
+  if (findItem) {
+    setEditData(findItem);
+  }
+  setModalType('edit');
+  openModal();
+}
+
+function handleDeleteTable(rowId: number) {
+  const data = mqttTopicDelete(rowId);
+  data.then(res => {
+    if (res.data?.success) {
+      window.$message?.success('删除成功!');
+    }
+  });
+  init();
+}
+
+const pagination: PaginationProps = reactive({
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 15, 20, 25, 30],
+  onChange: (page: number) => {
+    pagination.page = page;
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    pagination.page = 1;
+  }
+});
+
+function handleSearch() {
+  if (!topicName.value) {
+    window.$message?.warning('请输入主题名称');
+  } else {
+    startLoading();
+    /* const { data } = await fetchMqttTopicList(1, 10, topicName.value);
+    if (data) {
+      setTimeout(() => {
+        setTableData(data);
+        endLoading();
+      }, 1000);
+    } else {
+      endLoading();
+    } */
+  }
+}
+
+function init() {
+  getTableData();
+}
+
+// 初始化
+init();
+</script>
+
+<style scoped></style>

+ 187 - 0
src/views/mqtt/user/components/table-action-modal.vue

@@ -0,0 +1,187 @@
+<template>
+  <n-modal v-model:show="modalVisible" preset="card" :title="title" class="w-700px">
+    <n-form ref="formRef" label-placement="left" :label-width="80" :model="formModel" :rules="rules">
+      <n-grid :cols="24" :x-gap="18">
+        <n-form-item-grid-item :span="12" label="牧场名称" path="pasture_id">
+          <n-select v-model:value="formModel.pasture_id" :options="TopicAccessOptions" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="12" label="客户端ID" path="client_id">
+          <n-input v-model:value="formModel.client_id" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="12" label="用户名称" path="user_name">
+          <n-input v-model:value="formModel.user_name" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="12" label="用户密码" path="user_name">
+          <n-input v-model:value="formModel.password" type="password" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="12" label="主题名称(topic)" path="topic_id">
+          <n-select v-model:value="formModel.topic_id" :options="TopicAccessOptions" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="12" label="主题动作" path="access">
+          <n-radio-group v-model:value="formModel.access">
+            <n-radio v-for="item in TopicAccessOptions" :key="item.value" :value="item.value">{{ item.label }}</n-radio>
+          </n-radio-group>
+        </n-form-item-grid-item>
+      </n-grid>
+      <n-space class="w-full pt-16px" :size="24" justify="end">
+        <n-button class="w-72px" @click="closeModal">取消</n-button>
+        <n-button class="w-72px" type="primary" @click="handleSubmit">确定</n-button>
+      </n-space>
+    </n-form>
+  </n-modal>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, reactive, watch } from 'vue';
+import type { FormInst, FormItemRule } from 'naive-ui';
+import { TopicAccessOptions } from '@/constants';
+import { createRequiredFormRule } from '@/utils';
+import { mqttUserAdd, mqttUserEdit } from '@/service/api/mqtt';
+import { MD5 } from '@/utils/crypto';
+export interface Props {
+  /** 弹窗可见性 */
+  visible: boolean;
+  /**
+   * 弹窗类型
+   * add: 新增
+   * edit: 编辑
+   */
+  type?: 'add' | 'edit';
+  /** 编辑的表格行数据 */
+  editData?: Mqtt.User | null;
+}
+
+export type ModalType = NonNullable<Props['type']>;
+
+defineOptions({ name: 'TableActionModal' });
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'add',
+  editData: null
+});
+
+interface Emits {
+  (e: 'update:visible', visible: boolean): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const modalVisible = computed({
+  get() {
+    return props.visible;
+  },
+  set(visible) {
+    emit('update:visible', visible);
+  }
+});
+const closeModal = () => {
+  modalVisible.value = false;
+};
+const titles: Record<ModalType, string> = {
+  add: '添加用户',
+  edit: '编辑用户'
+};
+
+const title = computed(() => {
+  return titles[props.type];
+});
+
+const formRef = ref<HTMLElement & FormInst>();
+
+type FormModel = Pick<
+  Mqtt.User,
+  | 'id'
+  | 'pasture_id'
+  | 'user_name'
+  | 'password'
+  | 'client_id'
+  | 'topic_id'
+  | 'access'
+  | 'is_show'
+  | 'created_at_format'
+  | 'updated_at_format'
+>;
+
+const formModel = reactive<FormModel>(createDefaultFormModel());
+
+const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
+  pasture_id: createRequiredFormRule('请选择牧场'),
+  user_name: createRequiredFormRule('请输入用户名称'),
+  password: createRequiredFormRule('请输入用户密码'),
+  client_id: createRequiredFormRule('请输入客户端id'),
+  topic_id: createRequiredFormRule('请选择topic模板'),
+  access: createRequiredFormRule('请选择topic权限'),
+  is_show: createRequiredFormRule(),
+  created_at_format: createRequiredFormRule(),
+  updated_at_format: createRequiredFormRule(),
+  id: createRequiredFormRule()
+};
+
+function createDefaultFormModel(): FormModel {
+  return {
+    id: 0,
+    pasture_id: null,
+    user_name: '',
+    password: '',
+    client_id: '',
+    topic_id: null,
+    access: 0,
+    is_show: 1,
+    created_at_format: '',
+    updated_at_format: ''
+  };
+}
+
+function handleUpdateFormModel(model: Partial<FormModel>) {
+  Object.assign(formModel, model);
+}
+
+function handleUpdateFormModelByModalType() {
+  const handlers: Record<ModalType, () => void> = {
+    add: () => {
+      const defaultFormModel = createDefaultFormModel();
+      handleUpdateFormModel(defaultFormModel);
+    },
+    edit: () => {
+      if (props.editData) {
+        handleUpdateFormModel(props.editData);
+      }
+    }
+  };
+  handlers[props.type]();
+}
+
+async function handleSubmit() {
+  await formRef.value?.validate();
+  if (props.type === 'add') {
+    formModel.password = MD5(formModel.password).toString();
+    const data = mqttUserAdd(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+
+  if (props.type === 'edit') {
+    const data = mqttUserEdit(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+  closeModal();
+}
+
+watch(
+  () => props.visible,
+  newValue => {
+    if (newValue) {
+      handleUpdateFormModelByModalType();
+    }
+  }
+);
+</script>
+
+<style scoped></style>

+ 193 - 0
src/views/mqtt/user/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="h-full overflow-hidden">
+    <n-card title="用户管理" :bordered="false" class="rounded-16px shadow-sm">
+      <n-space class="pb-12px" justify="space-between">
+        <n-space>
+          <n-button type="primary" @click="handleAddTable">
+            <icon-ic-round-plus class="mr-4px text-20px" />
+            新增
+          </n-button>
+        </n-space>
+        <n-space align="center" :size="18">
+          <n-input-group>
+            <n-input v-model:value="userName" />
+            <n-button type="primary" @click="handleSearch">搜索</n-button>
+          </n-input-group>
+          <n-button size="small" type="primary" @click="getTableData">
+            <icon-mdi-refresh class="mr-4px text-16px" :class="{ 'animate-spin': loading }" />
+            刷新表格
+          </n-button>
+        </n-space>
+      </n-space>
+      <n-data-table :columns="columns" :data="tableData" :loading="loading" :pagination="pagination" />
+      <table-action-modal v-model:visible="visible" :type="modalType" :edit-data="editData" />
+    </n-card>
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { reactive, ref } from 'vue';
+import type { Ref } from 'vue';
+import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui';
+import type { DataTableColumns, PaginationProps } from 'naive-ui';
+import { MqttUserIsShowLabels } from '@/constants';
+import { useBoolean, useLoading } from '@/hooks';
+import { fetchMqttUserList, mqttAuthDelete } from '@/service/api/mqtt';
+import TableActionModal from '../user/components/table-action-modal.vue';
+import type { ModalType } from '../user/components/table-action-modal.vue';
+
+const { loading, startLoading, endLoading } = useLoading(false);
+const { bool: visible, setTrue: openModal } = useBoolean();
+const userName = ref('');
+const tableData = ref<Mqtt.User[]>([]);
+
+function setTableData(data: Mqtt.User[]) {
+  tableData.value = data;
+}
+
+async function getTableData() {
+  startLoading();
+  const { data } = await fetchMqttUserList(1, 10, userName.value);
+  if (data) {
+    setTimeout(() => {
+      setTableData(data);
+      endLoading();
+    }, 1000);
+  } else {
+    endLoading();
+  }
+}
+
+const columns: Ref<DataTableColumns<Mqtt.User>> = ref([
+  {
+    type: 'selection',
+    align: 'center'
+  },
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center'
+  },
+  {
+    key: 'pasture_name',
+    title: '牧场名称',
+    align: 'center'
+  },
+  {
+    key: 'user_name',
+    title: '用户名称',
+    align: 'center'
+  },
+  {
+    key: 'client_id',
+    title: '客户端id',
+    align: 'center'
+  },
+  {
+    key: 'is_show',
+    title: '是否启用',
+    align: 'center',
+    render: row => {
+      if (row.is_show) {
+        const tagTypes: Record<ApiMqttUser.IsShowKey, NaiveUI.ThemeColor> = {
+          '0': 'error',
+          '1': 'success',
+          '2': 'warning'
+        };
+        return <NTag type={tagTypes[row.is_show]}>{MqttUserIsShowLabels[row.is_show]}</NTag>;
+      }
+      return <span></span>;
+    }
+  },
+  {
+    key: 'actions',
+    title: '操作',
+    align: 'center',
+    render: row => {
+      return (
+        <NSpace justify={'center'}>
+          <NButton type="info" size={'small'} onClick={() => handleEditTable(row.id)}>
+            编辑
+          </NButton>
+          <NPopconfirm onPositiveClick={() => handleDeleteTable(row.id)}>
+            {{
+              default: () => '确认删除',
+              trigger: () => (
+                <NButton type="error" size={'small'}>
+                  删除
+                </NButton>
+              )
+            }}
+          </NPopconfirm>
+        </NSpace>
+      );
+    }
+  }
+]) as Ref<DataTableColumns<Mqtt.User>>;
+
+const editData = ref<Mqtt.User | null>(null);
+const modalType = ref<ModalType>('add');
+
+function setModalType(type: ModalType) {
+  modalType.value = type;
+}
+
+function setEditData(data: Mqtt.User | null) {
+  editData.value = data;
+}
+
+function handleAddTable() {
+  openModal();
+  setModalType('add');
+}
+
+function handleEditTable(rowId: number) {
+  const findItem = tableData.value.find(item => item.id === rowId);
+  if (findItem) {
+    setEditData(findItem);
+  }
+  setModalType('edit');
+  openModal();
+}
+
+function handleDeleteTable(rowId: number) {
+  const data = mqttAuthDelete(rowId);
+  data.then(res => {
+    if (res.data) {
+      window.$message?.success('删除成功!');
+    }
+  });
+  init();
+}
+
+const pagination: PaginationProps = reactive({
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 15, 20, 25, 30],
+  onChange: (page: number) => {
+    pagination.page = page;
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    pagination.pageSize = pageSize;
+    pagination.page = 1;
+  }
+});
+
+function handleSearch() {
+  if (!userName.value) {
+    window.$message?.warning('请输入主题名称');
+  } else {
+    startLoading();
+  }
+}
+
+function init() {
+  getTableData();
+}
+
+// 初始化
+init();
+</script>
+
+<style scoped></style>