219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useCallback, useEffect, useState } from "react";
|
|||
|
|
import { readApiResponse } from "./wecom-user-picker";
|
|||
|
|
|
|||
|
|
type CustomerChat = {
|
|||
|
|
chatId: string;
|
|||
|
|
name: string;
|
|||
|
|
ownerUserId: string;
|
|||
|
|
memberCount: number;
|
|||
|
|
status: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type BindResult = {
|
|||
|
|
customerChatId: string;
|
|||
|
|
customerChatName: string;
|
|||
|
|
joinConfigId: string;
|
|||
|
|
joinQrCode: string;
|
|||
|
|
qrError?: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function TaskCustomerChatModal({
|
|||
|
|
task,
|
|||
|
|
onClose,
|
|||
|
|
onBound,
|
|||
|
|
}: {
|
|||
|
|
task: { id: string; name: string; customerChatId: string; customerChatName: string };
|
|||
|
|
onClose: () => void;
|
|||
|
|
onBound: () => void;
|
|||
|
|
}) {
|
|||
|
|
const [chats, setChats] = useState<CustomerChat[]>([]);
|
|||
|
|
const [boundChatId, setBoundChatId] = useState(task.customerChatId);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [error, setError] = useState("");
|
|||
|
|
const [notice, setNotice] = useState("");
|
|||
|
|
const [selectedChatId, setSelectedChatId] = useState(task.customerChatId);
|
|||
|
|
const [bindResult, setBindResult] = useState<BindResult | null>(null);
|
|||
|
|
|
|||
|
|
const loadChats = useCallback(async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
setError("");
|
|||
|
|
try {
|
|||
|
|
const response = await fetch("/api/action", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ action: "task_customer_chats", taskId: task.id }),
|
|||
|
|
});
|
|||
|
|
const parsed = await readApiResponse<{
|
|||
|
|
chats?: CustomerChat[];
|
|||
|
|
boundChatId?: string;
|
|||
|
|
error?: string;
|
|||
|
|
}>(response, "获取客户群列表失败");
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(parsed.error || "获取客户群列表失败");
|
|||
|
|
}
|
|||
|
|
setChats(Array.isArray(parsed.chats) ? parsed.chats : []);
|
|||
|
|
const bound = String(parsed.boundChatId ?? "");
|
|||
|
|
setBoundChatId(bound);
|
|||
|
|
setSelectedChatId(bound);
|
|||
|
|
} catch (reason) {
|
|||
|
|
setError(reason instanceof Error ? reason.message : "获取客户群列表失败");
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}, [task.id]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const timer = window.setTimeout(() => {
|
|||
|
|
void loadChats();
|
|||
|
|
}, 0);
|
|||
|
|
return () => window.clearTimeout(timer);
|
|||
|
|
}, [loadChats]);
|
|||
|
|
|
|||
|
|
const submitBind = async (chatId: string) => {
|
|||
|
|
const chat = chats.find((item) => item.chatId === chatId);
|
|||
|
|
setSaving(true);
|
|||
|
|
setError("");
|
|||
|
|
setNotice("");
|
|||
|
|
try {
|
|||
|
|
const response = await fetch("/api/action", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
action: "task_bind_customer_chat",
|
|||
|
|
taskId: task.id,
|
|||
|
|
chatId,
|
|||
|
|
chatName: chat?.name ?? "",
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
const parsed = await readApiResponse<{ task?: BindResult; error?: string }>(
|
|||
|
|
response,
|
|||
|
|
"绑定客户群失败",
|
|||
|
|
);
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(parsed.error || "绑定客户群失败");
|
|||
|
|
}
|
|||
|
|
const result = parsed.task ?? null;
|
|||
|
|
setBindResult(result);
|
|||
|
|
setBoundChatId(chatId);
|
|||
|
|
setSelectedChatId(chatId);
|
|||
|
|
setNotice(
|
|||
|
|
chatId
|
|||
|
|
? result?.qrError
|
|||
|
|
? `已绑定,但${result.qrError},可重新保存刷新`
|
|||
|
|
: `已绑定「${result?.customerChatName || chat?.name || chatId}」,认领人扫码即可进群`
|
|||
|
|
: "已解绑,认领页不再展示进群二维码",
|
|||
|
|
);
|
|||
|
|
onBound();
|
|||
|
|
} catch (reason) {
|
|||
|
|
setError(reason instanceof Error ? reason.message : "绑定客户群失败");
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const unbind = async () => {
|
|||
|
|
if (!window.confirm("解绑后认领页将不再展示进群二维码,确定解绑?")) return;
|
|||
|
|
await submitBind("");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
|
|||
|
|
<div
|
|||
|
|
className="modal-card wecom-picker-modal"
|
|||
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|||
|
|
>
|
|||
|
|
<div className="modal-heading">
|
|||
|
|
<div>
|
|||
|
|
<h2>绑定客户群</h2>
|
|||
|
|
<p>
|
|||
|
|
列出任务负责人名下的企业微信客户群,绑定后生成进群活码,认领人在领取页扫码进群。
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<button type="button" onClick={onClose} aria-label="关闭">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="import-guide-strip">
|
|||
|
|
<span className="wecom-picker-cache-hint">
|
|||
|
|
任务:{task.name}
|
|||
|
|
{boundChatId ? ` · 当前绑定:${task.customerChatName || boundChatId}` : " · 未绑定"}
|
|||
|
|
</span>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="ghost-button"
|
|||
|
|
onClick={() => void loadChats()}
|
|||
|
|
disabled={loading || saving}
|
|||
|
|
>
|
|||
|
|
{loading ? "拉取中…" : "刷新列表"}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{error && <p className="form-error">{error}</p>}
|
|||
|
|
{notice && <p className="wecom-picker-cache-hint">{notice}</p>}
|
|||
|
|
{bindResult?.joinQrCode && (
|
|||
|
|
<div className="join-qr-preview">
|
|||
|
|
<img src={bindResult.joinQrCode} alt="进群活码" />
|
|||
|
|
<span>进群活码(已同步展示在认领页)</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div className="wecom-picker-list">
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="wecom-picker-empty">正在拉取负责人名下客户群…</div>
|
|||
|
|
) : chats.length === 0 ? (
|
|||
|
|
<div className="wecom-picker-empty">
|
|||
|
|
未拉取到客户群——请任务负责人先在企微客户端创建客户群(内外部成员都可拉入),
|
|||
|
|
再点击「刷新列表」。
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
chats.map((chat) => {
|
|||
|
|
const isBound = chat.chatId === boundChatId;
|
|||
|
|
return (
|
|||
|
|
<label key={chat.chatId} className="wecom-picker-row">
|
|||
|
|
<span className="wecom-picker-avatar placeholder" aria-hidden="true">
|
|||
|
|
{(chat.name || chat.chatId).slice(0, 1)}
|
|||
|
|
</span>
|
|||
|
|
<span className="wecom-picker-meta">
|
|||
|
|
<span className="wecom-picker-name">{chat.name || chat.chatId}</span>
|
|||
|
|
<span className="wecom-picker-sub">{chat.memberCount} 人</span>
|
|||
|
|
<code className="wecom-picker-id">{chat.chatId}</code>
|
|||
|
|
</span>
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name="customer-chat"
|
|||
|
|
checked={selectedChatId === chat.chatId}
|
|||
|
|
onChange={() => setSelectedChatId(chat.chatId)}
|
|||
|
|
/>
|
|||
|
|
{isBound ? <span className="wecom-picker-current">已绑定</span> : null}
|
|||
|
|
</label>
|
|||
|
|
);
|
|||
|
|
})
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="modal-actions">
|
|||
|
|
{boundChatId ? (
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="ghost-button"
|
|||
|
|
onClick={() => void unbind()}
|
|||
|
|
disabled={saving}
|
|||
|
|
>
|
|||
|
|
解绑当前群
|
|||
|
|
</button>
|
|||
|
|
) : null}
|
|||
|
|
<button type="button" className="ghost-button" onClick={onClose} disabled={saving}>
|
|||
|
|
关闭
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="primary-button"
|
|||
|
|
onClick={() => void submitBind(selectedChatId)}
|
|||
|
|
disabled={saving || loading || !selectedChatId || selectedChatId === boundChatId}
|
|||
|
|
>
|
|||
|
|
{saving ? "保存中…" : selectedChatId === boundChatId ? "已绑定" : "绑定所选群"}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|