feat(admin): 新增角色权限与客服群管理
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Spin,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SafetyOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
assignAdminRolePermissions,
|
||||
createAdminRole,
|
||||
deleteAdminRole,
|
||||
fetchAdminPermissions,
|
||||
fetchAdminRole,
|
||||
fetchAdminRoles,
|
||||
updateAdminRole,
|
||||
} from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import type { AdminPermission, AdminRoleDetail } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatText } from '@/shared/utils/format'
|
||||
|
||||
interface RoleFormValues {
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
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 : '请求失败,请稍后重试'
|
||||
}
|
||||
|
||||
const resourceLabels: Record<string, string> = {
|
||||
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<RoleFormValues>()
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AdminRoleDetail>()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [permissionRole, setPermissionRole] = useState<AdminRoleDetail>()
|
||||
const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([])
|
||||
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<string, AdminPermission[]>()
|
||||
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<AdminRoleDetail> = [
|
||||
{
|
||||
title: '角色',
|
||||
key: 'role',
|
||||
width: 240,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<strong>{row.name}</strong>
|
||||
<span className="admin-mono">{row.code}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
dataIndex: 'description',
|
||||
render: value => formatText(value),
|
||||
},
|
||||
{
|
||||
title: '权限数',
|
||||
dataIndex: 'perm_count',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
render: value => <Tag color="geekblue">{value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 230,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SafetyOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => openPermissions(row)}
|
||||
>
|
||||
权限
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => openEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => remove(row)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 权限"
|
||||
title="角色权限"
|
||||
description="维护后台角色与权限矩阵;系统内置角色不可删除。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {roles.length} 个角色</Tag>
|
||||
<Tag>权限项 {permissionListQuery.data?.length ?? 0}</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
{canManage ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建角色
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={() => void rolesQuery.refetch()} loading={rolesQuery.isFetching}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{!canManage ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="当前账号没有角色管理权限"
|
||||
description="需要 role:manage 权限。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{rolesQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="角色列表没有加载出来"
|
||||
description={errorText(rolesQuery.error)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminRoleDetail>
|
||||
title="后台角色"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={roles}
|
||||
loading={rolesQuery.isFetching}
|
||||
emptyDescription={canManage ? '还没有角色' : '当前账号没有查看权限'}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={editorOpen}
|
||||
title={editing ? `编辑角色 · ${editing.name}` : '新建角色'}
|
||||
width={560}
|
||||
maskClosable={!submitting}
|
||||
onCancel={() => (submitting ? undefined : setEditorOpen(false))}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setEditorOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" loading={submitting} onClick={() => void save()}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form<RoleFormValues> form={form} layout="vertical" disabled={submitting}>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="角色编码"
|
||||
rules={[{ required: true, message: '请输入角色编码' }]}
|
||||
extra="创建后不可修改"
|
||||
>
|
||||
<Input maxLength={64} disabled={Boolean(editing)} />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="角色名称" rules={[{ required: true }]}>
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={3} maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(permissionRole)}
|
||||
title={permissionRole ? `分配权限 · ${permissionRole.name}` : '分配权限'}
|
||||
width={820}
|
||||
maskClosable={!savingPermissions}
|
||||
onCancel={() => (savingPermissions ? undefined : setPermissionRole(undefined))}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setPermissionRole(undefined)} disabled={savingPermissions}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" loading={savingPermissions} onClick={() => void savePermissions()}>
|
||||
保存权限
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{roleDetailQuery.isPending ? (
|
||||
<div className="admin-role-permission-loading">
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message={`已选 ${selectedPermissionIds.length} 项权限`}
|
||||
description="权限按资源分组;保存后立即影响该角色下所有管理员。"
|
||||
/>
|
||||
<div className="admin-permission-groups">
|
||||
{permissionGroups.map(([resource, items]) => (
|
||||
<section key={resource} className="admin-permission-group">
|
||||
<Typography.Title level={5}>
|
||||
{resourceLabels[resource] ?? resource}
|
||||
</Typography.Title>
|
||||
<Checkbox.Group
|
||||
value={selectedPermissionIds}
|
||||
onChange={values =>
|
||||
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})`,
|
||||
}))}
|
||||
/>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<GroupFormValues>()
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AdminSupportGroup>()
|
||||
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<AdminSupportGroup> = [
|
||||
{
|
||||
title: '客服群',
|
||||
key: 'group',
|
||||
width: 240,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<Space size={6} wrap>
|
||||
<strong>{row.name}</strong>
|
||||
<Tag color={row.status === 'active' ? 'success' : 'default'}>
|
||||
{row.status === 'active' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<span className="admin-mono">{row.code}</span>
|
||||
<span>{formatText(row.description)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '成员',
|
||||
key: 'members',
|
||||
render: (_, row) =>
|
||||
row.members.length === 0 ? (
|
||||
<Typography.Text type="secondary">未分配</Typography.Text>
|
||||
) : (
|
||||
<Space size={4} wrap>
|
||||
{row.members.map(member => (
|
||||
<Tag key={member.id} color={member.support_status === 'online' ? 'green' : 'default'}>
|
||||
{member.nickname || member.username}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<Space size={4} wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => openEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!canManage}
|
||||
onClick={() => remove(row)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 客服"
|
||||
title="客服群组"
|
||||
description="维护客服分组、在线成员与排序,用于会话分配与订单客服群。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {groups.length} 个分组</Tag>
|
||||
<Tag color="green">在线客服 {admins.filter(item => item.support_status === 'online').length}</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
{canManage ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建客服群
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={groupsQuery.isFetching}
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
void groupsQuery.refetch()
|
||||
void adminsQuery.refetch()
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{!canManage ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="当前账号没有客服群管理权限"
|
||||
description="需要 chat:manage 权限。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{groupsQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="客服群没有加载出来"
|
||||
description={errorText(groupsQuery.error)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminSupportGroup>
|
||||
title="客服分组"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={groups}
|
||||
loading={groupsQuery.isFetching}
|
||||
emptyDescription={canManage ? '还没有客服群' : '当前账号没有查看权限'}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={editorOpen}
|
||||
title={editing ? `编辑客服群 · ${editing.name}` : '新建客服群'}
|
||||
width={640}
|
||||
maskClosable={!submitting}
|
||||
onCancel={() => (submitting ? undefined : setEditorOpen(false))}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setEditorOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" loading={submitting} onClick={() => void save()}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form<GroupFormValues> form={form} layout="vertical" disabled={submitting}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="分组编码"
|
||||
rules={[{ required: true, message: '请输入分组编码' }]}
|
||||
extra="创建后不可修改"
|
||||
>
|
||||
<Input maxLength={64} disabled={Boolean(editing)} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="name" label="分组名称" rules={[{ required: true }]}>
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select options={statusOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="sort_order" label="排序" extra="数值越小越靠前">
|
||||
<InputNumber min={0} max={9999} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="description" label="说明">
|
||||
<Input.TextArea rows={3} maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item name="member_ids" label="客服成员">
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="选择客服管理员"
|
||||
options={admins.map(member => ({
|
||||
value: member.id,
|
||||
label: `${member.nickname || member.username} · ${member.username} · ${member.support_status}`,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Typography.Paragraph type="secondary" className="admin-form-hint">
|
||||
<CustomerServiceOutlined /> 成员可同时属于多个分组;在线状态由管理员账号维护。
|
||||
</Typography.Paragraph>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<ApiResponse<AdminRoleDetail[]>>('/admin/roles')
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function fetchAdminRole(id: number) {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminRoleDetail>>(`/admin/roles/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function createAdminRole(payload: AdminRolePayload) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminRoleDetail>>('/admin/roles', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateAdminRole(id: number, payload: AdminRolePayload) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminRoleDetail>>(
|
||||
`/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<ApiResponse<AdminPermission[]>>('/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<ApiResponse<AdminSupportGroup[]>>(
|
||||
'/admin/chat-support-groups'
|
||||
)
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function fetchAdminSupportAdmins() {
|
||||
const { data } = await adminClient.get<ApiResponse<AdminSupportMember[]>>(
|
||||
'/admin/chat-support-groups/support-admins'
|
||||
)
|
||||
return data.data ?? []
|
||||
}
|
||||
|
||||
export async function createAdminSupportGroup(payload: AdminSupportGroupPayload) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminSupportGroup>>(
|
||||
'/admin/chat-support-groups',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateAdminSupportGroup(
|
||||
id: number,
|
||||
payload: AdminSupportGroupPayload
|
||||
) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminSupportGroup>>(
|
||||
`/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<ApiResponse<Partial<FinanceDisbursementPage>>>(
|
||||
'/admin/finance/disbursements',
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user