refactor: support i18n
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
|
||||
const windowActionButtonClass =
|
||||
"size-8 rounded-md border-0 bg-transparent text-muted-foreground shadow-none hover:bg-foreground/[0.06] hover:text-foreground focus-visible:ring-primary/20 dark:hover:bg-white/10"
|
||||
@@ -119,6 +120,7 @@ function isEmbeddedInHost() {
|
||||
}
|
||||
|
||||
export function KefuChatShell() {
|
||||
const t = useI18n()
|
||||
useKefuSystemTheme()
|
||||
|
||||
const messageListRef = useRef<KefuMessageListHandle | null>(null)
|
||||
@@ -271,7 +273,7 @@ export function KefuChatShell() {
|
||||
window.location.replace(getStandaloneClosedUrl())
|
||||
}
|
||||
} catch (closeError) {
|
||||
window.alert(closeError instanceof Error ? closeError.message : "关闭会话失败")
|
||||
window.alert(closeError instanceof Error ? closeError.message : t("kefu.closeConversationFailed"))
|
||||
} finally {
|
||||
setIsClosingConversation(false)
|
||||
}
|
||||
@@ -324,8 +326,8 @@ export function KefuChatShell() {
|
||||
{!isEmbedded && status !== "connected" ? (
|
||||
<WindowActionButton
|
||||
onClick={retry}
|
||||
aria-label="重新连接"
|
||||
title="重新连接"
|
||||
aria-label={t("kefu.retry")}
|
||||
title={t("kefu.retry")}
|
||||
>
|
||||
<RotateCwIcon className="size-4" />
|
||||
</WindowActionButton>
|
||||
@@ -333,18 +335,18 @@ export function KefuChatShell() {
|
||||
{isEmbedded ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<WindowActionButton aria-label="更多操作" title="更多操作" />}
|
||||
render={<WindowActionButton aria-label={t("kefu.moreActions")} title={t("kefu.moreActions")} />}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
<DropdownMenuItem onClick={retry}>
|
||||
<RotateCwIcon className="size-4" />
|
||||
重新连接
|
||||
{t("kefu.retry")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleMinimize}>
|
||||
<MinusIcon className="size-4" />
|
||||
收起窗口
|
||||
{t("kefu.minimize")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleToggleMaximize}>
|
||||
{isMaximized ? (
|
||||
@@ -352,22 +354,22 @@ export function KefuChatShell() {
|
||||
) : (
|
||||
<Maximize2Icon className="size-4" />
|
||||
)}
|
||||
{isMaximized ? "取消最大化" : "最大化"}
|
||||
{isMaximized ? t("kefu.restoreWindow") : t("kefu.maximize")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setIsCloseDialogOpen(true)}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
关闭窗口
|
||||
{t("kefu.closeWindow")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<WindowActionButton
|
||||
onClick={() => setIsCloseDialogOpen(true)}
|
||||
aria-label="关闭聊天窗口"
|
||||
title="关闭聊天窗口"
|
||||
aria-label={t("kefu.closeChatWindow")}
|
||||
title={t("kefu.closeChatWindow")}
|
||||
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
@@ -381,8 +383,8 @@ export function KefuChatShell() {
|
||||
<div className="flex items-center gap-0.5 rounded-lg bg-background/55 p-0.5 shadow-sm ring-1 ring-border/70 dark:bg-background/25 dark:ring-white/10">
|
||||
<WindowActionButton
|
||||
onClick={retry}
|
||||
aria-label="重新连接"
|
||||
title="重新连接"
|
||||
aria-label={t("kefu.retry")}
|
||||
title={t("kefu.retry")}
|
||||
>
|
||||
<RotateCwIcon className="size-4" />
|
||||
</WindowActionButton>
|
||||
@@ -390,15 +392,15 @@ export function KefuChatShell() {
|
||||
<>
|
||||
<WindowActionButton
|
||||
onClick={handleMinimize}
|
||||
aria-label="收起聊天窗口"
|
||||
title="收起聊天窗口"
|
||||
aria-label={t("kefu.minimize")}
|
||||
title={t("kefu.minimize")}
|
||||
>
|
||||
<MinusIcon className="size-4" />
|
||||
</WindowActionButton>
|
||||
<WindowActionButton
|
||||
onClick={handleToggleMaximize}
|
||||
aria-label={isMaximized ? "取消最大化" : "最大化聊天窗口"}
|
||||
title={isMaximized ? "取消最大化" : "最大化聊天窗口"}
|
||||
aria-label={isMaximized ? t("kefu.restoreWindow") : t("kefu.maximizeWindow")}
|
||||
title={isMaximized ? t("kefu.restoreWindow") : t("kefu.maximizeWindow")}
|
||||
>
|
||||
{isMaximized ? (
|
||||
<Minimize2Icon className="size-4" />
|
||||
@@ -410,8 +412,8 @@ export function KefuChatShell() {
|
||||
) : null}
|
||||
<WindowActionButton
|
||||
onClick={() => setIsCloseDialogOpen(true)}
|
||||
aria-label="关闭聊天窗口"
|
||||
title="关闭聊天窗口"
|
||||
aria-label={t("kefu.closeChatWindow")}
|
||||
title={t("kefu.closeChatWindow")}
|
||||
className="hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
@@ -457,9 +459,9 @@ export function KefuChatShell() {
|
||||
>
|
||||
<DialogContent className="max-w-[320px]" showCloseButton={!isClosingConversation}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>结束当前对话?</DialogTitle>
|
||||
<DialogTitle>{t("kefu.closeDialogTitle")}</DialogTitle>
|
||||
<DialogDescription className="text-xs leading-5">
|
||||
结束会话,客服将无法再查看您的消息记录,如需再次联系请重新发起对话。
|
||||
{t("kefu.closeDialogDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -469,7 +471,7 @@ export function KefuChatShell() {
|
||||
disabled={isClosingConversation}
|
||||
onClick={() => setIsCloseDialogOpen(false)}
|
||||
>
|
||||
继续对话
|
||||
{t("kefu.continueConversation")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -477,7 +479,7 @@ export function KefuChatShell() {
|
||||
disabled={isClosingConversation}
|
||||
onClick={() => void confirmCloseConversation()}
|
||||
>
|
||||
{isClosingConversation ? "结束中..." : "确认结束"}
|
||||
{isClosingConversation ? t("kefu.closing") : t("kefu.confirmClose")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
|
||||
type KefuConnectionStatusProps = {
|
||||
status: "connecting" | "connected" | "disconnected"
|
||||
}
|
||||
|
||||
const statusText: Record<KefuConnectionStatusProps["status"], string> = {
|
||||
connecting: "连接中",
|
||||
connected: "在线服务",
|
||||
disconnected: "连接已断开",
|
||||
}
|
||||
|
||||
export function KefuConnectionStatus({ status }: KefuConnectionStatusProps) {
|
||||
const t = useI18n()
|
||||
const toneClass =
|
||||
status === "connected"
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/70 dark:bg-emerald-950/50 dark:text-emerald-300"
|
||||
@@ -36,7 +32,7 @@ export function KefuConnectionStatus({ status }: KefuConnectionStatusProps) {
|
||||
: "bg-muted-foreground shadow-[0_0_0_4px_rgba(148,163,184,0.14)]"
|
||||
)}
|
||||
/>
|
||||
<span>{statusText[status]}</span>
|
||||
<span>{t(`kefu.${status}`)}</span>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"
|
||||
import type { ImMessage } from "@/lib/api/im"
|
||||
import { renderIMMessageHTML } from "@/lib/im-message"
|
||||
import { cn, formatDateTime } from "@/lib/utils"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
|
||||
type KefuMessageListProps = {
|
||||
messages?: ImMessage[] | null
|
||||
@@ -44,9 +45,12 @@ function getDayKey(value?: string) {
|
||||
).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function getTimelineLabel(value?: string) {
|
||||
function getTimelineLabel(
|
||||
value: string | undefined,
|
||||
t: (key: string, values?: Record<string, string | number>) => string
|
||||
) {
|
||||
if (!value) {
|
||||
return "刚刚"
|
||||
return t("kefu.justNow")
|
||||
}
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
@@ -58,7 +62,7 @@ function getTimelineLabel(value?: string) {
|
||||
date.getMinutes()
|
||||
).padStart(2, "0")}`
|
||||
if (currentDayKey === todayDayKey) {
|
||||
return `今天 ${timeText}`
|
||||
return t("kefu.todayAt", { time: timeText })
|
||||
}
|
||||
return `${currentDayKey} ${timeText}`
|
||||
}
|
||||
@@ -74,6 +78,7 @@ export const KefuMessageList = forwardRef<KefuMessageListHandle, KefuMessageList
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const t = useI18n()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const frameRef = useRef<number | null>(null)
|
||||
@@ -227,14 +232,14 @@ export const KefuMessageList = forwardRef<KefuMessageListHandle, KefuMessageList
|
||||
onClick={() => void handleLoadOlder()}
|
||||
className="h-7 rounded-full bg-background/90 text-xs text-muted-foreground shadow-sm hover:bg-background hover:text-sky-700 dark:hover:text-sky-400"
|
||||
>
|
||||
{loadingOlder ? "加载中…" : "加载更早的消息"}
|
||||
{loadingOlder ? t("kefu.loadingOlder") : t("kefu.loadOlder")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{safeMessages.length === 0 ? (
|
||||
<div className="flex min-h-32 items-center justify-center px-3 py-6 text-center text-sm leading-6 text-muted-foreground">
|
||||
请描述你的问题,我们会尽快为你处理。
|
||||
{t("kefu.emptyPrompt")}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -250,6 +255,7 @@ export const KefuMessageList = forwardRef<KefuMessageListHandle, KefuMessageList
|
||||
message={message}
|
||||
showTimeline={showTimeline}
|
||||
onImageSettled={handleImageSettled}
|
||||
timelineLabel={getTimelineLabel(message.sentAt, t)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -263,13 +269,15 @@ type MessageItemProps = {
|
||||
message: ImMessage
|
||||
showTimeline: boolean
|
||||
onImageSettled: () => void
|
||||
timelineLabel: string
|
||||
}
|
||||
|
||||
const MessageItem = memo(
|
||||
function MessageItem({ message, showTimeline, onImageSettled }: MessageItemProps) {
|
||||
function MessageItem({ message, showTimeline, onImageSettled, timelineLabel }: MessageItemProps) {
|
||||
const t = useI18n()
|
||||
const { open } = useImageLightbox()
|
||||
const isCustomer = message.senderType === "customer"
|
||||
const senderName = isCustomer ? "我" : message.senderName?.trim() || "客服"
|
||||
const senderName = isCustomer ? t("kefu.customerSelf") : message.senderName?.trim() || t("kefu.agentLabel")
|
||||
const avatarSrc =
|
||||
!isCustomer && message.senderAvatar?.trim() ? message.senderAvatar.trim() : undefined
|
||||
const htmlContent = renderIMMessageHTML(message)
|
||||
@@ -283,7 +291,7 @@ const MessageItem = memo(
|
||||
variant="outline"
|
||||
className="border-border bg-background/85 text-[11px] font-medium text-muted-foreground shadow-sm"
|
||||
>
|
||||
{getTimelineLabel(message.sentAt)}
|
||||
{timelineLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -293,7 +301,7 @@ const MessageItem = memo(
|
||||
<Avatar className="mt-5">
|
||||
{avatarSrc ? <AvatarImage src={avatarSrc} alt="" /> : null}
|
||||
<AvatarFallback className="bg-muted text-muted-foreground">
|
||||
{fallbackName || "客"}
|
||||
{fallbackName || t("kefu.customerFallback")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
) : null}
|
||||
@@ -308,7 +316,7 @@ const MessageItem = memo(
|
||||
<span className="font-medium">{senderName}</span>
|
||||
<span>{formatDateTime(message.sentAt)}</span>
|
||||
{isCustomer ? (
|
||||
<span>{message.agentRead ? "客服已读" : "客服未读"}</span>
|
||||
<span>{message.agentRead ? t("kefu.agentRead") : t("kefu.agentUnread")}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
@@ -338,6 +346,7 @@ const MessageItem = memo(
|
||||
(prevProps, nextProps) =>
|
||||
isSameMessageItemRender(prevProps.message, nextProps.message) &&
|
||||
prevProps.showTimeline === nextProps.showTimeline &&
|
||||
prevProps.timelineLabel === nextProps.timelineLabel &&
|
||||
prevProps.onImageSettled === nextProps.onImageSettled
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CheckIcon, CopyIcon } from "lucide-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
|
||||
import type { CSAgentConfig } from "@/lib/sdk/config-types"
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
|
||||
const STORAGE_KEY = "cs-agent-web-widget-test-config"
|
||||
const DEFAULT_JWT_TTL_MINUTES = "30"
|
||||
@@ -24,7 +25,7 @@ type WidgetDemoConfig = CSAgentConfig & {
|
||||
jwtTtlMinutes?: string
|
||||
}
|
||||
|
||||
function getDefaultConfig(): WidgetDemoConfig {
|
||||
function getDefaultConfig(defaultName: string): WidgetDemoConfig {
|
||||
if (typeof window === "undefined") {
|
||||
return INITIAL_CONFIG
|
||||
}
|
||||
@@ -42,7 +43,7 @@ function getDefaultConfig(): WidgetDemoConfig {
|
||||
authMode: (query.get("authMode") as AuthMode | null) ?? savedConfig.authMode ?? "guest",
|
||||
jwtSecret: savedConfig.jwtSecret ?? "",
|
||||
jwtUserId: query.get("userId") ?? savedConfig.jwtUserId ?? "demo-user-001",
|
||||
jwtName: query.get("name") ?? savedConfig.jwtName ?? "测试用户",
|
||||
jwtName: query.get("name") ?? savedConfig.jwtName ?? defaultName,
|
||||
jwtTtlMinutes: savedConfig.jwtTtlMinutes ?? DEFAULT_JWT_TTL_MINUTES,
|
||||
}
|
||||
}
|
||||
@@ -83,28 +84,28 @@ function buildWidgetConfig(config: WidgetDemoConfig): CSAgentConfig {
|
||||
apiBaseUrl: "",
|
||||
}
|
||||
if (config.authMode === "jwt") {
|
||||
nextConfig.getUserToken = () => signUserToken(config)
|
||||
nextConfig.getUserToken = undefined
|
||||
}
|
||||
return nextConfig
|
||||
}
|
||||
|
||||
async function signUserToken(config: WidgetDemoConfig) {
|
||||
async function signUserToken(config: WidgetDemoConfig, t: (key: string) => string) {
|
||||
const userId = (config.jwtUserId || "").trim()
|
||||
const name = (config.jwtName || "").trim()
|
||||
const secret = (config.jwtSecret || "").trim()
|
||||
const ttl = Number(config.jwtTtlMinutes || DEFAULT_JWT_TTL_MINUTES)
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("请填写 userId")
|
||||
throw new Error(t("widgetDemo.missingUserId"))
|
||||
}
|
||||
if (!name) {
|
||||
throw new Error("请填写用户名称")
|
||||
throw new Error(t("widgetDemo.missingName"))
|
||||
}
|
||||
if (!secret) {
|
||||
throw new Error("请填写 JWT Secret")
|
||||
throw new Error(t("widgetDemo.missingSecret"))
|
||||
}
|
||||
if (!Number.isFinite(ttl) || ttl <= 0) {
|
||||
throw new Error("有效期必须大于 0")
|
||||
throw new Error(t("widgetDemo.invalidTtl"))
|
||||
}
|
||||
|
||||
return new SignJWT({ userId, name })
|
||||
@@ -115,15 +116,16 @@ async function signUserToken(config: WidgetDemoConfig) {
|
||||
}
|
||||
|
||||
export function KefuWidgetDemo() {
|
||||
const t = useI18n()
|
||||
const [config, setConfig] = useState<WidgetDemoConfig>({
|
||||
...INITIAL_CONFIG,
|
||||
authMode: "guest",
|
||||
jwtSecret: "",
|
||||
jwtUserId: "demo-user-001",
|
||||
jwtName: "测试用户",
|
||||
jwtName: t("widgetDemo.defaultName"),
|
||||
jwtTtlMinutes: DEFAULT_JWT_TTL_MINUTES,
|
||||
})
|
||||
const [status, setStatus] = useState("请填写 channelId")
|
||||
const [status, setStatus] = useState(t("widgetDemo.missingChannel"))
|
||||
const [origin, setOrigin] = useState("")
|
||||
const [generatedToken, setGeneratedToken] = useState("")
|
||||
const [latestDirectChatUrl, setLatestDirectChatUrl] = useState("")
|
||||
@@ -139,6 +141,9 @@ export function KefuWidgetDemo() {
|
||||
getUserToken: undefined,
|
||||
}
|
||||
const nextConfig = buildWidgetConfig(cleanConfig)
|
||||
if (cleanConfig.authMode === "jwt") {
|
||||
nextConfig.getUserToken = () => signUserToken(cleanConfig, t)
|
||||
}
|
||||
setConfig(cleanConfig)
|
||||
setGeneratedToken("")
|
||||
setLatestDirectChatUrl("")
|
||||
@@ -146,30 +151,30 @@ export function KefuWidgetDemo() {
|
||||
|
||||
if (!nextConfig.channelId) {
|
||||
removeMountedWidget()
|
||||
setStatus("请填写 channelId")
|
||||
setStatus(t("widgetDemo.missingChannel"))
|
||||
return
|
||||
}
|
||||
|
||||
injectWidget(nextConfig)
|
||||
setStatus(
|
||||
cleanConfig.authMode === "jwt"
|
||||
? "Widget 已挂载:JWT 用户模式"
|
||||
: "Widget 已挂载:访客模式"
|
||||
? t("widgetDemo.mountedJwt")
|
||||
: t("widgetDemo.mountedGuest")
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
const initialConfig = getDefaultConfig()
|
||||
const initialConfig = getDefaultConfig(t("widgetDemo.defaultName"))
|
||||
setOrigin(window.location.origin)
|
||||
setConfig(initialConfig)
|
||||
setStatus(initialConfig.channelId ? "Widget 已挂载" : "请填写 channelId")
|
||||
setStatus(initialConfig.channelId ? t("widgetDemo.mounted") : t("widgetDemo.missingChannel"))
|
||||
|
||||
if (initialConfig.channelId) {
|
||||
void mountWidget(initialConfig).catch((error) => {
|
||||
removeMountedWidget()
|
||||
setGeneratedToken("")
|
||||
setStatus(error instanceof Error ? error.message : "挂载 Widget 失败")
|
||||
setStatus(error instanceof Error ? error.message : t("widgetDemo.mountFailed"))
|
||||
})
|
||||
}
|
||||
}, 0)
|
||||
@@ -178,7 +183,7 @@ export function KefuWidgetDemo() {
|
||||
window.clearTimeout(timer)
|
||||
removeMountedWidget()
|
||||
}
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
const snippet = useMemo(() => {
|
||||
const scriptSrc = origin
|
||||
@@ -215,7 +220,7 @@ ${configLines.join(",\n")}
|
||||
} catch (error) {
|
||||
removeMountedWidget()
|
||||
setGeneratedToken("")
|
||||
setStatus(error instanceof Error ? error.message : "挂载 Widget 失败")
|
||||
setStatus(error instanceof Error ? error.message : t("widgetDemo.mountFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +238,7 @@ ${configLines.join(",\n")}
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1600)
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "生成客服链接失败")
|
||||
setStatus(error instanceof Error ? error.message : t("widgetDemo.linkFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +254,7 @@ ${configLines.join(",\n")}
|
||||
}
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "生成客服链接失败")
|
||||
setStatus(error instanceof Error ? error.message : t("widgetDemo.linkFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +271,7 @@ ${configLines.join(",\n")}
|
||||
<main className="min-h-svh bg-slate-50 px-6 py-8 text-slate-950">
|
||||
<div className="mx-auto grid max-w-6xl gap-6 lg:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="text-base font-semibold">Widget 挂载测试</div>
|
||||
<div className="text-base font-semibold">{t("widgetDemo.title")}</div>
|
||||
<div className="mt-1 text-sm text-slate-500">{status}</div>
|
||||
|
||||
<div className="mt-5 grid gap-3">
|
||||
@@ -276,12 +281,12 @@ ${configLines.join(",\n")}
|
||||
onChange={(value) => updateField("channelId", value)}
|
||||
/>
|
||||
<SegmentedControl
|
||||
label="鉴权模式"
|
||||
label={t("widgetDemo.authMode")}
|
||||
value={config.authMode || "guest"}
|
||||
onChange={(value) => updateField("authMode", value)}
|
||||
options={[
|
||||
{ label: "访客", value: "guest" },
|
||||
{ label: "JWT 用户", value: "jwt" },
|
||||
{ label: t("widgetDemo.guest"), value: "guest" },
|
||||
{ label: t("widgetDemo.jwtUser"), value: "jwt" },
|
||||
]}
|
||||
/>
|
||||
{config.authMode === "jwt" ? (
|
||||
@@ -303,7 +308,7 @@ ${configLines.join(",\n")}
|
||||
type="password"
|
||||
/>
|
||||
<TextField
|
||||
label="有效期(分钟)"
|
||||
label={t("widgetDemo.ttlMinutes")}
|
||||
value={config.jwtTtlMinutes}
|
||||
onChange={(value) => updateField("jwtTtlMinutes", value)}
|
||||
type="number"
|
||||
@@ -318,26 +323,26 @@ ${configLines.join(",\n")}
|
||||
onClick={() => void handleMount()}
|
||||
className="rounded-md bg-slate-950 px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
挂载
|
||||
{t("widgetDemo.mount")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
removeMountedWidget()
|
||||
setStatus("Widget 已卸载")
|
||||
setStatus(t("widgetDemo.unmounted"))
|
||||
}}
|
||||
className="rounded-md border border-slate-200 bg-white px-4 py-2 text-sm font-medium"
|
||||
>
|
||||
卸载
|
||||
{t("widgetDemo.unmount")}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="text-base font-semibold">接入代码</div>
|
||||
<div className="text-base font-semibold">{t("widgetDemo.snippetTitle")}</div>
|
||||
{config.authMode === "jwt" ? (
|
||||
<div className="mt-2 rounded-md bg-amber-50 px-3 py-2 text-sm text-amber-800">
|
||||
当前页面仅用于本地模拟。正式接入时,userToken 应由业务系统后端签发。
|
||||
{t("widgetDemo.jwtNotice")}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="relative mt-4">
|
||||
@@ -345,8 +350,8 @@ ${configLines.join(",\n")}
|
||||
type="button"
|
||||
onClick={() => void handleCopySnippet()}
|
||||
className="absolute right-2 top-2 inline-flex size-8 items-center justify-center rounded-md border border-white/10 bg-white/10 text-slate-200 transition hover:bg-white/20 hover:text-white"
|
||||
aria-label={snippetCopied ? "已复制接入代码" : "复制接入代码"}
|
||||
title={snippetCopied ? "已复制" : "复制代码"}
|
||||
aria-label={snippetCopied ? t("widgetDemo.copiedSnippet") : t("widgetDemo.copySnippet")}
|
||||
title={snippetCopied ? t("widgetDemo.copied") : t("widgetDemo.copyCode")}
|
||||
>
|
||||
{snippetCopied ? (
|
||||
<CheckIcon className="size-4" />
|
||||
@@ -359,11 +364,11 @@ ${configLines.join(",\n")}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-sm font-medium text-slate-700">直接访问客户对话</div>
|
||||
<div className="text-sm font-medium text-slate-700">{t("widgetDemo.directChat")}</div>
|
||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row">
|
||||
<input
|
||||
readOnly
|
||||
value={latestDirectChatUrl || "点击复制或新窗口打开时生成最新链接"}
|
||||
value={latestDirectChatUrl || t("widgetDemo.directChatPlaceholder")}
|
||||
className="h-9 min-w-0 flex-1 rounded-md border border-slate-200 px-3 font-mono text-xs outline-none"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
@@ -373,7 +378,7 @@ ${configLines.join(",\n")}
|
||||
onClick={() => void handleCopyDirectUrl()}
|
||||
className="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{copied ? "已复制" : "复制"}
|
||||
{copied ? t("widgetDemo.copied") : t("widgetDemo.copy")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -381,14 +386,14 @@ ${configLines.join(",\n")}
|
||||
onClick={() => void handleOpenDirectChat()}
|
||||
className="rounded-md bg-slate-950 px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
新窗口打开
|
||||
{t("widgetDemo.openNewWindow")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{generatedToken ? (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm font-medium text-slate-700">当前 userToken</div>
|
||||
<div className="text-sm font-medium text-slate-700">{t("widgetDemo.currentToken")}</div>
|
||||
<textarea
|
||||
readOnly
|
||||
value={generatedToken}
|
||||
|
||||
Reference in New Issue
Block a user