From f4fe2006dbc8c10006d6907a5cb1a32597bfc124 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 17:35:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=94=AF=E6=8C=81=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E4=B8=8E?= =?UTF-8?q?=20OCR=20=E8=AF=86=E5=88=AB=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=AC=A2?= =?UTF-8?q?=E8=BF=8E=E8=AF=AD=E9=85=8D=E7=BD=AE=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/AppRouter.tsx | 6 + .../features/admin/AdminChatWelcomePage.tsx | 153 +++++++++++++++++ .../features/admin/AdminQrCodePoolPage.tsx | 158 +++++++++++++++++- frontend/src/features/admin/api.ts | 41 +++++ frontend/src/layouts/AdminLayout.tsx | 5 + 5 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/admin/AdminChatWelcomePage.tsx diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 81a4478..1597ea4 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -345,6 +345,11 @@ const AdminChatsPage = lazy(() => default: module.AdminChatsPage, })) ) +const AdminChatWelcomePage = lazy(() => + import('@/features/admin/AdminChatWelcomePage').then(module => ({ + default: module.AdminChatWelcomePage, + })) +) const AdminQrCodePoolPage = lazy(() => import('@/features/admin/AdminQrCodePoolPage').then(module => ({ default: module.AdminQrCodePoolPage, @@ -554,6 +559,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/features/admin/AdminChatWelcomePage.tsx b/frontend/src/features/admin/AdminChatWelcomePage.tsx new file mode 100644 index 0000000..29be1ab --- /dev/null +++ b/frontend/src/features/admin/AdminChatWelcomePage.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { Alert, App, Button, Card, Form, Input, Space, Tag, Typography } from 'antd' +import { ReloadOutlined } from '@ant-design/icons' + +import { + fetchChatAutoWelcome, + fetchListingGroupWelcome, + updateChatAutoWelcome, + updateListingGroupWelcome, +} from '@/features/admin/api' +import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader' +import { useAdminSessionStore } from '@/stores/adminSession' + +function errorText(error: unknown) { + const response = (error as { response?: { data?: { message?: string } } })?.response + return response?.data?.message || (error instanceof Error ? error.message : '请求失败') +} + +export function AdminChatWelcomePage() { + const { message } = App.useApp() + const queryClient = useQueryClient() + const [autoForm] = Form.useForm<{ message: string }>() + const [groupForm] = Form.useForm<{ message: string }>() + const [saving, setSaving] = useState<'auto' | 'group' | null>(null) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view')) + const canManage = Boolean( + permissions?.includes('*') || permissions?.includes('system_config:update') + ) + + const autoQuery = useQuery({ + queryKey: ['chat-auto-welcome'], + queryFn: fetchChatAutoWelcome, + enabled: canView, + }) + const groupQuery = useQuery({ + queryKey: ['chat-listing-group-welcome'], + queryFn: fetchListingGroupWelcome, + enabled: canView, + }) + + useEffect(() => { + if (autoQuery.data != null) autoForm.setFieldsValue({ message: autoQuery.data }) + }, [autoQuery.data, autoForm]) + + useEffect(() => { + if (groupQuery.data != null) groupForm.setFieldsValue({ message: groupQuery.data }) + }, [groupQuery.data, groupForm]) + + if (!canView) { + return ( +
+ + +
+ ) + } + + const save = async (kind: 'auto' | 'group') => { + const form = kind === 'auto' ? autoForm : groupForm + const values = await form.validateFields() + setSaving(kind) + try { + if (kind === 'auto') { + await updateChatAutoWelcome(values.message.trim()) + await queryClient.invalidateQueries({ queryKey: ['chat-auto-welcome'] }) + } else { + await updateListingGroupWelcome(values.message.trim()) + await queryClient.invalidateQueries({ queryKey: ['chat-listing-group-welcome'] }) + } + message.success('欢迎语已保存') + } catch (error) { + message.error(errorText(error)) + } finally { + setSaving(null) + } + } + + return ( +
+ + 订单群 + 账号群 + + } + extra={ + + } + /> + + +
+ + + + +
+
+ + +
+ + + + +
+
+ + + 欢迎语保存在系统配置中,修改后对新建立的群生效。 + +
+ ) +} diff --git a/frontend/src/features/admin/AdminQrCodePoolPage.tsx b/frontend/src/features/admin/AdminQrCodePoolPage.tsx index 6e9b86d..fdf5f49 100644 --- a/frontend/src/features/admin/AdminQrCodePoolPage.tsx +++ b/frontend/src/features/admin/AdminQrCodePoolPage.tsx @@ -27,11 +27,13 @@ import { import dayjs, { type Dayjs } from 'dayjs' import { + batchCreateAdminQrCodes, batchDeleteAdminQrCodes, createAdminQrCode, deleteAdminQrCode, fetchAdminQrCodeStats, fetchAdminQrCodes, + recognizeQrCodeGroupName, updateAdminQrCode, } from '@/features/admin/api' import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader' @@ -39,7 +41,7 @@ import { DataTable } from '@/features/admin/components/DataTable' import { FilterBar, FilterField } from '@/features/admin/components/FilterBar' import type { AdminQrCode } from '@/features/admin/types' import { useAdminSessionStore } from '@/stores/adminSession' -import { uploadFile } from '@/shared/api/files' +import { uploadAdminFile } from '@/shared/api/files' import { formatDateTime, formatText } from '@/shared/utils/format' interface Filters { @@ -89,6 +91,11 @@ export function AdminQrCodePoolPage() { const [createOpen, setCreateOpen] = useState(false) const [editing, setEditing] = useState() const [imageURL, setImageURL] = useState('') + const [imageFile, setImageFile] = useState() + const [ocrLoading, setOcrLoading] = useState(false) + const [batchOpen, setBatchOpen] = useState(false) + const [batchFiles, setBatchFiles] = useState>([]) + const [batchImporting, setBatchImporting] = useState(false) const [uploading, setUploading] = useState(false) const [submitting, setSubmitting] = useState(false) const [selectedRowKeys, setSelectedRowKeys] = useState([]) @@ -237,8 +244,9 @@ export function AdminQrCodePoolPage() { const upload = async (file: File) => { setUploading(true) + setImageFile(file) try { - const uploaded = await uploadFile(file, 'qrcode') + const uploaded = await uploadAdminFile(file, 'qrcode') setImageURL(uploaded.url) message.success('图片已上传') } catch (error) { @@ -385,6 +393,16 @@ export function AdminQrCodePoolPage() { > 刷新 + @@ -523,6 +541,30 @@ export function AdminQrCodePoolPage() { {imageURL ? '重新上传' : '上传图片'} + @@ -537,6 +579,118 @@ export function AdminQrCodePoolPage() { + (batchImporting ? undefined : setBatchOpen(false))} + onOk={async () => { + if (batchFiles.length === 0) return + setBatchImporting(true) + try { + const items: Array<{ image_url: string; group_name?: string }> = [] + for (const item of batchFiles) { + const uploaded = await uploadAdminFile(item.file, 'qrcode') + items.push({ image_url: uploaded.url, group_name: item.name || undefined }) + } + await batchCreateAdminQrCodes(items) + message.success(`已导入 ${items.length} 张二维码`) + setBatchOpen(false) + setBatchFiles([]) + await invalidate() + } catch (error) { + message.error(errorText(error)) + } finally { + setBatchImporting(false) + } + }} + maskClosable={!batchImporting} + > + + { + const f = file as File + setBatchFiles(current => + current.length >= 20 + ? current + : [...current, { uid: `${Date.now()}-${Math.random()}`, file: f, name: '' }] + ) + return false + }} + > + + +
+ {batchFiles.map(item => ( + + + {item.file.name} + + + setBatchFiles(current => + current.map(entry => + entry.uid === item.uid ? { ...entry, name: event.target.value } : entry + ) + ) + } + /> + + + + ))} + {batchFiles.length === 0 ? ( + 还没有选择图片 + ) : null} +
+
+ ) { ) return data.data } + +export async function fetchChatAutoWelcome() { + const { data } = await adminClient.get>( + '/admin/chats/auto-welcome' + ) + return data.data.message ?? '' +} + +export async function updateChatAutoWelcome(message: string) { + const { data } = await adminClient.put>( + '/admin/chats/auto-welcome', + { message } + ) + return data.data +} + +export async function fetchListingGroupWelcome() { + const { data } = await adminClient.get>( + '/admin/chats/listing-group-welcome' + ) + return data.data.message ?? '' +} + +export async function updateListingGroupWelcome(message: string) { + const { data } = await adminClient.put>( + '/admin/chats/listing-group-welcome', + { message } + ) + return data.data +} + +export async function recognizeQrCodeGroupName(file: File) { + const form = new FormData() + form.append('file', file) + const { data } = await adminClient.post<{ data: { group_name?: string; text?: string } }>( + '/admin/chats/qrcodes/ocr-group-name', + form, + { headers: { 'Content-Type': 'multipart/form-data' } } + ) + return data.data +} diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index 3d33c79..8c6731a 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -164,6 +164,7 @@ const allNavGroups: NavGroup[] = [ { key: '/qrcode-pool', label: '二维码池', permission: 'qrcode:view' }, { key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' }, { key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' }, + { key: '/chat-welcome', label: '欢迎语配置', permission: 'system_config:view' }, ], }, { @@ -242,6 +243,10 @@ const sectionMeta: Record = { title: '客服会话', description: '按阶段筛选会话、回复用户并转接客服。', }, + '/chat-welcome': { + title: '欢迎语配置', + description: '维护订单群与账号群的自动欢迎语。', + }, '/qrcode-pool': { title: '二维码池', description: '维护企业微信群二维码,按未使用顺序自动发放。',