Browse Source

feat(tenant): 实现租户及租户套餐管理功能

- 新增租户API接口,包括分页查询、新增、编辑及删除功能
- 新增租户套餐API接口,支持分页查询、新增、编辑、菜单权限分配及删除
- 实现租户列表页面,支持租户信息展示、搜索、编辑、新增及删除操作
- 实现租户套餐列表页面,支持套餐信息展示、编辑、新增、菜单权限分配及删除
- 租户编辑表单包括基本信息、管理员信息及套餐绑定等字段配置
- 套餐编辑支持菜单权限树形选择功能,提升权限管理便捷性
- 优化页面交互体验,添加加载状态及操作反馈提示
zouzexu 1 day ago
parent
commit
d0b1b37b9d

+ 92 - 0
src/service/api/tenant/index.ts

@@ -0,0 +1,92 @@
+import { request } from '../../request';
+
+// ============ 租户管理 ============
+
+/** 获取租户分页列表 */
+export function fetchGetTenantList(params?: { keywords?: string; pageNum: number; pageSize: number }) {
+  return request<any>({
+    url: '/smqjh-system/api/v1/tenants/page',
+    method: 'get',
+    params
+  });
+}
+
+/** 新增租户 */
+export function fetchAddTenant(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/tenants',
+    method: 'post',
+    data
+  });
+}
+
+/** 修改租户 */
+export function fetchEditTenant(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/tenants/${data.id}`,
+    method: 'put',
+    data
+  });
+}
+
+/** 删除租户 */
+export function fetchDeleteTenant(ids: string) {
+  return request({
+    url: `/smqjh-system/api/v1/tenants/${ids}`,
+    method: 'delete'
+  });
+}
+
+// ============ 租户套餐 ============
+
+/** 获取租户套餐分页列表 */
+export function fetchGetTenantPackageList(params?: { pageNum: number; pageSize: number }) {
+  return request<any>({
+    url: '/smqjh-system/api/v1/tenant-packages/page',
+    method: 'get',
+    params
+  });
+}
+
+/** 新增租户套餐 */
+export function fetchAddTenantPackage(data: any) {
+  return request({
+    url: '/smqjh-system/api/v1/tenant-packages',
+    method: 'post',
+    data
+  });
+}
+
+/** 修改租户套餐 */
+export function fetchEditTenantPackage(data: any) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${data.id}`,
+    method: 'put',
+    data
+  });
+}
+
+/** 获取套餐关联的菜单列表 */
+export function fetchGetTenantPackageMenus(packageId: number) {
+  return request<number[]>({
+    url: `/smqjh-system/api/v1/tenant-packages/${packageId}/menus`,
+    method: 'get'
+  });
+}
+
+/** 为套餐分配菜单权限 */
+export function fetchUpdateTenantPackageMenus(packageId: number, data: number[]) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${packageId}/menus`,
+    method: 'put',
+    data
+  });
+}
+
+/** 删除租户套餐 */
+export function fetchDeleteTenantPackage(ids: string) {
+  return request({
+    url: `/smqjh-system/api/v1/tenant-packages/${ids}`,
+    method: 'delete'
+  });
+}

+ 183 - 0
src/views/tenant/tenant-package/index.vue

@@ -0,0 +1,183 @@
+<script setup lang="tsx">
+import { ref } from 'vue';
+import { NButton, NInput, NPopconfirm, NSwitch, NTree } from 'naive-ui';
+import {
+  fetchAddTenantPackage,
+  fetchDeleteTenantPackage,
+  fetchEditTenantPackage,
+  fetchGetTenantPackageList,
+  fetchGetTenantPackageMenus,
+  fetchUpdateTenantPackageMenus
+} from '@/service/api/tenant';
+import { fetchGetMenuTree } from '@/service/api';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { $t } from '@/locales';
+import { useModal } from '@/components/zt/Modal/hooks/useModal';
+
+const columns: NaiveUI.TableColumn<any>[] = [
+  {
+    type: 'selection',
+    align: 'center',
+    width: 48
+  },
+  {
+    key: 'index',
+    title: $t('common.index'),
+    align: 'center',
+    width: 64,
+    render: (_, index) => index + 1
+  },
+  {
+    key: 'packageName',
+    title: '套餐名称',
+    align: 'center',
+    minWidth: 200
+  },
+  {
+    key: 'status',
+    title: '状态',
+    align: 'center',
+    width: 120,
+    render: row => {
+      return <NSwitch value={row.status === 1} />;
+    }
+  },
+  {
+    key: 'remark',
+    title: '备注',
+    align: 'center',
+    minWidth: 200
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '租户套餐列表',
+    showAddButton: true,
+    scrollX: 800
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteTenantPackage([row.id].join(','));
+  refresh();
+}
+
+// 新增/编辑套餐 - 带菜单权限树
+const menuData = ref<any[]>([]);
+const checkedKeys = ref<number[]>([]);
+const currentEditId = ref<number>();
+const packageName = ref('');
+const packageRemark = ref('');
+
+const [registerModalMenu, { openModal: openMenuModal, closeModal: closeMenuModal, setSubLoading }] = useModal({
+  title: '新增租户套餐',
+  height: 600
+});
+
+async function handleSubmitWithMenu() {
+  // 提交新增/编辑套餐并分配菜单
+  if (!packageName.value) {
+    window.$message?.error('请输入套餐名称');
+    return;
+  }
+  setSubLoading(true);
+  try {
+    const formData: any = {
+      packageName: packageName.value,
+      remark: packageRemark.value,
+      status: 1
+    };
+    if (currentEditId.value) {
+      formData.id = currentEditId.value;
+      await fetchEditTenantPackage(formData);
+      await fetchUpdateTenantPackageMenus(currentEditId.value, checkedKeys.value);
+    } else {
+      const res = await fetchAddTenantPackage(formData);
+      // 如果返回了id,分配菜单
+      if (res?.data?.id) {
+        await fetchUpdateTenantPackageMenus(res.data.id, checkedKeys.value);
+      }
+    }
+    closeMenuModal();
+    refresh();
+  } finally {
+    setSubLoading(false);
+  }
+}
+
+async function handleOpenAdd() {
+  currentEditId.value = undefined;
+  packageName.value = '';
+  packageRemark.value = '';
+  checkedKeys.value = [];
+  openMenuModal();
+  const { data } = await fetchGetMenuTree();
+  menuData.value = data as any[];
+}
+
+async function edit(row: Recordable) {
+  currentEditId.value = row.id;
+  packageName.value = row.packageName || '';
+  packageRemark.value = row.remark || '';
+  openMenuModal();
+  const { data: treeData } = await fetchGetMenuTree();
+  menuData.value = treeData as any[];
+  const { data: menuIds } = await fetchGetTenantPackageMenus(row.id);
+  checkedKeys.value = menuIds || [];
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns as any" :api="fetchGetTenantPackageList" @register="registerTable" @add="handleOpenAdd">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModal @register="registerModalMenu" @ok="handleSubmitWithMenu">
+      <div class="p-4">
+        <div class="mb-4">
+          <div class="mb-2 font-bold">
+            套餐名称
+            <span class="text-red-500">*</span>
+          </div>
+          <NInput v-model:value="packageName" placeholder="请输入套餐名称" />
+        </div>
+        <div class="mb-4">
+          <div class="mb-2 font-bold">菜单权限</div>
+          <NTree
+            v-model:checked-keys="checkedKeys"
+            :data="menuData"
+            key-field="value"
+            checkable
+            cascade
+            default-expand-all
+            block-line
+          />
+        </div>
+        <div class="mb-4">
+          <div class="mb-2 font-bold">备注</div>
+          <NInput v-model:value="packageRemark" type="textarea" :rows="3" placeholder="请输入备注" />
+        </div>
+      </div>
+    </BasicModal>
+  </LayoutTable>
+</template>
+
+<style scoped></style>

+ 284 - 0
src/views/tenant/tenant/index.vue

@@ -0,0 +1,284 @@
+<script setup lang="tsx">
+import { NButton, NPopconfirm, NTag } from 'naive-ui';
+import {
+  fetchAddTenant,
+  fetchDeleteTenant,
+  fetchEditTenant,
+  fetchGetTenantList,
+  fetchGetTenantPackageList
+} from '@/service/api/tenant';
+import { useTable } from '@/components/zt/Table/hooks/useTable';
+import { $t } from '@/locales';
+import { useModalFrom } from '@/components/zt/ModalForm/hooks/useModalForm';
+
+const columns: NaiveUI.TableColumn<any>[] = [
+  {
+    type: 'selection',
+    align: 'center',
+    width: 48
+  },
+  {
+    key: 'index',
+    title: $t('common.index'),
+    align: 'center',
+    width: 64,
+    render: (_, index) => index + 1
+  },
+  {
+    key: 'tenantCode',
+    title: '租户编号',
+    align: 'center',
+    minWidth: 120
+  },
+  {
+    key: 'contactName',
+    title: '联系人',
+    align: 'center',
+    minWidth: 100
+  },
+  {
+    key: 'contactMobile',
+    title: '联系电话',
+    align: 'center',
+    minWidth: 130
+  },
+  {
+    key: 'tenantName',
+    title: '企业名称',
+    align: 'center',
+    minWidth: 150
+  },
+  {
+    key: 'expireTime',
+    title: '过期时间',
+    align: 'center',
+    minWidth: 170
+  },
+  {
+    key: 'status',
+    title: '租户状态',
+    align: 'center',
+    width: 100,
+    render: row => {
+      if (row.status === null || row.status === undefined) {
+        return null;
+      }
+      const tagMap: Record<number, NaiveUI.ThemeColor> = {
+        1: 'success',
+        0: 'error'
+      };
+      const labelMap: Record<number, string> = {
+        1: '正常',
+        0: '停用'
+      };
+      return <NTag type={tagMap[row.status]}>{labelMap[row.status]}</NTag>;
+    }
+  }
+];
+
+const [registerTable, { refresh, setTableLoading }] = useTable({
+  searchFormConfig: {
+    schemas: [
+      {
+        field: 'keywords',
+        label: '关键字',
+        component: 'NInput',
+        componentProps: {
+          placeholder: '请输入租户名称/编码/域名'
+        }
+      }
+    ],
+    inline: false,
+    size: 'small',
+    labelPlacement: 'left',
+    isFull: false
+  },
+  tableConfig: {
+    keyField: 'id',
+    title: '租户列表',
+    showAddButton: true,
+    scrollX: 1200
+  }
+});
+
+async function handleDelete(row: Recordable) {
+  setTableLoading(true);
+  await fetchDeleteTenant([row.id].join(','));
+  refresh();
+}
+
+const [registerModalForm, { openModal, closeModal, getFieldsValue, setFieldsValue, setModalLoading }] = useModalFrom({
+  modalConfig: {
+    title: '租户',
+    width: 800,
+    isShowHeaderText: true
+  },
+  formConfig: {
+    schemas: [
+      {
+        field: 'id',
+        label: '',
+        component: 'NInput',
+        show: false
+      },
+      // 基本信息
+      {
+        field: 'tenantName',
+        label: '企业名称',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入企业名称'
+        }
+      },
+      {
+        field: 'contactName',
+        label: '联系人',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入联系人'
+        }
+      },
+      {
+        field: 'contactMobile',
+        label: '联系电话',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入联系电话',
+          maxlength: 11
+        }
+      },
+      // 管理员信息
+      {
+        field: 'adminUsername',
+        label: '管理员账号',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入管理员账号'
+        }
+      },
+      {
+        field: 'adminPassword',
+        label: '管理员密码',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入管理员密码',
+          type: 'password',
+          showPasswordOn: 'click'
+        }
+      },
+      // 租户设置
+      {
+        field: 'packageId',
+        label: '租户套餐',
+        component: 'ApiSelect',
+        required: true,
+        componentProps: {
+          api: () => fetchGetTenantPackageList({ pageNum: 1, pageSize: 100 }),
+          labelFeild: 'packageName',
+          valueFeild: 'id',
+          resultFeild: 'data.list',
+          placeholder: '请选择租户套餐'
+        }
+      },
+      {
+        field: 'tenantCode',
+        label: '租户编号',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入租户编号'
+        }
+      },
+      {
+        field: 'expireTime',
+        label: '过期时间',
+        component: 'NDatePicker',
+        componentProps: {
+          type: 'datetime',
+          placeholder: '选择日期时间'
+        } as any
+      },
+      {
+        field: 'tenantDomain',
+        label: '绑定域名',
+        component: 'NInput',
+        required: true,
+        componentProps: {
+          placeholder: '请输入'
+        }
+      },
+      {
+        field: 'status',
+        label: '租户状态',
+        component: 'NRadioGroup',
+        required: true,
+        componentProps: {
+          options: [
+            {
+              label: '正常',
+              value: 1
+            },
+            {
+              label: '停用',
+              value: 0
+            }
+          ]
+        },
+        defaultValue: 1
+      }
+    ],
+    labelWidth: 120,
+    gridProps: {
+      cols: '1'
+    }
+  }
+});
+
+async function handleSubmit() {
+  const form = await getFieldsValue();
+  if (form.id) {
+    const { error } = await fetchEditTenant(form);
+    if (!error) {
+      closeModal();
+      refresh();
+    }
+  } else {
+    const { error } = await fetchAddTenant(form);
+    if (!error) {
+      closeModal();
+      refresh();
+    }
+  }
+}
+
+async function edit(row: Recordable) {
+  setModalLoading(true);
+  openModal(row);
+  setFieldsValue({ ...row });
+  setModalLoading(false);
+}
+</script>
+
+<template>
+  <LayoutTable>
+    <ZTable :columns="columns as any" :api="fetchGetTenantList" @register="registerTable" @add="openModal">
+      <template #op="{ row }">
+        <NButton size="small" ghost type="primary" @click="edit(row)">编辑</NButton>
+        <NPopconfirm @positive-click="handleDelete(row)">
+          <template #trigger>
+            <NButton size="small" type="error" ghost>删除</NButton>
+          </template>
+          确定删除吗?
+        </NPopconfirm>
+      </template>
+    </ZTable>
+    <BasicModelForm @register-modal-form="registerModalForm" @submit-form="handleSubmit"></BasicModelForm>
+  </LayoutTable>
+</template>
+
+<style scoped></style>