Browse Source

feat(release): mqtt: add user,pasture,acl moudel

Yi 1 year ago
parent
commit
c43a30631d

+ 6 - 0
src/constants/business.ts

@@ -167,6 +167,12 @@ export const MqttAuthIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
   2: '否'
 };
 
+export const MqttTopicIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
+  0: '无效',
+  1: '是',
+  2: '否'
+};
+
 export const MqttUserIsShowLabels: Record<ApiMqttAuth.IsShowKey, string> = {
   0: '无效',
   1: '是',

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

@@ -30,7 +30,17 @@ const mqtt: AuthRoute.Route = {
       meta: {
         title: 'topic配置',
         requiresAuth: true,
-        icon: 'ps:ping'
+        icon: 'icon-park-outline:topic'
+      }
+    },
+    {
+      name: 'mqtt_category',
+      path: '/mqtt/category',
+      component: 'self',
+      meta: {
+        title: '分类管理',
+        requiresAuth: true,
+        icon: 'carbon:category'
       }
     },
     {

+ 22 - 0
src/service/api/mqtt.adapter.ts

@@ -50,3 +50,25 @@ export function adapterOfMqttPastureEnumList(data: Common.OptionWithKey<any>[] |
   if (!data) return [];
   return data;
 }
+
+export function adapterOfMqttTopicEnumList(data: Common.OptionWithKey<any>[] | null): Common.OptionWithKey<any>[] {
+  if (!data) return [];
+  return data;
+}
+
+export function adapterOfMqttCategoryList(data: Mqtt.Category[] | null): ApiMqttCategory.Category[] {
+  if (!data) return [];
+  return data.map(item => {
+    const field: ApiMqttCategory.Category = {
+      index: item.id,
+      key: item.id,
+      ...item
+    };
+    return field;
+  });
+}
+
+export function adapterOfMqttCategoryEnumList(data: Common.OptionWithKey<any>[] | null): Common.OptionWithKey<any>[] {
+  if (!data) return [];
+  return data;
+}

+ 46 - 18
src/service/api/mqtt.ts

@@ -1,20 +1,21 @@
 import { adapter } from '@/utils';
 import {
   adapterOfMqttAuthList,
+  adapterOfMqttCategoryEnumList,
+  adapterOfMqttCategoryList,
   adapterOfMqttPastureEnumList,
   adapterOfMqttPastureList,
+  adapterOfMqttTopicEnumList,
   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<Mqtt.Auth[] | null>(
-    '/mqtt/auth/list',
-    { topic },
-    { headers: { page, page_size: pageSize } }
-  );
+export const fetchMqttAuthList = async (page: number | undefined, pageSize: number | undefined, topic: string) => {
+  const data = await backgroundRequest.post<Mqtt.Auth[] | null>(`/mqtt/auth/list?page=${page}&page_size=${pageSize}`, {
+    topic
+  });
   return adapter(adapterOfMqttAuthList, data);
 };
 
@@ -36,9 +37,10 @@ export const mqttAuthEdit = (param: Mqtt.Auth) => {
 /** 获取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 } }
+    `/mqtt/topic/list?page=${page}&page_size=${pageSize}`,
+    {
+      topic_name
+    }
   );
   return adapter(adapterOfMqttTopicList, data);
 };
@@ -59,11 +61,9 @@ export const mqttTopicDelete = (authId: number) => {
 
 /** 获取用户列表 */
 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 } }
-  );
+  const data = await backgroundRequest.post<Mqtt.User[] | null>(`/mqtt/user/list?page=${page}&page_size=${pageSize}`, {
+    name
+  });
   return adapter(adapterOfMqttUserList, data);
 };
 
@@ -80,9 +80,8 @@ export const mqttUserEdit = (param: Mqtt.User) => {
 /** 获取牧场列表 */
 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 } }
+    `/mqtt/pasture/list?page=${page}&page_size=${pageSize}`,
+    { name }
   );
   return adapter(adapterOfMqttPastureList, data);
 };
@@ -98,6 +97,35 @@ export const mqttPastureEdit = (param: Mqtt.Pasture) => {
 };
 
 export const pastureEnumList = async () => {
-  const data = await backgroundRequest.post<Common.OptionWithKey<any>[] | null>('/mqtt/pasture/enum/list');
+  const data = await backgroundRequest.get<Common.OptionWithKey<any>[] | null>('/mqtt/pasture/enum/list');
   return adapter(adapterOfMqttPastureEnumList, data);
 };
