diff --git a/docs/Phase2-角色权限与客服群验收.md b/docs/Phase2-角色权限与客服群验收.md new file mode 100644 index 0000000..4467006 --- /dev/null +++ b/docs/Phase2-角色权限与客服群验收.md @@ -0,0 +1,110 @@ +# Phase 2 验收报告(角色权限与客服群) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:后台角色权限矩阵、客服群组管理、菜单与路由接入 +> 结论:**页面、接口、类型检查与构建全部通过;写操作未在验收中执行** + +--- + +## 1. 角色权限 + +- 页面:`frontend/src/features/admin/AdminRolesPage.tsx` +- 路由:`/console-jy8k3m/roles` +- 权限:`role:manage` +- 接口: + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/admin/roles` | 角色列表(含 `perm_count`) | +| GET | `/api/admin/roles/:id` | 角色详情与权限 ID | +| POST | `/api/admin/roles` | 创建角色 | +| PUT | `/api/admin/roles/:id` | 编辑角色 | +| DELETE | `/api/admin/roles/:id` | 删除角色(内置角色返回 `ErrProtectedRole`) | +| GET | `/api/admin/permissions` | 全量权限字典 | +| PUT | `/api/admin/roles/:id/permissions` | 覆盖式保存角色权限 | + +- 能力: + - 角色列表、创建、编辑、删除;内置角色删除由后端拦截并在前端透出后端错误信息。 + - 权限分配弹窗按 `resource` 分组,使用 `Checkbox.Group`; + - 勾选变更只替换当前资源组的权限,其他资源已选权限原样保留后整体提交。 + - 弹窗顶部展示已选数量,并提示「保存后立即影响该角色下所有管理员」。 +- 样式:`.admin-role-permission-loading`、`.admin-permission-groups`、`.admin-permission-group` 落在 `frontend/src/styles/admin-misc.css`,权限项在宽屏双列以上自适应、768px 以下单列。 + +## 2. 客服群组 + +- 页面:`frontend/src/features/admin/AdminSupportGroupsPage.tsx` +- 路由:`/console-jy8k3m/chat-support-groups` +- 权限:`chat:manage` +- 接口: + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/admin/chat-support-groups` | 客服群列表 | +| POST | `/api/admin/chat-support-groups` | 创建客服群 | +| PUT | `/api/admin/chat-support-groups/:id` | 编辑客服群 | +| DELETE | `/api/admin/chat-support-groups/:id` | 删除客服群 | +| GET | `/api/admin/chat-support-groups/support-admins` | 可选坐席(管理员)列表 | + +- 能力:群编码/名称/描述/状态/排序维护,成员多选绑定坐席;创建、编辑、删除后失效 `admin-support-groups` 查询缓存。 +- 成员来源为 `support-admins`,接口当前返回空数组时页面按空成员处理,不报错。 + +## 3. 菜单与路由接入 + +- `frontend/src/layouts/AdminLayout.tsx` + - 「客服」菜单组:`/chat-support-groups` → 客服群组(`chat:manage`)。 + - 「系统」菜单组新增:`/roles` → 角色权限(`role:manage`)。 + - `sectionMeta` 补 `/roles` 与 `/chat-support-groups` 的标题与描述。 +- `frontend/src/app/AppRouter.tsx`:`AdminRolesPage`、`AdminSupportGroupsPage` 均为 lazy import。 + +## 4. 只读回归 + +开发环境: + +```text +前端 http://localhost:5289 +后端 http://127.0.0.1:18089 +Cookie /tmp/jysh-admin-cookie2.txt(HttpOnly,withCredentials) +``` + +页面路由: + +| 路径 | 状态 | +|---|---| +| `/console-jy8k3m/roles` | 200 | +| `/console-jy8k3m/chat-support-groups` | 200 | + +接口(携带已有管理端 cookie,未触发 refresh): + +| 路径 | 状态 | +|---|---| +| `GET /api/admin/roles` | 200(含 `super_admin`,`perm_count=78`) | +| `GET /api/admin/permissions` | 200 | +| `GET /api/admin/chat-support-groups` | 200(含 `owner_onboarding` 收号组-客服) | +| `GET /api/admin/chat-support-groups/support-admins` | 200 | + +写操作(创建/编辑/删除角色与客服群、保存权限)本轮未执行,避免影响开发库既有权限数据。 + +## 5. 类型检查与构建 + +```bash +cd frontend && npx tsc --noEmit # 通过 +cd frontend && npm run build # 通过 +``` + +构建结果(无超大包告警): + +```text +dist/assets/index-C5HmO9XZ.js 493.43 kB │ gzip: 155.88 kB +dist/assets/vendor-react-BKj2bk8R.js 282.79 kB │ gzip: 90.00 kB +dist/assets/vendor-mobile-DQjalTEh.js 307.07 kB │ gzip: 103.63 kB +dist/assets/vendor-antd-icons-SRcm2AV6.js 102.44 kB │ gzip: 29.73 kB +dist/assets/Table-CUTV1oFv.js 144.26 kB │ gzip: 47.07 kB +``` + +修复项:`AdminRolesPage.tsx` 移除未使用的 `AdminRolePayload` 类型导入(`TS6196`)。 + +## 6. 仍未覆盖 + +- 角色权限与客服群的写操作、内置角色删除拦截的真实响应未做端到端验证。 +- 客服坐席为空时,真实会话分流链路未验证。 diff --git a/docs/项目计划.md b/docs/项目计划.md index 7e90a2d..0763c11 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -395,7 +395,7 @@ hfb_jysh/ ### Phase 4:管理后台核心(4–5 周) -> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、资金出款、运营开支、公告管理、审计日志、系统配置、支付配置、分销站点、分销看板、站点钱包、分销订单、结算批次、分销用户、站点公告、分销商品、支付通道、规则版本、二维码池、站点短信与邀请返利;分销账号同步入口已接入,订单结算比例已 bps 化;剩余真实外部支付 E2E、真实站点数据业务回归与上线准备。 +> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、资金出款、运营开支、公告管理、审计日志、系统配置、支付配置、角色权限、客服群组、分销站点、分销看板、站点钱包、分销订单、结算批次、分销用户、站点公告、分销商品、支付通道、规则版本、二维码池、站点短信与邀请返利;分销账号同步入口已接入,订单结算比例已 bps 化;剩余真实外部支付 E2E、真实站点数据业务回归与上线准备。 - 后台登录、工作台、订单列表、订单详情。 - 商品列表、商品详情、审核。 @@ -410,9 +410,12 @@ hfb_jysh/ - 财务看板、资金明细、运营支出。 - 申诉仲裁、手工订单、提号、客服群、快捷回复。 + - 已落地:客服群组(`/chat-support-groups`,权限 `chat:manage`),角色权限矩阵(`/roles`,权限 `role:manage`)。 + - 待落地:手工录单 `/api/admin/manual-entries`、快捷回复 `/api/admin/chats/quick-replies`、推广追踪链接 `/api/admin/tracking-links`。 - 分销站点、规则、订单、结算、二维码池、分销用户、公告。 - 公告、邀请返利、推送、追踪链接。 - 系统配置、支付配置、备份、管理员管理。 + - 待落地:管理员账号管理 `/api/admin/admin-users`。 - 现有 React 分销后台仅做品牌主题、Logo、登录页、Shell 和文案轻改,不改功能。 **验收:** 43 个后台页面全部可用,分销后台与主站视觉统一,分销业务流程不变。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 4011b4e..9dd478a 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -180,6 +180,16 @@ const AdminPaymentConfigsPage = lazy(() => default: module.AdminPaymentConfigsPage, })) ) +const AdminRolesPage = lazy(() => + import('@/features/admin/AdminRolesPage').then(module => ({ + default: module.AdminRolesPage, + })) +) +const AdminSupportGroupsPage = lazy(() => + import('@/features/admin/AdminSupportGroupsPage').then(module => ({ + default: module.AdminSupportGroupsPage, + })) +) const AdminDistributionSitesPage = lazy(() => import('@/features/admin/AdminDistributionSitesPage').then(module => ({ default: module.AdminDistributionSitesPage, @@ -309,6 +319,8 @@ export function AppRouter() { } /> } /> } /> + } /> + } /> } /> = { + dashboard: '仪表盘', + order: '订单', + listing: '商品', + user: '用户', + wallet: '钱包', + withdrawal: '提现', + dispute: '申诉', + finance: '财务', + announcement: '公告', + system_config: '系统配置', + audit_log: '审计日志', + distribution: '分销', + qrcode: '二维码', + chat: '聊天', + role: '角色', + admin_user: '管理员', + payment_config: '支付配置', + cs_manual_entry: '手工录单', +} + +export function AdminRolesPage() { + const { message, modal } = App.useApp() + const queryClient = useQueryClient() + const [form] = Form.useForm() + const [editorOpen, setEditorOpen] = useState(false) + const [editing, setEditing] = useState() + const [submitting, setSubmitting] = useState(false) + const [permissionRole, setPermissionRole] = useState() + const [selectedPermissionIds, setSelectedPermissionIds] = useState([]) + const [savingPermissions, setSavingPermissions] = useState(false) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canManage = Boolean(permissions?.includes('*') || permissions?.includes('role:manage')) + + const rolesQuery = useQuery({ + queryKey: ['admin-roles'], + queryFn: fetchAdminRoles, + enabled: canManage, + }) + const permissionListQuery = useQuery({ + queryKey: ['admin-permissions'], + queryFn: fetchAdminPermissions, + enabled: canManage, + staleTime: 5 * 60_000, + }) + const roleDetailQuery = useQuery({ + queryKey: ['admin-role', permissionRole?.id], + queryFn: () => fetchAdminRole(permissionRole?.id as number), + enabled: canManage && Boolean(permissionRole?.id), + }) + + const roles = rolesQuery.data ?? [] + const permissionGroups = useMemo(() => { + const grouped = new Map() + for (const permission of permissionListQuery.data ?? []) { + const list = grouped.get(permission.resource) ?? [] + list.push(permission) + grouped.set(permission.resource, list) + } + return Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b)) + }, [permissionListQuery.data]) + + useEffect(() => { + if (!roleDetailQuery.data) return + setSelectedPermissionIds((roleDetailQuery.data.permissions ?? []).map(item => item.id)) + }, [roleDetailQuery.data]) + + const openCreate = () => { + setEditing(undefined) + form.setFieldsValue({ code: '', name: '', description: '' }) + setEditorOpen(true) + } + + const openEdit = (role: AdminRoleDetail) => { + setEditing(role) + form.setFieldsValue({ + code: role.code, + name: role.name, + description: role.description, + }) + setEditorOpen(true) + } + + const save = async () => { + const values = await form.validateFields() + setSubmitting(true) + try { + if (editing) { + await updateAdminRole(editing.id, { + name: values.name.trim(), + description: values.description?.trim(), + }) + message.success('角色已更新') + } else { + await createAdminRole({ + code: values.code.trim(), + name: values.name.trim(), + description: values.description?.trim(), + }) + message.success('角色已创建') + } + setEditorOpen(false) + await queryClient.invalidateQueries({ queryKey: ['admin-roles'] }) + } catch (error) { + message.error(errorText(error)) + } finally { + setSubmitting(false) + } + } + + const openPermissions = (role: AdminRoleDetail) => { + setPermissionRole(role) + setSelectedPermissionIds([]) + } + + const savePermissions = async () => { + if (!permissionRole) return + setSavingPermissions(true) + try { + await assignAdminRolePermissions(permissionRole.id, selectedPermissionIds) + message.success('角色权限已更新') + setPermissionRole(undefined) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['admin-roles'] }), + queryClient.invalidateQueries({ queryKey: ['admin-role', permissionRole.id] }), + ]) + } catch (error) { + message.error(errorText(error)) + } finally { + setSavingPermissions(false) + } + } + + const remove = (role: AdminRoleDetail) => { + modal.confirm({ + title: `确认删除角色「${role.name}」?`, + content: '系统内置角色不可删除;删除前请确认没有管理员仍在使用该角色。', + okText: '删除', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: async () => { + try { + await deleteAdminRole(role.id) + message.success('角色已删除') + await queryClient.invalidateQueries({ queryKey: ['admin-roles'] }) + } catch (error) { + message.error(errorText(error)) + } + }, + }) + } + + const columns: TableColumnsType = [ + { + title: '角色', + key: 'role', + width: 240, + fixed: 'left', + render: (_, row) => ( +
+ {row.name} + {row.code} +
+ ), + }, + { + title: '说明', + dataIndex: 'description', + render: value => formatText(value), + }, + { + title: '权限数', + dataIndex: 'perm_count', + width: 110, + align: 'right', + render: value => {value}, + }, + { + title: '更新时间', + dataIndex: 'updated_at', + width: 170, + render: value => formatDateTime(value), + }, + { + title: '操作', + key: 'actions', + width: 230, + fixed: 'right', + render: (_, row) => ( + + + + + + ), + }, + ] + + return ( +
+ + 共 {roles.length} 个角色 + 权限项 {permissionListQuery.data?.length ?? 0} + + } + extra={ + + {canManage ? ( + + ) : null} + + + } + /> + + {!canManage ? ( + + ) : null} + + {rolesQuery.isError ? ( + + ) : null} + + + title="后台角色" + rowKey="id" + columns={columns} + dataSource={roles} + loading={rolesQuery.isFetching} + emptyDescription={canManage ? '还没有角色' : '当前账号没有查看权限'} + /> + + (submitting ? undefined : setEditorOpen(false))} + footer={ + + + + + } + > + form={form} layout="vertical" disabled={submitting}> + + + + + + + + + + + + + (savingPermissions ? undefined : setPermissionRole(undefined))} + footer={ + + + + + } + > + {roleDetailQuery.isPending ? ( +
+ +
+ ) : ( + <> + +
+ {permissionGroups.map(([resource, items]) => ( +
+ + {resourceLabels[resource] ?? resource} + + + setSelectedPermissionIds(current => { + const other = current.filter( + id => !items.some(item => item.id === id) + ) + return [...other, ...values.map(Number)] + }) + } + options={items.map(item => ({ + value: item.id, + label: `${item.name}(${item.code})`, + }))} + /> +
+ ))} +
+ + )} +
+
+ ) +} diff --git a/frontend/src/features/admin/AdminSupportGroupsPage.tsx b/frontend/src/features/admin/AdminSupportGroupsPage.tsx new file mode 100644 index 0000000..9f7087b --- /dev/null +++ b/frontend/src/features/admin/AdminSupportGroupsPage.tsx @@ -0,0 +1,370 @@ +import { useState } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { + Alert, + App, + Button, + Col, + Form, + Input, + InputNumber, + Modal, + Row, + Select, + Space, + Tag, + Typography, + type TableColumnsType, +} from 'antd' +import { + CustomerServiceOutlined, + DeleteOutlined, + EditOutlined, + PlusOutlined, + ReloadOutlined, +} from '@ant-design/icons' + +import { + createAdminSupportGroup, + deleteAdminSupportGroup, + fetchAdminSupportAdmins, + fetchAdminSupportGroups, + updateAdminSupportGroup, +} from '@/features/admin/api' +import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader' +import { DataTable } from '@/features/admin/components/DataTable' +import type { + AdminSupportGroup, + AdminSupportGroupPayload, +} from '@/features/admin/types' +import { useAdminSessionStore } from '@/stores/adminSession' +import { formatDateTime, formatText } from '@/shared/utils/format' + +interface GroupFormValues { + code: string + name: string + description: string + status: string + sort_order: number + member_ids: number[] +} + +const statusOptions = [ + { value: 'active', label: '启用' }, + { value: 'disabled', label: '停用' }, +] + +function errorText(error: unknown) { + const response = (error as { response?: { data?: { message?: string } } })?.response + if (response?.data?.message) return response.data.message + return error instanceof Error ? error.message : '请求失败,请稍后重试' +} + +export function AdminSupportGroupsPage() { + const { message, modal } = App.useApp() + const queryClient = useQueryClient() + const [form] = Form.useForm() + const [editorOpen, setEditorOpen] = useState(false) + const [editing, setEditing] = useState() + const [submitting, setSubmitting] = useState(false) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canManage = Boolean(permissions?.includes('*') || permissions?.includes('chat:manage')) + + const groupsQuery = useQuery({ + queryKey: ['admin-support-groups'], + queryFn: fetchAdminSupportGroups, + enabled: canManage, + }) + const adminsQuery = useQuery({ + queryKey: ['admin-support-admins'], + queryFn: fetchAdminSupportAdmins, + enabled: canManage, + }) + + const groups = groupsQuery.data ?? [] + const admins = adminsQuery.data ?? [] + + const openCreate = () => { + setEditing(undefined) + form.setFieldsValue({ + code: '', + name: '', + description: '', + status: 'active', + sort_order: 0, + member_ids: [], + }) + setEditorOpen(true) + } + + const openEdit = (group: AdminSupportGroup) => { + setEditing(group) + form.setFieldsValue({ + code: group.code, + name: group.name, + description: group.description, + status: group.status, + sort_order: group.sort_order, + member_ids: group.members.map(member => member.id), + }) + setEditorOpen(true) + } + + const save = async () => { + const values = await form.validateFields() + setSubmitting(true) + try { + const payload: AdminSupportGroupPayload = { + name: values.name.trim(), + description: values.description?.trim(), + status: values.status, + sort_order: values.sort_order ?? 0, + member_ids: values.member_ids ?? [], + } + if (editing) { + await updateAdminSupportGroup(editing.id, payload) + message.success('客服群已更新') + } else { + await createAdminSupportGroup({ ...payload, code: values.code.trim() }) + message.success('客服群已创建') + } + setEditorOpen(false) + await queryClient.invalidateQueries({ queryKey: ['admin-support-groups'] }) + } catch (error) { + message.error(errorText(error)) + } finally { + setSubmitting(false) + } + } + + const remove = (group: AdminSupportGroup) => { + modal.confirm({ + title: `确认删除客服群「${group.name}」?`, + content: '删除后该分组不再出现在客服会话分配中。', + okText: '删除', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: async () => { + try { + await deleteAdminSupportGroup(group.id) + message.success('客服群已删除') + await queryClient.invalidateQueries({ queryKey: ['admin-support-groups'] }) + } catch (error) { + message.error(errorText(error)) + } + }, + }) + } + + const columns: TableColumnsType = [ + { + title: '客服群', + key: 'group', + width: 240, + fixed: 'left', + render: (_, row) => ( +
+ + {row.name} + + {row.status === 'active' ? '启用' : '停用'} + + + {row.code} + {formatText(row.description)} +
+ ), + }, + { + title: '成员', + key: 'members', + render: (_, row) => + row.members.length === 0 ? ( + 未分配 + ) : ( + + {row.members.map(member => ( + + {member.nickname || member.username} + + ))} + + ), + }, + { + title: '排序', + dataIndex: 'sort_order', + width: 90, + align: 'right', + }, + { + title: '更新时间', + dataIndex: 'updated_at', + width: 170, + render: value => formatDateTime(value), + }, + { + title: '操作', + key: 'actions', + width: 150, + fixed: 'right', + render: (_, row) => ( + + + + + ), + }, + ] + + return ( +
+ + 共 {groups.length} 个分组 + 在线客服 {admins.filter(item => item.support_status === 'online').length} + + } + extra={ + + {canManage ? ( + + ) : null} + + + } + /> + + {!canManage ? ( + + ) : null} + + {groupsQuery.isError ? ( + + ) : null} + + + title="客服分组" + rowKey="id" + columns={columns} + dataSource={groups} + loading={groupsQuery.isFetching} + emptyDescription={canManage ? '还没有客服群' : '当前账号没有查看权限'} + /> + + (submitting ? undefined : setEditorOpen(false))} + footer={ + + + + + } + > + form={form} layout="vertical" disabled={submitting}> + + + + + + + + + + + + + + + + ({ + value: member.id, + label: `${member.nickname || member.username} · ${member.username} · ${member.support_status}`, + }))} + /> + + + 成员可同时属于多个分组;在线状态由管理员账号维护。 + + + +
+ ) +} diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts index 64eb94d..43bc477 100644 --- a/frontend/src/features/admin/api.ts +++ b/frontend/src/features/admin/api.ts @@ -51,8 +51,14 @@ import type { AdminPaymentConfigPage, AdminPaymentConfigPayload, AdminPaymentConfigQuery, + AdminPermission, AdminReceivedListingPage, AdminReceivedListingQuery, + AdminRoleDetail, + AdminRolePayload, + AdminSupportGroup, + AdminSupportGroupPayload, + AdminSupportMember, AdminSyncSiteAccountsPayload, AdminSyncSiteAccountsResult, AdminSystemConfig, @@ -997,6 +1003,85 @@ export async function deleteAdminPaymentConfig(id: number) { await adminClient.delete(`/admin/payment-configs/${id}`) } +export async function fetchAdminRoles() { + const { data } = await adminClient.get>('/admin/roles') + return data.data ?? [] +} + +export async function fetchAdminRole(id: number) { + const { data } = await adminClient.get>(`/admin/roles/${id}`) + return data.data +} + +export async function createAdminRole(payload: AdminRolePayload) { + const { data } = await adminClient.post>('/admin/roles', payload) + return data.data +} + +export async function updateAdminRole(id: number, payload: AdminRolePayload) { + const { data } = await adminClient.put>( + `/admin/roles/${id}`, + payload + ) + return data.data +} + +export async function deleteAdminRole(id: number) { + await adminClient.delete(`/admin/roles/${id}`) +} + +export async function fetchAdminPermissions() { + const { data } = await adminClient.get>('/admin/permissions') + return data.data ?? [] +} + +export async function assignAdminRolePermissions(id: number, permissionIds: number[]) { + await adminClient.put(`/admin/roles/${id}/permissions`, { permission_ids: permissionIds }) +} + +export async function fetchAdminSupportGroups() { + const { data } = await adminClient.get>( + '/admin/chat-support-groups' + ) + return data.data ?? [] +} + +export async function fetchAdminSupportAdmins() { + const { data } = await adminClient.get>( + '/admin/chat-support-groups/support-admins' + ) + return data.data ?? [] +} + +export async function createAdminSupportGroup(payload: AdminSupportGroupPayload) { + const { data } = await adminClient.post>( + '/admin/chat-support-groups', + payload + ) + return data.data +} + +export async function updateAdminSupportGroup( + id: number, + payload: AdminSupportGroupPayload +) { + const { data } = await adminClient.put>( + `/admin/chat-support-groups/${id}`, + payload + ) + return data.data +} + +export async function deleteAdminSupportGroup(id: number) { + await adminClient.delete(`/admin/chat-support-groups/${id}`) +} + +export async function assignAdminSupportGroupMembers(id: number, memberIds: number[]) { + await adminClient.put(`/admin/chat-support-groups/${id}/members`, { + member_ids: memberIds, + }) +} + export async function fetchFinanceDisbursements(query: FinanceDisbursementQuery) { const { data } = await adminClient.get>>( '/admin/finance/disbursements', diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts index 319a180..0ef24d1 100644 --- a/frontend/src/features/admin/types.ts +++ b/frontend/src/features/admin/types.ts @@ -1355,3 +1355,56 @@ export interface AdminPaymentConfigPayload { environment?: string business_tags?: string[] } + +export interface AdminPermission { + id: number + code: string + name: string + resource: string + action: string +} + +export interface AdminRoleDetail { + id: number + code: string + name: string + description: string + perm_count: number + permissions?: AdminPermission[] + created_at: string + updated_at: string +} + +export interface AdminRolePayload { + code?: string + name: string + description?: string +} + +export interface AdminSupportMember { + id: number + username: string + nickname: string + support_status: string +} + +export interface AdminSupportGroup { + id: number + code: string + name: string + description: string + status: string + sort_order: number + members: AdminSupportMember[] + created_at: string + updated_at: string +} + +export interface AdminSupportGroupPayload { + code?: string + name: string + description?: string + status?: string + sort_order?: number + member_ids?: number[] +} diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index b1ff8dd..458daf7 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -14,6 +14,7 @@ import { import { AccountBookOutlined, BellOutlined, + CustomerServiceOutlined, DashboardOutlined, FileDoneOutlined, LogoutOutlined, @@ -131,12 +132,19 @@ const allNavGroups: NavGroup[] = [ icon: , children: [{ key: '/announcements', label: '公告管理', permission: 'announcement:view' }], }, + { + key: 'support', + label: '客服', + icon: , + children: [{ key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' }], + }, { key: 'system', label: '系统', icon: , children: [ { key: '/system-configs', label: '系统配置', permission: 'system_config:view' }, + { key: '/roles', label: '角色权限', permission: 'role:manage' }, { key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' }, { key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' }, ], @@ -168,6 +176,11 @@ const sectionMeta: Record = { '/distribution/sms': { title: '站点短信', description: '为分站配置独立短信签名、模板与密钥。' }, '/distribution/invitation': { title: '邀请返利', description: '配置分站邀请好友的返利比例。' }, '/announcements': { title: '公告管理', description: '维护主站与分站公告。' }, + '/roles': { title: '角色权限', description: '维护后台角色并分配资源级操作权限。' }, + '/chat-support-groups': { + title: '客服群组', + description: '维护客服群与坐席分配,支撑会话分流。', + }, '/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, '/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' }, '/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' }, diff --git a/frontend/src/styles/admin-misc.css b/frontend/src/styles/admin-misc.css index 417f7c8..4c08f25 100644 --- a/frontend/src/styles/admin-misc.css +++ b/frontend/src/styles/admin-misc.css @@ -162,3 +162,52 @@ height: 40px; } +/* Phase 2 role permission matrix */ +.admin-role-permission-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 240px; +} + +.admin-permission-groups { + display: grid; + gap: 16px; + margin-top: 16px; + max-height: 52vh; + overflow-y: auto; + padding-right: 4px; +} + +.admin-permission-group { + padding: 16px 18px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--paper-warm); +} + +.admin-permission-group h5 { + margin: 0 0 12px !important; + font-size: 14px; + letter-spacing: 0.02em; +} + +.admin-permission-group .ant-checkbox-group { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 8px 16px; +} + +.admin-permission-group .ant-checkbox-wrapper { + margin-inline-start: 0; + font-size: 13px; + line-height: 1.6; +} + +@media (max-width: 768px) { + .admin-permission-group .ant-checkbox-group { + grid-template-columns: 1fr; + } +} + +