refactor: support i18n

This commit is contained in:
mlogclub
2026-05-25 12:06:15 +08:00
parent 309ac1fe9e
commit 988f55c80d
179 changed files with 10968 additions and 3763 deletions
@@ -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"
+116 -93
View File
@@ -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>