Merge branch 'feature/bug' into develop
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ export function fetchTransferFriends(params: {
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
workbenchId: number;
|
||||
isRecycle?: number; // 0=未回收, 1=已回收, undefined=全部
|
||||
}) {
|
||||
return request("/v1/workbench/transfer-friends", params, "GET");
|
||||
}
|
||||
|
||||
@@ -35,10 +35,11 @@ const SendRcrodModal: React.FC<SendRcrodModalProps> = ({
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [recycleFilter, setRecycleFilter] = useState<number | undefined>(undefined); // undefined=全部, 0=未回收, 1=已回收
|
||||
const pageSize = 20;
|
||||
|
||||
// 获取分发记录数据
|
||||
const fetchSendRecords = async (page = 1, keyword = "") => {
|
||||
const fetchSendRecords = async (page = 1, keyword = "", isRecycle?: number) => {
|
||||
if (!ruleId) return;
|
||||
|
||||
setLoading(true);
|
||||
@@ -48,6 +49,7 @@ const SendRcrodModal: React.FC<SendRcrodModalProps> = ({
|
||||
page,
|
||||
limit: pageSize,
|
||||
keyword,
|
||||
isRecycle,
|
||||
});
|
||||
console.log(detailRes);
|
||||
|
||||
@@ -68,21 +70,29 @@ const SendRcrodModal: React.FC<SendRcrodModalProps> = ({
|
||||
setCurrentPage(1);
|
||||
setSearchQuery("");
|
||||
setSearchKeyword("");
|
||||
fetchSendRecords(1, "");
|
||||
setRecycleFilter(undefined);
|
||||
fetchSendRecords(1, "", undefined);
|
||||
}
|
||||
}, [visible, ruleId]);
|
||||
|
||||
// 搜索关键词变化时触发搜索
|
||||
useEffect(() => {
|
||||
if (!visible || !ruleId || searchKeyword === "") return;
|
||||
if (!visible || !ruleId) return;
|
||||
setCurrentPage(1);
|
||||
fetchSendRecords(1, searchKeyword);
|
||||
fetchSendRecords(1, searchKeyword, recycleFilter);
|
||||
}, [searchKeyword]);
|
||||
|
||||
// 筛选条件变化时触发搜索
|
||||
useEffect(() => {
|
||||
if (!visible || !ruleId) return;
|
||||
setCurrentPage(1);
|
||||
fetchSendRecords(1, searchKeyword, recycleFilter);
|
||||
}, [recycleFilter]);
|
||||
|
||||
// 页码变化
|
||||
useEffect(() => {
|
||||
if (!visible || !ruleId) return;
|
||||
fetchSendRecords(currentPage, searchKeyword);
|
||||
fetchSendRecords(currentPage, searchKeyword, recycleFilter);
|
||||
}, [currentPage]);
|
||||
|
||||
// 处理页码变化
|
||||
@@ -160,6 +170,37 @@ const SendRcrodModal: React.FC<SendRcrodModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 回收状态筛选 */}
|
||||
<div className={style.filterBar}>
|
||||
<div className={style.filterLabel}>回收状态:</div>
|
||||
<div className={style.filterOptions}>
|
||||
<div
|
||||
className={`${style.filterOption} ${
|
||||
recycleFilter === undefined ? style.active : ""
|
||||
}`}
|
||||
onClick={() => setRecycleFilter(undefined)}
|
||||
>
|
||||
全部
|
||||
</div>
|
||||
<div
|
||||
className={`${style.filterOption} ${
|
||||
recycleFilter === 0 ? style.active : ""
|
||||
}`}
|
||||
onClick={() => setRecycleFilter(0)}
|
||||
>
|
||||
未回收
|
||||
</div>
|
||||
<div
|
||||
className={`${style.filterOption} ${
|
||||
recycleFilter === 1 ? style.active : ""
|
||||
}`}
|
||||
onClick={() => setRecycleFilter(1)}
|
||||
>
|
||||
已回收
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分发记录列表 */}
|
||||
<div className={style.accountList}>
|
||||
{loading ? (
|
||||
|
||||
@@ -251,6 +251,52 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filterBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.filterLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filterOptions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
.filterOption {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accountModalFooter {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
@@ -174,7 +174,8 @@ class AutomaticAssign extends BaseController
|
||||
public function allotWechatFriend($data = [],$isInner = false,$errorNum = 0)
|
||||
{
|
||||
// 获取授权token
|
||||
$authorization = trim($this->request->header('authorization', $this->authorization));
|
||||
$authorization = $this->authorization;
|
||||
|
||||
if (empty($authorization)) {
|
||||
if($isInner){
|
||||
return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
|
||||
@@ -209,8 +210,8 @@ class AutomaticAssign extends BaseController
|
||||
// 发送请求
|
||||
$url = $this->baseUrl . 'api/WechatFriend/allot?wechatFriendId='.$wechatFriendId.'¬ifyReceiver='.$notifyReceiver.'&comment='.$comment.'&toAccountId='.$toAccountId.'&optFrom='.$optFrom;
|
||||
$result = requestCurl($url, [], 'PUT', $header, 'json');
|
||||
|
||||
if (empty($result)) {
|
||||
$response = handleApiResponse($result);
|
||||
if (empty($response)) {
|
||||
if($isInner){
|
||||
return json_encode(['code'=>200,'msg'=>'微信好友分配成功']);
|
||||
}else{
|
||||
|
||||
@@ -15,7 +15,8 @@ class WechatChatroomController extends BaseController
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$groupIds = $this->request->param('groupIds', '');
|
||||
$groupIds = $this->request->param('groupId', '');
|
||||
$ownerWechatId = $this->request->param('ownerWechatId', '');
|
||||
$accountId = $this->getUserInfo('s2_accountId');
|
||||
if (empty($accountId)){
|
||||
return ResponseHelper::error('请先登录');
|
||||
@@ -37,6 +38,10 @@ class WechatChatroomController extends BaseController
|
||||
$query->where('groupIds', $groupIds);
|
||||
}
|
||||
|
||||
if (!empty($ownerWechatId)) {
|
||||
$query->where('ownerWechatId', $ownerWechatId);
|
||||
}
|
||||
|
||||
$query->order('id desc');
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $limit)->select();
|
||||
|
||||
@@ -14,7 +14,8 @@ class WechatFriendController extends BaseController
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$groupIds = $this->request->param('groupIds', '');
|
||||
$groupIds = $this->request->param('groupId', '');
|
||||
$ownerWechatId = $this->request->param('ownerWechatId', '');
|
||||
$accountId = $this->getUserInfo('s2_accountId');
|
||||
if (empty($accountId)) {
|
||||
return ResponseHelper::error('请先登录');
|
||||
@@ -38,6 +39,10 @@ class WechatFriendController extends BaseController
|
||||
$query->where('groupIds', $groupIds);
|
||||
}
|
||||
|
||||
if (!empty($ownerWechatId)) {
|
||||
$query->where('ownerWechatId', $ownerWechatId);
|
||||
}
|
||||
|
||||
$query->order('id desc');
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $limit)->select();
|
||||
|
||||
@@ -136,6 +136,7 @@ Route::group('v1/', function () {
|
||||
// 好友相关
|
||||
Route::group('friend', function () {
|
||||
Route::get('', 'app\cunkebao\controller\friend\GetFriendListV1Controller@index'); // 获取好友列表
|
||||
Route::post('transfer', 'app\cunkebao\controller\friend\GetFriendListV1Controller@transfer'); // 好友转移
|
||||
});
|
||||
|
||||
//群相关
|
||||
|
||||
@@ -1884,6 +1884,7 @@ class WorkbenchController extends Controller
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$workbenchId = $this->request->param('workbenchId', '');
|
||||
$isRecycle = $this->request->param('isRecycle', '');
|
||||
if (empty($workbenchId)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
@@ -1897,7 +1898,7 @@ class WorkbenchController extends Controller
|
||||
->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
|
||||
->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
|
||||
->field([
|
||||
'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime',
|
||||
'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
|
||||
'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
|
||||
'u.account', 'u.username'
|
||||
])
|
||||
@@ -1908,11 +1909,18 @@ class WorkbenchController extends Controller
|
||||
$query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
if ($isRecycle != '' || $isRecycle != null) {
|
||||
$query->where('isRecycle',$isRecycle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $limit)->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||
$item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use app\common\model\Device as DeviceModel;
|
||||
use app\common\model\DeviceUser as DeviceUserModel;
|
||||
use app\common\model\WechatFriendShip as WechatFriendShipModel;
|
||||
use app\cunkebao\controller\BaseController;
|
||||
use app\api\controller\AutomaticAssign;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
@@ -66,29 +67,45 @@ class GetFriendListV1Controller extends BaseController
|
||||
$where[] = ['ownerWechatId','in',$wechatIds];
|
||||
|
||||
$data = Db::table('s2_wechat_friend')
|
||||
->field(['nickname','avatar','alias','id','wechatId','ownerNickname','ownerAlias','ownerWechatId','createTime'])
|
||||
->field([
|
||||
'id', 'nickname', 'avatar', 'alias', 'wechatId',
|
||||
'gender', 'phone', 'createTime', 'updateTime', 'deleteTime',
|
||||
'ownerNickname', 'ownerAlias', 'ownerWechatId',
|
||||
'accountUserName', 'accountNickname', 'accountRealName'
|
||||
])
|
||||
->where($where);
|
||||
$total = $data->count();
|
||||
$list = $data->page($page, $limit)->order('id DESC')->select();
|
||||
|
||||
|
||||
// $data = WechatFriendShipModel::alias('wf')
|
||||
// ->field(['wa1.nickname','wa1.avatar','wa1.alias','wf.id','wf.wechatId','wa2.nickname as ownerNickname','wa2.alias as ownerAlias','wa2.wechatId as ownerWechatId','wf.createTime'])
|
||||
// ->Join('wechat_account wa1','wf.wechatId = wa1.wechatId')
|
||||
// ->Join('wechat_account wa2','wf.ownerWechatId = wa2.wechatId')
|
||||
// ->where($where);
|
||||
//
|
||||
// $total = $data->count();
|
||||
// $list = $data->page($page, $limit)->order('wf.id DESC')->group('wf.id')->select();
|
||||
|
||||
|
||||
|
||||
// 格式化时间字段和处理数据
|
||||
$formattedList = [];
|
||||
foreach ($list as $item) {
|
||||
$formattedItem = [
|
||||
'id' => $item['id'],
|
||||
'nickname' => $item['nickname'] ?? '',
|
||||
'avatar' => $item['avatar'] ?? '',
|
||||
'alias' => $item['alias'] ?? '',
|
||||
'wechatId' => $item['wechatId'] ?? '',
|
||||
'gender' => $item['gender'] ?? 0,
|
||||
'phone' => $item['phone'] ?? '',
|
||||
'account' => $item['accountUserName'] ?? '',
|
||||
'username' => $item['accountRealName'] ?? '',
|
||||
'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '1970-01-01 08:00:00',
|
||||
'updateTime' => !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '1970-01-01 08:00:00',
|
||||
'deleteTime' => !empty($item['deleteTime']) ? date('Y-m-d H:i:s', $item['deleteTime']) : '1970-01-01 08:00:00',
|
||||
'ownerNickname' => $item['ownerNickname'] ?? '',
|
||||
'ownerAlias' => $item['ownerAlias'] ?? '',
|
||||
'ownerWechatId' => $item['ownerWechatId'] ?? '',
|
||||
'accountNickname' => $item['accountNickname'] ?? ''
|
||||
];
|
||||
$formattedList[] = $formattedItem;
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'list' => $formattedList,
|
||||
'total' => $total,
|
||||
'companyId' => $this->getUserInfo('companyId')
|
||||
]
|
||||
@@ -100,4 +117,95 @@ class GetFriendListV1Controller extends BaseController
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友转移
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function transfer()
|
||||
{
|
||||
$friendId = $this->request->param('friendId', 0);
|
||||
$toAccountId = $this->request->param('toAccountId', '');
|
||||
$comment = $this->request->param('comment', '');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 参数验证
|
||||
if (empty($friendId)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '好友ID不能为空'
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($toAccountId)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '目标账号ID不能为空'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证目标账号是否存在且属于当前公司
|
||||
$accountInfo = Db::table('s2_company_account')
|
||||
->where('id', $toAccountId)
|
||||
->where('departmentId', $companyId)
|
||||
->field('id as accountId, userName as accountUserName, realName as accountRealName, nickname as accountNickname, tenantId')
|
||||
->find();
|
||||
|
||||
if (empty($accountInfo)) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '目标账号不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// 调用 AutomaticAssign 进行好友转移
|
||||
$automaticAssign = new AutomaticAssign();
|
||||
$result = $automaticAssign->allotWechatFriend([
|
||||
'wechatFriendId' => $friendId,
|
||||
'toAccountId' => $toAccountId,
|
||||
'comment' => $comment,
|
||||
'notifyReceiver' => false,
|
||||
'optFrom' => 4
|
||||
], true);
|
||||
|
||||
$resultData = json_decode($result, true);
|
||||
|
||||
if (!empty($resultData) && $resultData['code'] == 200) {
|
||||
// 转移成功后更新数据库
|
||||
$updateData = [
|
||||
'accountId' => $accountInfo['accountId'],
|
||||
'accountUserName' => $accountInfo['accountUserName'],
|
||||
'accountRealName' => $accountInfo['accountRealName'],
|
||||
'accountNickname' => $accountInfo['accountNickname'],
|
||||
'updateTime' => time()
|
||||
];
|
||||
|
||||
Db::table('s2_wechat_friend')
|
||||
->where('id', $friendId)
|
||||
->update($updateData);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '好友转移成功',
|
||||
'data' => [
|
||||
'friendId' => $friendId,
|
||||
'toAccountId' => $toAccountId
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '好友转移失败:' . ($resultData['msg'] ?? '未知错误')
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '好友转移失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,12 @@ class GetWechatOnDeviceFriendsV1Controller extends BaseController
|
||||
[
|
||||
'w.id', 'w.nickname', 'w.avatar', 'w.wechatId',
|
||||
'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
|
||||
'f.memo', 'f.tags'
|
||||
'f.memo', 'f.tags',
|
||||
'ff.accountUserName', 'ff.accountRealName','ff.id AS friendId'
|
||||
]
|
||||
)
|
||||
->join('wechat_account w', 'w.wechatId = f.wechatId');
|
||||
->join('wechat_account w', 'w.wechatId = f.wechatId')
|
||||
->join(['s2_wechat_friend' => 'ff'], 'ff.id = f.id');
|
||||
|
||||
foreach ($where as $key => $value) {
|
||||
if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
|
||||
|
||||
@@ -77,7 +77,7 @@ class WorkbenchGroupCreateJob
|
||||
{
|
||||
try {
|
||||
// 1. 查询启用了建群功能的数据
|
||||
$workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0])->order('id desc')->select();
|
||||
$workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0,'id' => 315])->order('id desc')->select();
|
||||
foreach ($workbenches as $workbench) {
|
||||
// 获取工作台配置
|
||||
$config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
|
||||
@@ -120,7 +120,6 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($groupMemberWechatId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -150,7 +149,7 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
// 计算随机群人数(不包含管理员,只减去群主成员数)
|
||||
$groupRandNum = mt_rand($config['groupSizeMin'], $config['groupSizeMax']) - count($groupMember);
|
||||
|
||||
|
||||
// 分批处理待入群用户
|
||||
$addGroupUser = [];
|
||||
$totalRows = count($joinUser);
|
||||
@@ -168,7 +167,7 @@ class WorkbenchGroupCreateJob
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
|
||||
// 遍历每批用户
|
||||
foreach ($addGroupUser as $batchUsers) {
|
||||
$this->processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket);
|
||||
@@ -201,7 +200,7 @@ class WorkbenchGroupCreateJob
|
||||
$groupOwnerWechatIds[] = $member['ownerWechatId'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 如果从好友表获取不到,使用群主成员微信ID列表(作为备用)
|
||||
if (empty($groupOwnerWechatIds)) {
|
||||
@@ -225,19 +224,20 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
}
|
||||
|
||||
exit_data($adminWechatIds);
|
||||
|
||||
// 3. 从流量池用户中筛选出是群主好友的用户(按微信账号分组)
|
||||
$ownerFriendIdsByAccount = [];
|
||||
$wechatIds = [];
|
||||
|
||||
// 获取群主的好友关系(从流量池中筛选)
|
||||
$ownerFriends = Db::name('wechat_friendship')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId')
|
||||
->where('f.companyId', $workbench->companyId)
|
||||
$ownerFriends = Db::table('s2_wechat_friend')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('f.ownerWechatId', $groupOwnerWechatIds)
|
||||
->whereIn('a.wechatId', $groupOwnerWechatIds)
|
||||
->where('f.isDeleted', 0)
|
||||
->field('f.id,f.wechatId,a.id as wechatAccountId')
|
||||
->select();
|
||||
|
||||
if (empty($ownerFriends)) {
|
||||
Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}");
|
||||
return;
|
||||
@@ -252,13 +252,12 @@ class WorkbenchGroupCreateJob
|
||||
$ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
}
|
||||
|
||||
|
||||
// 4. 遍历每个微信账号,创建群
|
||||
foreach ($ownerFriendIdsByAccount as $wechatAccountId => $ownerFriendIds) {
|
||||
// 4.1 获取当前账号的管理员好友ID
|
||||
$currentAdminFriendIds = [];
|
||||
$accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId');
|
||||
|
||||
foreach ($adminFriendIds as $adminFriendId) {
|
||||
$adminFriend = Db::table('s2_wechat_friend')->where('id', $adminFriendId)->find();
|
||||
if ($adminFriend && $adminFriend['ownerWechatId'] == $accountWechatId) {
|
||||
@@ -278,10 +277,10 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4.3 限制群主好友数量(按随机群人数)
|
||||
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
|
||||
|
||||
|
||||
// 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选)
|
||||
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds, $limitedOwnerFriendIds);
|
||||
|
||||
@@ -379,12 +378,12 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
|
||||
// 从流量池用户中筛选出是管理员好友的用户
|
||||
$adminFriendsFromPool = Db::name('wechat_friendship')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId')
|
||||
->where('f.companyId', $workbench->companyId)
|
||||
$adminFriendsFromPool = Db::table('s2_wechat_friend')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('f.ownerWechatId', $adminWechatIds)
|
||||
->whereIn('a.wechatId', $adminWechatIds)
|
||||
->where('a.id', $wechatAccountId)
|
||||
->where('f.isDeleted', 0)
|
||||
->field('f.id,f.wechatId')
|
||||
->select();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user