feat: 本次提交更新内容如下
微信号管理完成
This commit is contained in:
29
nkebao/src/pages/mine/wechat-accounts/detail/api.ts
Normal file
29
nkebao/src/pages/mine/wechat-accounts/detail/api.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 获取微信号详情
|
||||||
|
export function getWechatAccountDetail(id: string) {
|
||||||
|
return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信号好友列表
|
||||||
|
export function getWechatFriends(params: {
|
||||||
|
wechatAccount: string;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
keyword?: string;
|
||||||
|
}) {
|
||||||
|
return request(
|
||||||
|
`/v1/wechats/${params.wechatAccount}/friends`,
|
||||||
|
{
|
||||||
|
page: params.page,
|
||||||
|
limit: params.limit,
|
||||||
|
keyword: params.keyword,
|
||||||
|
},
|
||||||
|
"GET"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信好友详情
|
||||||
|
export function getWechatFriendDetail(id: string) {
|
||||||
|
return request("/v1/WechatFriend/detail", { id }, "GET");
|
||||||
|
}
|
||||||
54
nkebao/src/pages/mine/wechat-accounts/detail/data.ts
Normal file
54
nkebao/src/pages/mine/wechat-accounts/detail/data.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export interface WechatAccountSummary {
|
||||||
|
accountAge: string;
|
||||||
|
activityLevel: {
|
||||||
|
allTimes: number;
|
||||||
|
dayTimes: number;
|
||||||
|
};
|
||||||
|
accountWeight: {
|
||||||
|
scope: number;
|
||||||
|
ageWeight: number;
|
||||||
|
activityWeigth: number;
|
||||||
|
restrictWeight: number;
|
||||||
|
realNameWeight: number;
|
||||||
|
};
|
||||||
|
statistics: {
|
||||||
|
todayAdded: number;
|
||||||
|
addLimit: number;
|
||||||
|
};
|
||||||
|
restrictions: {
|
||||||
|
id: number;
|
||||||
|
level: string;
|
||||||
|
reason: string;
|
||||||
|
date: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Friend {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
wechatId: string;
|
||||||
|
remark: string;
|
||||||
|
addTime: string;
|
||||||
|
lastInteraction: string;
|
||||||
|
tags: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}>;
|
||||||
|
region: string;
|
||||||
|
source: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WechatFriendDetail {
|
||||||
|
id: number;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
region: string;
|
||||||
|
wechatId: string;
|
||||||
|
addDate: string;
|
||||||
|
tags: string[];
|
||||||
|
memo: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
.wechat-account-detail-page {
|
.wechat-account-detail-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: linear-gradient(to bottom, #f0f8ff, #ffffff);
|
|
||||||
min-height: 100vh;
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -435,6 +433,20 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.friends-list {
|
.friends-list {
|
||||||
@@ -463,80 +475,83 @@
|
|||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer;
|
}
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
.friend-item-static {
|
||||||
background: #f5f5f5;
|
display: flex;
|
||||||
border-color: #d9d9d9;
|
align-items: center;
|
||||||
}
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.friend-avatar {
|
.friend-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-info {
|
.friend-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
.friend-header {
|
.friend-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
.friend-name {
|
.friend-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.friend-remark {
|
|
||||||
color: #666;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-arrow {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.friend-wechat-id {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.friend-remark {
|
||||||
|
color: #666;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-tags {
|
.friend-arrow {
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
flex-wrap: wrap;
|
color: #ccc;
|
||||||
gap: 4px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.friend-tag {
|
.friend-wechat-id {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
padding: 2px 6px;
|
color: #666;
|
||||||
border-radius: 6px;
|
margin-bottom: 4px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.friend-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loading-more {
|
.loading-more {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -624,97 +639,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.loading-detail {
|
|
||||||
display: flex;
|
.pagination-wrapper {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
padding: 40px 0;
|
align-items: center;
|
||||||
}
|
padding: 20px 0;
|
||||||
|
margin-top: 16px;
|
||||||
.error-detail {
|
border-top: 1px solid #f0f0f0;
|
||||||
text-align: center;
|
}
|
||||||
color: #ff4d4f;
|
|
||||||
padding: 40px 0;
|
.summary-grid {
|
||||||
|
display: flex;
|
||||||
p {
|
gap: 16px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
}
|
.summary-item {
|
||||||
|
flex: 1;
|
||||||
.friend-detail-content {
|
background: #fafbfc;
|
||||||
.friend-detail-header {
|
border-radius: 10px;
|
||||||
display: flex;
|
padding: 16px 0 8px 0;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
gap: 12px;
|
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||||
margin-bottom: 20px;
|
}
|
||||||
padding-bottom: 16px;
|
.summary-value {
|
||||||
border-bottom: 1px solid #f0f0f0;
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
.friend-detail-avatar {
|
color: #222;
|
||||||
width: 48px;
|
margin-bottom: 2px;
|
||||||
height: 48px;
|
}
|
||||||
border-radius: 50%;
|
.summary-value-green {
|
||||||
}
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
.friend-detail-info {
|
color: #27ae60;
|
||||||
.friend-detail-name {
|
margin-bottom: 2px;
|
||||||
font-size: 16px;
|
}
|
||||||
font-weight: 600;
|
.summary-value-blue {
|
||||||
color: #333;
|
font-size: 24px;
|
||||||
margin: 0 0 4px 0;
|
font-weight: 600;
|
||||||
}
|
color: #3498db;
|
||||||
|
margin-bottom: 2px;
|
||||||
.friend-detail-wechat-id {
|
}
|
||||||
font-size: 12px;
|
.summary-label {
|
||||||
color: #666;
|
font-size: 13px;
|
||||||
margin: 0;
|
color: #888;
|
||||||
}
|
}
|
||||||
}
|
.summary-progress-row {
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.friend-detail-items {
|
font-size: 14px;
|
||||||
.detail-item {
|
color: #666;
|
||||||
display: flex;
|
margin-bottom: 4px;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
align-items: flex-start;
|
}
|
||||||
padding: 12px 0;
|
.summary-progress-text {
|
||||||
border-bottom: 1px solid #f0f0f0;
|
font-weight: 500;
|
||||||
|
color: #222;
|
||||||
&:last-child {
|
}
|
||||||
border-bottom: none;
|
.summary-progress-bar {
|
||||||
}
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
.detail-label {
|
}
|
||||||
font-size: 14px;
|
.progress-bg {
|
||||||
color: #666;
|
width: 100%;
|
||||||
flex-shrink: 0;
|
height: 8px;
|
||||||
width: 80px;
|
background: #f0f0f0;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
.detail-value {
|
}
|
||||||
font-size: 14px;
|
.progress-fill {
|
||||||
color: #333;
|
height: 8px;
|
||||||
text-align: right;
|
background: #3498db;
|
||||||
flex: 1;
|
border-radius: 6px 0 0 6px;
|
||||||
margin-left: 16px;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
|
.device-card {
|
||||||
.detail-tags {
|
background: #fafbfc;
|
||||||
display: flex;
|
border-radius: 10px;
|
||||||
flex-wrap: wrap;
|
padding: 16px;
|
||||||
gap: 4px;
|
margin-top: 12px;
|
||||||
justify-content: flex-end;
|
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||||
flex: 1;
|
}
|
||||||
margin-left: 16px;
|
.device-title {
|
||||||
|
font-size: 15px;
|
||||||
.detail-tag {
|
font-weight: 600;
|
||||||
font-size: 10px;
|
margin-bottom: 8px;
|
||||||
padding: 2px 6px;
|
color: #222;
|
||||||
border-radius: 6px;
|
}
|
||||||
}
|
.device-row {
|
||||||
}
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
}
|
font-size: 14px;
|
||||||
}
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.device-label {
|
||||||
|
color: #888;
|
||||||
|
min-width: 70px;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
557
nkebao/src/pages/mine/wechat-accounts/detail/index.tsx
Normal file
557
nkebao/src/pages/mine/wechat-accounts/detail/index.tsx
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
NavBar,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
SpinLoading,
|
||||||
|
Popup,
|
||||||
|
Toast,
|
||||||
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
} from "antd-mobile";
|
||||||
|
import { Input, Pagination } from "antd";
|
||||||
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import style from "./detail.module.scss";
|
||||||
|
import { getWechatAccountDetail, getWechatFriends } from "./api";
|
||||||
|
|
||||||
|
import { WechatAccountSummary, Friend } from "./data";
|
||||||
|
|
||||||
|
const WechatAccountDetail: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [accountSummary, setAccountSummary] =
|
||||||
|
useState<WechatAccountSummary | null>(null);
|
||||||
|
const [accountInfo, setAccountInfo] = useState<any>(null);
|
||||||
|
const [showRestrictions, setShowRestrictions] = useState(false);
|
||||||
|
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loadingInfo, setLoadingInfo] = useState(true);
|
||||||
|
|
||||||
|
// 好友列表相关状态
|
||||||
|
const [friends, setFriends] = useState<Friend[]>([]);
|
||||||
|
const [friendsPage, setFriendsPage] = useState(1);
|
||||||
|
const [friendsTotal, setFriendsTotal] = useState(0);
|
||||||
|
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
|
||||||
|
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
|
||||||
|
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
|
||||||
|
|
||||||
|
// 获取基础信息
|
||||||
|
const fetchAccountInfo = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setLoadingInfo(true);
|
||||||
|
try {
|
||||||
|
const response = await getWechatAccountDetail(id);
|
||||||
|
if (response && response.userInfo) {
|
||||||
|
setAccountInfo(response.userInfo);
|
||||||
|
// 构造 summary 数据结构
|
||||||
|
setAccountSummary({
|
||||||
|
accountAge: response.accountAge,
|
||||||
|
activityLevel: response.activityLevel,
|
||||||
|
accountWeight: response.accountWeight,
|
||||||
|
statistics: response.statistics,
|
||||||
|
restrictions: response.restrictions || [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Toast.show({
|
||||||
|
content: "获取账号信息失败",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: "获取账号信息失败", position: "top" });
|
||||||
|
} finally {
|
||||||
|
setLoadingInfo(false);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// 获取好友列表 - 封装为独立函数
|
||||||
|
const fetchFriendsList = useCallback(
|
||||||
|
async (page: number = 1, keyword: string = "") => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
setIsFetchingFriends(true);
|
||||||
|
setHasFriendLoadError(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getWechatFriends({
|
||||||
|
wechatAccount: id,
|
||||||
|
page: page,
|
||||||
|
limit: 20,
|
||||||
|
keyword: keyword,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFriends = response.list.map((friend: any) => ({
|
||||||
|
id: friend.id.toString(),
|
||||||
|
avatar: friend.avatar || "/placeholder.svg",
|
||||||
|
nickname: friend.nickname || "未知用户",
|
||||||
|
wechatId: friend.wechatId || "",
|
||||||
|
remark: friend.memo || "",
|
||||||
|
addTime: friend.createTime || new Date().toISOString().split("T")[0],
|
||||||
|
lastInteraction:
|
||||||
|
friend.lastInteraction || new Date().toISOString().split("T")[0],
|
||||||
|
tags: friend.tags
|
||||||
|
? friend.tags.map((tag: string, index: number) => ({
|
||||||
|
id: `tag-${index}`,
|
||||||
|
name: tag,
|
||||||
|
color: getRandomTagColor(),
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
region: friend.region || "未知",
|
||||||
|
source: friend.source || "未知",
|
||||||
|
notes: friend.notes || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
setFriends(newFriends);
|
||||||
|
setFriendsTotal(response.total);
|
||||||
|
setFriendsPage(page);
|
||||||
|
setIsFriendsEmpty(newFriends.length === 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取好友列表失败:", error);
|
||||||
|
setHasFriendLoadError(true);
|
||||||
|
setFriends([]);
|
||||||
|
setIsFriendsEmpty(true);
|
||||||
|
Toast.show({
|
||||||
|
content: "获取好友列表失败,请检查网络连接",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingFriends(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 搜索好友
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
setFriendsPage(1);
|
||||||
|
fetchFriendsList(1, searchQuery);
|
||||||
|
}, [searchQuery, fetchFriendsList]);
|
||||||
|
|
||||||
|
// 刷新好友列表
|
||||||
|
const handleRefreshFriends = useCallback(() => {
|
||||||
|
fetchFriendsList(friendsPage, searchQuery);
|
||||||
|
}, [friendsPage, searchQuery, fetchFriendsList]);
|
||||||
|
|
||||||
|
// 分页切换
|
||||||
|
const handlePageChange = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
setFriendsPage(page);
|
||||||
|
fetchFriendsList(page, searchQuery);
|
||||||
|
},
|
||||||
|
[searchQuery, fetchFriendsList]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchAccountInfo();
|
||||||
|
}
|
||||||
|
}, [id, fetchAccountInfo]);
|
||||||
|
|
||||||
|
// 监听标签切换 - 只在切换到好友列表时请求一次
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "friends" && id) {
|
||||||
|
setIsFriendsEmpty(false);
|
||||||
|
setHasFriendLoadError(false);
|
||||||
|
fetchFriendsList(1, searchQuery);
|
||||||
|
}
|
||||||
|
}, [activeTab, id, fetchFriendsList, searchQuery]);
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getRandomTagColor = (): string => {
|
||||||
|
const colors = [
|
||||||
|
"bg-blue-100 text-blue-800",
|
||||||
|
"bg-green-100 text-green-800",
|
||||||
|
"bg-red-100 text-red-800",
|
||||||
|
"bg-pink-100 text-pink-800",
|
||||||
|
"bg-emerald-100 text-emerald-800",
|
||||||
|
"bg-amber-100 text-amber-800",
|
||||||
|
];
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateAccountAge = (registerTime: string) => {
|
||||||
|
const registerDate = new Date(registerTime);
|
||||||
|
const now = new Date();
|
||||||
|
const diffTime = Math.abs(now.getTime() - registerDate.getTime());
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
const years = Math.floor(diffDays / 365);
|
||||||
|
const months = Math.floor((diffDays % 365) / 30);
|
||||||
|
return { years, months };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAccountAge = (age: { years: number; months: number }) => {
|
||||||
|
if (age.years > 0) {
|
||||||
|
return `${age.years}年${age.months}个月`;
|
||||||
|
}
|
||||||
|
return `${age.months}个月`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightColor = (weight: number) => {
|
||||||
|
if (weight >= 80) return "text-green-600";
|
||||||
|
if (weight >= 60) return "text-yellow-600";
|
||||||
|
return "text-red-600";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightDescription = (weight: number) => {
|
||||||
|
if (weight >= 80) return "账号质量优秀,可以正常使用";
|
||||||
|
if (weight >= 60) return "账号质量良好,需要注意使用频率";
|
||||||
|
return "账号质量较差,建议谨慎使用";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransferFriends = () => {
|
||||||
|
setShowTransferConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmTransferFriends = () => {
|
||||||
|
Toast.show({
|
||||||
|
content: "好友转移计划已创建,请在场景获客中查看详情",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
setShowTransferConfirm(false);
|
||||||
|
navigate("/scenarios");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRestrictionLevelColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case "high":
|
||||||
|
return "text-red-600";
|
||||||
|
case "medium":
|
||||||
|
return "text-yellow-600";
|
||||||
|
default:
|
||||||
|
return "text-gray-600";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date
|
||||||
|
.toLocaleString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(/\//g, "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout header={<NavCommon title="微信号详情" />} loading={loadingInfo}>
|
||||||
|
<div className={style["wechat-account-detail-page"]}>
|
||||||
|
{/* 账号基本信息卡片 */}
|
||||||
|
<Card className={style["account-card"]}>
|
||||||
|
<div className={style["account-info"]}>
|
||||||
|
<div className={style["avatar-section"]}>
|
||||||
|
<Avatar
|
||||||
|
src={accountInfo?.avatar || "/placeholder.svg"}
|
||||||
|
className={style["avatar"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={style["info-section"]}>
|
||||||
|
<div className={style["name-row"]}>
|
||||||
|
<h2 className={style["nickname"]}>
|
||||||
|
{accountInfo?.nickname || "未知昵称"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className={style["wechat-id"]}>
|
||||||
|
微信号:{accountInfo?.wechatId || "未知"}
|
||||||
|
</p>
|
||||||
|
<div className={style["action-buttons"]}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
className={style["action-btn"]}
|
||||||
|
onClick={handleTransferFriends}
|
||||||
|
>
|
||||||
|
<UserOutlined /> 好友转移
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 标签页 */}
|
||||||
|
<Card className={style["tabs-card"]}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
className={style["tabs"]}
|
||||||
|
>
|
||||||
|
<Tabs.Tab title="账号概览" key="overview">
|
||||||
|
<div className={style["overview-content"]}>
|
||||||
|
<div className={style["summary-grid"]}>
|
||||||
|
<div className={style["summary-item"]}>
|
||||||
|
<div className={style["summary-value"]}>
|
||||||
|
{accountInfo?.friendShip?.totalFriend ?? "-"}
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-label"]}>好友数量</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-item"]}>
|
||||||
|
<div className={style["summary-value-green"]}>
|
||||||
|
+{accountSummary?.statistics.todayAdded ?? "-"}
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-label"]}>今日新增</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-progress-row"]}>
|
||||||
|
<span>今日可添加:</span>
|
||||||
|
<span className={style["summary-progress-text"]}>
|
||||||
|
{accountSummary?.statistics.todayAdded ?? 0}/
|
||||||
|
{accountSummary?.statistics.addLimit ?? 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-progress-bar"]}>
|
||||||
|
<div className={style["progress-bg"]}>
|
||||||
|
<div
|
||||||
|
className={style["progress-fill"]}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(((accountSummary?.statistics.todayAdded ?? 0) / (accountSummary?.statistics.addLimit || 1)) * 100, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-grid"]}>
|
||||||
|
<div className={style["summary-item"]}>
|
||||||
|
<div className={style["summary-value-blue"]}>
|
||||||
|
{accountInfo?.friendShip?.groupNumber ?? "-"}
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-label"]}>群聊数量</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-item"]}>
|
||||||
|
<div className={style["summary-value-green"]}>
|
||||||
|
{accountInfo?.activity?.yesterdayMsgCount ?? "-"}
|
||||||
|
</div>
|
||||||
|
<div className={style["summary-label"]}>今日消息</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["device-card"]}>
|
||||||
|
<div className={style["device-title"]}>设备信息</div>
|
||||||
|
<div className={style["device-row"]}>
|
||||||
|
<span className={style["device-label"]}>设备名称:</span>
|
||||||
|
<span>{accountInfo?.deviceName ?? "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["device-row"]}>
|
||||||
|
<span className={style["device-label"]}>系统类型:</span>
|
||||||
|
<span>{accountInfo?.deviceType ?? "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["device-row"]}>
|
||||||
|
<span className={style["device-label"]}>系统版本:</span>
|
||||||
|
<span>{accountInfo?.deviceVersion ?? "-"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab
|
||||||
|
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
||||||
|
key="friends"
|
||||||
|
>
|
||||||
|
<div className={style["friends-content"]}>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className={style["search-bar"]}>
|
||||||
|
<div className={style["search-input-wrapper"]}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索好友昵称/微信号"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e: any) => setSearchQuery(e.target.value)}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={handleRefreshFriends}
|
||||||
|
loading={isFetchingFriends}
|
||||||
|
className={style["refresh-btn"]}
|
||||||
|
>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 好友列表 */}
|
||||||
|
<div className={style["friends-list"]}>
|
||||||
|
{isFetchingFriends && friends.length === 0 ? (
|
||||||
|
<div className={style["loading"]}>
|
||||||
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
|
</div>
|
||||||
|
) : isFriendsEmpty ? (
|
||||||
|
<div className={style["empty"]}>暂无好友数据</div>
|
||||||
|
) : hasFriendLoadError ? (
|
||||||
|
<div className={style["error"]}>
|
||||||
|
<p>加载失败,请重试</p>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
fetchFriendsList(friendsPage, searchQuery)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{friends.map((friend) => (
|
||||||
|
<div key={friend.id} className={style["friend-item"]}>
|
||||||
|
<Avatar
|
||||||
|
src={friend.avatar}
|
||||||
|
className={style["friend-avatar"]}
|
||||||
|
/>
|
||||||
|
<div className={style["friend-info"]}>
|
||||||
|
<div className={style["friend-header"]}>
|
||||||
|
<div className={style["friend-name"]}>
|
||||||
|
{friend.nickname}
|
||||||
|
{friend.remark && (
|
||||||
|
<span className={style["friend-remark"]}>
|
||||||
|
({friend.remark})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["friend-wechat-id"]}>
|
||||||
|
{friend.wechatId}
|
||||||
|
</div>
|
||||||
|
<div className={style["friend-tags"]}>
|
||||||
|
{friend.tags?.map((tag, index) => (
|
||||||
|
<Tag
|
||||||
|
key={index}
|
||||||
|
className={style["friend-tag"]}
|
||||||
|
>
|
||||||
|
{typeof tag === "string" ? tag : tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
{friendsTotal > 20 &&
|
||||||
|
!isFriendsEmpty &&
|
||||||
|
!hasFriendLoadError && (
|
||||||
|
<div className={style["pagination-wrapper"]}>
|
||||||
|
<Pagination
|
||||||
|
total={Math.ceil(friendsTotal / 20)}
|
||||||
|
current={friendsPage}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
showText={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 限制记录详情弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showRestrictions}
|
||||||
|
onMaskClick={() => setShowRestrictions(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div className={style["popup-header"]}>
|
||||||
|
<h3>限制记录详情</h3>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowRestrictions(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className={style["popup-description"]}>每次限制恢复时间为24小时</p>
|
||||||
|
{accountSummary && accountSummary.restrictions && (
|
||||||
|
<div className={style["restrictions-detail"]}>
|
||||||
|
{accountSummary.restrictions.map((restriction) => (
|
||||||
|
<div
|
||||||
|
key={restriction.id}
|
||||||
|
className={style["restriction-detail-item"]}
|
||||||
|
>
|
||||||
|
<div className={style["restriction-detail-info"]}>
|
||||||
|
<div className={style["restriction-detail-reason"]}>
|
||||||
|
{restriction.reason}
|
||||||
|
</div>
|
||||||
|
<div className={style["restriction-detail-date"]}>
|
||||||
|
{formatDateTime(restriction.date)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`${style["restriction-detail-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
||||||
|
>
|
||||||
|
{restriction.level === "high"
|
||||||
|
? "高风险"
|
||||||
|
: restriction.level === "medium"
|
||||||
|
? "中风险"
|
||||||
|
: "低风险"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
{/* 好友转移确认弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showTransferConfirm}
|
||||||
|
onMaskClick={() => setShowTransferConfirm(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div className={style["popup-header"]}>
|
||||||
|
<h3>确认好友转移</h3>
|
||||||
|
</div>
|
||||||
|
<p className={style["popup-description"]}>
|
||||||
|
确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。
|
||||||
|
</p>
|
||||||
|
<div className={style["popup-actions"]}>
|
||||||
|
<Button block color="primary" onClick={confirmTransferFriends}>
|
||||||
|
确认转移
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="danger"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowTransferConfirm(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
{/* 好友详情弹窗 */}
|
||||||
|
{/* Removed */}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WechatAccountDetail;
|
||||||
@@ -1,30 +1,18 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import { Button, SpinLoading, Toast } from "antd-mobile";
|
||||||
NavBar,
|
|
||||||
List,
|
|
||||||
Card,
|
|
||||||
Button,
|
|
||||||
SpinLoading,
|
|
||||||
Popup,
|
|
||||||
Toast,
|
|
||||||
} from "antd-mobile";
|
|
||||||
import { Pagination, Input, Tooltip } from "antd";
|
import { Pagination, Input, Tooltip } from "antd";
|
||||||
import {
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
ArrowLeftOutlined,
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { getWechatAccounts } from "./api";
|
import { getWechatAccounts } from "./api";
|
||||||
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
|
||||||
interface WechatAccount {
|
interface WechatAccount {
|
||||||
id: number;
|
id: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
wechatAccount: string;
|
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
times: number; // 今日可添加
|
times: number; // 今日可添加
|
||||||
addedCount: number; // 今日新增
|
addedCount: number; // 今日新增
|
||||||
@@ -44,10 +32,6 @@ const WechatAccounts: React.FC = () => {
|
|||||||
const [totalAccounts, setTotalAccounts] = useState(0);
|
const [totalAccounts, setTotalAccounts] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
|
||||||
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchAccounts = async (page = 1, keyword = "") => {
|
const fetchAccounts = async (page = 1, keyword = "") => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -91,8 +75,7 @@ const WechatAccounts: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAccountClick = (account: WechatAccount) => {
|
const handleAccountClick = (account: WechatAccount) => {
|
||||||
setSelectedAccount(account);
|
navigate(`/wechat-accounts/detail/${account.wechatId}`);
|
||||||
setPopupVisible(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransferFriends = (account: WechatAccount) => {
|
const handleTransferFriends = (account: WechatAccount) => {
|
||||||
@@ -104,20 +87,7 @@ const WechatAccounts: React.FC = () => {
|
|||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<NavBar
|
<NavCommon title="微信号管理" />
|
||||||
back={null}
|
|
||||||
style={{ background: "#fff" }}
|
|
||||||
left={
|
|
||||||
<div className={style["nav-title"]}>
|
|
||||||
<ArrowLeftOutlined
|
|
||||||
twoToneColor="#1677ff"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className={style["nav-title"]}>微信号管理</span>
|
|
||||||
</NavBar>
|
|
||||||
<div className="search-bar">
|
<div className="search-bar">
|
||||||
<div className="search-input-wrapper">
|
<div className="search-input-wrapper">
|
||||||
<Input
|
<Input
|
||||||
@@ -193,7 +163,7 @@ const WechatAccounts: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["wechat-id"]}>
|
<div className={style["wechat-id"]}>
|
||||||
微信号:{account.wechatAccount}
|
微信号:{account.wechatId}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,48 +229,6 @@ const WechatAccounts: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Popup
|
|
||||||
visible={popupVisible}
|
|
||||||
onMaskClick={() => setPopupVisible(false)}
|
|
||||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
|
||||||
>
|
|
||||||
{selectedAccount && (
|
|
||||||
<div className={style["popup-content"]}>
|
|
||||||
<div style={{ textAlign: "center", margin: 16 }}>
|
|
||||||
<img
|
|
||||||
src={selectedAccount.avatar}
|
|
||||||
alt="avatar"
|
|
||||||
style={{ width: 60, height: 60, borderRadius: 30 }}
|
|
||||||
/>
|
|
||||||
<div style={{ fontWeight: 600, marginTop: 8 }}>
|
|
||||||
{selectedAccount.nickname}
|
|
||||||
</div>
|
|
||||||
<div style={{ color: "#888", fontSize: 12 }}>
|
|
||||||
微信号:{selectedAccount.wechatAccount}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ margin: 16 }}>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/wechat-accounts/detail/${selectedAccount.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
color="danger"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setPopupVisible(false)}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Popup>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import request from "@/api/request";
|
|
||||||
|
|
||||||
// 获取微信号详情
|
|
||||||
export function getWechatAccountDetail(id: string) {
|
|
||||||
return request("/WechatAccount/detail", { id }, "GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取微信号summary
|
|
||||||
export function getWechatAccountSummary(id: string) {
|
|
||||||
return request(`/v1/wechats/${id}/summary`, {}, "GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取微信号好友列表
|
|
||||||
export function getWechatFriends(params: {
|
|
||||||
wechatAccountKeyword: string;
|
|
||||||
pageIndex: number;
|
|
||||||
pageSize: number;
|
|
||||||
friendKeyword?: string;
|
|
||||||
}) {
|
|
||||||
return request("/WechatFriend/friendlistData", params, "POST");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取微信好友详情
|
|
||||||
export function getWechatFriendDetail(id: string) {
|
|
||||||
return request("/v1/WechatFriend/detail", { id }, "GET");
|
|
||||||
}
|
|
||||||
@@ -1,940 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
NavBar,
|
|
||||||
Card,
|
|
||||||
Tabs,
|
|
||||||
Button,
|
|
||||||
SpinLoading,
|
|
||||||
Popup,
|
|
||||||
Toast,
|
|
||||||
Input,
|
|
||||||
Avatar,
|
|
||||||
Tag,
|
|
||||||
} from "antd-mobile";
|
|
||||||
import NavCommon from "@/components/NavCommon";
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
ClockCircleOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
StarOutlined,
|
|
||||||
ExclamationCircleOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import Layout from "@/components/Layout/Layout";
|
|
||||||
import style from "./detail.module.scss";
|
|
||||||
import {
|
|
||||||
getWechatAccountDetail,
|
|
||||||
getWechatAccountSummary,
|
|
||||||
getWechatFriends,
|
|
||||||
getWechatFriendDetail,
|
|
||||||
} from "./api";
|
|
||||||
|
|
||||||
interface WechatAccountSummary {
|
|
||||||
accountAge: string;
|
|
||||||
activityLevel: {
|
|
||||||
allTimes: number;
|
|
||||||
dayTimes: number;
|
|
||||||
};
|
|
||||||
accountWeight: {
|
|
||||||
scope: number;
|
|
||||||
ageWeight: number;
|
|
||||||
activityWeigth: number;
|
|
||||||
restrictWeight: number;
|
|
||||||
realNameWeight: number;
|
|
||||||
};
|
|
||||||
statistics: {
|
|
||||||
todayAdded: number;
|
|
||||||
addLimit: number;
|
|
||||||
};
|
|
||||||
restrictions: {
|
|
||||||
id: number;
|
|
||||||
level: string;
|
|
||||||
reason: string;
|
|
||||||
date: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Friend {
|
|
||||||
id: string;
|
|
||||||
avatar: string;
|
|
||||||
nickname: string;
|
|
||||||
wechatId: string;
|
|
||||||
remark: string;
|
|
||||||
addTime: string;
|
|
||||||
lastInteraction: string;
|
|
||||||
tags: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
}>;
|
|
||||||
region: string;
|
|
||||||
source: string;
|
|
||||||
notes: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WechatFriendDetail {
|
|
||||||
id: number;
|
|
||||||
avatar: string;
|
|
||||||
nickname: string;
|
|
||||||
region: string;
|
|
||||||
wechatId: string;
|
|
||||||
addDate: string;
|
|
||||||
tags: string[];
|
|
||||||
memo: string;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WechatAccountDetail: React.FC = () => {
|
|
||||||
const { id } = useParams<{ id: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [accountSummary, setAccountSummary] =
|
|
||||||
useState<WechatAccountSummary | null>(null);
|
|
||||||
const [accountInfo, setAccountInfo] = useState<any>(null);
|
|
||||||
const [showRestrictions, setShowRestrictions] = useState(false);
|
|
||||||
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
|
||||||
const [showFriendDetail, setShowFriendDetail] = useState(false);
|
|
||||||
const [selectedFriend, setSelectedFriend] = useState<Friend | null>(null);
|
|
||||||
const [friendDetail, setFriendDetail] = useState<WechatFriendDetail | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false);
|
|
||||||
const [friendDetailError, setFriendDetailError] = useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [loadingInfo, setLoadingInfo] = useState(true);
|
|
||||||
const [loadingSummary, setLoadingSummary] = useState(true);
|
|
||||||
|
|
||||||
// 好友列表相关状态
|
|
||||||
const [friends, setFriends] = useState<Friend[]>([]);
|
|
||||||
const [friendsPage, setFriendsPage] = useState(1);
|
|
||||||
const [friendsTotal, setFriendsTotal] = useState(0);
|
|
||||||
const [hasMoreFriends, setHasMoreFriends] = useState(true);
|
|
||||||
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
|
|
||||||
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
|
|
||||||
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
|
|
||||||
const friendsObserver = useRef<IntersectionObserver | null>(null);
|
|
||||||
const friendsLoadingRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
// 获取基础信息
|
|
||||||
const fetchAccountInfo = useCallback(async () => {
|
|
||||||
if (!id) return;
|
|
||||||
setLoadingInfo(true);
|
|
||||||
try {
|
|
||||||
const response = await getWechatAccountDetail(id);
|
|
||||||
if (response && response.data) {
|
|
||||||
setAccountInfo(response.data);
|
|
||||||
} else {
|
|
||||||
Toast.show({
|
|
||||||
content: response?.msg || "获取账号信息失败",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show({ content: "获取账号信息失败", position: "top" });
|
|
||||||
} finally {
|
|
||||||
setLoadingInfo(false);
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
// 获取summary
|
|
||||||
const fetchAccountSummary = useCallback(async () => {
|
|
||||||
if (!id) return;
|
|
||||||
setLoadingSummary(true);
|
|
||||||
try {
|
|
||||||
const response = await getWechatAccountSummary(id);
|
|
||||||
if (response && response.data) {
|
|
||||||
setAccountSummary(response.data);
|
|
||||||
} else {
|
|
||||||
Toast.show({
|
|
||||||
content: response?.msg || "获取账号概览失败",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show({ content: "获取账号概览失败", position: "top" });
|
|
||||||
} finally {
|
|
||||||
setLoadingSummary(false);
|
|
||||||
}
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
// 获取好友列表
|
|
||||||
const fetchFriends = useCallback(
|
|
||||||
async (page: number = 1, isNewSearch: boolean = false) => {
|
|
||||||
if (!id || isFetchingFriends) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsFetchingFriends(true);
|
|
||||||
setHasFriendLoadError(false);
|
|
||||||
const response = await getWechatFriends({
|
|
||||||
wechatAccountKeyword: id,
|
|
||||||
pageIndex: page,
|
|
||||||
pageSize: 20,
|
|
||||||
friendKeyword: searchQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response && response.data) {
|
|
||||||
const newFriends = response.data.list.map((friend: any) => ({
|
|
||||||
id: friend.id.toString(),
|
|
||||||
avatar: friend.avatar || "/placeholder.svg",
|
|
||||||
nickname: friend.nickname || "未知用户",
|
|
||||||
wechatId: friend.wechatId || "",
|
|
||||||
remark: friend.memo || "",
|
|
||||||
addTime:
|
|
||||||
friend.createTime || new Date().toISOString().split("T")[0],
|
|
||||||
lastInteraction:
|
|
||||||
friend.lastInteraction || new Date().toISOString().split("T")[0],
|
|
||||||
tags: friend.tags
|
|
||||||
? friend.tags.map((tag: string, index: number) => ({
|
|
||||||
id: `tag-${index}`,
|
|
||||||
name: tag,
|
|
||||||
color: getRandomTagColor(),
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
region: friend.region || "未知",
|
|
||||||
source: friend.source || "未知",
|
|
||||||
notes: friend.notes || "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (isNewSearch) {
|
|
||||||
setFriends(newFriends);
|
|
||||||
if (newFriends.length === 0) {
|
|
||||||
setIsFriendsEmpty(true);
|
|
||||||
setHasMoreFriends(false);
|
|
||||||
} else {
|
|
||||||
setIsFriendsEmpty(false);
|
|
||||||
setHasMoreFriends(newFriends.length === 20);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFriends((prev) => [...prev, ...newFriends]);
|
|
||||||
setHasMoreFriends(newFriends.length === 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFriendsTotal(response.data.total);
|
|
||||||
setFriendsPage(page);
|
|
||||||
} else {
|
|
||||||
setHasFriendLoadError(true);
|
|
||||||
if (isNewSearch) {
|
|
||||||
setFriends([]);
|
|
||||||
setIsFriendsEmpty(true);
|
|
||||||
setHasMoreFriends(false);
|
|
||||||
}
|
|
||||||
Toast.show({
|
|
||||||
content: response?.msg || "获取好友列表失败",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取好友列表失败:", error);
|
|
||||||
setHasFriendLoadError(true);
|
|
||||||
if (isNewSearch) {
|
|
||||||
setFriends([]);
|
|
||||||
setIsFriendsEmpty(true);
|
|
||||||
setHasMoreFriends(false);
|
|
||||||
}
|
|
||||||
Toast.show({
|
|
||||||
content: "获取好友列表失败,请检查网络连接",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsFetchingFriends(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[id, searchQuery, isFetchingFriends]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (id) {
|
|
||||||
fetchAccountInfo();
|
|
||||||
fetchAccountSummary();
|
|
||||||
if (activeTab === "friends") {
|
|
||||||
fetchFriends(1, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
// 监听标签切换
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab === "friends" && id) {
|
|
||||||
setIsFriendsEmpty(false);
|
|
||||||
setHasFriendLoadError(false);
|
|
||||||
fetchFriends(1, true);
|
|
||||||
}
|
|
||||||
}, [activeTab, id, fetchFriends]);
|
|
||||||
|
|
||||||
// 无限滚动加载好友
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!friendsLoadingRef.current ||
|
|
||||||
!hasMoreFriends ||
|
|
||||||
isFetchingFriends ||
|
|
||||||
isFriendsEmpty
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
friendsObserver.current = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
if (
|
|
||||||
entries[0].isIntersecting &&
|
|
||||||
hasMoreFriends &&
|
|
||||||
!isFetchingFriends &&
|
|
||||||
!isFriendsEmpty
|
|
||||||
) {
|
|
||||||
fetchFriends(friendsPage + 1, false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.1 }
|
|
||||||
);
|
|
||||||
|
|
||||||
friendsObserver.current.observe(friendsLoadingRef.current);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (friendsObserver.current) {
|
|
||||||
friendsObserver.current.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
hasMoreFriends,
|
|
||||||
isFetchingFriends,
|
|
||||||
friendsPage,
|
|
||||||
fetchFriends,
|
|
||||||
isFriendsEmpty,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const getRandomTagColor = (): string => {
|
|
||||||
const colors = [
|
|
||||||
"bg-blue-100 text-blue-800",
|
|
||||||
"bg-green-100 text-green-800",
|
|
||||||
"bg-red-100 text-red-800",
|
|
||||||
"bg-pink-100 text-pink-800",
|
|
||||||
"bg-emerald-100 text-emerald-800",
|
|
||||||
"bg-amber-100 text-amber-800",
|
|
||||||
];
|
|
||||||
return colors[Math.floor(Math.random() * colors.length)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateAccountAge = (registerTime: string) => {
|
|
||||||
const registerDate = new Date(registerTime);
|
|
||||||
const now = new Date();
|
|
||||||
const diffTime = Math.abs(now.getTime() - registerDate.getTime());
|
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
const years = Math.floor(diffDays / 365);
|
|
||||||
const months = Math.floor((diffDays % 365) / 30);
|
|
||||||
return { years, months };
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAccountAge = (age: { years: number; months: number }) => {
|
|
||||||
if (age.years > 0) {
|
|
||||||
return `${age.years}年${age.months}个月`;
|
|
||||||
}
|
|
||||||
return `${age.months}个月`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWeightColor = (weight: number) => {
|
|
||||||
if (weight >= 80) return "text-green-600";
|
|
||||||
if (weight >= 60) return "text-yellow-600";
|
|
||||||
return "text-red-600";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWeightDescription = (weight: number) => {
|
|
||||||
if (weight >= 80) return "账号质量优秀,可以正常使用";
|
|
||||||
if (weight >= 60) return "账号质量良好,需要注意使用频率";
|
|
||||||
return "账号质量较差,建议谨慎使用";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransferFriends = () => {
|
|
||||||
setShowTransferConfirm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmTransferFriends = () => {
|
|
||||||
Toast.show({
|
|
||||||
content: "好友转移计划已创建,请在场景获客中查看详情",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
setShowTransferConfirm(false);
|
|
||||||
navigate("/scenarios");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFriendClick = async (friend: Friend) => {
|
|
||||||
setSelectedFriend(friend);
|
|
||||||
setShowFriendDetail(true);
|
|
||||||
setIsLoadingFriendDetail(true);
|
|
||||||
setFriendDetailError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await getWechatFriendDetail(friend.id);
|
|
||||||
if (response && response.data) {
|
|
||||||
setFriendDetail(response.data);
|
|
||||||
} else {
|
|
||||||
setFriendDetailError(response?.msg || "获取好友详情失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取好友详情失败:", error);
|
|
||||||
setFriendDetailError("网络错误,请稍后重试");
|
|
||||||
} finally {
|
|
||||||
setIsLoadingFriendDetail(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRestrictionLevelColor = (level: string) => {
|
|
||||||
switch (level) {
|
|
||||||
case "high":
|
|
||||||
return "text-red-600";
|
|
||||||
case "medium":
|
|
||||||
return "text-yellow-600";
|
|
||||||
default:
|
|
||||||
return "text-gray-600";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (dateString: string) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date
|
|
||||||
.toLocaleString("zh-CN", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
hour12: false,
|
|
||||||
})
|
|
||||||
.replace(/\//g, "-");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
setIsFriendsEmpty(false);
|
|
||||||
setHasFriendLoadError(false);
|
|
||||||
fetchFriends(1, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
|
||||||
setActiveTab(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingInfo || loadingSummary) {
|
|
||||||
return (
|
|
||||||
<Layout
|
|
||||||
header={
|
|
||||||
<NavBar back={null} style={{ background: "#fff" }}>
|
|
||||||
<span className={style["nav-title"]}>微信号详情</span>
|
|
||||||
</NavBar>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={style["loading"]}>
|
|
||||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout header={<NavCommon title="微信号详情" />}>
|
|
||||||
<div className={style["wechat-account-detail-page"]}>
|
|
||||||
{/* 账号基本信息卡片 */}
|
|
||||||
<Card className={style["account-card"]}>
|
|
||||||
<div className={style["account-info"]}>
|
|
||||||
<div className={style["avatar-section"]}>
|
|
||||||
<Avatar
|
|
||||||
src={accountInfo?.avatar || "/placeholder.svg"}
|
|
||||||
className={style["avatar"]}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`${style["status-dot"]} ${accountInfo?.wechatStatus === 1 ? style["status-normal"] : style["status-abnormal"]}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={style["info-section"]}>
|
|
||||||
<div className={style["name-row"]}>
|
|
||||||
<h2 className={style["nickname"]}>
|
|
||||||
{accountInfo?.nickname || "未知昵称"}
|
|
||||||
</h2>
|
|
||||||
<Tag
|
|
||||||
color={accountInfo?.wechatStatus === 1 ? "success" : "danger"}
|
|
||||||
className={style["status-tag"]}
|
|
||||||
>
|
|
||||||
{accountInfo?.wechatStatus === 1 ? "正常" : "异常"}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
<p className={style["wechat-id"]}>
|
|
||||||
微信号:{accountInfo?.wechatAccount || "未知"}
|
|
||||||
</p>
|
|
||||||
<div className={style["action-buttons"]}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
className={style["action-btn"]}
|
|
||||||
>
|
|
||||||
<UserOutlined /> {accountInfo?.deviceMemo || "未知设备"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
className={style["action-btn"]}
|
|
||||||
onClick={handleTransferFriends}
|
|
||||||
>
|
|
||||||
<UserOutlined /> 好友转移
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 标签页 */}
|
|
||||||
<Card className={style["tabs-card"]}>
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
className={style["tabs"]}
|
|
||||||
>
|
|
||||||
<Tabs.Tab title="账号概览" key="overview">
|
|
||||||
<div className={style["overview-content"]}>
|
|
||||||
{/* 账号基础信息 */}
|
|
||||||
<div className={style["info-grid"]}>
|
|
||||||
<div className={style["info-card"]}>
|
|
||||||
<div className={style["info-header"]}>
|
|
||||||
<ClockCircleOutlined className={style["info-icon"]} />
|
|
||||||
<div className={style["info-title"]}>
|
|
||||||
<div className={style["title-text"]}>账号年龄</div>
|
|
||||||
{accountSummary && (
|
|
||||||
<div className={style["title-sub"]}>
|
|
||||||
注册于{" "}
|
|
||||||
{new Date(
|
|
||||||
accountSummary.accountAge
|
|
||||||
).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{accountSummary && (
|
|
||||||
<div className={style["info-value"]}>
|
|
||||||
{formatAccountAge(
|
|
||||||
calculateAccountAge(accountSummary.accountAge)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style["info-card"]}>
|
|
||||||
<div className={style["info-header"]}>
|
|
||||||
<MessageOutlined className={style["info-icon"]} />
|
|
||||||
<div className={style["info-title"]}>
|
|
||||||
<div className={style["title-text"]}>活跃程度</div>
|
|
||||||
{accountSummary && (
|
|
||||||
<div className={style["title-sub"]}>
|
|
||||||
总聊天{" "}
|
|
||||||
{accountSummary.activityLevel.allTimes.toLocaleString()}{" "}
|
|
||||||
次
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{accountSummary && (
|
|
||||||
<div className={style["info-value"]}>
|
|
||||||
{accountSummary.activityLevel.dayTimes.toLocaleString()}
|
|
||||||
<span className={style["value-unit"]}>次/天</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 账号权重评估 */}
|
|
||||||
{accountSummary && (
|
|
||||||
<div className={style["weight-card"]}>
|
|
||||||
<div className={style["weight-header"]}>
|
|
||||||
<StarOutlined className={style["weight-icon"]} />
|
|
||||||
<span className={style["weight-title"]}>
|
|
||||||
账号权重评估
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className={`${style["weight-score"]} ${getWeightColor(accountSummary.accountWeight.scope)}`}
|
|
||||||
>
|
|
||||||
<span className={style["score-value"]}>
|
|
||||||
{accountSummary.accountWeight.scope}
|
|
||||||
</span>
|
|
||||||
<span className={style["score-unit"]}>分</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className={style["weight-description"]}>
|
|
||||||
{getWeightDescription(accountSummary.accountWeight.scope)}
|
|
||||||
</p>
|
|
||||||
<div className={style["weight-items"]}>
|
|
||||||
<div className={style["weight-item"]}>
|
|
||||||
<span className={style["item-label"]}>账号年龄</span>
|
|
||||||
<div className={style["progress-bar"]}>
|
|
||||||
<div
|
|
||||||
className={style["progress-fill"]}
|
|
||||||
style={{
|
|
||||||
width: `${accountSummary.accountWeight.ageWeight}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={style["item-value"]}>
|
|
||||||
{accountSummary.accountWeight.ageWeight}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={style["weight-item"]}>
|
|
||||||
<span className={style["item-label"]}>活跃度</span>
|
|
||||||
<div className={style["progress-bar"]}>
|
|
||||||
<div
|
|
||||||
className={style["progress-fill"]}
|
|
||||||
style={{
|
|
||||||
width: `${accountSummary.accountWeight.activityWeigth}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={style["item-value"]}>
|
|
||||||
{accountSummary.accountWeight.activityWeigth}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 限制记录 */}
|
|
||||||
{accountSummary &&
|
|
||||||
accountSummary.restrictions &&
|
|
||||||
accountSummary.restrictions.length > 0 && (
|
|
||||||
<div className={style["restrictions-card"]}>
|
|
||||||
<div className={style["restrictions-header"]}>
|
|
||||||
<ExclamationCircleOutlined
|
|
||||||
className={style["restrictions-icon"]}
|
|
||||||
/>
|
|
||||||
<span className={style["restrictions-title"]}>
|
|
||||||
限制记录
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setShowRestrictions(true)}
|
|
||||||
className={style["restrictions-btn"]}
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={style["restrictions-list"]}>
|
|
||||||
{accountSummary.restrictions
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((restriction) => (
|
|
||||||
<div
|
|
||||||
key={restriction.id}
|
|
||||||
className={style["restriction-item"]}
|
|
||||||
>
|
|
||||||
<div className={style["restriction-info"]}>
|
|
||||||
<span className={style["restriction-reason"]}>
|
|
||||||
{restriction.reason}
|
|
||||||
</span>
|
|
||||||
<span className={style["restriction-date"]}>
|
|
||||||
{formatDateTime(restriction.date)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`${style["restriction-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
|
||||||
>
|
|
||||||
{restriction.level === "high"
|
|
||||||
? "高风险"
|
|
||||||
: restriction.level === "medium"
|
|
||||||
? "中风险"
|
|
||||||
: "低风险"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tabs.Tab>
|
|
||||||
|
|
||||||
<Tabs.Tab
|
|
||||||
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
|
||||||
key="friends"
|
|
||||||
>
|
|
||||||
<div className={style["friends-content"]}>
|
|
||||||
{/* 搜索栏 */}
|
|
||||||
<div className={style["search-bar"]}>
|
|
||||||
<div className={style["search-input-wrapper"]}>
|
|
||||||
<Input
|
|
||||||
placeholder="搜索好友昵称/微信号"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
allowClear
|
|
||||||
size="large"
|
|
||||||
onPressEnter={handleSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={handleSearch}
|
|
||||||
loading={isFetchingFriends}
|
|
||||||
className={style["search-btn"]}
|
|
||||||
>
|
|
||||||
<ReloadOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 好友列表 */}
|
|
||||||
<div className={style["friends-list"]}>
|
|
||||||
{isFriendsEmpty ? (
|
|
||||||
<div className={style["empty"]}>暂无好友数据</div>
|
|
||||||
) : hasFriendLoadError ? (
|
|
||||||
<div className={style["error"]}>
|
|
||||||
<p>加载失败,请重试</p>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => fetchFriends(1, true)}
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{friends.map((friend) => (
|
|
||||||
<div
|
|
||||||
key={friend.id}
|
|
||||||
className={style["friend-item"]}
|
|
||||||
onClick={() => handleFriendClick(friend)}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
src={friend.avatar}
|
|
||||||
className={style["friend-avatar"]}
|
|
||||||
/>
|
|
||||||
<div className={style["friend-info"]}>
|
|
||||||
<div className={style["friend-header"]}>
|
|
||||||
<div className={style["friend-name"]}>
|
|
||||||
{friend.nickname}
|
|
||||||
{friend.remark && (
|
|
||||||
<span className={style["friend-remark"]}>
|
|
||||||
({friend.remark})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<RightOutlined
|
|
||||||
className={style["friend-arrow"]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={style["friend-wechat-id"]}>
|
|
||||||
{friend.wechatId}
|
|
||||||
</div>
|
|
||||||
<div className={style["friend-tags"]}>
|
|
||||||
{friend.tags?.map((tag, index) => (
|
|
||||||
<Tag
|
|
||||||
key={index}
|
|
||||||
size="small"
|
|
||||||
className={style["friend-tag"]}
|
|
||||||
>
|
|
||||||
{typeof tag === "string" ? tag : tag.name}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{hasMoreFriends && !isFriendsEmpty && (
|
|
||||||
<div
|
|
||||||
ref={friendsLoadingRef}
|
|
||||||
className={style["loading-more"]}
|
|
||||||
>
|
|
||||||
<SpinLoading
|
|
||||||
color="primary"
|
|
||||||
style={{ fontSize: 24 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 限制记录详情弹窗 */}
|
|
||||||
<Popup
|
|
||||||
visible={showRestrictions}
|
|
||||||
onMaskClick={() => setShowRestrictions(false)}
|
|
||||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
|
||||||
>
|
|
||||||
<div className={style["popup-content"]}>
|
|
||||||
<div className={style["popup-header"]}>
|
|
||||||
<h3>限制记录详情</h3>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setShowRestrictions(false)}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className={style["popup-description"]}>每次限制恢复时间为24小时</p>
|
|
||||||
{accountSummary && accountSummary.restrictions && (
|
|
||||||
<div className={style["restrictions-detail"]}>
|
|
||||||
{accountSummary.restrictions.map((restriction) => (
|
|
||||||
<div
|
|
||||||
key={restriction.id}
|
|
||||||
className={style["restriction-detail-item"]}
|
|
||||||
>
|
|
||||||
<div className={style["restriction-detail-info"]}>
|
|
||||||
<div className={style["restriction-detail-reason"]}>
|
|
||||||
{restriction.reason}
|
|
||||||
</div>
|
|
||||||
<div className={style["restriction-detail-date"]}>
|
|
||||||
{formatDateTime(restriction.date)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`${style["restriction-detail-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
|
||||||
>
|
|
||||||
{restriction.level === "high"
|
|
||||||
? "高风险"
|
|
||||||
: restriction.level === "medium"
|
|
||||||
? "中风险"
|
|
||||||
: "低风险"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
{/* 好友转移确认弹窗 */}
|
|
||||||
<Popup
|
|
||||||
visible={showTransferConfirm}
|
|
||||||
onMaskClick={() => setShowTransferConfirm(false)}
|
|
||||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
|
||||||
>
|
|
||||||
<div className={style["popup-content"]}>
|
|
||||||
<div className={style["popup-header"]}>
|
|
||||||
<h3>确认好友转移</h3>
|
|
||||||
</div>
|
|
||||||
<p className={style["popup-description"]}>
|
|
||||||
确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。
|
|
||||||
</p>
|
|
||||||
<div className={style["popup-actions"]}>
|
|
||||||
<Button block color="primary" onClick={confirmTransferFriends}>
|
|
||||||
确认转移
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
color="danger"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setShowTransferConfirm(false)}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
{/* 好友详情弹窗 */}
|
|
||||||
<Popup
|
|
||||||
visible={showFriendDetail}
|
|
||||||
onMaskClick={() => setShowFriendDetail(false)}
|
|
||||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
|
||||||
>
|
|
||||||
<div className={style["popup-content"]}>
|
|
||||||
<div className={style["popup-header"]}>
|
|
||||||
<h3>好友详情</h3>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setShowFriendDetail(false)}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoadingFriendDetail ? (
|
|
||||||
<div className={style["loading-detail"]}>
|
|
||||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
|
||||||
</div>
|
|
||||||
) : friendDetailError ? (
|
|
||||||
<div className={style["error-detail"]}>
|
|
||||||
<p>{friendDetailError}</p>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleFriendClick(selectedFriend!)}
|
|
||||||
>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : friendDetail && selectedFriend ? (
|
|
||||||
<div className={style["friend-detail-content"]}>
|
|
||||||
<div className={style["friend-detail-header"]}>
|
|
||||||
<Avatar
|
|
||||||
src={selectedFriend.avatar}
|
|
||||||
className={style["friend-detail-avatar"]}
|
|
||||||
/>
|
|
||||||
<div className={style["friend-detail-info"]}>
|
|
||||||
<h4 className={style["friend-detail-name"]}>
|
|
||||||
{selectedFriend.nickname}
|
|
||||||
</h4>
|
|
||||||
<p className={style["friend-detail-wechat-id"]}>
|
|
||||||
微信号:{selectedFriend.wechatId}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style["friend-detail-items"]}>
|
|
||||||
<div className={style["detail-item"]}>
|
|
||||||
<span className={style["detail-label"]}>地区</span>
|
|
||||||
<span className={style["detail-value"]}>
|
|
||||||
{friendDetail.region || "未知"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={style["detail-item"]}>
|
|
||||||
<span className={style["detail-label"]}>添加时间</span>
|
|
||||||
<span className={style["detail-value"]}>
|
|
||||||
{friendDetail.addDate}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={style["detail-item"]}>
|
|
||||||
<span className={style["detail-label"]}>来源</span>
|
|
||||||
<span className={style["detail-value"]}>
|
|
||||||
{friendDetail.source || "未知"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{friendDetail.memo && (
|
|
||||||
<div className={style["detail-item"]}>
|
|
||||||
<span className={style["detail-label"]}>备注</span>
|
|
||||||
<span className={style["detail-value"]}>
|
|
||||||
{friendDetail.memo}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{friendDetail.tags && friendDetail.tags.length > 0 && (
|
|
||||||
<div className={style["detail-item"]}>
|
|
||||||
<span className={style["detail-label"]}>标签</span>
|
|
||||||
<div className={style["detail-tags"]}>
|
|
||||||
{friendDetail.tags.map((tag, index) => (
|
|
||||||
<Tag
|
|
||||||
key={index}
|
|
||||||
size="small"
|
|
||||||
className={style["detail-tag"]}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WechatAccountDetail;
|
|
||||||
@@ -296,6 +296,9 @@ const AutoGroupList: React.FC = () => {
|
|||||||
<ClockCircleOutline style={{ marginRight: 4 }} />
|
<ClockCircleOutline style={{ marginRight: 4 }} />
|
||||||
更新时间:{task.lastCreateTime}
|
更新时间:{task.lastCreateTime}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={style.footerRight}>
|
||||||
|
创建时间:{task.createTime}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import Devices from "@/pages/devices/Devices";
|
|
||||||
import DeviceDetail from "@/pages/devices/DeviceDetail";
|
|
||||||
|
|
||||||
const deviceRoutes = [
|
|
||||||
{
|
|
||||||
path: "/devices",
|
|
||||||
element: <Devices />,
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/devices/:id",
|
|
||||||
element: <DeviceDetail />,
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default deviceRoutes;
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import Home from "@/pages/home/index";
|
import Home from "@/pages/home/index";
|
||||||
import Mine from "@/pages/mine/index";
|
import WechatAccounts from "@/pages/mine/wechat-accounts/list/index";
|
||||||
import WechatAccounts from "@/pages/wechat-accounts/list/index";
|
import WechatAccountDetail from "@/pages/mine/wechat-accounts/detail/index";
|
||||||
import WechatAccountDetail from "@/pages/wechat-accounts/detail/index";
|
|
||||||
import Recharge from "@/pages/mine/recharge/index";
|
import Recharge from "@/pages/mine/recharge/index";
|
||||||
import UserSetting from "@/pages/mine/userSet/index";
|
import UserSetting from "@/pages/mine/userSet/index";
|
||||||
|
|
||||||
@@ -12,11 +11,6 @@ const routes = [
|
|||||||
element: <Home />,
|
element: <Home />,
|
||||||
auth: true, // 需要登录
|
auth: true, // 需要登录
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/mine",
|
|
||||||
element: <Mine />,
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
// 微信号管理路由
|
// 微信号管理路由
|
||||||
{
|
{
|
||||||
path: "/wechat-accounts",
|
path: "/wechat-accounts",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Profile from "@/pages/profile/Profile";
|
|
||||||
import Plans from "@/pages/plans/Plans";
|
import Plans from "@/pages/plans/Plans";
|
||||||
import PlanDetail from "@/pages/plans/PlanDetail";
|
import PlanDetail from "@/pages/plans/PlanDetail";
|
||||||
import Orders from "@/pages/orders/Orders";
|
import Orders from "@/pages/orders/Orders";
|
||||||
@@ -6,16 +5,6 @@ import ContactImport from "@/pages/contact-import/ContactImport";
|
|||||||
import SelectionTest from "@/components/SelectionTest";
|
import SelectionTest from "@/components/SelectionTest";
|
||||||
|
|
||||||
const otherRoutes = [
|
const otherRoutes = [
|
||||||
{
|
|
||||||
path: "/mine",
|
|
||||||
element: <Profile />,
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/profile",
|
|
||||||
element: <Profile />,
|
|
||||||
auth: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/plans",
|
path: "/plans",
|
||||||
element: <Plans />,
|
element: <Plans />,
|
||||||
|
|||||||
23
nkebao/src/router/module/users.tsx
Normal file
23
nkebao/src/router/module/users.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Mine from "@/pages/mine/main/index";
|
||||||
|
import Devices from "@/pages/mine/devices/index";
|
||||||
|
import DeviceDetail from "@/pages/mine/devices/DeviceDetail";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/mine",
|
||||||
|
element: <Mine />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/devices",
|
||||||
|
element: <Devices />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/devices/:id",
|
||||||
|
element: <DeviceDetail />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import WechatAccounts from "@/pages/wechat-accounts/list";
|
import WechatAccounts from "@/pages/mine/wechat-accounts/list";
|
||||||
import WechatAccountDetail from "@/pages/wechat-accounts/detail";
|
import WechatAccountDetail from "@/pages/mine/wechat-accounts/detail";
|
||||||
|
|
||||||
const wechatAccountRoutes = [
|
const wechatAccountRoutes = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user