+
+export const topicEnumList = async () => {
+  const data = await backgroundRequest.get<Common.OptionWithKey<any>[] | null>('/mqtt/topic/enum/list');
+  return adapter(adapterOfMqttTopicEnumList, data);
+};
+
+/** 获取category列表 */
+export const fetchMqttCategoryList = async (page: number | undefined, pageSize: number | undefined, name: string) => {
+  const data = await backgroundRequest.post<Mqtt.Category[] | null>(
+    `/mqtt/topic-category/list?page=${page}&page_size=${pageSize}`,
+    { name }
+  );
+  return adapter(adapterOfMqttCategoryList, data);
+};
+
+/** 添加分类 */
+export const mqttCategoryAdd = (params: Mqtt.Category) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/topic-category/add', params);
+};
+
+/** 编辑分类 */
+export const mqttCategoryEdit = (param: Mqtt.Category) => {
+  return backgroundRequest.post<ApiBoolean.OK | null>('/mqtt/topic-category/edit', param);
+};
+
+export const categoryEnumList = async () => {
+  const data = await backgroundRequest.get<Common.OptionWithKey<any>[] | null>('/mqtt/topic-category/enum/list');
+  return adapter(adapterOfMqttCategoryEnumList, data);
+};

+ 17 - 0
src/typings/business.d.ts

@@ -204,3 +204,20 @@ declare namespace ApiMqttPasture {
    */
   type IsGroupKey = NonNullable<Pasture['is_group']>;
 }
+
+declare namespace ApiMqttCategory {
+  interface Category extends Mqtt.Category {
+    /** 序号 */
+    index: number;
+    /** 表格的key(id) */
+    key: number;
+  }
+
+  /**
+   * 是否启动
+   * 0 无效
+   * 1:是
+   * 2: 否
+   */
+  type IsShowKey = NonNullable<Category['is_show']>;
+}

+ 20 - 2
src/typings/mqtt.d.ts

