R50 通知推送:dispatcher 投递服务、业务事件接入与员工推送开关
这个提交包含在:
@@ -4,11 +4,13 @@ import { toast } from "sonner";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import {
|
||||
createNotificationChannel,
|
||||
fetchNotificationDeliveries,
|
||||
fetchNotificationChannels,
|
||||
testNotificationChannel,
|
||||
updateNotificationChannel,
|
||||
type NotificationChannel,
|
||||
type NotificationChannelConfig,
|
||||
type NotificationDeliveryStatus,
|
||||
type NotificationChannelPayload,
|
||||
type NotificationChannelTemplate,
|
||||
type NotificationChannelWriteConfig,
|
||||
@@ -171,6 +173,12 @@ function formatMode(mode: string) {
|
||||
return mode === "wecom_webhook" ? "Webhook" : "Mock";
|
||||
}
|
||||
|
||||
function formatDeliveryStatus(status: NotificationDeliveryStatus) {
|
||||
if (status === "success") return "成功";
|
||||
if (status === "failed") return "失败";
|
||||
return "处理中";
|
||||
}
|
||||
|
||||
function formatSummary(values: string[], labelMap: Record<string, string>, emptyLabel: string) {
|
||||
if (!values.length) return emptyLabel;
|
||||
return values.map((value) => labelMap[value] ?? value).join("、");
|
||||
@@ -219,11 +227,23 @@ export function NotificationChannelsPage() {
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [form, setForm] = useState<ChannelFormState>(buildInitialForm);
|
||||
const [lastTestResult, setLastTestResult] = useState<TestResultState | null>(null);
|
||||
const [deliveryFilters, setDeliveryFilters] = useState<{ channelId: string; status: NotificationDeliveryStatus | "" }>({
|
||||
channelId: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const channelsQuery = useQuery({
|
||||
queryKey: ["notification-channels"],
|
||||
queryFn: () => fetchNotificationChannels(),
|
||||
});
|
||||
const deliveriesQuery = useQuery({
|
||||
queryKey: ["notification-deliveries", deliveryFilters],
|
||||
queryFn: () =>
|
||||
fetchNotificationDeliveries({
|
||||
channelId: deliveryFilters.channelId || null,
|
||||
status: deliveryFilters.status || null,
|
||||
}),
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -308,7 +328,7 @@ export function NotificationChannelsPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="通知渠道"
|
||||
subtitle="维护企微群机器人渠道、业务域路由与测试发送。IM 继续通过 Outbox 异步派发。"
|
||||
subtitle="维护企微群机器人渠道、业务域路由、测试发送与最近投递记录。目标业务通知会在提交后同步尝试外推。"
|
||||
actions={
|
||||
<Button type="button" variant="secondary" onClick={startCreate} data-testid="notification-channels-create">
|
||||
新建渠道
|
||||
@@ -404,6 +424,97 @@ export function NotificationChannelsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">最近投递记录</h3>
|
||||
<p className="text-sm text-zinc-500">展示业务通知同步外推的最近状态,不包含页面里的“测试发送”结果。</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<select
|
||||
className="select-std"
|
||||
value={deliveryFilters.channelId}
|
||||
onChange={(event) => setDeliveryFilters((prev) => ({ ...prev, channelId: event.target.value }))}
|
||||
>
|
||||
<option value="">全部渠道</option>
|
||||
{channels.map((channel) => (
|
||||
<option key={channel.id} value={channel.id}>
|
||||
{channel.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="select-std"
|
||||
value={deliveryFilters.status}
|
||||
onChange={(event) =>
|
||||
setDeliveryFilters((prev) => ({
|
||||
...prev,
|
||||
status: event.target.value as NotificationDeliveryStatus | "",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="failed">失败</option>
|
||||
<option value="pending">处理中</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deliveriesQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
||||
|
||||
{deliveriesQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{resolveError(deliveriesQuery.error)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!deliveriesQuery.isLoading && !deliveriesQuery.isError ? (
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<Table data-testid="notification-deliveries-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
<TableHead>通知</TableHead>
|
||||
<TableHead>渠道</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>尝试次数</TableHead>
|
||||
<TableHead>结果码</TableHead>
|
||||
<TableHead>最近尝试</TableHead>
|
||||
<TableHead>错误摘要</TableHead>
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(deliveriesQuery.data?.items ?? []).length ? (
|
||||
(deliveriesQuery.data?.items ?? []).map((item) => (
|
||||
<TableRow key={`${item.notificationId}:${item.channelId}`}>
|
||||
<TableCell className="max-w-sm">
|
||||
<div className="font-medium text-zinc-900">{item.notificationTitle || item.notificationType}</div>
|
||||
<div className="text-xs text-zinc-500">{item.notificationType}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium text-zinc-900">{item.channelName}</div>
|
||||
<div className="text-xs text-zinc-500">{formatChannelType(item.channelType)}</div>
|
||||
</TableCell>
|
||||
<TableCell>{formatDeliveryStatus(item.status)}</TableCell>
|
||||
<TableCell>{item.attemptCount}</TableCell>
|
||||
<TableCell className="text-xs text-zinc-600">{item.resultCode ?? "-"}</TableCell>
|
||||
<TableCell className="text-xs text-zinc-600">{formatDateTime(item.latestAttemptAt)}</TableCell>
|
||||
<TableCell className="max-w-xs text-xs text-zinc-600">{item.errorMessage ?? "-"}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="py-4 text-center text-sm text-zinc-500">
|
||||
暂无投递记录。
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
|
||||
在新工单中引用
屏蔽一个用户