好友转移
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user