diff --git a/src/api/subscription/consumer/index.ts b/src/api/subscription/consumer/index.ts
new file mode 100644
index 00000000..c25862db
--- /dev/null
+++ b/src/api/subscription/consumer/index.ts
@@ -0,0 +1,29 @@
+import type { Consumer, GetConsumerListParams } from './types'
+import { defHttp } from '@/utils/http/axios'
+
+export function getConsumerList(params: GetConsumerListParams) {
+  return defHttp.get<PageResult<Consumer>>({
+    url: '/server/consumer/page',
+    params,
+  })
+}
+
+export function createConsumer(data: Partial<Consumer>) {
+  return defHttp.post({
+    url: '/server/consumer/save',
+    data,
+  })
+}
+
+export function updateConsumer(data: Partial<Consumer>) {
+  return defHttp.post({
+    url: '/server/consumer/update',
+    data,
+  })
+}
+
+export function deleteConsumer(id: string) {
+  return defHttp.post({
+    url: `/server/consumer/remove?id=${id}`,
+  })
+}
diff --git a/src/api/subscription/consumer/types.ts b/src/api/subscription/consumer/types.ts
new file mode 100644
index 00000000..444b873c
--- /dev/null
+++ b/src/api/subscription/consumer/types.ts
@@ -0,0 +1,11 @@
+export interface GetConsumerListParams extends PageParam {
+  consumerName?: string
+}
+
+export interface Consumer {
+  id: string
+  consumerName: string
+  consumerToken: string
+  createTime: string
+  subscribes: string
+}
diff --git a/src/api/subscription/list/index.ts b/src/api/subscription/list/index.ts
index a4617473..f8ee4511 100644
--- a/src/api/subscription/list/index.ts
+++ b/src/api/subscription/list/index.ts
@@ -27,3 +27,18 @@ export function deleteSubscription(id: string) {
     url: `/server/subscribe/remove?id=${id}`,
   })
 }
