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
| | |
| | | - 提示与确认框使用 `AntdApp.useApp()` 提供的 `message` / `modal`,不要用静态方法。 |
| | | - 使用 Ant Design 5 API,不使用已废弃属性(如 Card 用 `variant="borderless"`)。 |
| | | - 页面只负责内容区域,外层布局由 `BasicLayout` 提供。 |
| | | - 不要在页面内添加标题 / 说明区块(如 `.page-header`),菜单和面包屑已标明页面名称,直接展示内容即可。 |
| | | - 页面级样式与页面同目录,使用 BEM 类名。 |
| | | - 不添加权限相关代码:本项目已移除权限体系。 |
| | | |
| | |
| | | - [ ] 全部使用箭头函数 |
| | | - [ ] 提示与确认框通过 `AntdApp.useApp()` 获取 |
| | | - [ ] 路由已注册,菜单与面包屑可正常显示 |
| | | - [ ] 页面没有重复的标题 / 说明区块(不使用 `.page-header`) |
| | | - [ ] `npm run typecheck` 通过 |
| | |
| | | |
| | | 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} |
| | |
| | | |
| | | 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} |
| | |
| | | │ ├── login/ |
| | | │ ├── dashboard/ |
| | | │ ├── system/users/ |
| | | │ ├── business/{orders,customers}/ |
| | | │ ├── business/orders/ |
| | | │ ├── customers/list/ |
| | | │ └── error/NotFound.tsx |
| | | │ # 页面目录:index.tsx、columns.tsx、types.ts、server.ts、同名 .scss(按需) |
| | | ├── router/ |
| | |
| | | |
| | | `src/router/routeConfig.tsx` 是**唯一数据源**。新增页面只需在这里配置 `path`、`title`、`icon`、`element`,侧边栏、面包屑会自动读取。 |
| | | |
| | | - 支持 2-3 级嵌套,例如:`系统管理 → 用户管理`。 |
| | | - 支持 2-3 级嵌套,例如:`用户与权限 → 用户管理`。 |
| | | - `src/router/index.tsx` 把菜单配置转换成 React Router 的嵌套路由。 |
| | | - 分组路由(无 `element`)通过 `<Outlet />` 渲染子页面,默认跳转到第一个子页面。 |
| | | - 未匹配路由显示 404,普通页面组件位于 `BasicLayout` 内。 |
| | |
| | | |
| | | ## 七、接口与状态约定 |
| | | |
| | | - **每个接口都必须为其请求(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。 |
| | |
| | | ```tsx |
| | | { |
| | | path: '/system', |
| | | title: '系统管理', |
| | | icon: <SettingOutlined />, |
| | | title: '用户与权限', |
| | | icon: <SafetyCertificateOutlined />, |
| | | children: [ |
| | | { |
| | | path: '/system/users', |
| | |
| | | | -------- | ----------------------- | ---------------------------------- | |
| | | | 工作台 | `/dashboard` | 数据卡片、快捷入口、动态、最近订单 | |
| | | | 用户管理 | `/system/users` | 搜索、新增 / 编辑 / 删除用户 | |
| | | | 角色管理 | `/system/roles` | 角色列表、详情、启用 / 停用 | |
| | | | 订单管理 | `/business/orders` | 筛选、处理订单、导出 | |
| | | | 客户管理 | `/business/customers` | 筛选、新增 / 编辑客户 | |
| | | | 客户列表 | `/customers/list` | 接口数据,按归属销售员筛选 | |
| | | | 404 | 任意未知地址 | 页面不存在提示 | |
| | | |
| | | ## 目录结构 |
| New file |
| | |
| | | import { useAuthStore } from '@/store/auth'; |
| | | |
| | | /** 接口要求 Bearer JWT,从登录态读取 token 生成请求头。 */ |
| | | export const getAuthHeaders = () => { |
| | | const token = useAuthStore.getState().token; |
| | | return token ? { Authorization: `Bearer ${token}` } : undefined; |
| | | }; |
| New file |
| | |
| | | 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), |
| | | }, |
| | | ]; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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(), |
| | | }); |
| New file |
| | |
| | | /** 客户类型。 */ |
| | | 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; |
| | | } |
| | |
| | | { |
| | | title: '客户管理', |
| | | description: '维护客户资料', |
| | | path: '/business/customers', |
| | | path: '/customers', |
| | | icon: <ContactsOutlined />, |
| | | color: '#d97706', |
| | | background: '#fef3c7', |
| New file |
| | |
| | | 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> |
| | | ); |
| | | }, |
| | | }, |
| | | ]; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | 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(), |
| | | }); |
| New file |
| | |
| | | /** 角色类型。 */ |
| | | 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; |
| | | } |
| | |
| | | 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 { |
| | |
| | | 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 />, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | title: '订单管理', |
| | | icon: <ProfileOutlined />, |
| | | element: <Orders />, |
| | | }, |
| | | { |
| | | path: '/business/customers', |
| | | title: '客户管理', |
| | | icon: <ContactsOutlined />, |
| | | element: <Customers />, |
| | | }, |
| | | ], |
| | | }, |