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={
+ }
+ loading={autoQuery.isFetching || groupQuery.isFetching}
+ onClick={() => {
+ void autoQuery.refetch()
+ void groupQuery.refetch()
+ }}
+ >
+ 刷新
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 欢迎语保存在系统配置中,修改后对新建立的群生效。
+
+
+ )
+}
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() {
>
刷新
+ }
+ disabled={!canManage}
+ onClick={() => {
+ setBatchFiles([])
+ setBatchOpen(true)
+ }}
+ >
+ 批量导入
+
} disabled={!canManage} onClick={openCreate}>
添加二维码
@@ -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: '维护企业微信群二维码,按未使用顺序自动发放。',