= [
+ {
+ title: '二维码',
+ key: 'image',
+ width: 120,
+ render: (_, row) => (
+
+ ),
+ },
+ {
+ title: '群名 / 备注',
+ key: 'group',
+ width: 240,
+ render: (_, row) => (
+
+ {formatText(row.group_name)}
+ {formatText(row.note)}
+
+ ),
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 170,
+ render: (_, row) => {
+ const meta = statusMeta[row.status] ?? { label: row.status, color: 'default' }
+ return (
+
+
+ {meta.label}
+
+ {row.wecom_renamed ? (
+
+ 企微已改名
+
+ ) : null}
+
+ )
+ },
+ },
+ {
+ title: '绑定会话',
+ key: 'bound',
+ render: (_, row) => formatText(row.bound_conversation_title || row.conversation_id),
+ },
+ {
+ title: '发放 / 使用时间',
+ key: 'time',
+ width: 190,
+ render: (_, row) => (
+
+ {formatDateTime(row.used_at)}
+ 创建 {formatDateTime(row.created_at)}
+
+ ),
+ },
+ {
+ title: '过期时间',
+ dataIndex: 'expires_at',
+ width: 170,
+ render: value => (value ? formatDateTime(value) : '—'),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 150,
+ fixed: 'right',
+ render: (_, row) => (
+
+ }
+ disabled={!canManage}
+ onClick={() => openEdit(row)}
+ >
+ 编辑
+
+ }
+ disabled={!canManage}
+ onClick={() => removeOne(row)}
+ >
+ 删除
+
+
+ ),
+ },
+ ]
+
+ if (!canView) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index bcda9a1..2bed14f 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -115,6 +115,11 @@ import type {
AdminChatMessage,
AdminChatMessagePage,
AdminChatPage,
+ AdminQrCode,
+ AdminQrCodePage,
+ AdminQrCodePayload,
+ AdminQrCodeStats,
+ AdminQrCodeUpdatePayload,
ManualDisbursementPayload,
OperatingExpensePage,
OperatingExpensePayload,
@@ -1640,5 +1645,64 @@ export async function fetchAdminChatSupportAdmins() {
return data.data ?? []
}
+export async function fetchAdminQrCodes(query: {
+ status?: string
+ keyword?: string
+ wecom_renamed?: string
+ bound?: string
+ expire_status?: string
+ page?: number
+ limit?: number
+}) {
+ const { data } = await adminClient.get<{ data: Partial }>(
+ '/admin/chats/qrcodes',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.limit ?? 20,
+ }
+}
+
+export async function fetchAdminQrCodeStats() {
+ const { data } = await adminClient.get<{ data: AdminQrCodeStats }>('/admin/chats/qrcodes/stats')
+ return data.data
+}
+
+export async function createAdminQrCode(payload: AdminQrCodePayload) {
+ const { data } = await adminClient.post<{ data: AdminQrCode }>('/admin/chats/qrcodes', payload)
+ return data.data
+}
+
+export async function batchCreateAdminQrCodes(items: AdminQrCodePayload[]) {
+ const { data } = await adminClient.post<{ data: AdminQrCode[] }>(
+ '/admin/chats/qrcodes/batch',
+ { items }
+ )
+ return data.data ?? []
+}
+
+export async function batchDeleteAdminQrCodes(ids: number[]) {
+ const { data } = await adminClient.post<{ data: { deleted: number } }>(
+ '/admin/chats/qrcodes/batch-delete',
+ { ids }
+ )
+ return data.data.deleted ?? 0
+}
+
+export async function updateAdminQrCode(id: number, payload: AdminQrCodeUpdatePayload) {
+ const { data } = await adminClient.patch<{ data: AdminQrCode }>(
+ `/admin/chats/qrcodes/${id}`,
+ payload
+ )
+ return data.data
+}
+
+export async function deleteAdminQrCode(id: number) {
+ await adminClient.delete(`/admin/chats/qrcodes/${id}`)
+}
+
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index e6bc18b..9919048 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -827,6 +827,54 @@ export interface AdminChatCounts {
stages: Record
}
+export interface AdminQrCode {
+ id: number
+ image_url: string
+ group_name: string
+ distribution_site_id?: number | null
+ status: string
+ conversation_id?: number | null
+ used_at?: string | null
+ expires_at?: string | null
+ created_by: number
+ note: string
+ wecom_renamed: boolean
+ created_at: string
+ updated_at: string
+ bound_conversation_title?: string
+}
+
+export interface AdminQrCodePage {
+ items: AdminQrCode[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminQrCodeStats {
+ unused_count: number
+ used_count: number
+ disabled_count: number
+ total_count: number
+}
+
+export interface AdminQrCodePayload {
+ image_url: string
+ group_name?: string
+ note?: string
+ expires_at?: string | null
+}
+
+export interface AdminQrCodeUpdatePayload {
+ image_url?: string
+ group_name?: string
+ note?: string
+ status?: string
+ wecom_renamed?: boolean
+ expires_at?: string | null
+ clear_expires_at?: boolean
+}
+
export interface AdminAnnouncementQuery {
category?: string
status?: string
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index d7bbd87..55d7caf 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -143,6 +143,7 @@ const allNavGroups: NavGroup[] = [
children: [
{ key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' },
{ key: '/chats', label: '客服会话', permission: 'chat:view' },
+ { key: '/qrcode-pool', label: '二维码池', permission: 'qrcode:view' },
{ key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' },
{ key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' },
],
@@ -212,6 +213,10 @@ const sectionMeta: Record = {
title: '客服会话',
description: '按阶段筛选会话、回复用户并转接客服。',
},
+ '/qrcode-pool': {
+ title: '二维码池',
+ description: '维护企业微信群二维码,按未使用顺序自动发放。',
+ },
'/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
'/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' },
'/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' },