wsj
5 days ago b09eb09acd24d885bbd610fd96c2114b4711e99b
feat: 新增客户列表与角色管理页面

- 客户管理改为一级分组,新增客户列表页对接 GET /tenant/customers
- 用户与权限分组下新增角色管理页,对接角色列表 / 详情 / 启停接口
- 抽取 getAuthHeaders 统一注入 Bearer Token
- 更新 new-page skill 模板,移除页面标题说明区块
- 同步 README 与 CLAUDE.md 文档

Co-Authored-By: Claude Code <noreply@anthropic.com>
2 files deleted
7 files modified
9 files added
1153 ■■■■■ changed files
.claude/skills/new-page/SKILL.md 2 ●●●●● patch | view | raw | blame | history
.claude/skills/new-page/templates/non-table-page/index.tsx 7 ●●●●● patch | view | raw | blame | history
.claude/skills/new-page/templates/table-page/index.tsx 7 ●●●●● patch | view | raw | blame | history
CLAUDE.md 30 ●●●●● patch | view | raw | blame | history
README.md 7 ●●●●● patch | view | raw | blame | history
src/api/authHeaders.ts 7 ●●●●● patch | view | raw | blame | history
src/pages/business/customers/Customers.scss 57 ●●●●● patch | view | raw | blame | history
src/pages/business/customers/index.tsx 417 ●●●●● patch | view | raw | blame | history
src/pages/customers/list/columns.tsx 158 ●●●●● patch | view | raw | blame | history
src/pages/customers/list/index.tsx 43 ●●●●● patch | view | raw | blame | history
src/pages/customers/list/server.ts 12 ●●●●● patch | view | raw | blame | history
src/pages/customers/list/types.ts 78 ●●●●● patch | view | raw | blame | history
src/pages/dashboard/index.tsx 2 ●●●●● patch | view | raw | blame | history
src/pages/system/roles/columns.tsx 85 ●●●●● patch | view | raw | blame | history
src/pages/system/roles/index.tsx 126 ●●●●● patch | view | raw | blame | history
src/pages/system/roles/server.ts 34 ●●●●● patch | view | raw | blame | history
src/pages/system/roles/types.ts 45 ●●●●● patch | view | raw | blame | history
src/router/routeConfig.tsx 36 ●●●●● patch | view | raw | blame | history
.claude/skills/new-page/SKILL.md
@@ -90,6 +90,7 @@
- 提示与确认框使用 `AntdApp.useApp()` 提供的 `message` / `modal`,不要用静态方法。
- 使用 Ant Design 5 API,不使用已废弃属性(如 Card 用 `variant="borderless"`)。
- 页面只负责内容区域,外层布局由 `BasicLayout` 提供。
- 不要在页面内添加标题 / 说明区块(如 `.page-header`),菜单和面包屑已标明页面名称,直接展示内容即可。
- 页面级样式与页面同目录,使用 BEM 类名。
- 不添加权限相关代码:本项目已移除权限体系。
@@ -136,4 +137,5 @@
- [ ] 全部使用箭头函数
- [ ] 提示与确认框通过 `AntdApp.useApp()` 获取
- [ ] 路由已注册,菜单与面包屑可正常显示
- [ ] 页面没有重复的标题 / 说明区块(不使用 `.page-header`)
- [ ] `npm run typecheck` 通过
.claude/skills/new-page/templates/non-table-page/index.tsx
@@ -24,13 +24,6 @@
  return (
    <div className="xxx-page">
      <div className="page-header">
        <div>
          <h1 className="page-header__title">页面标题</h1>
          <p className="page-header__desc">页面说明。</p>
        </div>
      </div>
      <Card variant="borderless">
        <Form<XxxFormValues>
          form={form}
.claude/skills/new-page/templates/table-page/index.tsx
@@ -37,13 +37,6 @@
  return (
    <div className="xxx-page">
      <div className="page-header">
        <div>
          <h1 className="page-header__title">页面标题</h1>
          <p className="page-header__desc">页面说明。</p>
        </div>
      </div>
      <ProTable<XxxRecord, XxxQuery>
        rowKey="key"
        actionRef={actionRef}
CLAUDE.md
@@ -38,7 +38,8 @@
│   ├── login/
│   ├── dashboard/
│   ├── system/users/
│   ├── business/{orders,customers}/
│   ├── business/orders/
│   ├── customers/list/
│   └── error/NotFound.tsx
│   # 页面目录:index.tsx、columns.tsx、types.ts、server.ts、同名 .scss(按需)
├── router/
@@ -58,7 +59,7 @@
`src/router/routeConfig.tsx` 是**唯一数据源**。新增页面只需在这里配置 `path`、`title`、`icon`、`element`,侧边栏、面包屑会自动读取。
- 支持 2-3 级嵌套,例如:`系统管理 → 用户管理`。
- 支持 2-3 级嵌套,例如:`用户与权限 → 用户管理`。
- `src/router/index.tsx` 把菜单配置转换成 React Router 的嵌套路由。
- 分组路由(无 `element`)通过 `<Outlet />` 渲染子页面,默认跳转到第一个子页面。
- 未匹配路由显示 404,普通页面组件位于 `BasicLayout` 内。
@@ -202,6 +203,31 @@
## 七、接口与状态约定
- **每个接口都必须为其请求(request)与响应(response)定义 `interface`**,并在 `server.ts` 中通过 `request<TReq, TRes>` 显式标注泛型;禁止使用 `any` 或省略类型。
  - 类型统一定义在页面目录的 `types.ts` 中(跨页面复用的放在 `src/api/` 对应业务域文件)。
  - 命名约定:`<业务>Request` / `<业务>Response`,例如 `TenantRoleListRequest`、`TenantRoleListResponse`。
  - 请求体 / 查询参数建模为 `XxxRequest`;响应建模为 `XxxResponse`,对应接口返回 `data` 的结构(若后端是 `Result<T>`,`Response` 指 `data` 部分)。
  - 无请求参数时也应显式定义,例如 `type TenantRoleListRequest = Record<string, never>`,不要留空或使用 `undefined`。
```ts
// types.ts
export interface TenantRoleToggleStatusRequest {
  id: number;
}
export interface TenantRoleToggleStatusResponse {
  id: number;
  status: RoleStatus;
}
// server.ts
export const toggleRoleStatus = ({ id }: TenantRoleToggleStatusRequest) =>
  request<TenantRoleToggleStatusRequest, TenantRoleToggleStatusResponse>({
    url: `/tenant/roles/${id}/toggle-status`,
    method: 'PUT',
    headers: getAuthHeaders(),
  });
```
- 页面级接口调用统一放在页面目录下的 `server.ts`,页面组件通过它调用接口,不要在组件内直接写请求实现。
- 跨页面复用的接口可放在 `src/api/` 下,按业务域拆分文件。
- 网络请求统一使用 `src/api/request.ts` 的 `request`(基于 axios),不要在页面或 `server.ts` 中直接使用 axios。
README.md
@@ -51,8 +51,8 @@
```tsx
{
  path: '/system',
  title: '系统管理',
  icon: <SettingOutlined />,
  title: '用户与权限',
  icon: <SafetyCertificateOutlined />,
  children: [
    {
      path: '/system/users',
@@ -101,8 +101,9 @@
| -------- | ----------------------- | ---------------------------------- |
| 工作台   | `/dashboard`            | 数据卡片、快捷入口、动态、最近订单 |
| 用户管理 | `/system/users`         | 搜索、新增 / 编辑 / 删除用户       |
| 角色管理 | `/system/roles`         | 角色列表、详情、启用 / 停用        |
| 订单管理 | `/business/orders`      | 筛选、处理订单、导出               |
| 客户管理 | `/business/customers`   | 筛选、新增 / 编辑客户              |
| 客户列表 | `/customers/list`       | 接口数据,按归属销售员筛选         |
| 404      | 任意未知地址            | 页面不存在提示                     |
## 目录结构
src/api/authHeaders.ts
New file
@@ -0,0 +1,7 @@
import { useAuthStore } from '@/store/auth';
/** 接口要求 Bearer JWT,从登录态读取 token 生成请求头。 */
export const getAuthHeaders = () => {
  const token = useAuthStore.getState().token;
  return token ? { Authorization: `Bearer ${token}` } : undefined;
};
src/pages/business/customers/Customers.scss
File was deleted
src/pages/business/customers/index.tsx
File was deleted
src/pages/customers/list/columns.tsx
New file
@@ -0,0 +1,158 @@
import { Tag } from 'antd';
import type { ProColumns } from '@ant-design/pro-components';
import type {
  CustomerLevel,
  CustomerStage,
  CustomerType,
  CustomerVO,
} from './types';
const customerTypeMeta: Record<CustomerType, { text: string; color: string }> = {
  ENTERPRISE: { text: '企业', color: 'blue' },
  PERSONAL: { text: '个人', color: 'default' },
};
const levelMeta: Record<CustomerLevel, { text: string; color: string }> = {
  VIP: { text: 'VIP', color: 'purple' },
  A: { text: 'A 类', color: 'gold' },
  B: { text: 'B 类', color: 'blue' },
  C: { text: 'C 类', color: 'default' },
};
const stageMeta: Record<CustomerStage, { text: string; color: string }> = {
  NEW_LEAD: { text: '新线索', color: 'default' },
  PENDING_ASSIGN: { text: '待分配', color: 'orange' },
  CONTACTING: { text: '沟通中', color: 'processing' },
  NEEDS_COMM: { text: '需求沟通', color: 'cyan' },
  QUOTING: { text: '报价中', color: 'blue' },
  NEGOTIATING: { text: '谈判中', color: 'gold' },
  DEAL: { text: '已成交', color: 'success' },
  LOST: { text: '已流失', color: 'error' },
};
const renderTags = (tags?: string) => {
  const items = tags?.split(/[,,]/).map((tag) => tag.trim()).filter(Boolean) ?? [];
  if (!items.length) return '-';
  return (
    <>
      {items.map((tag) => (
        <Tag key={tag} bordered={false}>
          {tag}
        </Tag>
      ))}
    </>
  );
};
export const customerColumns: ProColumns<CustomerVO>[] = [
  {
    title: '客户名称',
    dataIndex: 'name',
    width: 220,
    ellipsis: true,
    copyable: true,
    search: false,
  },
  {
    title: '客户类型',
    dataIndex: 'customerType',
    width: 100,
    search: false,
    render: (_value, record) => {
      const meta = record.customerType ? customerTypeMeta[record.customerType] : null;
      return meta ? (
        <Tag color={meta.color} bordered={false}>
          {meta.text}
        </Tag>
      ) : (
        '-'
      );
    },
  },
  {
    title: '所属行业',
    dataIndex: 'industry',
    width: 130,
    ellipsis: true,
    search: false,
    render: (_value, record) => record.industry ?? '-',
  },
  {
    title: '所在地区',
    dataIndex: 'region',
    width: 120,
    ellipsis: true,
    search: false,
    render: (_value, record) => record.region ?? '-',
  },
  {
    title: '联系人',
    dataIndex: 'contactName',
    width: 110,
    search: false,
    render: (_value, record) => record.contactName ?? '-',
  },
  {
    title: '联系电话',
    dataIndex: 'contactPhone',
    width: 140,
    search: false,
    render: (_value, record) => record.contactPhone ?? '-',
  },
  {
    title: '客户等级',
    dataIndex: 'level',
    width: 100,
    search: false,
    render: (_value, record) => {
      const meta = record.level ? levelMeta[record.level] : null;
      return meta ? (
        <Tag color={meta.color} bordered={false}>
          {meta.text}
        </Tag>
      ) : (
        '-'
      );
    },
  },
  {
    title: '客户阶段',
    dataIndex: 'stage',
    width: 110,
    search: false,
    render: (_value, record) => {
      const meta = record.stage ? stageMeta[record.stage] : null;
      return meta ? (
        <Tag color={meta.color} bordered={false}>
          {meta.text}
        </Tag>
      ) : (
        '-'
      );
    },
  },
  {
    title: '归属销售员',
    dataIndex: 'ownerId',
    width: 130,
    valueType: 'digit',
    tooltip: '按归属销售员 ID 筛选,不填表示全部',
    fieldProps: { placeholder: '销售员 ID', min: 1, precision: 0 },
    render: (_value, record) => (record.ownerId ? `#${record.ownerId}` : '-'),
  },
  {
    title: '最近跟进',
    dataIndex: 'lastFollowAt',
    width: 170,
    valueType: 'dateTime',
    search: false,
  },
  {
    title: '标签',
    dataIndex: 'tags',
    width: 180,
    search: false,
    render: (_value, record) => renderTags(record.tags),
  },
];
src/pages/customers/list/index.tsx
New file
@@ -0,0 +1,43 @@
import { useMemo } from 'react';
import { ProTable } from '@ant-design/pro-components';
import { customerColumns } from './columns';
import { getCustomerList } from './server';
import type { CustomerListQuery, CustomerVO } from './types';
const CustomerList = () => {
  const columns = useMemo(() => customerColumns, []);
  return (
    <div className="customer-list-page">
      <ProTable<CustomerVO, CustomerListQuery>
        rowKey="id"
        columns={columns}
        cardProps={{ className: 'table-card' }}
        search={{ labelWidth: 'auto', defaultCollapsed: false }}
        scroll={{ x: 1510 }}
        options={false}
        pagination={{
          defaultPageSize: 10,
          showSizeChanger: true,
          hideOnSinglePage: false,
          showTotal: (total) => `共 ${total} 条`,
        }}
        request={async (params) => {
          const result = await getCustomerList({
            page: params.current,
            size: params.pageSize,
            ownerId: params.ownerId,
          });
          return {
            data: result.records ?? [],
            total: result.total ?? 0,
            success: true,
          };
        }}
      />
    </div>
  );
};
export default CustomerList;
src/pages/customers/list/server.ts
New file
@@ -0,0 +1,12 @@
import { getAuthHeaders } from '@/api/authHeaders';
import { request } from '@/api/request';
import type { TenantCustomerPageQuery, TenantCustomerPageResult } from './types';
/** 查询本租户客户列表(只读)。 */
export const getCustomerList = (params: TenantCustomerPageQuery) =>
  request<TenantCustomerPageQuery, TenantCustomerPageResult>({
    url: '/tenant/customers',
    method: 'GET',
    params,
    headers: getAuthHeaders(),
  });
