refactor: support i18n
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
renderIMMessageHTML,
|
||||
} from "@/lib/im-message";
|
||||
import { formatDateTime } from "@/lib/utils";
|
||||
import { useI18n } from "@/i18n/provider";
|
||||
|
||||
type ConversationDetailDialogProps = {
|
||||
open: boolean;
|
||||
@@ -34,7 +35,7 @@ type ConversationDetailDialogProps = {
|
||||
item: AdminConversation | null;
|
||||
detail: AdminConversationDetail | null;
|
||||
messages?: AdminMessage[] | null;
|
||||
/** 是否还有更早消息(cursor 分页) */
|
||||
/** Whether older messages are available through cursor pagination. */
|
||||
messagesHasMore?: boolean;
|
||||
loadingMoreMessages?: boolean;
|
||||
onLoadMoreMessages?: () => void | Promise<void>;
|
||||
@@ -46,44 +47,53 @@ type ConversationDetailDialogProps = {
|
||||
onOpenClose: () => void;
|
||||
};
|
||||
|
||||
function getStatusMeta(status: number) {
|
||||
function getStatusMeta(
|
||||
status: number,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return { label: "AI接待中", variant: "secondary" as const };
|
||||
return { label: t("conversation.filterAiServing"), variant: "secondary" as const };
|
||||
case 2:
|
||||
return { label: "待接入", variant: "outline" as const };
|
||||
return { label: t("conversation.filterPending"), variant: "outline" as const };
|
||||
case 3:
|
||||
return { label: "处理中", variant: "secondary" as const };
|
||||
return { label: t("conversation.filterActive"), variant: "secondary" as const };
|
||||
case 4:
|
||||
return { label: "已关闭", variant: "outline" as const };
|
||||
return { label: t("conversation.filterClosed"), variant: "outline" as const };
|
||||
default:
|
||||
return { label: "未知", variant: "outline" as const };
|
||||
return { label: t("conversationMonitor.unknown"), variant: "outline" as const };
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceModeLabel(mode: number) {
|
||||
function getServiceModeLabel(
|
||||
mode: number,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
switch (mode) {
|
||||
case 1:
|
||||
return "AI 接待";
|
||||
return t("conversationMonitor.serviceAi");
|
||||
case 2:
|
||||
return "人工接待";
|
||||
return t("conversationMonitor.serviceHuman");
|
||||
case 3:
|
||||
return "AI 优先";
|
||||
return t("conversationMonitor.serviceAiFirst");
|
||||
default:
|
||||
return "未定义";
|
||||
return t("conversationMonitor.serviceUndefined");
|
||||
}
|
||||
}
|
||||
|
||||
function getSenderLabel(message: AdminMessage) {
|
||||
function getSenderLabel(
|
||||
message: AdminMessage,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
switch (message.senderType) {
|
||||
case "agent":
|
||||
return message.senderName || "客服";
|
||||
return message.senderName || t("conversationMonitor.senderAgent");
|
||||
case "customer":
|
||||
return message.senderName || "用户";
|
||||
return message.senderName || t("conversationMonitor.senderCustomer");
|
||||
case "ai":
|
||||
return "AI";
|
||||
case "system":
|
||||
return "系统";
|
||||
return t("conversationMonitor.senderSystem");
|
||||
default:
|
||||
return message.senderType;
|
||||
}
|
||||
@@ -150,12 +160,13 @@ export function ConversationDetailDialog({
|
||||
onRead,
|
||||
onOpenClose,
|
||||
}: ConversationDetailDialogProps) {
|
||||
const t = useI18n();
|
||||
const messages = Array.isArray(rawMessages) ? rawMessages : [];
|
||||
const currentConversation = detail ?? item;
|
||||
const isClosedConversation = currentConversation?.status === 4;
|
||||
const isPendingConversation = currentConversation?.status === 2;
|
||||
const statusMeta = currentConversation
|
||||
? getStatusMeta(currentConversation.status)
|
||||
? getStatusMeta(currentConversation.status, t)
|
||||
: null;
|
||||
const messageBottomRef = useRef<HTMLDivElement | null>(null);
|
||||
const messagesScrollRootRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -249,7 +260,7 @@ export function ConversationDetailDialog({
|
||||
onOpenChange={onOpenChange}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{currentConversation?.customerName || "会话详情"}</span>
|
||||
<span>{currentConversation?.customerName || t("conversationMonitor.detailFallbackTitle")}</span>
|
||||
|
||||
<div>
|
||||
{statusMeta ? (
|
||||
@@ -268,8 +279,10 @@ export function ConversationDetailDialog({
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{currentConversation
|
||||
? `最后活跃:${formatDateTime(currentConversation.lastMessageAt)}`
|
||||
: "暂无会话信息"}
|
||||
? t("conversationMonitor.lastActivePrefix", {
|
||||
time: formatDateTime(currentConversation.lastMessageAt),
|
||||
})
|
||||
: t("conversationMonitor.noConversationInfo")}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
@@ -278,7 +291,7 @@ export function ConversationDetailDialog({
|
||||
disabled={saving || !currentConversation || currentConversation.status !== 2}
|
||||
>
|
||||
<MessageCircleMoreIcon />
|
||||
{saving ? "处理中..." : "分配会话"}
|
||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.assign")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -288,7 +301,7 @@ export function ConversationDetailDialog({
|
||||
}
|
||||
>
|
||||
<MessageCircleMoreIcon />
|
||||
{saving ? "处理中..." : "重试分配"}
|
||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.retryDispatch")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -296,7 +309,7 @@ export function ConversationDetailDialog({
|
||||
disabled={saving || !currentConversation}
|
||||
>
|
||||
<CheckCheckIcon />
|
||||
{saving ? "处理中..." : "标记已读"}
|
||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.markRead")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -305,7 +318,7 @@ export function ConversationDetailDialog({
|
||||
disabled={saving || !currentConversation || currentConversation.status !== 3}
|
||||
>
|
||||
<MessageCircleMoreIcon />
|
||||
{saving ? "处理中..." : "转接会话"}
|
||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.transfer")}
|
||||
</Button>
|
||||
{!isClosedConversation ? (
|
||||
<Button
|
||||
@@ -314,7 +327,7 @@ export function ConversationDetailDialog({
|
||||
disabled={saving || !currentConversation}
|
||||
>
|
||||
<EyeIcon />
|
||||
{saving ? "处理中..." : "关闭会话"}
|
||||
{saving ? t("conversationMonitor.processing") : t("conversationMonitor.close")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -323,7 +336,7 @@ export function ConversationDetailDialog({
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
正在加载会话详情...
|
||||
{t("conversationMonitor.loadingDetail")}
|
||||
</div>
|
||||
) : currentConversation ? (
|
||||
<div className="flex min-h-0 flex-1 flex-row overflow-hidden border-t">
|
||||
@@ -331,32 +344,32 @@ export function ConversationDetailDialog({
|
||||
<div className="space-y-4 p-6">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<InfoItem
|
||||
label="接待模式"
|
||||
value={getServiceModeLabel(currentConversation.serviceMode)}
|
||||
label={t("conversationMonitor.columnServiceMode")}
|
||||
value={getServiceModeLabel(currentConversation.serviceMode, t)}
|
||||
/>
|
||||
<InfoItem
|
||||
label="当前客服"
|
||||
label={t("conversationMonitor.currentAssignee")}
|
||||
value={currentConversation.currentAssigneeName || "-"}
|
||||
/>
|
||||
<InfoItem
|
||||
label="渠道ID"
|
||||
label={t("conversation.channelId")}
|
||||
value={`${currentConversation.channelId || "-"}`}
|
||||
/>
|
||||
<InfoItem
|
||||
label="客服未读"
|
||||
label={t("conversationMonitor.agentUnread")}
|
||||
value={`${currentConversation.agentUnreadCount}`}
|
||||
/>
|
||||
<InfoItem
|
||||
label="用户未读"
|
||||
label={t("conversationMonitor.customerUnread")}
|
||||
value={`${currentConversation.customerUnreadCount}`}
|
||||
/>
|
||||
<InfoItem
|
||||
label="最后活跃"
|
||||
label={t("conversationMonitor.columnLastActive")}
|
||||
value={formatDateTime(currentConversation.lastMessageAt)}
|
||||
fullWidth
|
||||
/>
|
||||
<InfoItem
|
||||
label="关闭时间"
|
||||
label={t("conversationMonitor.closedAt")}
|
||||
value={formatDateTime(currentConversation.closedAt)}
|
||||
fullWidth
|
||||
/>
|
||||
@@ -369,9 +382,13 @@ export function ConversationDetailDialog({
|
||||
<div className="space-y-4 p-6">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">参与方</p>
|
||||
<p className="text-sm font-medium">
|
||||
{t("conversationMonitor.participants")}
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{detail?.participants?.length ?? 0} 人
|
||||
{t("conversationMonitor.participantCount", {
|
||||
count: detail?.participants?.length ?? 0,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{detail?.participants?.length ? (
|
||||
@@ -387,17 +404,21 @@ export function ConversationDetailDialog({
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
标识:{getParticipantIdentity(participant)}
|
||||
{t("conversationMonitor.participantIdentity", {
|
||||
value: getParticipantIdentity(participant),
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
加入时间:{formatDateTime(participant.joinedAt)}
|
||||
{t("conversationMonitor.participantJoinedAt", {
|
||||
time: formatDateTime(participant.joinedAt),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-background p-4 text-sm text-muted-foreground">
|
||||
暂无参与方信息
|
||||
{t("conversationMonitor.emptyParticipants")}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -406,16 +427,6 @@ export function ConversationDetailDialog({
|
||||
</aside>
|
||||
|
||||
<section className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-background">
|
||||
{/* <div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">聊天记录</p>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
消息数:{messages.length}
|
||||
{messagesHasMore ? " · 向上滑可加载更早" : ""}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div ref={messagesScrollRootRef} className="min-h-0 flex-1">
|
||||
<ScrollArea className="h-full min-h-0 bg-muted/10">
|
||||
<div className="space-y-4 px-6 py-5">
|
||||
@@ -425,8 +436,8 @@ export function ConversationDetailDialog({
|
||||
className="flex min-h-8 flex-col items-center justify-center py-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{loadingMoreMessages
|
||||
? "正在加载更早的消息…"
|
||||
: "继续上滑加载更早消息"}
|
||||
? t("conversationMonitor.loadingOlder")
|
||||
: t("conversationMonitor.loadOlderHint")}
|
||||
</div>
|
||||
) : null}
|
||||
{messages.length ? (
|
||||
@@ -449,13 +460,13 @@ export function ConversationDetailDialog({
|
||||
<div
|
||||
className={`text-xs text-muted-foreground ${layout.metaClassName}`}
|
||||
>
|
||||
<span>{getSenderLabel(message)}</span>
|
||||
<span>{getSenderLabel(message, t)}</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span>{formatDateTime(message.sentAt)}</span>
|
||||
{isRecalled ? (
|
||||
<>
|
||||
<span className="mx-2">·</span>
|
||||
<span>已撤回</span>
|
||||
<span>{t("conversationMonitor.messageRecalled")}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -463,7 +474,9 @@ export function ConversationDetailDialog({
|
||||
className={`rounded-2xl border px-4 py-3 text-sm leading-6 ${layout.bubbleClassName}`}
|
||||
>
|
||||
{isRecalled ? (
|
||||
<div className="text-muted-foreground">该消息已撤回</div>
|
||||
<div className="text-muted-foreground">
|
||||
{t("conversationMonitor.messageRecalledBody")}
|
||||
</div>
|
||||
) : isHtmlMessage ? (
|
||||
<ImMessageHTML
|
||||
html={renderIMMessageHTML(message)}
|
||||
@@ -485,8 +498,14 @@ export function ConversationDetailDialog({
|
||||
<div
|
||||
className={`text-xs text-muted-foreground ${layout.metaClassName}`}
|
||||
>
|
||||
客服 {message.agentRead ? "已读" : "未读"} / 用户{" "}
|
||||
{message.customerRead ? "已读" : "未读"}
|
||||
{t("conversationMonitor.readStatus", {
|
||||
agent: message.agentRead
|
||||
? t("conversationMonitor.read")
|
||||
: t("conversationMonitor.unread"),
|
||||
customer: message.customerRead
|
||||
? t("conversationMonitor.read")
|
||||
: t("conversationMonitor.unread"),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,7 +513,7 @@ export function ConversationDetailDialog({
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full min-h-80 items-center justify-center rounded-xl border border-dashed bg-background text-sm text-muted-foreground">
|
||||
暂无消息记录
|
||||
{t("conversationMonitor.emptyMessages")}
|
||||
</div>
|
||||
)}
|
||||
<div ref={messageBottomRef} />
|
||||
@@ -505,7 +524,7 @@ export function ConversationDetailDialog({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
暂无可展示的会话详情
|
||||
{t("conversationMonitor.emptyDetail")}
|
||||
</div>
|
||||
)}
|
||||
</ProjectDialog>
|
||||
@@ -534,10 +553,11 @@ type MessageImageProps = {
|
||||
};
|
||||
|
||||
function MessageImage({ src, alt, onPreview }: MessageImageProps) {
|
||||
const t = useI18n();
|
||||
if (!src) {
|
||||
return (
|
||||
<div className="text-sm whitespace-pre-wrap break-words">
|
||||
{alt || "[图片]"}
|
||||
{alt || t("conversationMonitor.imageFallback")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -550,7 +570,7 @@ function MessageImage({ src, alt, onPreview }: MessageImageProps) {
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt || "消息图片"}
|
||||
alt={alt || t("conversationMonitor.messageImageAlt")}
|
||||
width={480}
|
||||
height={360}
|
||||
className="max-h-64 w-auto max-w-full rounded-md object-contain"
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
} from "lucide-react"
|
||||
import { useCallback, useEffect, useRef, useState, type KeyboardEvent } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { ConversationCloseDialog } from "@/components/conversation-actions/close-dialog"
|
||||
@@ -33,13 +33,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -66,50 +59,57 @@ import {
|
||||
type PageResult,
|
||||
type TagTree,
|
||||
} from "@/lib/api/admin"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { ConversationDetailDialog } from "./_components/detail"
|
||||
|
||||
const RECONNECT_BASE_DELAY = 2000
|
||||
const RECONNECT_MAX_DELAY = 30000
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "all", label: "全部状态" },
|
||||
{ value: "1", label: "AI接待中" },
|
||||
{ value: "2", label: "待接入" },
|
||||
{ value: "3", label: "处理中" },
|
||||
{ value: "4", label: "已关闭" },
|
||||
] as const
|
||||
|
||||
function getStatusMeta(status: number) {
|
||||
function getStatusMeta(
|
||||
status: number,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return { label: "AI接待中", variant: "secondary" as const }
|
||||
return { label: t("conversation.filterAiServing"), variant: "secondary" as const }
|
||||
case 2:
|
||||
return { label: "待接入", variant: "outline" as const }
|
||||
return { label: t("conversation.filterPending"), variant: "outline" as const }
|
||||
case 3:
|
||||
return { label: "处理中", variant: "secondary" as const }
|
||||
return { label: t("conversation.filterActive"), variant: "secondary" as const }
|
||||
case 4:
|
||||
return { label: "已关闭", variant: "outline" as const }
|
||||
return { label: t("conversation.filterClosed"), variant: "outline" as const }
|
||||
default:
|
||||
return { label: "未知", variant: "outline" as const }
|
||||
return { label: t("conversationMonitor.unknown"), variant: "outline" as const }
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceModeLabel(mode: number) {
|
||||
function getServiceModeLabel(
|
||||
mode: number,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
switch (mode) {
|
||||
case 1:
|
||||
return "AI 接待"
|
||||
return t("conversationMonitor.serviceAi")
|
||||
case 2:
|
||||
return "人工接待"
|
||||
return t("conversationMonitor.serviceHuman")
|
||||
case 3:
|
||||
return "AI 优先"
|
||||
return t("conversationMonitor.serviceAiFirst")
|
||||
default:
|
||||
return "未定义"
|
||||
return t("conversationMonitor.serviceUndefined")
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(value: string) {
|
||||
return statusOptions.find((item) => item.value === value)?.label ?? "全部状态"
|
||||
function getStatusOptions(
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
return [
|
||||
{ value: "all", label: t("conversationMonitor.allStatuses") },
|
||||
{ value: "1", label: t("conversation.filterAiServing") },
|
||||
{ value: "2", label: t("conversation.filterPending") },
|
||||
{ value: "3", label: t("conversation.filterActive") },
|
||||
{ value: "4", label: t("conversation.filterClosed") },
|
||||
]
|
||||
}
|
||||
|
||||
function buildTagOptions(
|
||||
@@ -131,6 +131,8 @@ function buildTagOptions(
|
||||
}
|
||||
|
||||
export default function DashboardConversationsPage() {
|
||||
const t = useI18n()
|
||||
const statusOptions = useMemo(() => getStatusOptions(t), [t])
|
||||
const [keywordInput, setKeywordInput] = useState("")
|
||||
const [statusFilterInput, setStatusFilterInput] = useState("all")
|
||||
const [tagFilterInput, setTagFilterInput] = useState("0")
|
||||
@@ -144,13 +146,13 @@ export default function DashboardConversationsPage() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [tagOptions, setTagOptions] = useState<ComboboxOption[]>([
|
||||
{ value: "0", label: "全部标签" },
|
||||
{ value: "0", label: t("conversationMonitor.allTags") },
|
||||
])
|
||||
const [assigneeOptions, setAssigneeOptions] = useState<ComboboxOption[]>([
|
||||
{ value: "0", label: "全部指派人" },
|
||||
{ value: "0", label: t("conversationMonitor.allAssignees") },
|
||||
])
|
||||
const [agentTeamOptions, setAgentTeamOptions] = useState<ComboboxOption[]>([
|
||||
{ value: "0", label: "全部客服组" },
|
||||
{ value: "0", label: t("conversationMonitor.allTeams") },
|
||||
])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
@@ -194,11 +196,11 @@ export default function DashboardConversationsPage() {
|
||||
})
|
||||
setResult(data)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "加载会话列表失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.loadListFailed"))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [keyword, limit, page, statusFilter, tagFilter, assigneeFilter, agentTeamFilter])
|
||||
}, [keyword, limit, page, statusFilter, tagFilter, assigneeFilter, agentTeamFilter, t])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -212,18 +214,18 @@ export default function DashboardConversationsPage() {
|
||||
])
|
||||
if (!cancelled) {
|
||||
setTagOptions([
|
||||
{ value: "0", label: "全部标签" },
|
||||
{ value: "0", label: t("conversationMonitor.allTags") },
|
||||
...buildTagOptions(tagData),
|
||||
])
|
||||
setAssigneeOptions([
|
||||
{ value: "0", label: "全部指派人" },
|
||||
{ value: "0", label: t("conversationMonitor.allAssignees") },
|
||||
...assigneeData.map((item: AdminAgentProfile) => ({
|
||||
value: String(item.userId),
|
||||
label: item.displayName || item.nickname || item.username || `#${item.userId}`,
|
||||
})),
|
||||
])
|
||||
setAgentTeamOptions([
|
||||
{ value: "0", label: "全部客服组" },
|
||||
{ value: "0", label: t("conversationMonitor.allTeams") },
|
||||
...teamData.map((item: AdminAgentTeam) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
@@ -232,7 +234,7 @@ export default function DashboardConversationsPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
toast.error(error instanceof Error ? error.message : "加载筛选项失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.loadFiltersFailed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +244,7 @@ export default function DashboardConversationsPage() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
detailItemRef.current = detailItem
|
||||
@@ -290,7 +292,7 @@ export default function DashboardConversationsPage() {
|
||||
try {
|
||||
socket = new WebSocket(createAdminWebSocketUrl())
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "连接实时服务失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.realtimeConnectFailed"))
|
||||
scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -382,7 +384,7 @@ export default function DashboardConversationsPage() {
|
||||
}
|
||||
subscribedConversationIdRef.current = null
|
||||
}
|
||||
}, [loadConversations])
|
||||
}, [loadConversations, t])
|
||||
|
||||
useEffect(() => {
|
||||
const socket = websocketRef.current
|
||||
@@ -466,7 +468,7 @@ export default function DashboardConversationsPage() {
|
||||
setDetailMessagesNextCursor(messages.cursor ?? "")
|
||||
setDetailMessagesHasMore(Boolean(messages.hasMore))
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "加载会话详情失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.loadDetailFailed"))
|
||||
} finally {
|
||||
setDetailLoading(false)
|
||||
}
|
||||
@@ -492,7 +494,7 @@ export default function DashboardConversationsPage() {
|
||||
setDetailMessagesNextCursor(page.cursor ?? "")
|
||||
setDetailMessagesHasMore(Boolean(page.hasMore))
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "加载更多消息失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.loadMoreMessagesFailed"))
|
||||
} finally {
|
||||
setDetailMessagesLoadingMore(false)
|
||||
}
|
||||
@@ -501,6 +503,7 @@ export default function DashboardConversationsPage() {
|
||||
detailMessagesHasMore,
|
||||
detailMessagesLoadingMore,
|
||||
detailMessagesNextCursor,
|
||||
t,
|
||||
])
|
||||
|
||||
async function openDetail(item: AdminConversation) {
|
||||
@@ -603,13 +606,13 @@ export default function DashboardConversationsPage() {
|
||||
setActionLoadingId(item.id)
|
||||
try {
|
||||
await markConversationRead(item.id)
|
||||
toast.success(`已标记已读:${item.customerName || `#${item.id}`}`)
|
||||
toast.success(t("conversationMonitor.markedRead", { name: item.customerName || `#${item.id}` }))
|
||||
await loadConversations()
|
||||
if (detailItem?.id === item.id) {
|
||||
await refreshDetail()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "标记已读失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.markReadFailed"))
|
||||
} finally {
|
||||
setActionLoadingId(null)
|
||||
}
|
||||
@@ -619,13 +622,13 @@ export default function DashboardConversationsPage() {
|
||||
setActionLoadingId(item.id)
|
||||
try {
|
||||
await dispatchConversation(item.id)
|
||||
toast.success(`已触发自动分配:${item.customerName || `#${item.id}`}`)
|
||||
toast.success(t("conversationMonitor.dispatchTriggered", { name: item.customerName || `#${item.id}` }))
|
||||
await loadConversations()
|
||||
if (detailItem?.id === item.id) {
|
||||
await refreshDetail()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "自动分配失败")
|
||||
toast.error(error instanceof Error ? error.message : t("conversationMonitor.dispatchFailed"))
|
||||
} finally {
|
||||
setActionLoadingId(null)
|
||||
}
|
||||
@@ -649,7 +652,7 @@ export default function DashboardConversationsPage() {
|
||||
disabled={loading || refreshing}
|
||||
>
|
||||
<RefreshCwIcon className={loading || refreshing ? "animate-spin" : ""} />
|
||||
刷新
|
||||
{t("conversationMonitor.refresh")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
@@ -659,29 +662,25 @@ export default function DashboardConversationsPage() {
|
||||
value={keywordInput}
|
||||
onChange={(event) => setKeywordInput(event.target.value)}
|
||||
onKeyDown={handleFilterKeyDown}
|
||||
placeholder="按主题或摘要筛选"
|
||||
placeholder={t("conversationMonitor.keywordPlaceholder")}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilterInput} onValueChange={handleStatusFilterChange}>
|
||||
<SelectTrigger className="w-full sm:w-36">
|
||||
<SelectValue>{getStatusLabel(statusFilterInput)}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{statusOptions.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="w-full sm:w-36">
|
||||
<OptionCombobox
|
||||
value={statusFilterInput}
|
||||
options={statusOptions}
|
||||
placeholder={t("conversationMonitor.allStatuses")}
|
||||
onChange={handleStatusFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-64">
|
||||
<OptionCombobox
|
||||
value={tagFilterInput}
|
||||
options={tagOptions}
|
||||
placeholder="选择标签"
|
||||
searchPlaceholder="搜索标签路径"
|
||||
emptyText="没有匹配标签"
|
||||
placeholder={t("conversationMonitor.selectTag")}
|
||||
searchPlaceholder={t("conversationMonitor.searchTagPath")}
|
||||
emptyText={t("conversationMonitor.emptyTags")}
|
||||
onChange={setTagFilterInput}
|
||||
/>
|
||||
</div>
|
||||
@@ -689,9 +688,9 @@ export default function DashboardConversationsPage() {
|
||||
<OptionCombobox
|
||||
value={assigneeFilterInput}
|
||||
options={assigneeOptions}
|
||||
placeholder="选择指派人"
|
||||
searchPlaceholder="搜索指派人"
|
||||
emptyText="没有匹配指派人"
|
||||
placeholder={t("conversationMonitor.selectAssignee")}
|
||||
searchPlaceholder={t("conversationMonitor.searchAssignee")}
|
||||
emptyText={t("conversationMonitor.emptyAssignees")}
|
||||
onChange={setAssigneeFilterInput}
|
||||
/>
|
||||
</div>
|
||||
@@ -699,15 +698,15 @@ export default function DashboardConversationsPage() {
|
||||
<OptionCombobox
|
||||
value={agentTeamFilterInput}
|
||||
options={agentTeamOptions}
|
||||
placeholder="选择客服组"
|
||||
searchPlaceholder="搜索客服组"
|
||||
emptyText="没有匹配客服组"
|
||||
placeholder={t("conversationMonitor.selectTeam")}
|
||||
searchPlaceholder={t("conversationMonitor.searchTeam")}
|
||||
emptyText={t("conversationMonitor.emptyTeams")}
|
||||
onChange={setAgentTeamFilterInput}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" onClick={applyFilters} disabled={loading}>
|
||||
<SearchIcon />
|
||||
查询
|
||||
{t("conversationMonitor.query")}
|
||||
</Button>
|
||||
</DashboardToolbar>
|
||||
|
||||
@@ -726,39 +725,51 @@ export default function DashboardConversationsPage() {
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/40">
|
||||
<TableRow>
|
||||
<TableHead>会话信息</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>接待模式</TableHead>
|
||||
<TableHead>当前客服</TableHead>
|
||||
<TableHead>未读</TableHead>
|
||||
<TableHead>最后活跃</TableHead>
|
||||
<TableHead className="w-28 text-right">操作</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnConversation")}</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnStatus")}</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnServiceMode")}</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnAssignee")}</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnUnread")}</TableHead>
|
||||
<TableHead>{t("conversationMonitor.columnLastActive")}</TableHead>
|
||||
<TableHead className="w-28 text-right">
|
||||
{t("conversationMonitor.columnActions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{result.results.map((item) => {
|
||||
const statusMeta = getStatusMeta(item.status)
|
||||
const statusMeta = getStatusMeta(item.status, t)
|
||||
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="max-w-60">
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">{item.customerName || `客户 #${item.customerId || item.id}`}</div>
|
||||
<div className="font-medium">
|
||||
{item.customerName ||
|
||||
t("conversationMonitor.customerFallback", {
|
||||
id: item.customerId || item.id,
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
渠道ID:{item.channelId || "-"}
|
||||
{t("conversationMonitor.channelIdValue", {
|
||||
id: item.channelId || "-",
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||||
{item.lastMessageSummary || "暂无最新消息摘要"}
|
||||
{item.lastMessageSummary || t("conversationMonitor.noLatestSummary")}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusMeta.variant}>{statusMeta.label}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{getServiceModeLabel(item.serviceMode)}</TableCell>
|
||||
<TableCell>{getServiceModeLabel(item.serviceMode, t)}</TableCell>
|
||||
<TableCell>{item.currentAssigneeName || "-"}</TableCell>
|
||||
<TableCell>
|
||||
客服 {item.agentUnreadCount} / 用户 {item.customerUnreadCount}
|
||||
{t("conversationMonitor.unreadSummary", {
|
||||
agent: item.agentUnreadCount,
|
||||
customer: item.customerUnreadCount,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>{formatDateTime(item.lastMessageAt)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
@@ -769,12 +780,14 @@ export default function DashboardConversationsPage() {
|
||||
onClick={() => void openDetail(item)}
|
||||
disabled={actionLoadingId === item.id}
|
||||
>
|
||||
查看
|
||||
{t("conversationMonitor.view")}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="outline" size="icon-sm" />}
|
||||
aria-label={`更多操作 ${item.customerName || item.id}`}
|
||||
aria-label={t("conversationMonitor.moreActions", {
|
||||
name: item.customerName || item.id,
|
||||
})}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
@@ -784,28 +797,36 @@ export default function DashboardConversationsPage() {
|
||||
disabled={actionLoadingId === item.id || item.status !== 2}
|
||||
>
|
||||
<MessageCircleMoreIcon />
|
||||
{actionLoadingId === item.id ? "处理中..." : "分配会话"}
|
||||
{actionLoadingId === item.id
|
||||
? t("conversationMonitor.processing")
|
||||
: t("conversationMonitor.assign")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void handleDispatch(item)}
|
||||
disabled={actionLoadingId === item.id || item.status !== 2}
|
||||
>
|
||||
<RefreshCwIcon />
|
||||
{actionLoadingId === item.id ? "处理中..." : "重试分配"}
|
||||
{actionLoadingId === item.id
|
||||
? t("conversationMonitor.processing")
|
||||
: t("conversationMonitor.retryDispatch")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void handleRead(item)}
|
||||
disabled={actionLoadingId === item.id}
|
||||
>
|
||||
<CheckCheckIcon />
|
||||
{actionLoadingId === item.id ? "处理中..." : "标记已读"}
|
||||
{actionLoadingId === item.id
|
||||
? t("conversationMonitor.processing")
|
||||
: t("conversationMonitor.markRead")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => openTransfer(item)}
|
||||
disabled={actionLoadingId === item.id || item.status !== 3}
|
||||
>
|
||||
<MessageCircleMoreIcon />
|
||||
{actionLoadingId === item.id ? "处理中..." : "转接会话"}
|
||||
{actionLoadingId === item.id
|
||||
? t("conversationMonitor.processing")
|
||||
: t("conversationMonitor.transfer")}
|
||||
</DropdownMenuItem>
|
||||
{item.status !== 4 ? (
|
||||
<DropdownMenuItem
|
||||
@@ -813,7 +834,9 @@ export default function DashboardConversationsPage() {
|
||||
disabled={actionLoadingId === item.id}
|
||||
>
|
||||
<RefreshCwIcon />
|
||||
{actionLoadingId === item.id ? "处理中..." : "关闭会话"}
|
||||
{actionLoadingId === item.id
|
||||
? t("conversationMonitor.processing")
|
||||
: t("conversationMonitor.close")}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
@@ -827,8 +850,8 @@ export default function DashboardConversationsPage() {
|
||||
<DashboardTableStateRow
|
||||
colSpan={7}
|
||||
loading={loading}
|
||||
loadingText="正在加载会话记录..."
|
||||
emptyText="没有匹配的会话记录"
|
||||
loadingText={t("conversationMonitor.loadingRows")}
|
||||
emptyText={t("conversationMonitor.emptyRows")}
|
||||
/>
|
||||
) : null}
|
||||
</TableBody>
|
||||
|
||||
Reference in New Issue
Block a user