Forráskód Böngészése

feat(menu): 新增 IP 列表页面

- 添加 IP 列表页面路由和组件
- 实现 IP 列表的数据获取和展示
- 添加 IP 列表的搜索、新增、编辑和删除功能
- 更新菜单数据,增加网关组管理和 IP 列表菜单项
fusu 1 hónapja
szülő
commit
6f3259d26b

+ 9 - 0
api/v1/gateWayGroupIp.go

@@ -16,3 +16,12 @@ type GateWayGroupIpRequest struct {
 type DeleteGateWayGroupIpRequest struct {
 	Id int `json:"id" form:"id" binding:"required"`
 }
+type SearchGatewayGroupIpParams struct {
+	Name     string `form:"name" json:"name"`
+	GatewayGroupId   int	`form:"gatewayGroupId" json:"gatewayGroupId"`
+	Operator int	`form:"operator" json:"operator"`
+	Current  int	`form:"current" json:"current" default:"1"`
+	PageSize int	`form:"pageSize" json:"pageSize" default:"10"`
+	Column   string `form:"column" json:"column" default:"id"`
+	Order    string `form:"order" json:"order" default:"desc"`
+}

+ 17 - 1
internal/handler/gatewaygroupip.go

@@ -87,4 +87,20 @@ func (h *GateWayGroupIpHandler) DeleteGateWayGroupIp(ctx *gin.Context)  {
 	}
 	v1.HandleSuccess(ctx, nil)
 
-}
+}
+
+func (h *GateWayGroupIpHandler) GetGateWayGroupIpList(ctx *gin.Context)  {
+	req := new(v1.SearchGatewayGroupIpParams)
+	if err := ctx.ShouldBind(req); err != nil {
+		v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, err.Error())
+		return
+	}
+
+	res, err := h.gateWayGroupIpService.GetGateWayGroupIpAdmin(ctx, req)
+	if err != nil {
+		v1.HandleError(ctx, http.StatusInternalServerError, err, err.Error())
+		return
+	}
+	v1.HandleSuccess(ctx, res)
+
+}

+ 8 - 8
internal/model/gatewaygroupip.go