src/pages/customers/list/types.ts
New file
@@ -0,0 +1,78 @@
/** 客户类型。 */
export type CustomerType = 'ENTERPRISE' | 'PERSONAL';
/** 客户来源。 */
export type CustomerSource =
  | 'MINI_PROGRAM'
  | 'SALES_INPUT'
  | 'EXHIBITION'
  | 'REFERRAL'
  | 'WEBSITE';
/** 客户等级。 */
export type CustomerLevel = 'VIP' | 'A' | 'B' | 'C';
/** 客户阶段。 */
export type CustomerStage =
  | 'NEW_LEAD'
  | 'PENDING_ASSIGN'
  | 'CONTACTING'
  | 'NEEDS_COMM'
  | 'QUOTING'
  | 'NEGOTIATING'
  | 'DEAL'
  | 'LOST';
/** 客户列表请求参数(TenantCustomerPageQuery)。 */
export interface TenantCustomerPageQuery {
  /** 页码。 */
  page?: number;
  /** 每页条数。 */
  size?: number;
  /** 归属销售员 ID,不填表示全部。 */
  ownerId?: number;
}
/** 客户列表项(CustomerVO)。 */
export interface CustomerVO {
  id: number;
  name: string;
  customerType?: CustomerType;
  industry?: string;
  region?: string;
  contactName?: string;
  contactPhone?: string;
  contactPosition?: string;
  contactTel?: string;
  email?: string;
  address?: string;
  demand?: string;
  source?: CustomerSource;
  level?: CustomerLevel;
  tags?: string;
  orgId?: number;
  ownerId?: number;
  stage?: CustomerStage;
  lastFollowAt?: string;
  nextFollowAt?: string;
  dealAt?: string;
  lostAt?: string;
  lostReason?: string;
}
/** 通用分页响应(PageResult)。 */
export interface PageResult<T> {
  page: number;
  size: number;
  total: number;
  totalPages: number;
  records: T[];
}
/** 客户列表响应数据(ResultPageResultCustomerVO 的 data)。 */
export type TenantCustomerPageResult = PageResult<CustomerVO>;
/** ProTable 搜索表单参数,字段与请求参数中可筛选的字段保持一致。 */
export interface CustomerListQuery {
  ownerId?: number;
}
src/pages/dashboard/index.tsx
@@ -100,7 +100,7 @@
  {
    title: '客户管理',
    description: '维护客户资料',
    path: '/business/customers',
    path: '/customers',
    icon: <ContactsOutlined />,
    color: '#d97706',
    background: '#fef3c7',
src/pages/system/roles/columns.tsx
New file
@@ -0,0 +1,85 @@
import { Button, Space, Tag } from 'antd';
import type { ProColumns } from '@ant-design/pro-components';
import type { RoleStatus, RoleType, RoleVO } from './types';
export const roleTypeMeta: Record<RoleType, { text: string; color: string }> = {
  PRESET: { text: '预置', color: 'blue' },
  CUSTOM: { text: '自建', color: 'cyan' },
};
export const statusMeta: Record<RoleStatus, { text: string; color: string }> = {
  ENABLED: { text: '启用', color: 'green' },
  DISABLED: { text: '禁用', color: 'default' },
  DELETED: { text: '已删除', color: 'red' },
};
export const createRoleColumns = (options: {
  onDetail: (record: RoleVO) => void;
  onToggle: (record: RoleVO) => void;
}): ProColumns<RoleVO>[] => [
  {
    title: '角色名称',
    dataIndex: 'roleName',
    width: 180,
    ellipsis: true,
    fieldProps: { placeholder: '名称 / 编码' },
  },
  {
    title: '角色编码',
    dataIndex: 'roleCode',
    width: 200,
    ellipsis: true,
    search: false,
    render: (_value, record) => <Tag bordered={false}>{record.roleCode}</Tag>,
  },
  {
    title: '描述',
    dataIndex: 'description',
    ellipsis: true,
    search: false,
    render: (_value, record) => record.description || '-',
  },
  {
    title: '状态',
    dataIndex: 'status',
    width: 110,
    valueType: 'select',
    valueEnum: {
      ENABLED: { text: '启用' },
      DISABLED: { text: '禁用' },
      DELETED: { text: '已删除' },
    },
    render: (_value, record) => (
      <Tag color={statusMeta[record.status].color} bordered={false}>
        {statusMeta[record.status].text}
      </Tag>
    ),
  },
  {
    title: '操作',
    key: 'action',
    valueType: 'option',
    width: 140,
    render: (_value, record) => {
      const isEnabled = record.status === 'ENABLED';
      return (
        <Space size={0}>
          <Button type="link" size="small" onClick={() => options.onDetail(record)}>
            详情
          </Button>
          {record.roleType === 'CUSTOM' && record.status !== 'DELETED' && (
            <Button
              type="link"
              size="small"
              danger={isEnabled}
              onClick={() => options.onToggle(record)}
            >
              {isEnabled ? '禁用' : '启用'}
            </Button>
          )}
        </Space>
      );
    },
  },
];
src/pages/system/roles/index.tsx
New file
@@ -0,0 +1,126 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { ProTable } from '@ant-design/pro-components';
import type { ActionType } from '@ant-design/pro-components';
import { App as AntdApp, Descriptions, Drawer, Tag } from 'antd';
import { createRoleColumns, roleTypeMeta, statusMeta } from './columns';
import { getRoleDetail, getRoleList, toggleRoleStatus } from './server';
import type { RoleListQuery, RoleVO } from './types';
const Roles = () => {
  const actionRef = useRef<ActionType>();
  const { message, modal } = AntdApp.useApp();
  const [detail, setDetail] = useState<RoleVO | null>(null);
  const [detailOpen, setDetailOpen] = useState(false);
  const [detailLoading, setDetailLoading] = useState(false);
  const handleDetail = useCallback(async (record: RoleVO) => {
    setDetailOpen(true);
    setDetailLoading(true);
    try {
      const data = await getRoleDetail({ id: record.id });
      setDetail(data);
    } catch {
      // 错误已由 request 统一提示
      setDetailOpen(false);
    } finally {
      setDetailLoading(false);
    }
  }, []);
  const handleToggle = useCallback(
    (record: RoleVO) => {
      const nextAction = record.status === 'ENABLED' ? '禁用' : '启用';
      modal.confirm({
        title: `${nextAction}角色`,
        content: `确定要${nextAction}角色「${record.roleName}」吗?`,
        okText: nextAction,
        cancelText: '取消',
        okButtonProps: { danger: record.status === 'ENABLED' },
        onOk: async () => {
          await toggleRoleStatus({ id: record.id });
          message.success(`角色已${nextAction}`);
          actionRef.current?.reload();
        },
      });
    },
    [message, modal],
  );
  const columns = useMemo(
    () => createRoleColumns({ onDetail: handleDetail, onToggle: handleToggle }),
    [handleDetail, handleToggle],
  );
  return (
    <div className="role-page">
      <ProTable<RoleVO, RoleListQuery>
        rowKey="id"
        actionRef={actionRef}
        columns={columns}
        cardProps={{ className: 'table-card' }}
        search={{ labelWidth: 'auto', defaultCollapsed: false }}
        scroll={{ x: 960 }}
        options={false}
        pagination={{
          defaultPageSize: 10,
          showSizeChanger: true,
          hideOnSinglePage: false,
          showTotal: (total) => `共 ${total} 条`,
        }}
        request={async (params) => {
          const { current = 1, pageSize = 10, roleName, status } = params;
          const keyword = roleName?.trim().toLowerCase();
          const list = await getRoleList();
          const filtered = list.filter((role) => {
            const matchesKeyword =
              !keyword ||
              role.roleName.toLowerCase().includes(keyword) ||
              role.roleCode.toLowerCase().includes(keyword);
            const matchesStatus = !status || role.status === status;
            return matchesKeyword && matchesStatus;
          });
          const start = (current - 1) * pageSize;
          return {
            data: filtered.slice(start, start + pageSize),
            total: filtered.length,
            success: true,
          };
        }}
      />
      <Drawer
        title="角色详情"
        width={460}
        open={detailOpen}
        loading={detailLoading}
        onClose={() => {
          setDetailOpen(false);
          setDetail(null);
        }}
      >
        {detail && (
          <Descriptions column={1} bordered size="small">
            <Descriptions.Item label="角色名称">{detail.roleName}</Descriptions.Item>
            <Descriptions.Item label="角色编码">{detail.roleCode}</Descriptions.Item>
            <Descriptions.Item label="角色类型">
              <Tag color={roleTypeMeta[detail.roleType].color} bordered={false}>
                {roleTypeMeta[detail.roleType].text}
              </Tag>
            </Descriptions.Item>
            <Descriptions.Item label="状态">
              <Tag color={statusMeta[detail.status].color} bordered={false}>
                {statusMeta[detail.status].text}
              </Tag>
            </Descriptions.Item>
            <Descriptions.Item label="描述">{detail.description || '-'}</Descriptions.Item>
          </Descriptions>
        )}
      </Drawer>
    </div>
  );
};
export default Roles;
src/pages/system/roles/server.ts
New file
@@ -0,0 +1,34 @@
import { getAuthHeaders } from '@/api/authHeaders';
import { request } from '@/api/request';
import type {
  TenantRoleDetailRequest,
  TenantRoleDetailResponse,
  TenantRoleListRequest,
  TenantRoleListResponse,
  TenantRoleToggleStatusRequest,
  TenantRoleToggleStatusResponse,
} from './types';
/** 当前租户角色列表(含预置角色)。 */
export const getRoleList = () =>
  request<TenantRoleListRequest, TenantRoleListResponse>({
    url: '/tenant/roles',
    method: 'GET',
    headers: getAuthHeaders(),
  });
/** 角色详情。 */
export const getRoleDetail = ({ id }: TenantRoleDetailRequest) =>
  request<TenantRoleDetailRequest, TenantRoleDetailResponse>({
    url: `/tenant/roles/${id}`,
    method: 'GET',
    headers: getAuthHeaders(),
  });
/** 启用 / 停用自建角色。 */
export const toggleRoleStatus = ({ id }: TenantRoleToggleStatusRequest) =>
  request<TenantRoleToggleStatusRequest, TenantRoleToggleStatusResponse>({
    url: `/tenant/roles/${id}/toggle-status`,
    method: 'PUT',
    headers: getAuthHeaders(),
  });
src/pages/system/roles/types.ts
New file
@@ -0,0 +1,45 @@
/** 角色类型。 */
export type RoleType = 'PRESET' | 'CUSTOM';
/** 角色状态。 */
export type RoleStatus = 'ENABLED' | 'DISABLED' | 'DELETED';
/** 角色响应体(RoleVO)。 */
export interface RoleVO {
  id: number;
  roleName: string;
  roleCode: string;
  description?: string;
  roleType: RoleType;
  status: RoleStatus;
}
/** GET /tenant/roles 请求参数:该接口未定义查询参数。 */
export type TenantRoleListRequest = Record<string, never>;
/** GET /tenant/roles 响应数据(ResultListRoleVO 的 data)。 */
export type TenantRoleListResponse = RoleVO[];
/** GET /tenant/roles/{id} 请求参数。 */
export interface TenantRoleDetailRequest {
  /** 角色 ID(路径参数)。 */
  id: number;
}
/** GET /tenant/roles/{id} 响应数据(ResultRoleVO 的 data)。 */
export type TenantRoleDetailResponse = RoleVO;
/** PUT /tenant/roles/{id}/toggle-status 请求参数。 */
export interface TenantRoleToggleStatusRequest {
  /** 角色 ID(路径参数)。 */
  id: number;
}
/** PUT /tenant/roles/{id}/toggle-status 响应数据(ResultVoid 的 data)。 */
export type TenantRoleToggleStatusResponse = void;
/** ProTable 筛选参数。列表接口没有查询参数,这些字段仅用于前端本地筛选。 */
export interface RoleListQuery {
  roleName?: string;
  status?: RoleStatus;
}
src/router/routeConfig.tsx
@@ -4,12 +4,15 @@
  ContactsOutlined,
  DashboardOutlined,
  ProfileOutlined,
  SettingOutlined,
  SafetyCertificateOutlined,
  SafetyOutlined,
  TeamOutlined,
  UnorderedListOutlined,
} from '@ant-design/icons';
import Customers from '@/pages/business/customers';
import Orders from '@/pages/business/orders';
import CustomerList from '@/pages/customers/list';
import Dashboard from '@/pages/dashboard';
import Roles from '@/pages/system/roles';
import Users from '@/pages/system/users';
export interface AppRouteItem {
@@ -35,15 +38,34 @@
    element: <Dashboard />,
  },
  {
    path: '/customers',
    title: '客户管理',
    icon: <ContactsOutlined />,
    children: [
      {
        path: '/customers/list',
        title: '客户列表',
        icon: <UnorderedListOutlined />,
        element: <CustomerList />,
      },
    ],
  },
  {
    path: '/system',
    title: '系统管理',
    icon: <SettingOutlined />,
    title: '用户与权限',
    icon: <SafetyCertificateOutlined />,
    children: [
      {
        path: '/system/users',
        title: '用户管理',
        icon: <TeamOutlined />,
        element: <Users />,
      },
      {
        path: '/system/roles',
        title: '角色管理',
        icon: <SafetyOutlined />,
        element: <Roles />,
      },
    ],
  },
@@ -57,12 +79,6 @@
        title: '订单管理',
        icon: <ProfileOutlined />,
        element: <Orders />,
      },
      {
        path: '/business/customers',
        title: '客户管理',
        icon: <ContactsOutlined />,
        element: <Customers />,
      },
    ],
  },