@@ -6,10 +6,16 @@ declare namespace Mqtt {
     client_id: string | null;
     mount_point: string | null;
     user_name: string | null;
+    password: string;
+    pasture_id: number | null;
+    pasture_name: string | null;
+    topic_ids: string | null;
+    access: number | null;
     publish_acl: string | null;
     subscribe_acl: string | null;
     is_show: number | null;
-    remark: string | null;
+    created_at_format: string | null;
+    updated_at_format: string | null;
   }
 
   interface User {
@@ -20,7 +26,7 @@ declare namespace Mqtt {
     password: string;
     client_id: string | null;
     pasture_id: number | null;
-    topic_id: number | null;
+    topic_ids: string | null;
     is_show: number | null;
     access: number | null;
     created_at_format: string | null;
@@ -30,6 +36,8 @@ declare namespace Mqtt {
   interface Topic {
     /** id */
     id: number;
+    category_id: number | null;
+    category_name: string | null;
     /** 字段名 */
     topic_name: string | null;
     topic_template: string | null;
@@ -51,4 +59,14 @@ declare namespace Mqtt {
     created_at_format: string | null;
     updated_at_format: string | null;
   }
+
+  interface Category {
+    /** id */
+    id: number;
+    /** 分类名称 */
+    name: string | null;
+    is_show: number | null;
+    created_at_format: string | null;
+    updated_at_format: string | null;
+  }
 }

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

@@ -61,6 +61,7 @@ declare namespace PageRoute {
     | 'mqtt_authentication'
     | 'mqtt_pasture'
     | 'mqtt_topic'
+    | 'mqtt_category'
     | 'mqtt_user'
     | 'multi-menu'
     | 'multi-menu_first'
@@ -123,6 +124,7 @@ declare namespace PageRoute {
     | 'mqtt_authentication'
     | 'mqtt_pasture'
     | 'mqtt_topic'
+    | 'mqtt_category'
     | 'mqtt_user'
     | 'multi-menu_first_second-new_third'
     | 'multi-menu_first_second'

+ 1 - 0
src/views/index.ts

@@ -40,6 +40,7 @@ export const views: Record<
   mqtt_authentication: () => import('./mqtt/authentication/index.vue'),
   mqtt_pasture: () => import('./mqtt/pasture/index.vue'),
   mqtt_topic: () => import('./mqtt/topic/index.vue'),
+  mqtt_category: () => import('./mqtt/topic-category/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'),

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

@@ -1,27 +1,26 @@
 <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="48" :x-gap="18">
-        <n-form-item-grid-item :span="25" label="挂载点" path="mount_point">
-          <n-input v-model:value="formModel.mount_point" />
-        </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="客户端ID" path="client_id">
+    <n-form ref="formRef" label-placement="left" :label-width="90" :model="formModel" :rules="rules">
+      <n-grid :cols="50" :x-gap="18">
+        <n-form-item-grid-item :span="18" label="客户端ID" size="large" path="client_id">
           <n-input v-model:value="formModel.client_id" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="用户名称" path="user_name">
-          <n-input v-model:value="formModel.user_name" />
+        <n-form-item-grid-item :span="18" label="牧场名称" size="large" path="pasture_id">
+          <n-select v-model:value="formModel.pasture_id" :options="pastureEnumListData" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="发布topic" path="publish_acl">
-          <n-input v-model:value="formModel.publish_acl" />
+        <n-form-item-grid-item :span="18" label="用户名称" size="large" path="user_name">
+          <n-input v-model:value="formModel.user_name" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="订阅topic" path="subscribe_acl">
-          <n-input v-model:value="formModel.subscribe_acl" />
+        <n-form-item-grid-item :span="18" label="用户密码" size="large" path="user_name">
+          <n-input v-model:value="formModel.password" type="password" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="是否显示" path="is_show">
-          <n-select v-model:value="formModel.is_show" :options="EventIsShowOptions" />
+        <n-form-item-grid-item :span="18" label="主题名称(topic)" size="large" path="topic_ids">
+          <n-select v-model:value="formModel.topic_ids" multiple :options="topicEnumListData" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="25" label="描述" path="remarks">
-          <n-input v-model:value="formModel.remark" type="textarea" placeholder="请输入描述" />
+        <n-form-item-grid-item :span="20" label="主题动作" size="large" 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">
@@ -34,10 +33,11 @@
 
 <script setup lang="ts">
 import { computed, ref, reactive, watch } from 'vue';
-import type { FormInst, FormItemRule } from 'naive-ui';
-import { EventIsShowOptions } from '@/constants';
+import type { FormInst, FormItemRule, SelectOption } from 'naive-ui';
+import { TopicAccessOptions } from '@/constants';
 import { createRequiredFormRule } from '@/utils';
 import { mqttAuthAdd, mqttAuthEdit } from '@/service/api/mqtt';
+import { MD5 } from '@/utils/crypto';
 export interface Props {
   /** 弹窗可见性 */
   visible: boolean;
@@ -48,7 +48,9 @@ export interface Props {
    */
   type?: 'add' | 'edit';
   /** 编辑的表格行数据 */
-  editData?: ApiMqttAuth.Auth | null;
+  editData?: Mqtt.User | null;
+  pastureEnumListData: SelectOption[];
+  topicEnumListData: SelectOption[];
 }
 
 export type ModalType = NonNullable<Props['type']>;
@@ -74,12 +76,14 @@ const modalVisible = computed({
     emit('update:visible', visible);
   }
 });
+
 const closeModal = () => {
   modalVisible.value = false;
 };
+
 const titles: Record<ModalType, string> = {
-  add: '添加鉴权',
-  edit: '编辑鉴权'
+  add: '添加用户',
+  edit: '编辑用户'
 };
 
 const title = computed(() => {
@@ -90,32 +94,57 @@ const formRef = ref<HTMLElement & FormInst>();
 
 type FormModel = Pick<
   Mqtt.Auth,
-  'id' | 'client_id' | 'mount_point' | 'user_name' | 'publish_acl' | 'subscribe_acl' | 'remark' | 'is_show'
+  | 'id'
+  | 'mount_point'
+  | 'pasture_id'
+  | 'pasture_name'
+  | 'user_name'
+  | 'password'
+  | 'client_id'
+  | 'topic_ids'
+  | 'publish_acl'
+  | 'subscribe_acl'
+  | 'access'
+  | 'is_show'
+  | 'created_at_format'
+  | 'updated_at_format'
 >;
 
 const formModel = reactive<FormModel>(createDefaultFormModel());
 
 const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
-  client_id: createRequiredFormRule('请输入客户端id名称'),
-  is_show: createRequiredFormRule(),
-  publish_acl: createRequiredFormRule('请输入发布主题topic'),
-  subscribe_acl: createRequiredFormRule('请输入订阅主题topic'),
-  remark: createRequiredFormRule('请输入描述'),
-  mount_point: createRequiredFormRule('请输入挂载点'),
+  pasture_id: createRequiredFormRule('请选择牧场'),
   user_name: createRequiredFormRule('请输入用户名称'),
+  password: createRequiredFormRule('请输入用户密码'),
+  client_id: createRequiredFormRule('请输入客户端id'),
+  topic_ids: createRequiredFormRule('请选择topic模板'),
+  access: createRequiredFormRule('请选择topic权限'),
+  is_show: createRequiredFormRule(),
+  mount_point: createRequiredFormRule(),
+  pasture_name: createRequiredFormRule(),
+  subscribe_acl: createRequiredFormRule(),
+  publish_acl: createRequiredFormRule(),
+  created_at_format: createRequiredFormRule(),
+  updated_at_format: createRequiredFormRule(),
   id: createRequiredFormRule()
 };
 
 function createDefaultFormModel(): FormModel {
   return {
     id: 0,
-    client_id: '',
-    user_name: '',
     mount_point: '',
+    pasture_id: null,
+    pasture_name: '',
+    user_name: '',
+    password: '',
+    client_id: '',
+    topic_ids: '',
+    access: 0,
+    is_show: 1,
     publish_acl: '',
     subscribe_acl: '',
-    remark: '',
-    is_show: 1
+    created_at_format: '',
+    updated_at_format: ''
   };
 }
 
@@ -141,6 +170,7 @@ function handleUpdateFormModelByModalType() {
 async function handleSubmit() {
   await formRef.value?.validate();
   if (props.type === 'add') {
+    formModel.password = MD5(formModel.password).toString();
     const data = mqttAuthAdd(formModel);
     data.then(res => {
       if (res.data?.success) {

+ 103 - 53
src/views/mqtt/authentication/index.vue

@@ -3,14 +3,14 @@
     <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>
-            <n-input v-model:value="topicName" />
+            <n-input v-model:value="topic" />
             <n-button type="primary" @click="handleSearch">搜索</n-button>
           </n-input-group>
           <n-button size="small" type="primary" @click="getTableData">
@@ -20,7 +20,13 @@
         </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" />
+      <table-action-modal
+        v-model:visible="visible"
+        :type="modalType"
+        :edit-data="editData"
+        :pasture-enum-list-data="pastureEnumListData"
+        :topic-enum-list-data="topicEnumListData"
+      />
     </n-card>
   </div>
 </template>
@@ -28,35 +34,73 @@
 <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 { NButton, NSpace, NTag, NPopconfirm } from 'naive-ui';
+import type { DataTableColumns, PaginationProps, SelectOption } from 'naive-ui';
 import { MqttAuthIsShowLabels } from '@/constants';
 import { useBoolean, useLoading } from '@/hooks';
-import { fetchMqttAuthList } from '@/service/api/mqtt';
+import { fetchMqttAuthList, mqttAuthDelete, pastureEnumList, topicEnumList } 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 } = useBoolean();
-const topicName = ref('');
+const { bool: visible, setTrue: openModal } = useBoolean();
+const topic = ref('');
 const tableData = ref<ApiMqttAuth.Auth[]>([]);
+const editData = ref<Mqtt.Auth | null>(null);
+const modalType = ref<ModalType>('add');
+const pastureEnumListData = ref<SelectOption[]>([]);
+const topicEnumListData = ref<SelectOption[]>([]);
+
+function setPastureList(data: SelectOption[]) {
+  pastureEnumListData.value = data;
+}
+
+function setTopicList(data: SelectOption[]) {
+  topicEnumListData.value = data;
+}
+
+function setEditData(data: Mqtt.Auth | null) {
+  editData.value = data;
+}
+
 function setTableData(data: ApiMqttAuth.Auth[]) {
   tableData.value = data;
 }
 
+function setModalType(type: ModalType) {
+  modalType.value = type;
+}
+
 async function getTableData() {
   startLoading();
-  const { data } = await fetchMqttAuthList(1, 10, topicName.value);
+  // eslint-disable-next-line @typescript-eslint/no-use-before-define
+  const { data } = await fetchMqttAuthList(pagination.page, pagination.pageSize, topic.value);
   if (data) {
-    setTimeout(() => {
-      setTableData(data);
-      endLoading();
-    }, 1000);
+    setTableData(data);
+    endLoading();
   } else {
     endLoading();
   }
 }
 
+async function getPastureEnumList() {
+  const { data } = await pastureEnumList();
+  if (data) {
+    setTimeout(() => {
+      setPastureList(data);
+    }, 1000);
+  }
+}
+
+async function getTopicEnumList() {
+  const { data } = await topicEnumList();
+  if (data) {
+    setTimeout(() => {
+      setTopicList(data);
+    }, 1000);
+  }
+}
+
 const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
   {
     type: 'selection',
@@ -67,6 +111,11 @@ const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
     title: '序号',
     align: 'center'
   },
+  {
+    key: 'pasture_name',
+    title: '牧场名称',
+    align: 'center'
+  },
   {
     key: 'mount_point',
     title: '挂载点',
@@ -83,12 +132,17 @@ const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
     align: 'center'
   },
   {
-    key: 'publish_acl',
-    title: '发布topic',
+    key: 'topic_name_stings',
+    title: 'topic名称',
     align: 'center'
   },
   {
     key: 'subscribe_acl',
+    title: '发布topic',
+    align: 'center'
+  },
+  {
+    key: 'publish_acl',
     title: '订阅topic',
     align: 'center'
   },
@@ -107,7 +161,7 @@ const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
       }
       return <span></span>;
     }
-  } /*,
+  },
   {
     key: 'actions',
     title: '操作',
@@ -131,24 +185,36 @@ const columns: Ref<DataTableColumns<ApiMqttAuth.Auth>> = ref([
         </NSpace>
       );
     }
-  } */
+  }
 ]) as Ref<DataTableColumns<ApiMqttAuth.Auth>>;
 
-const editData = ref<ApiMqttAuth.Auth | null>(null);
-const modalType = ref<ModalType>('add');
-/*
-function setModalType(type: ModalType) {
-  modalType.value = type;
-}
-
-function setEditData(data: ApiMqttAuth.Auth | null) {
-  editData.value = data;
-}
-
+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 handleAddTable() {
-  openModal();
-  setModalType('add');
+async function handleSearch() {
+  if (!topic.value) {
+    window.$message?.warning('请输入主题名称');
+  } else {
+    startLoading();
+    const { data } = await fetchMqttAuthList(pagination.page, pagination.pageSize, topic.value);
+    if (data) {
+      setTableData(data);
+      endLoading();
+    } else {
+      endLoading();
+    }
+  }
 }
 
 function handleEditTable(rowId: number) {
@@ -169,32 +235,16 @@ function handleDeleteTable(rowId: number) {
   });
   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();
-  }
+function handleAddTable() {
+  openModal();
+  setModalType('add');
 }
 
 function init() {
   getTableData();
+  getPastureEnumList();
+  getTopicEnumList();
 }
 
 // 初始化

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

@@ -0,0 +1,145 @@
+<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-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 { mqttCategoryAdd, mqttCategoryEdit } from '@/service/api/mqtt';
+export interface Props {
+  /** 弹窗可见性 */
+  visible: boolean;
+  /**
+   * 弹窗类型
+   * add: 新增
+   * edit: 编辑
+   */
+  type?: 'add' | 'edit';
+  /** 编辑的表格行数据 */
+  editData?: Mqtt.Category | 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.Category, 'id' | 'name' | 'is_show' | 'created_at_format' | 'updated_at_format'>;
+
+const formModel = reactive<FormModel>(createDefaultFormModel());
+
+const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
+  name: createRequiredFormRule('请输入分类名称'),
+  is_show: createRequiredFormRule(),
+  created_at_format: createRequiredFormRule(),
+  updated_at_format: createRequiredFormRule(),
+  id: createRequiredFormRule()
+};
+
+function createDefaultFormModel(): FormModel {
+  return {
+    id: 0,
+    name: '',
+    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 = mqttCategoryAdd(formModel);
+    data.then(res => {
+      if (res.data?.success) {
+        window.$message?.success(`${titles[props.type]}成功!`);
+      }
+    });
+  }
+
+  if (props.type === 'edit') {
+    const data = mqttCategoryEdit(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/topic-category/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="name" />
+            <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 { MqttTopicIsShowLabels } from '@/constants';
+import { useBoolean, useLoading } from '@/hooks';
+import { fetchMqttCategoryList } from '@/service/api/mqtt';
+import TableActionModal from '../topic-category/components/table-action-modal.vue';
+import type { ModalType } from '../topic-category/components/table-action-modal.vue';
+
+const { loading, startLoading, endLoading } = useLoading(false);
+const { bool: visible, setTrue: openModal } = useBoolean();
+const name = ref('');
+const tableData = ref<ApiMqttCategory.Category[]>([]);
+function setTableData(data: ApiMqttCategory.Category[]) {
+  tableData.value = data;
+}
+
+async function getTableData() {
+  startLoading();
+  const { data } = await fetchMqttCategoryList(1, 10, name.value);
+  if (data) {
+    setTimeout(() => {
+      setTableData(data);
+      endLoading();
+    }, 1000);
+  } else {
+    endLoading();
+  }
+}
+
+const columns: Ref<DataTableColumns<ApiMqttCategory.Category>> = ref([
+  {
+    type: 'selection',
+    align: 'center'
+  },
+  {
+    key: 'index',
+    title: '序号',
+    align: 'center'
+  },
+  {
+    key: 'name',
+    title: '分类名称',
+    align: 'center'
+  },
+  {
+    key: 'created_at_format',
+    title: '创建时间',
+    align: 'center'
+  },
+  {
+    key: 'is_show',
+    title: '是否启用',
+    align: 'center',
+    render: row => {
+      if (row.is_show) {
+        const tagTypes: Record<ApiMqttCategory.IsShowKey, NaiveUI.ThemeColor> = {
+          '0': 'error',
+          '1': 'success',
+          '2': 'warning'
+        };
+        return <NTag type={tagTypes[row.is_show]}>{MqttTopicIsShowLabels[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>
+        </NSpace>
+      );
+    }
+  }
+]) as Ref<DataTableColumns<ApiMqttCategory.Category>>;
+
+const editData = ref<ApiMqttCategory.Category | null>(null);
+const modalType = ref<ModalType>('add');
+
+function setModalType(type: ModalType) {
+  modalType.value = type;
+}
+
+function setEditData(data: ApiMqttCategory.Category | 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;
+  }
+});
+
+async function handleSearch() {
+  if (!name.value) {
+    window.$message?.warning('请输入分类名称');
+  } else {
+    startLoading();
+    const { data } = await fetchMqttCategoryList(pagination.page, pagination.pageSize, name.value);
+    if (data) {
+      setTimeout(() => {
+        setTableData(data);
+        endLoading();
+      }, 1000);
+    } else {
+      endLoading();
+    }
+  }
+}
+
+function init() {
+  getTableData();
+}
+
+// 初始化
+init();
+</script>
+
+<style scoped></style>

+ 21 - 6
src/views/mqtt/topic/components/table-action-modal.vue

@@ -2,10 +2,13 @@
   <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-form-item-grid-item :span="25" label="分类名称" path="category_id">
+          <n-select v-model:value="formModel.category_id" size="large" :options="categoryEnumListData" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="25" label="topic名称" size="large" 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-form-item-grid-item :span="25" label="topic模板" size="large" path="topic_template">
           <n-input v-model:value="formModel.topic_template" />
         </n-form-item-grid-item>
       </n-grid>
@@ -19,7 +22,7 @@
 
 <script setup lang="ts">
 import { computed, ref, reactive, watch } from 'vue';
-import type { FormInst, FormItemRule } from 'naive-ui';
+import type { FormInst, FormItemRule, SelectOption } from 'naive-ui';
 import { createRequiredFormRule } from '@/utils';
 import { mqttTopicAdd, mqttTopicEdit } from '@/service/api/mqtt';
 export interface Props {
@@ -33,6 +36,7 @@ export interface Props {
   type?: 'add' | 'edit';
   /** 编辑的表格行数据 */
   editData?: Mqtt.Topic | null;
+  categoryEnumListData: SelectOption[];
 }
 
 export type ModalType = NonNullable<Props['type']>;
@@ -74,14 +78,23 @@ const formRef = ref<HTMLElement & FormInst>();
 
 type FormModel = Pick<
   Mqtt.Topic,
-  'id' | 'topic_name' | 'topic_template' | 'created_at_format' | 'is_show' | 'updated_at_format'
+  | 'id'
+  | 'category_id'
+  | 'category_name'
+  | '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(),
+  category_id: createRequiredFormRule('请选择分类名称'),
+  category_name: createRequiredFormRule('请选择分类名称1'),
+  topic_name: createRequiredFormRule('请输入主题名称'),
+  topic_template: createRequiredFormRule('请输入主题'),
   is_show: createRequiredFormRule(),
   created_at_format: createRequiredFormRule(),
   updated_at_format: createRequiredFormRule(),
@@ -91,6 +104,8 @@ const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
 function createDefaultFormModel(): FormModel {
   return {
     id: 0,
+    category_id: null,
+    category_name: '',
     topic_name: '',
     topic_template: '',
     created_at_format: '',

+ 29 - 3
src/views/mqtt/topic/index.vue

@@ -20,7 +20,12 @@
         </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" />
+      <table-action-modal
+        v-model:visible="visible"
+        :type="modalType"
+        :edit-data="editData"
+        :category-enum-list-data="categoryEnumListData"
+      />
     </n-card>
   </div>
 </template>
@@ -29,9 +34,9 @@
 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 type { DataTableColumns, PaginationProps, SelectOption } from 'naive-ui';
 import { useBoolean, useLoading } from '@/hooks';
-import { fetchMqttTopicList, mqttTopicDelete } from '@/service/api/mqtt';
+import { categoryEnumList, 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';
 
@@ -39,10 +44,16 @@ const { loading, startLoading, endLoading } = useLoading(false);
 const { bool: visible, setTrue: openModal } = useBoolean();
 const topicName = ref('');
 const tableData = ref<ApiMqttTopic.Topic[]>([]);
+const categoryEnumListData = ref<SelectOption[]>([]);
+
 function setTableData(data: ApiMqttTopic.Topic[]) {
   tableData.value = data;
 }
 
+function setCategoryList(data: SelectOption[]) {
+  categoryEnumListData.value = data;
+}
+
 async function getTableData() {
   startLoading();
   const { data } = await fetchMqttTopicList(1, 10, topicName.value);
@@ -56,6 +67,15 @@ async function getTableData() {
   }
 }
 
+async function getCategoryEnumList() {
+  const { data } = await categoryEnumList();
+  if (data) {
+    setTimeout(() => {
+      setCategoryList(data);
+    }, 1000);
+  }
+}
+
 const columns: Ref<DataTableColumns<ApiMqttTopic.Topic>> = ref([
   {
     type: 'selection',
@@ -66,6 +86,11 @@ const columns: Ref<DataTableColumns<ApiMqttTopic.Topic>> = ref([
     title: '序号',
     align: 'center'
   },
+  {
+    key: 'category_name',
+    title: '分类名称',
+    align: 'center'
+  },
   {
     key: 'topic_name',
     title: 'topic名称',
@@ -175,6 +200,7 @@ function handleSearch() {
 
 function init() {
   getTableData();
+  getCategoryEnumList();
 }
 
 // 初始化

+ 19 - 15
src/views/mqtt/user/components/table-action-modal.vue

@@ -1,23 +1,23 @@
 <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-form ref="formRef" label-placement="left" :label-width="90" :model="formModel" :rules="rules">
+      <n-grid :cols="50" :x-gap="18">
+        <n-form-item-grid-item :span="18" label="客户端ID" size="large" 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-form-item-grid-item :span="18" label="牧场名称" size="large" path="pasture_id">
+          <n-select v-model:value="formModel.pasture_id" :options="pastureEnumListData" />
+        </n-form-item-grid-item>
+        <n-form-item-grid-item :span="18" label="用户名称" size="large" 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-form-item-grid-item :span="18" label="用户密码" size="large" 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 :span="18" label="主题名称(topic)" size="large" path="topic_ids">
+          <n-select v-model:value="formModel.topic_ids" multiple :options="topicEnumListData" />
         </n-form-item-grid-item>
-        <n-form-item-grid-item :span="12" label="主题动作" path="access">
+        <n-form-item-grid-item :span="20" label="主题动作" size="large" 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>
@@ -33,7 +33,7 @@
 
 <script setup lang="ts">
 import { computed, ref, reactive, watch } from 'vue';
-import type { FormInst, FormItemRule } from 'naive-ui';
+import type { FormInst, FormItemRule, SelectOption } from 'naive-ui';
 import { TopicAccessOptions } from '@/constants';
 import { createRequiredFormRule } from '@/utils';
 import { mqttUserAdd, mqttUserEdit } from '@/service/api/mqtt';
@@ -49,6 +49,8 @@ export interface Props {
   type?: 'add' | 'edit';
   /** 编辑的表格行数据 */
   editData?: Mqtt.User | null;
+  pastureEnumListData: SelectOption[];
+  topicEnumListData: SelectOption[];
 }
 
 export type ModalType = NonNullable<Props['type']>;
@@ -74,9 +76,11 @@ const modalVisible = computed({
     emit('update:visible', visible);
   }
 });
+
 const closeModal = () => {
   modalVisible.value = false;
 };
+
 const titles: Record<ModalType, string> = {
   add: '添加用户',
   edit: '编辑用户'
@@ -95,7 +99,7 @@ type FormModel = Pick<
   | 'user_name'
   | 'password'
   | 'client_id'
-  | 'topic_id'
+  | 'topic_ids'
   | 'access'
   | 'is_show'
   | 'created_at_format'
@@ -109,7 +113,7 @@ const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
   user_name: createRequiredFormRule('请输入用户名称'),
   password: createRequiredFormRule('请输入用户密码'),
   client_id: createRequiredFormRule('请输入客户端id'),
-  topic_id: createRequiredFormRule('请选择topic模板'),
+  topic_ids: createRequiredFormRule('请选择topic模板'),
   access: createRequiredFormRule('请选择topic权限'),
   is_show: createRequiredFormRule(),
   created_at_format: createRequiredFormRule(),
@@ -124,7 +128,7 @@ function createDefaultFormModel(): FormModel {
     user_name: '',
     password: '',
     client_id: '',
-    topic_id: null,
+    topic_ids: '',
     access: 0,
     is_show: 1,
     created_at_format: '',

+ 39 - 3
src/views/mqtt/user/index.vue

@@ -20,7 +20,13 @@
         </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" />
+      <table-action-modal
+        v-model:visible="visible"
+        :type="modalType"
+        :edit-data="editData"
+        :pasture-enum-list-data="pastureEnumListData"
+        :topic-enum-list-data="topicEnumListData"
+      />
     </n-card>
   </div>
 </template>
@@ -29,10 +35,10 @@
 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 type { DataTableColumns, PaginationProps, SelectOption } from 'naive-ui';
 import { MqttUserIsShowLabels } from '@/constants';
 import { useBoolean, useLoading } from '@/hooks';
-import { fetchMqttUserList, mqttAuthDelete } from '@/service/api/mqtt';
+import { fetchMqttUserList, mqttAuthDelete, pastureEnumList, topicEnumList } from '@/service/api/mqtt';
 import TableActionModal from '../user/components/table-action-modal.vue';
 import type { ModalType } from '../user/components/table-action-modal.vue';
 
@@ -40,11 +46,21 @@ const { loading, startLoading, endLoading } = useLoading(false);
 const { bool: visible, setTrue: openModal } = useBoolean();
 const userName = ref('');
 const tableData = ref<Mqtt.User[]>([]);
+const pastureEnumListData = ref<SelectOption[]>([]);
+const topicEnumListData = ref<SelectOption[]>([]);
 
 function setTableData(data: Mqtt.User[]) {
   tableData.value = data;
 }
 
+function setPastureList(data: SelectOption[]) {
+  pastureEnumListData.value = data;
+}
+
+function setTopicList(data: SelectOption[]) {
+  topicEnumListData.value = data;
+}
+
 async function getTableData() {
   startLoading();
   const { data } = await fetchMqttUserList(1, 10, userName.value);
@@ -58,6 +74,24 @@ async function getTableData() {
   }
 }
 
+async function getPastureEnumList() {
+  const { data } = await pastureEnumList();
+  if (data) {
+    setTimeout(() => {
+      setPastureList(data);
+    }, 1000);
+  }
+}
+
+async function getTopicEnumList() {
+  const { data } = await topicEnumList();
+  if (data) {
+    setTimeout(() => {
+      setTopicList(data);
+    }, 1000);
+  }
+}
+
 const columns: Ref<DataTableColumns<Mqtt.User>> = ref([
   {
     type: 'selection',
@@ -184,6 +218,8 @@ function handleSearch() {
 
 function init() {
   getTableData();
+  getPastureEnumList();
+  getTopicEnumList();
 }
 
 // 初始化