好友转移

This commit is contained in:
wong
2025-12-11 17:30:40 +08:00
parent 4537305dfc
commit 84a51b8f91
4 changed files with 415 additions and 94 deletions

View File

@@ -55,6 +55,20 @@ export function transferWechatFriends(params: {
return request("/v1/wechats/transfer-friends", params, "POST");
}
// 获取客服账号列表
export function getKefuAccountsList() {
return request("/v1/kefu/accounts/list", {}, "GET");
}
// 转移好友到客服账号
export function transferFriend(params: {
friendId: string;
toAccountId: string;
comment?: string;
}) {
return request("/v1/friend/transfer", params, "POST");
}
// 导出朋友圈接口(直接下载文件)
export async function exportWechatMoments(params: {
wechatId: string;

View File

@@ -102,9 +102,12 @@ export interface WechatAccountSummary {
export interface Friend {
id: string;
friendId?: string;
avatar: string;
nickname: string;
wechatId: string;
accountUserName: string;
accountRealName: string;
remark: string;
addTime: string;
lastInteraction: string;

View File

@@ -684,99 +684,114 @@
.friend-card {
display: flex;
align-items: center;
padding: 14px;
align-items: flex-start;
padding: 16px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
margin-bottom: 10px;
margin-bottom: 12px;
gap: 12px;
transition: box-shadow 0.2s, border-color 0.2s;
transition: all 0.2s;
cursor: pointer;
&:hover {
border-color: #cfe2ff;
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15);
&:active {
background: #f8f9fa;
border-color: #1677ff;
transform: scale(0.98);
}
}
.friend-avatar {
width: 48px;
height: 48px;
flex-shrink: 0;
width: 52px;
height: 52px;
.adm-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
width: 52px;
height: 52px;
border-radius: 50%;
border: 2px solid #f0f0f0;
}
}
.friend-main {
flex: 1;
min-width: 0;
}
.friend-name-row {
display: flex;
align-items: center;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 4px;
}
.friend-name {
font-size: 15px;
.friend-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.friend-name {
font-size: 16px;
font-weight: 600;
color: #111;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.friend-value {
flex-shrink: 0;
.value-amount {
font-size: 15px;
font-weight: 600;
color: #fa541c;
white-space: nowrap;
}
}
.friend-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.friend-info-item {
font-size: 13px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
.info-label {
color: #999;
flex-shrink: 0;
}
.info-value {
color: #666;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.friend-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
gap: 6px;
margin-top: 4px;
}
.friend-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: #f5f5f5;
color: #666;
}
.friend-id-row {
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
.friend-status-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.friend-status-chip {
padding: 4px 10px;
border-radius: 12px;
background: #f0f7ff;
color: #1677ff;
font-size: 11px;
padding: 2px 8px;
border-radius: 8px;
}
.friend-value {
text-align: right;
.value-label {
font-size: 11px;
color: #999;
margin-bottom: 4px;
}
.value-amount {
font-size: 14px;
font-weight: 600;
color: #fa541c;
}
font-weight: 500;
white-space: nowrap;
}
}
@@ -851,11 +866,98 @@
margin-top: 20px;
}
.popup-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.popup-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.friend-info-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
.friend-info-text {
flex: 1;
.friend-info-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.friend-info-id {
font-size: 12px;
color: #999;
}
}
}
.loading-accounts,
.empty-accounts {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #999;
font-size: 14px;
}
.kefu-accounts-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
.kefu-account-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: white;
&:hover {
border-color: #1677ff;
background: #f0f7ff;
}
&.selected {
border-color: #1677ff;
background: #e6f4ff;
}
.account-info {
flex: 1;
.account-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.account-id {
font-size: 12px;
color: #999;
}
}
.selected-icon {
color: #1677ff;
font-size: 18px;
font-weight: bold;
}
}
}
.export-form {
margin-top: 20px;

View File

@@ -14,7 +14,7 @@ import {
DatePicker,
InfiniteScroll,
} from "antd-mobile";
import { Input } from "antd";
import { Input, Select } from "antd";
import NavCommon from "@/components/NavCommon";
import {
SearchOutlined,
@@ -34,6 +34,8 @@ import {
getWechatAccountOverview,
getWechatMoments,
exportWechatMoments,
getKefuAccountsList,
transferFriend,
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
@@ -84,6 +86,15 @@ const WechatAccountDetail: React.FC = () => {
const [showEndTimePicker, setShowEndTimePicker] = useState(false);
const [exportLoading, setExportLoading] = useState(false);
// 迁移好友相关状态
const [showTransferFriendPopup, setShowTransferFriendPopup] = useState(false);
const [selectedFriend, setSelectedFriend] = useState<Friend | null>(null);
const [kefuAccounts, setKefuAccounts] = useState<any[]>([]);
const [selectedKefuAccountId, setSelectedKefuAccountId] = useState<string>("");
const [transferComment, setTransferComment] = useState<string>("");
const [transferFriendLoading, setTransferFriendLoading] = useState(false);
const [loadingKefuAccounts, setLoadingKefuAccounts] = useState(false);
// 获取基础信息
const fetchAccountInfo = useCallback(async () => {
if (!id) return;
@@ -237,9 +248,12 @@ const WechatAccountDetail: React.FC = () => {
return {
id: friend.id.toString(),
friendId: friend.friendId || friend.id?.toString() || "",
avatar: friend.avatar || "/placeholder.svg",
nickname: friend.nickname || "未知用户",
wechatId: friend.wechatId || "",
accountUserName: friend.accountUserName || "",
accountRealName: friend.accountRealName || "",
remark: friend.notes || "",
addTime:
friend.createTime || new Date().toISOString().split("T")[0],
@@ -466,7 +480,66 @@ const WechatAccountDetail: React.FC = () => {
};
const handleFriendClick = (friend: Friend) => {
navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`);
setSelectedFriend(friend);
setShowTransferFriendPopup(true);
// 加载客服账号列表
fetchKefuAccounts();
};
// 获取客服账号列表
const fetchKefuAccounts = useCallback(async () => {
setLoadingKefuAccounts(true);
try {
const response = await getKefuAccountsList();
// 数据结构:{ code: 200, msg: "success", data: { total: 7, list: [...] } }
const accountsList = response?.data?.list || response?.list || (Array.isArray(response) ? response : []);
setKefuAccounts(accountsList);
} catch (error) {
console.error("获取客服账号列表失败:", error);
Toast.show({
content: "获取客服账号列表失败",
position: "top",
});
} finally {
setLoadingKefuAccounts(false);
}
}, []);
// 确认转移好友
const handleConfirmTransferFriend = async () => {
if (!selectedFriend) {
Toast.show({ content: "请选择好友", position: "top" });
return;
}
if (!selectedKefuAccountId) {
Toast.show({ content: "请选择目标客服账号", position: "top" });
return;
}
setTransferFriendLoading(true);
try {
await transferFriend({
friendId: selectedFriend.friendId || selectedFriend.id,
toAccountId: selectedKefuAccountId,
comment: transferComment || undefined,
});
Toast.show({ content: "转移成功", position: "top" });
setShowTransferFriendPopup(false);
setSelectedFriend(null);
setSelectedKefuAccountId("");
setTransferComment("");
// 刷新好友列表
fetchFriendsList(1, searchQuery, false);
} catch (error: any) {
console.error("转移好友失败:", error);
Toast.show({
content: error?.message || "转移失败,请重试",
position: "top",
});
} finally {
setTransferFriendLoading(false);
}
};
const handleLoadMoreMoments = async () => {
@@ -903,38 +976,53 @@ const WechatAccountDetail: React.FC = () => {
<Avatar src={friend.avatar} />
</div>
<div className={style["friend-main"]}>
<div className={style["friend-name-row"]}>
<div className={style["friend-header"]}>
<div className={style["friend-name"]}>
{friend.nickname || "未知好友"}
</div>
<div className={style["friend-value"]}>
<div className={style["value-amount"]}>
{friend.valueFormatted
|| (typeof friend.value === "number"
? `¥${friend.value.toLocaleString()}`
: "¥3")}
</div>
</div>
</div>
<div className={style["friend-id-row"]}>
ID: {friend.wechatId || "-"}
</div>
<div className={style["friend-status-row"]}>
{friend.statusTags?.map((tag, idx) => (
<span
key={idx}
className={style["friend-status-chip"]}
>
{tag}
</span>
))}
{friend.remark && (
<span className={style["friend-status-chip"]}>
{friend.remark}
<div className={style["friend-info"]}>
<div className={style["friend-info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{friend.wechatId || "-"}
</span>
</div>
{(friend.accountUserName || friend.accountRealName) && (
<div className={style["friend-info-item"]}>
<span className={style["info-label"]}></span>
<span className={style["info-value"]}>
{friend.accountUserName || ""}
{friend.accountRealName && `(${friend.accountRealName})`}
</span>
</div>
)}
</div>
</div>
<div className={style["friend-value"]}>
<div className={style["value-amount"]}>
{friend.valueFormatted
|| (typeof friend.value === "number"
? `¥${friend.value.toLocaleString()}`
: "估值 -")}
</div>
{(friend.statusTags?.length > 0 || friend.remark) && (
<div className={style["friend-tags"]}>
{friend.statusTags?.map((tag, idx) => (
<span
key={idx}
className={style["friend-tag"]}
>
{tag}
</span>
))}
{friend.remark && (
<span className={style["friend-tag"]}>
{friend.remark}
</span>
)}
</div>
)}
</div>
</div>
))}
@@ -1405,8 +1493,122 @@ const WechatAccountDetail: React.FC = () => {
</div>
</Popup>
{/* 好友详情弹窗 */}
{/* Removed */}
{/* 迁移好友弹窗 */}
<Popup
visible={showTransferFriendPopup}
onMaskClick={() => {
setShowTransferFriendPopup(false);
setSelectedFriend(null);
setSelectedKefuAccountId("");
setTransferComment("");
}}
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
>
<div className={style["popup-content"]}>
<div className={style["popup-header"]}>
<h3></h3>
<Button
size="small"
fill="outline"
onClick={() => {
setShowTransferFriendPopup(false);
setSelectedFriend(null);
setSelectedKefuAccountId("");
setTransferComment("");
}}
>
</Button>
</div>
<div className={style["export-form"]}>
{/* 好友信息 */}
{selectedFriend && (
<div className={style["form-item"]}>
<label></label>
<div className={style["friend-info-card"]}>
<Avatar src={selectedFriend.avatar} style={{ width: 40, height: 40 }} />
<div className={style["friend-info-text"]}>
<div className={style["friend-info-name"]}>
{selectedFriend.nickname || "未知好友"}
</div>
<div className={style["friend-info-id"]}>
{selectedFriend.wechatId || "-"}
</div>
</div>
</div>
</div>
)}
{/* 选择客服账号 */}
<div className={style["form-item"]}>
<label></label>
{loadingKefuAccounts ? (
<div className={style["loading-accounts"]}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span style={{ marginLeft: 8 }}>...</span>
</div>
) : kefuAccounts.length === 0 ? (
<div className={style["empty-accounts"]}></div>
) : (
<Select
placeholder="请选择客服账号"
value={selectedKefuAccountId || undefined}
onChange={(value: string) => setSelectedKefuAccountId(value)}
style={{ width: "100%" }}
allowClear
>
{kefuAccounts.map((account: any) => {
const displayName = `${account.userName || ""}(${account.realName || ""})`;
return (
<Select.Option key={account.id} value={String(account.id)}>
{displayName}
</Select.Option>
);
})}
</Select>
)}
</div>
{/* 备注 */}
<div className={style["form-item"]}>
<label></label>
<Input
placeholder="请输入备注信息"
value={transferComment}
onChange={e => setTransferComment(e.target.value)}
allowClear
/>
</div>
</div>
<div className={style["popup-footer"]}>
<Button
block
color="primary"
onClick={handleConfirmTransferFriend}
loading={transferFriendLoading}
disabled={transferFriendLoading || !selectedKefuAccountId}
>
{transferFriendLoading ? "转移中..." : "确认转移"}
</Button>
<Button
block
color="danger"
fill="outline"
onClick={() => {
setShowTransferFriendPopup(false);
setSelectedFriend(null);
setSelectedKefuAccountId("");
setTransferComment("");
}}
style={{ marginTop: 12 }}
>
</Button>
</div>
</div>
</Popup>
</Layout>
);
};