@@ -3,14 +3,14 @@ package model
 import "time"
 
 type GateWayGroupIp struct {
-	Id               int `gorm:"primary"`
-	GatewayGroupId   int `gorm:"not null"`
-	Ip               string `gorm:"not null"`
-	Tag              string `gorm:"null"`
-	Comment          string `gorm:"null"`
-	OriginPlace      string `gorm:"null"`
-	CreatedAt        time.Time
-	UpdatedAt        time.Time
+	Id               int `gorm:"primary" json:"id" form:"id"`
+	GatewayGroupId   int `gorm:"not null" json:"gatewayGroupId" form:"gatewayGroupId"`
+	Ip               string `gorm:"not null" json:"ip" form:"ip"`
+	Tag              string `gorm:"null" json:"tag" form:"tag"`
+	Comment          string `gorm:"null" json:"comment" form:"comment"`
+	OriginPlace      string `gorm:"null" json:"originPlace" form:"originPlace"`
+	CreatedAt        time.Time `json:"createdAt" form:"createdAt"`
+	UpdatedAt        time.Time `json:"updatedAt" form:"updatedAt"`
 }
 
 func (m *GateWayGroupIp) TableName() string {

+ 71 - 0
internal/repository/gatewaygroupip.go

@@ -2,6 +2,9 @@ package repository
 
 import (
 	"context"
+	"fmt"
+	v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
+	"math"
 
 	"github.com/go-nunu/nunu-layout-advanced/internal/model"
 )
@@ -14,6 +17,7 @@ type GateWayGroupIpRepository interface {
 	GetGateWayGroupIpByGatewayGroupId(ctx context.Context, gatewayGroupId int) ([]model.GateWayGroupIp, error)
 	GetGateWayGroupFirstIpByGatewayGroupId(ctx context.Context, gatewayGroupId int) (string, error)
 	GetGateWayGroupAllIpByGatewayGroupId(ctx context.Context, gatewayGroupId int) ([]string, error)
+	GetGatewayGroupIpList(ctx context.Context,req v1.SearchGatewayGroupIpParams) (*v1.PaginatedResponse[model.GateWayGroupIp], error)
 }
 
 func NewGateWayGroupIpRepository(
@@ -81,3 +85,70 @@ func (r *gateWayGroupIpRepository) GetGateWayGroupAllIpByGatewayGroupId(ctx cont
 	}
 	return res, nil
 }
+
+func (r *gateWayGroupIpRepository) GetGatewayGroupIpList(ctx context.Context,req v1.SearchGatewayGroupIpParams) (*v1.PaginatedResponse[model.GateWayGroupIp], error) {
+	var res []model.GateWayGroupIp
+	var total int64
+
+	query := r.db.WithContext(ctx).Model(&model.GateWayGroupIp{})
+
+	if  req.Name != "" {
+		// 使用 LIKE 进行模糊匹配
+		query = query.Where("name LIKE ?", fmt.Sprintf("%%%s%%", req.Name))
+	}
+
+	// 如果 RuleId 被提供了
+	if req.GatewayGroupId != 0 {
+		query = query.Where("gateway_group_id = ?", req.GatewayGroupId)
+	}
+
+	// 如果 Operator 被提供了
+	if req.Operator != 0 {
+		query = query.Where("operator = ?", req.Operator)
+	}
+
+	if req.Column != "" {
+		if req.Column == "createTime" {
+			query = query.Order("created_at" + " " + req.Order)
+		}
+	}
+
+	if err := query.Count(&total).Error; err != nil {
+		// 如果连计数都失败了,直接返回错误
+		return nil, err
+	}
+
+
+	page := req.Current
+	pageSize := req.PageSize
+
+	if page <= 0 {
+		page = 1
+	}
+
+	if pageSize <= 0 {
+		pageSize = 10 // 默认每页 10 条
+	} else if pageSize > 100 {
+		pageSize = 100 // 每页最多 100 条
+	}
+
+	// 计算 offset (偏移量)
+	// 例如,第 1 页,offset = (1-1)*10 = 0 (从第0条开始)
+	// 第 2 页,offset = (2-1)*10 = 10 (从第10条开始)
+	offset := (page - 1) * pageSize
+	// 3. 执行最终的查询
+	// 在所有条件都添加完毕后,再执行 .Find()
+	result := query.Offset(offset).Limit(pageSize).Find(&res)
+	if result.Error != nil {
+		// 这里的错误可能是数据库连接问题等,而不是“未找到记录”
+		return nil, result.Error
+	}
+	return &v1.PaginatedResponse[model.GateWayGroupIp]{
+		Records: res,
+		Page: page,
+		PageSize: pageSize,
+		Total: total,
+		TotalPages: int(math.Ceil(float64(total) / float64(pageSize))),
+
+	}, nil
+}

+ 1 - 1
internal/server/http.go

@@ -168,7 +168,7 @@ func NewHTTPServer(
 			strictAuthRouter.PUT("/gateway/edit", gatewayHandler.EditGatewayGroup)
 			strictAuthRouter.DELETE("/gateway/del", gatewayHandler.DeleteGatewayGroup)
 
-			strictAuthRouter.GET("/gatewayIp/get", gatewayIpHandler.GetGateWayGroupIpByGatewayGroupId)
+			strictAuthRouter.GET("/gatewayIp/get", gatewayIpHandler.GetGateWayGroupIpList)
 			strictAuthRouter.POST("/gatewayIp/add", gatewayIpHandler.AddGateWayGroupIp)
 			strictAuthRouter.PUT("/gatewayIp/edit", gatewayIpHandler.EditGateWayGroupIp)
 			strictAuthRouter.DELETE("/gatewayIp/del", gatewayIpHandler.DeleteGateWayGroupIp)

+ 10 - 0
internal/service/gatewaygroupip.go

@@ -14,6 +14,7 @@ type GateWayGroupIpService interface {
 	AddGateWayGroupIp(ctx context.Context,  req *v1.GateWayGroupIpRequest) error
 	EditGateWayGroupIp(ctx context.Context,  req *v1.GateWayGroupIpRequest) error
 	DeleteGateWayGroupIp(ctx context.Context, req *v1.DeleteGateWayGroupIpRequest) error
+	GetGateWayGroupIpAdmin(ctx context.Context,req *v1.SearchGatewayGroupIpParams) (*v1.PaginatedResponse[model.GateWayGroupIp], error)
 }
 
 func NewGateWayGroupIpService(
@@ -78,3 +79,12 @@ func (s *gateWayGroupIpService) DeleteGateWayGroupIp(ctx context.Context, req *v
 	}
 	return nil
 }
+
+func (s *gateWayGroupIpService) GetGateWayGroupIpAdmin(ctx context.Context,req *v1.SearchGatewayGroupIpParams) (*v1.PaginatedResponse[model.GateWayGroupIp], error) {
+	res, err := s.gateWayGroupIpRepository.GetGatewayGroupIpList(ctx, *req)
+	if err != nil {
+		return nil, err
+	}
+	return res, nil
+
+}

+ 21 - 0
web/servers/routes/menu/index.ts

@@ -445,6 +445,27 @@ const menuData = [
     keepAlive: true,
     locale: 'menu.menu4.menu2',
   },
+  {
+    id: 48,
+    parentId: null,
+    title: '网关组管理',
+    icon: 'ControlOutlined',
+    component: 'RouteView',
+    path: '/gateway',
+    redirect: '/menu/ip-list',
+    name: 'GatewayManagement',
+    locale: 'menu.gateway',
+  },
+  {
+    id: 49,
+    parentId: 48,
+    title: 'IP列表',
+    path: '/menu/ip-list',
+    name: 'IpList',
+    component: '/gateway/ip-list',
+    locale: 'menu.gateway.ip-list',
+    hidden: true,
+  },
 ]
 
 export const accessMenuData = [

+ 18 - 0
web/src/api/list/gateway-group-ip-list.js

@@ -0,0 +1,18 @@
+import request, {useGet, usePost, usePut, useDelete } from '~@/utils/request.js'
+
+
+export function getListApi(params) {
+  return useGet('/v1/gatewayIp/get', params)
+}
+
+export function createApi(data) {
+  return usePost('/v1/gatewayIp/add', data)
+}
+
+export function updateApi(data) {
+  return usePut('/v1/gatewayIp/edit', data)
+}
+
+export function deleteApi(params) {
+  return useDelete('/v1/gatewayIp/del', params)
+}

+ 1 - 0
web/src/locales/lang/global/zh-CN.js

@@ -102,6 +102,7 @@ export default {
   'menu.menu.menu4': '菜单2-1',
   'menu.menu4.menu1': '菜单2-1-1',
   'menu.menu4.menu2': '菜单2-1-2',
+  'menu.menu4.ip-list': 'IP列表',
   'menu.access': '权限模块',
   'menu.access.common': '通用权限',
   'menu.access.roles': '角色管理',

+ 68 - 0
web/src/pages/menu/components/ip-list-modal.vue

@@ -0,0 +1,68 @@
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { Form, Modal } from 'ant-design-vue'
+import { createApi, updateApi } from '~@/api/list/gateway-group-ip-list.js'
+
+const emit = defineEmits(['ok'])
+const useForm = Form.useForm
+
+const visible = ref(false)
+const isEdit = computed(() => !!formState.id)
+const title = computed(() => isEdit.value ? '编辑IP' : '新增IP')
+
+const formState = reactive({
+  id: undefined,
+  gatewayGroupId: undefined,
+  ip: '',
+  comment: '',
+})
+
+const { resetFields, validate, validateInfos } = useForm(formState, {
+  ip: [{ required: true, message: '请输入IP地址' }],
+})
+
+function open(record) {
+  visible.value = true
+  if (record) {
+    Object.assign(formState, record)
+  }
+}
+
+function close() {
+  visible.value = false
+  resetFields()
+}
+
+async function handleOk() {
+  try {
+    await validate()
+    const res = isEdit.value ? await updateApi(formState.id, formState) : await createApi(formState)
+    if (res.code === 0) {
+      Modal.success({
+        title: '操作成功',
+        onOk: () => {
+          emit('ok')
+          close()
+        },
+      })
+    }
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+defineExpose({ open })
+</script>
+
+<template>
+  <a-modal :visible="visible" :title="title" @ok="handleOk" @cancel="close">
+    <a-form :label-col="{ span: 4 }">
+      <a-form-item label="IP地址" v-bind="validateInfos.ip">
+        <a-input v-model:value="formState.ip" placeholder="请输入IP地址" />
+      </a-form-item>
+      <a-form-item label="备注">
+        <a-input v-model:value="formState.comment" placeholder="请输入备注" />
+      </a-form-item>
+    </a-form>
+  </a-modal>
+</template>

+ 129 - 0
web/src/pages/menu/ip-list.vue

@@ -0,0 +1,129 @@
+<script setup>
+import { ref, shallowRef } from 'vue'
+import { useRoute } from 'vue-router'
+import { PlusOutlined } from '@ant-design/icons-vue'
+import CrudTableModal from './components/ip-list-modal.vue'
+import { getListApi, deleteApi } from '~@/api/list/gateway-group-ip-list.js'
+import { useTableQuery } from '~@/composables/table-query'
+
+const route = useRoute()
+const message = useMessage()
+
+const columns = shallowRef([
+  { title: 'ID', dataIndex: 'id', key: 'id' },
+  { title: '网关组ID', dataIndex: 'gatewayGroupId', key: 'gatewayGroupId' },
+  { title: '网关组名称', dataIndex: 'tag', key: 'tag' },
+  { title: 'IP地址', dataIndex: 'ip', key: 'ip' },
+  { title: '备注', dataIndex: 'comment', key: 'comment' },
+  { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
+  { title: '更新时间', dataIndex: 'updatedAt', key: 'updatedAt' },
+  { title: '操作', dataIndex: 'action', key: 'action' },
+])
+
+const { state, initQuery, resetQuery, query } = useTableQuery({
+  queryApi: getListApi,
+  queryParams: {
+    gatewayGroupId: route.query.id,
+    ip: undefined,
+  },
+  afterQuery: (res) => {
+    console.log('afterQuery', res)
+    if (!res || !Array.isArray(res.records)) {
+      // 如果数据格式不正确,返回一个空状态,防止程序崩溃
+      return { list: [], total: 0 }
+    }
+    return res
+  },
+})
+
+const crudTableModal = ref()
+
+async function handleDelete(record) {
+  if (!record.id) return message.error('id 不能为空')
+  try {
+    const res = await deleteApi(record.id)
+    if (res.code === 0) {
+      await query()
+      message.success('删除成功')
+    }
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+function handleAdd() {
+  crudTableModal.value?.open({ gatewayGroupId: route.query.id })
+}
+
+function handleEdit(record) {
+  crudTableModal.value?.open(record)
+}
+</script>
+
+<template>
+  <page-container>
+    <a-card mb-4>
+      <a-form class="system-crud-wrapper" :label-col="{ span: 7 }" :model="state.queryParams">
+        <a-row :gutter="[15, 0]">
+          <a-col :span="6">
+            <a-form-item name="ip" label="IP地址">
+              <a-input v-model:value="state.queryParams.ip" placeholder="请输入IP地址" />
+            </a-form-item>
+          </a-col>
+          <a-col :span="6">
+            <a-space flex justify-end w-full>
+              <a-button :loading="state.loading" type="primary" @click="initQuery">
+                查询
+              </a-button>
+              <a-button :loading="state.loading" @click="resetQuery">
+                重置
+              </a-button>
+            </a-space>
+          </a-col>
+        </a-row>
+      </a-form>
+    </a-card>
+
+    <a-card title="IP列表">
+      <template #extra>
+        <a-space size="middle">
+          <a-button type="primary" @click="handleAdd">
+            <template #icon>
+              <PlusOutlined />
+            </template>
+            新增
+          </a-button>
+        </a-space>
+      </template>
+      <a-table
+        row-key="id"
+        :loading="state.loading"
+        :columns="columns"
+        :data-source="state.dataSource"
+        :pagination="state.pagination"
+      >
+        <template #bodyCell="scope">
+          <template v-if="scope?.column?.dataIndex === 'action'">
+            <div flex gap-2>
+              <a-button type="link" @click="handleEdit(scope?.record)">
+                编辑
+              </a-button>
+              <a-popconfirm
+                title="确定删除该条数据?"
+                ok-text="确定"
+                cancel-text="取消"
+                @confirm="handleDelete(scope?.record)"
+              >
+                <a-button type="link">
+                  删除
+                </a-button>
+              </a-popconfirm>
+            </div>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+
+    <CrudTableModal ref="crudTableModal" @ok="query" />
+  </page-container>
+</template>

+ 9 - 5
web/src/pages/menu/menu1.vue

@@ -3,6 +3,7 @@ import { PlusOutlined } from '@ant-design/icons-vue'
 import CrudTableModal from './components/crud-table-modal.vue'
 import { deleteApi, getListApi } from '~@/api/list/gateway-list.js'
 import { useTableQuery } from '~@/composables/table-query'
+import { useRouter } from 'vue-router'
 
 const message = useMessage()
 const columns = shallowRef([
@@ -95,6 +96,11 @@ function handleEdit(record) {
   crudTableModal.value?.open(record)
 }
 
+const router = useRouter()
+function handleViewIpList(record) {
+  router.push(`/menu/ip-list?id=${record.id}`)
+}
+
 </script>
 
 <template>
@@ -160,11 +166,9 @@ function handleEdit(record) {
                   删除
                 </a-button>
               </a-popconfirm>
-              <router-link :to="{ name: 'menu-ip-list', params: { id: scope?.record?.id } }">
-                <a-button type="link">
-                  查看IP列表
-                </a-button>
-              </router-link>
+              <a-button type="link" @click="handleViewIpList(scope?.record)">
+                查看IP列表
+              </a-button>
             </div>
           </template>
 

+ 9 - 0
web/src/router/dynamic-routes.js

@@ -133,6 +133,15 @@ export default [
           title: '网关组管理',
         },
       },
+      {
+        path: '/menu/ip-list',
+        name: 'IpList',
+        component: () => import('~/pages/menu/ip-list.vue'),
+        meta: {
+          title: 'IP列表',
+          hidden: true,
+        },
+      },
       {
         path: '/menu/menu2',
         name: 'MenuMenu12',