+
+export function getAllSubscription() {
+  return defHttp.get<SubScription[]>({
+    url: '/server/subscribe/select',
+  })
+}
+
+export function getSubscriptionDetail(id: string) {
+  return defHttp.get({
+    url: '/server/consumer/detail',
+    params: {
+      id,
+    },
+  })
+}
diff --git a/src/views/subscription/consumer/ConsumerFormModal.vue b/src/views/subscription/consumer/ConsumerFormModal.vue
new file mode 100644
index 00000000..2f61c615
--- /dev/null
+++ b/src/views/subscription/consumer/ConsumerFormModal.vue
@@ -0,0 +1,66 @@
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { formSchema } from './data'
+import { useMessage } from '@/hooks/web/useMessage'
+import { BasicForm, useForm } from '@/components/Form'
+import { BasicModal, useModalInner } from '@/components/Modal'
+import { createConsumer, updateConsumer } from '@/api/subscription/consumer'
+import type { Consumer } from '@/api/subscription/consumer/types'
+import { getSubscriptionDetail } from '@/api/subscription/list'
+import { noop } from '@/utils'
+
+defineOptions({ name: 'ConsumerFormModal' })
+
+const emit = defineEmits(['success', 'register'])
+
+const isUpdate = ref(false)
+const [registerForm, { setFieldsValue, validate }] = useForm({
+  labelWidth: 100,
+  baseColProps: { span: 24 },
+  schemas: formSchema,
+  showActionButtonGroup: false,
+  actionColOptions: { span: 23 },
+})
+
+const [registerModal, { setModalProps, closeModal }] = useModalInner(async (id: string) => {
+  isUpdate.value = true
+  setModalProps({ confirmLoading: true, loading: true })
+  getSubscriptionDetail(id)
+    .then((data) => {
+      setFieldsValue({ ...data, subscribeIds: data.subscribes ? data.subscribes.split(',') : [] })
+    })
+    .catch(noop)
+    .finally(() => {
+      setModalProps({ confirmLoading: false, loading: false })
+    })
+})
+
+async function handleSubmit() {
+  try {
+    const values = await validate<Consumer & { subscribeIds: string[] | string }>()
+    setModalProps({ confirmLoading: true })
+    await (isUpdate.value ? updateConsumer(values) : createConsumer(values))
+    closeModal()
+    emit('success')
+    useMessage().createMessage.success('保存成功')
+  }
+  catch {}
+  finally {
+    setModalProps({ confirmLoading: false })
+  }
+}
+</script>
+
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    width="30%"
+    :min-height="100"
+    :title="isUpdate ? '编辑' : '新增'"
+    :after-close="() => isUpdate = false"
+    @register="registerModal"
+    @ok="handleSubmit"
+  >
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
diff --git a/src/views/subscription/consumer/data.ts b/src/views/subscription/consumer/data.ts
new file mode 100644
index 00000000..0d9020c3
--- /dev/null
+++ b/src/views/subscription/consumer/data.ts
@@ -0,0 +1,50 @@
+import type { BasicColumn, FormSchema } from '@/components/Table'
+import { getAllSubscription } from '@/api/subscription/list'
+
+export const columns: BasicColumn[] = [
+  {
+    title: '消费组名称',
+    dataIndex: 'consumerName',
+  },
+  {
+    title: '消费组 Token',
+    dataIndex: 'consumerToken',
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+  },
+]
+
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'consumerName',
+    label: '消费组名称',
+    component: 'Input',
+    colProps: {
+      span: 6,
+    },
+  },
+]
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'consumerName',
+    fields: ['id'],
+    label: '消费组名称',
+    required: true,
+    component: 'Input',
+  },
+  {
+    field: 'subscribeIds',
+    label: '订阅',
+    required: true,
+    component: 'ApiSelect',
+    componentProps: {
+      api: getAllSubscription,
+      mode: 'multiple',
+      valueField: 'id',
+      labelField: 'productName',
+    },
+  },
+]
diff --git a/src/views/subscription/consumer/index.vue b/src/views/subscription/consumer/index.vue
new file mode 100644
index 00000000..66541b0b
--- /dev/null
+++ b/src/views/subscription/consumer/index.vue
@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+import { PlusOutlined } from '@ant-design/icons-vue'
+import { columns, searchFormSchema } from './data'
+import ConsumerFormModal from './ConsumerFormModal.vue'
+import { BasicTable, TableAction, useTable } from '@/components/Table'
+import { deleteConsumer, getConsumerList } from '@/api/subscription/consumer'
+import { useModal } from '@/components/Modal'
+import type { Consumer } from '@/api/subscription/consumer/types'
+import { useMessage } from '@/hooks/web/useMessage'
+
+defineOptions({ name: 'Consumer' })
+
+const [registerModal, { openModal }] = useModal<string>()
+
+const [register, { reload }] = useTable({
+  api: getConsumerList,
+  columns,
+  formConfig: {
+    labelWidth: 80,
+    schemas: searchFormSchema,
+  },
+  bordered: true,
+  canResize: false,
+  isTreeTable: true,
+  useSearchForm: true,
+  pagination: false,
+  actionColumn: {
+    width: 160,
+    title: '操作',
+    dataIndex: 'action',
+    fixed: 'right',
+  },
+})
+
+async function handleDelete(id: string) {
+  try {
+    await deleteConsumer(id)
+    useMessage().createMessage.success('删除成功')
+    reload()
+  }
+  catch {}
+}
+</script>
+
+<template>
+  <div>
+    <BasicTable :api="async () => ([] as Consumer[])" @register="register">
+      <template #tableTitle>
+        <a-button type="primary" @click="openModal()">
+          <PlusOutlined />
+          创建
+        </a-button>
+      </template>
+
+      <template #bodyCell="{ column, record }">
+        <template v-if="column.key === 'action'">
+          <TableAction
+            :actions="[
+              {
+                icon: 'i-ant-design:edit-outlined',
+                label: '修改',
+                onClick: () => openModal(true, record.id),
+              },
+              {
+                icon: 'i-ant-design:delete-outlined',
+                danger: true,
+                label: '删除',
+                popConfirm: {
+                  title: '确定要删除数据吗?',
+                  placement: 'left',
+                  confirm: () => handleDelete(record.id),
+                },
+              },
+            ]"
+          />
+        </template>
+      </template>
+    </BasicTable>
+
+    <ConsumerFormModal @register="registerModal" @success="reload" />
+  </div>
+</template>