From fd95bff747b1ca708f5aa299a67bdf9256694dbe Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 16:53:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=96=B0=E5=A2=9E=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=AE=A2=E6=9C=8D=E4=BC=9A=E8=AF=9D=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Phase2-客服会话工作台验收.md | 61 +++ docs/项目计划.md | 2 +- frontend/src/app/AppRouter.tsx | 6 + .../src/features/admin/AdminChatsPage.tsx | 485 ++++++++++++++++++ frontend/src/features/admin/api.ts | 87 ++++ frontend/src/features/admin/types.ts | 78 +++ frontend/src/layouts/AdminLayout.tsx | 5 + frontend/src/styles/admin-chat.css | 182 +++++++ frontend/src/styles/global.css | 1 + 9 files changed, 906 insertions(+), 1 deletion(-) create mode 100644 docs/Phase2-客服会话工作台验收.md create mode 100644 frontend/src/features/admin/AdminChatsPage.tsx create mode 100644 frontend/src/styles/admin-chat.css diff --git a/docs/Phase2-客服会话工作台验收.md b/docs/Phase2-客服会话工作台验收.md new file mode 100644 index 0000000..3a72a57 --- /dev/null +++ b/docs/Phase2-客服会话工作台验收.md @@ -0,0 +1,61 @@ +# Phase 2 验收报告(客服会话工作台) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:后台客服会话列表、消息查看、回复、转接、备注、全部已读 +> 结论:**页面与路由已落地,tsc/build 通过,只读接口回归 200;发送与转接未在验收中执行** + +--- + +## 1. 页面与路由 + +- 页面:`frontend/src/features/admin/AdminChatsPage.tsx` +- 路由:`/console-jy8k3m/chats` +- 权限:查看 `chat:view`,发送/转接/备注 `chat:send` +- 样式:`frontend/src/styles/admin-chat.css`(已接入 `styles/global.css`) + +## 2. 接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/admin/chats` | 会话列表(`filter` / `stage` / `keyword` / 分页) | +| GET | `/api/admin/chats/counts` | 归属与阶段统计 | +| GET | `/api/admin/chats/:id` | 会话详情 | +| GET | `/api/admin/chats/:id/messages` | 消息列表 | +| POST | `/api/admin/chats/:id/messages` | 发送消息 | +| POST | `/api/admin/chats/:id/read` | 标记已读 | +| POST | `/api/admin/chats/read-all` | 全部已读 | +| GET | `/api/admin/chats/support-admins` | 可转接客服 | +| POST | `/api/admin/chats/:id/transfer` | 转接会话 | +| POST | `/api/admin/chats/:id/remark` | 保存后台备注 | + +## 3. 能力 + +- 左侧会话列表:`filter`(全部 / 我负责的)、`stage`(全部 / 待处理 / 未进群 / 交接中 / 租用中 / 售后 / 已结束)、关键字搜索;未读 Badge、待回复标签、订单号标签、最近消息与相对时间,15 秒轮询。 +- 右侧会话面板:会话元信息(订单号、订单状态、客服场景)、消息气泡(本人消息右对齐、图片消息内联预览)、8 秒轮询,切换会话自动滚动到底部。 +- 回复区:快捷回复下拉插入(复用 `GET /admin/chats/quick-replies`)、文本输入(2000 字上限、Enter 发送 / Shift+Enter 换行)。 +- 会话操作:转接客服(下拉选择 + 二次点击确认)、后台备注保存、全部已读。 +- 选中未读会话时自动调用已读接口并刷新列表与详情。 + +## 4. 回归结果 + +```text +前端 http://localhost:5289 +后端 http://127.0.0.1:18089 +Cookie /tmp/jysh-admin-cookie2.txt +``` + +| 检查项 | 状态 | +|---|---| +| `GET /console-jy8k3m/chats` | 200 | +| `GET /api/admin/chats?page=1&page_size=5` | 200 | +| `GET /api/admin/chats/counts` | 200 | +| `GET /api/admin/chats/support-admins` | 200 | + +`npx tsc --noEmit` 通过;`npm run build` 通过,主入口 `498.83 kB` / gzip `157.03 kB`,无超大包告警。 + +## 5. 未覆盖 + +- 发送消息、转接、备注保存、已读均未执行真实写操作(开发库无真实会话)。 +- 实时推送依赖 SSE(`/admin/chats/events`),本页采用轮询兜底,未接入 SSE。 +- 自动欢迎语与商品群欢迎配置(`/admin/chats/auto-welcome`、`/listing-group-welcome`)尚未做页面。 diff --git a/docs/项目计划.md b/docs/项目计划.md index cc4a2f9..64e6a95 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -414,7 +414,7 @@ hfb_jysh/ - 已落地:`/wallet`(余额 + 流水)、`/wallet/payment-accounts`、`/wallet/withdrawal`、移动端 `/m/wallet/withdrawal`、卖家 `/seller/earnings`、`/seller/handoffs`、`/seller/pickups`。 - 申诉仲裁、手工订单、提号、客服群、快捷回复。 - 已落地:客服群组(`/chat-support-groups`,权限 `chat:manage`),角色权限矩阵(`/roles`,权限 `role:manage`),手工录单(`/manual-entries`,权限 `cs_manual_entry:*`),快捷回复(`/chat-quick-replies`,权限 `chat:view` / `chat:send`),线下提号(`/pickup`、`/pickup/:id`,权限 `order:pickup`,口径统一为「线下提号」)。 - - 待落地:客服会话工作台 `/chats`、主站二维码池 `/qrcode-pool`。 + - 待落地:主站二维码池 `/qrcode-pool`、自动欢迎与商品群欢迎配置。 - 分销站点、规则、订单、结算、二维码池、分销用户、公告。 - 公告、邀请返利、推送、追踪链接。 - 已落地:公告管理、邀请返利、推广追踪(`/tracking-links`,权限 `ad_tracking:manage`)。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index c0af8a8..500df95 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -241,6 +241,11 @@ const AdminPickupsPage = lazy(() => default: module.AdminPickupsPage, })) ) +const AdminChatsPage = lazy(() => + import('@/features/admin/AdminChatsPage').then(module => ({ + default: module.AdminChatsPage, + })) +) const AdminPickupDetailPage = lazy(() => import('@/features/admin/AdminPickupDetailPage').then(module => ({ default: module.AdminPickupDetailPage, @@ -408,6 +413,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/features/admin/AdminChatsPage.tsx b/frontend/src/features/admin/AdminChatsPage.tsx new file mode 100644 index 0000000..de26836 --- /dev/null +++ b/frontend/src/features/admin/AdminChatsPage.tsx @@ -0,0 +1,485 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query' +import { + Alert, + App, + Badge, + Button, + Card, + Empty, + Input, + Select, + Space, + Spin, + Tag, + Typography, +} from 'antd' +import { ReloadOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons' + +import { + fetchAdminChatDetail, + fetchAdminChatMessages, + fetchAdminChats, + fetchAdminChatSupportAdmins, + fetchAdminQuickReplies, + markAdminChatRead, + markAllAdminChatsRead, + sendAdminChatMessage, + transferAdminChat, + updateAdminChatRemark, +} from '@/features/admin/api' +import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader' +import type { AdminChatConversation } from '@/features/admin/types' +import { useAdminSessionStore } from '@/stores/adminSession' +import { formatDateTime, formatRelativeTime, formatText } from '@/shared/utils/format' + +const filterOptions = [ + { value: 'all', label: '全部会话' }, + { value: 'mine', label: '我负责的' }, +] + +const stageOptions = [ + { value: 'all', label: '全部阶段' }, + { value: 'pending', label: '待处理' }, + { value: 'unjoined', label: '未进群' }, + { value: 'handoff', label: '交接中' }, + { value: 'renting', label: '租用中' }, + { value: 'after_sale', label: '售后' }, + { value: 'ended', 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 AdminChatsPage() { + const { message } = App.useApp() + const queryClient = useQueryClient() + const [filter, setFilter] = useState('all') + const [stage, setStage] = useState('all') + const [keyword, setKeyword] = useState('') + const [appliedKeyword, setAppliedKeyword] = useState('') + const [selectedId, setSelectedId] = useState() + const [draft, setDraft] = useState('') + const [sending, setSending] = useState(false) + const [remarkDraft, setRemarkDraft] = useState('') + const [remarkSaving, setRemarkSaving] = useState(false) + const [transferTarget, setTransferTarget] = useState() + const [transferring, setTransferring] = useState(false) + const bottomRef = useRef(null) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canView = Boolean(permissions?.includes('*') || permissions?.includes('chat:view')) + const canSend = Boolean(permissions?.includes('*') || permissions?.includes('chat:send')) + + const listQuery = useQuery({ + queryKey: ['admin-chats', filter, stage, appliedKeyword], + queryFn: () => + fetchAdminChats({ + filter, + stage, + keyword: appliedKeyword || undefined, + page: 1, + page_size: 50, + }), + enabled: canView, + placeholderData: keepPreviousData, + refetchInterval: 15_000, + }) + + const conversations = listQuery.data?.items ?? [] + + useEffect(() => { + if (selectedId || conversations.length === 0) return + setSelectedId(conversations[0].id) + }, [conversations, selectedId]) + + const detailQuery = useQuery({ + queryKey: ['admin-chat', selectedId], + queryFn: () => fetchAdminChatDetail(selectedId as number), + enabled: canView && Boolean(selectedId), + }) + + const messagesQuery = useQuery({ + queryKey: ['admin-chat-messages', selectedId], + queryFn: () => fetchAdminChatMessages(selectedId as number, 1, 100), + enabled: canView && Boolean(selectedId), + refetchInterval: 8_000, + placeholderData: keepPreviousData, + }) + + const quickRepliesQuery = useQuery({ + queryKey: ['admin-quick-replies'], + queryFn: fetchAdminQuickReplies, + enabled: canSend, + }) + + const supportAdminsQuery = useQuery({ + queryKey: ['admin-support-admins'], + queryFn: fetchAdminChatSupportAdmins, + enabled: canSend, + }) + + const messages = messagesQuery.data?.items ?? [] + const conversation = detailQuery.data + + useEffect(() => { + if (!selectedId) return + setRemarkDraft(conversation?.admin_remark ?? '') + }, [conversation?.admin_remark, selectedId]) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages.length, selectedId]) + + const selectConversation = async (item: AdminChatConversation) => { + setSelectedId(item.id) + setConstraintReset() + if (item.unread_count > 0) { + try { + await markAdminChatRead(item.id) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['admin-chats'] }), + queryClient.invalidateQueries({ queryKey: ['admin-chat', item.id] }), + ]) + } catch { + // 已读失败不阻塞会话浏览 + } + } + } + + const setConstraintReset = () => { + setDraft('') + setTransferTarget(undefined) + } + + const send = async () => { + if (!selectedId || !draft.trim()) return + setSending(true) + try { + await sendAdminChatMessage(selectedId, { content: draft.trim() }) + setDraft('') + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['admin-chat-messages', selectedId] }), + queryClient.invalidateQueries({ queryKey: ['admin-chats'] }), + ]) + } catch (error) { + message.error(errorText(error)) + } finally { + setSending(false) + } + } + + const saveRemark = async () => { + if (!selectedId) return + setRemarkSaving(true) + try { + await updateAdminChatRemark(selectedId, remarkDraft.trim()) + message.success('备注已保存') + await queryClient.invalidateQueries({ queryKey: ['admin-chat', selectedId] }) + } catch (error) { + message.error(errorText(error)) + } finally { + setRemarkSaving(false) + } + } + + const transfer = async () => { + if (!selectedId || !transferTarget) return + setTransferring(true) + try { + await transferAdminChat(selectedId, transferTarget) + message.success('会话已转接') + setTransferTarget(undefined) + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['admin-chats'] }), + queryClient.invalidateQueries({ queryKey: ['admin-chat', selectedId] }), + ]) + } catch (error) { + message.error(errorText(error)) + } finally { + setTransferring(false) + } + } + + const quickReplyOptions = useMemo( + () => + (quickRepliesQuery.data ?? []).map(item => ({ + value: item.content, + label: item.title, + })), + [quickRepliesQuery.data] + ) + + if (!canView) { + return ( +
+ + +
+ ) + } + + return ( +
+ + 共 {listQuery.data?.total ?? 0} 个会话 + + 未读 {conversations.reduce((sum, item) => sum + item.unread_count, 0)} + + 待回复 {conversations.filter(item => item.needs_reply).length} + + } + extra={ + + + + + } + /> + +
+ setStage(value)} + /> + setKeyword(event.target.value)} + onSearch={value => setAppliedKeyword(value.trim())} + /> +
+ + {listQuery.isError ? ( + + ) : null} + +
+ + {listQuery.isFetching && conversations.length === 0 ? ( +
+ +
+ ) : conversations.length === 0 ? ( + + ) : ( +
+ {conversations.map(item => ( + + ))} +
+ )} +
+ + + {!selectedId ? ( + + ) : ( +
+
+ + {conversation?.latest_order_no ? ( + + 订单 {conversation.latest_order_no} + + ) : null} + {conversation?.latest_order_status ? ( + {conversation.latest_order_status} + ) : null} + {conversation?.support_scene ? ( + {conversation.support_scene} + ) : null} + + {formatDateTime(conversation?.updated_at)} + + +
+ +
+ {messagesQuery.isFetching && messages.length === 0 ? ( +
+ +
+ ) : messages.length === 0 ? ( + + ) : ( + messages.map(item => ( +
+
+ {formatText(item.sender_name || item.sender_role)} + + {formatDateTime(item.created_at)} + +
+
+ {item.content_type === 'image' && item.attachment_urls?.length ? ( + + {item.attachment_urls.map(url => ( + + 会话图片 + + ))} + + ) : ( + formatText(item.content) + )} +
+
+ )) + )} +
+
+ +
+ {quickReplyOptions.length ? ( + ({ + value: admin.id, + label: `${admin.nickname || admin.username}(${admin.support_status})`, + }))} + onChange={value => setTransferTarget(value)} + /> + + setRemarkDraft(event.target.value)} + onPressEnter={() => void saveRemark()} + /> + + + +
+
+
+ )} +
+
+
+ ) +} diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts index c63e8e8..bcda9a1 100644 --- a/frontend/src/features/admin/api.ts +++ b/frontend/src/features/admin/api.ts @@ -110,6 +110,11 @@ import type { AdminPickupProcessEvent, AdminPickupQuery, AdminPickupShop, + AdminChatConversation, + AdminChatCounts, + AdminChatMessage, + AdminChatMessagePage, + AdminChatPage, ManualDisbursementPayload, OperatingExpensePage, OperatingExpensePayload, @@ -1554,4 +1559,86 @@ export async function settleAdminPickupFinancialAdjustment( return data.data } +export async function fetchAdminChats(query: { + filter?: string + stage?: string + keyword?: string + page?: number + page_size?: number +}) { + const { data } = await adminClient.get>>('/admin/chats', { + 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.page_size ?? 20, + } +} + +export async function fetchAdminChatCounts(query: { + filter?: string + stage?: string + keyword?: string +}) { + const { data } = await adminClient.get>('/admin/chats/counts', { + params: compactParams(query), + }) + return data.data +} + +export async function fetchAdminChatDetail(id: number) { + const { data } = await adminClient.get>(`/admin/chats/${id}`) + return data.data +} + +export async function fetchAdminChatMessages(id: number, page = 1, pageSize = 50) { + const { data } = await adminClient.get>>( + `/admin/chats/${id}/messages`, + { params: { page, page_size: pageSize } } + ) + return { + items: (data.data.items ?? []) as AdminChatMessage[], + total: data.data.total ?? 0, + page: data.data.page ?? page, + page_size: data.data.page_size ?? pageSize, + } +} + +export async function sendAdminChatMessage( + id: number, + payload: { content: string; attachment_urls?: string[] } +) { + const { data } = await adminClient.post>( + `/admin/chats/${id}/messages`, + payload + ) + return data.data +} + +export async function markAdminChatRead(id: number) { + await adminClient.post(`/admin/chats/${id}/read`) +} + +export async function markAllAdminChatsRead() { + await adminClient.post('/admin/chats/read-all') +} + +export async function transferAdminChat(id: number, toAdminId: number) { + await adminClient.post(`/admin/chats/${id}/transfer`, { to_admin_id: toAdminId }) +} + +export async function updateAdminChatRemark(id: number, remark: string) { + await adminClient.post(`/admin/chats/${id}/remark`, { remark }) +} + +export async function fetchAdminChatSupportAdmins() { + const { data } = await adminClient.get< + ApiResponse> + >('/admin/chats/support-admins') + return data.data ?? [] +} + + diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts index 6b1c7ea..e6bc18b 100644 --- a/frontend/src/features/admin/types.ts +++ b/frontend/src/features/admin/types.ts @@ -749,6 +749,84 @@ export interface AdminPickupFinancialAdjustment { created_at: string } +export interface AdminChatParticipant { + id: number + conversation_id: number + participant_type: string + participant_id: number + role: string + remark: string + display_name: string + avatar_url: string + last_read_at?: string | null + joined_at: string +} + +export interface AdminChatConversation { + id: number + order_id?: number | null + listing_id?: number | null + distribution_site_id?: number | null + latest_order_id?: number | null + latest_order_no?: string + latest_order_status?: string + latest_handoff_status?: string + latest_refund_status?: string + type: string + support_scene?: string + title: string + status: string + role: string + admin_remark?: string + participants?: AdminChatParticipant[] + last_message_id?: number | null + last_message_preview: string + last_message_at?: string | null + last_sender_type?: string + last_sender_id?: number + last_sender_role?: string + unread_count: number + needs_reply: boolean + created_at: string + updated_at: string +} + +export interface AdminChatPage { + items: AdminChatConversation[] + total: number + page: number + page_size: number +} + +export interface AdminChatMessage { + id: number + conversation_id: number + sender_type: string + sender_id: number + sender_role: string + sender_name: string + sender_avatar: string + is_self: boolean + is_read: boolean + content_type: string + content: string + attachment_urls?: string[] + admin_attention_type?: string + created_at: string +} + +export interface AdminChatMessagePage { + items: AdminChatMessage[] + total: number + page: number + page_size: number +} + +export interface AdminChatCounts { + ownership: Record + stages: Record +} + export interface AdminAnnouncementQuery { category?: string status?: string diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index 5a839be..d7bbd87 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -142,6 +142,7 @@ const allNavGroups: NavGroup[] = [ icon: , children: [ { key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' }, + { key: '/chats', label: '客服会话', permission: 'chat:view' }, { key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' }, { key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' }, ], @@ -207,6 +208,10 @@ const sectionMeta: Record = { title: '客服群组', description: '维护客服群与坐席分配,支撑会话分流。', }, + '/chats': { + title: '客服会话', + description: '按阶段筛选会话、回复用户并转接客服。', + }, '/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, '/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' }, '/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' }, diff --git a/frontend/src/styles/admin-chat.css b/frontend/src/styles/admin-chat.css new file mode 100644 index 0000000..a5ae66b --- /dev/null +++ b/frontend/src/styles/admin-chat.css @@ -0,0 +1,182 @@ +/* 后台客服会话工作台 */ +.admin-chat-page { + display: grid; + gap: 16px; +} + +.admin-chat-toolbar { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.admin-chat-layout { + display: grid; + grid-template-columns: 340px minmax(0, 1fr); + gap: 16px; + align-items: stretch; + min-height: 620px; +} + +.admin-chat-list, +.admin-chat-panel { + border-radius: 12px; + overflow: hidden; +} + +.admin-chat-list { + max-height: 720px; +} + +.admin-chat-list__items { + max-height: 660px; + overflow-y: auto; +} + +.admin-chat-item { + display: grid; + gap: 6px; + width: 100%; + padding: 12px 14px; + border: 0; + border-bottom: 1px solid var(--line); + background: transparent; + text-align: left; + cursor: pointer; + font: inherit; + color: inherit; +} + +.admin-chat-item:hover { + background: var(--paper); +} + +.admin-chat-item.is-active { + background: #f2ece0; + box-shadow: inset 3px 0 0 var(--brand-ink); +} + +.admin-chat-item__top { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + +.admin-chat-item__top strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-chat-item__time { + flex: none; + font-size: 12px; + color: var(--ink-muted); +} + +.admin-chat-item__mid { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.admin-chat-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; +} + +.admin-chat-panel__body { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + gap: 12px; + min-height: 560px; +} + +.admin-chat-meta { + padding-bottom: 8px; + border-bottom: 1px solid var(--line); +} + +.admin-chat-meta__time { + font-size: 12px; + color: var(--ink-muted); +} + +.admin-chat-messages { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 460px; + overflow-y: auto; + padding: 4px 2px; +} + +.admin-chat-bubble { + align-self: flex-start; + max-width: 78%; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--paper-warm); +} + +.admin-chat-bubble.is-admin { + align-self: flex-end; + border-color: #d9e6e1; + background: #eaf2ee; +} + +.admin-chat-bubble__head { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 4px; + font-size: 12px; + color: var(--ink-muted); +} + +.admin-chat-bubble__content { + white-space: pre-wrap; + word-break: break-word; + line-height: 1.7; +} + +.admin-chat-image { + max-width: 200px; + border-radius: 8px; + border: 1px solid var(--line); +} + +.admin-chat-composer { + display: grid; + gap: 8px; + padding-top: 8px; + border-top: 1px solid var(--line); +} + +.admin-chat-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +@media (max-width: 1024px) { + .admin-chat-layout { + grid-template-columns: 1fr; + } + + .admin-chat-list { + max-height: 320px; + } + + .admin-chat-list__items { + max-height: 260px; + } +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 7e3cef3..270ffd3 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -10,6 +10,7 @@ @import './admin-login.css'; @import './admin-shell.css'; @import './admin-distribution.css'; +@import './admin-chat.css'; @import './mobile-base.css'; @import './mobile-listing.css'; @import './mobile-profile-orders.css';