feat: 本次提交更新内容如下

微信号管理完成
This commit is contained in:
笔记本里的永平
2025-07-24 11:08:15 +08:00
parent fcfd857e5c
commit fc01bff0ad
20 changed files with 850 additions and 1236 deletions

View 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");
}

View 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;
}

View File

@@ -1,7 +1,5 @@
.wechat-account-detail-page {
padding: 16px;
background: linear-gradient(to bottom, #f0f8ff, #ffffff);
min-height: 100vh;
.loading {
display: flex;
@@ -435,6 +433,20 @@
padding: 8px 12px;
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 {
@@ -463,80 +475,83 @@
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
&:hover {
background: #f5f5f5;
border-color: #d9d9d9;
}
.friend-item-static {
display: flex;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 8px;
}
.friend-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
}
.friend-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
}
.friend-info {
flex: 1;
min-width: 0;
.friend-info {
flex: 1;
min-width: 0;
.friend-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
.friend-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
.friend-name {
font-size: 14px;
font-weight: 500;
color: #333;
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;
.friend-name {
font-size: 14px;
font-weight: 500;
color: #333;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.friend-remark {
color: #666;
margin-left: 4px;
}
}
.friend-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
.friend-arrow {
font-size: 12px;
color: #ccc;
}
}
.friend-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
}
.friend-wechat-id {
font-size: 12px;
color: #666;
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 {
display: flex;
justify-content: center;
padding: 16px 0;
}
.loading-more {
display: flex;
justify-content: center;
padding: 16px 0;
}
}
}
@@ -624,97 +639,102 @@
}
}
}
.loading-detail {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.error-detail {
text-align: center;
color: #ff4d4f;
padding: 40px 0;
p {
margin-bottom: 12px;
}
}
.friend-detail-content {
.friend-detail-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
.friend-detail-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
.friend-detail-info {
.friend-detail-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 4px 0;
}
.friend-detail-wechat-id {
font-size: 12px;
color: #666;
margin: 0;
}
}
}
.friend-detail-items {
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.detail-label {
font-size: 14px;
color: #666;
flex-shrink: 0;
width: 80px;
}
.detail-value {
font-size: 14px;
color: #333;
text-align: right;
flex: 1;
margin-left: 16px;
}
.detail-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: flex-end;
flex: 1;
margin-left: 16px;
.detail-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 6px;
}
}
}
}
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 20px 0;
margin-top: 16px;
border-top: 1px solid #f0f0f0;
}
.summary-grid {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.summary-item {
flex: 1;
background: #fafbfc;
border-radius: 10px;
padding: 16px 0 8px 0;
text-align: center;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}
.summary-value {
font-size: 24px;
font-weight: 600;
color: #222;
margin-bottom: 2px;
}
.summary-value-green {
font-size: 24px;
font-weight: 600;
color: #27ae60;
margin-bottom: 2px;
}
.summary-value-blue {
font-size: 24px;
font-weight: 600;
color: #3498db;
margin-bottom: 2px;
}
.summary-label {
font-size: 13px;
color: #888;
}
.summary-progress-row {
display: flex;
align-items: center;
font-size: 14px;
color: #666;
margin-bottom: 4px;
gap: 8px;
}
.summary-progress-text {
font-weight: 500;
color: #222;
}
.summary-progress-bar {
width: 100%;
margin-bottom: 16px;
}
.progress-bg {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 8px;
background: #3498db;
border-radius: 6px 0 0 6px;
transition: width 0.3s;
}
.device-card {
background: #fafbfc;
border-radius: 10px;
padding: 16px;
margin-top: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}
.device-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 8px;
color: #222;
}
.device-row {
display: flex;
align-items: center;
font-size: 14px;
margin-bottom: 4px;
}
.device-label {
color: #888;
min-width: 70px;
margin-right: 8px;
}

View 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;

View File

@@ -1,30 +1,18 @@
import React, { useState, useEffect } from "react";
import {
NavBar,
List,
Card,
Button,
SpinLoading,
Popup,
Toast,
} from "antd-mobile";
import { Button, SpinLoading, Toast } from "antd-mobile";
import { Pagination, Input, Tooltip } from "antd";
import {
ArrowLeftOutlined,
SearchOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import { getWechatAccounts } from "./api";
import NavCommon from "@/components/NavCommon";
interface WechatAccount {
id: number;
nickname: string;
avatar: string;
wechatId: string;
wechatAccount: string;
deviceId: number;
times: number; // 今日可添加
addedCount: number; // 今日新增
@@ -44,10 +32,6 @@ const WechatAccounts: React.FC = () => {
const [totalAccounts, setTotalAccounts] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [popupVisible, setPopupVisible] = useState(false);
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(
null
);
const fetchAccounts = async (page = 1, keyword = "") => {
setIsLoading(true);
@@ -91,8 +75,7 @@ const WechatAccounts: React.FC = () => {
};
const handleAccountClick = (account: WechatAccount) => {
setSelectedAccount(account);
setPopupVisible(true);
navigate(`/wechat-accounts/detail/${account.wechatId}`);
};
const handleTransferFriends = (account: WechatAccount) => {
@@ -104,20 +87,7 @@ const WechatAccounts: React.FC = () => {
<Layout
header={
<>
<NavBar
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>
<NavCommon title="微信号管理" />
<div className="search-bar">
<div className="search-input-wrapper">
<Input
@@ -193,7 +163,7 @@ const WechatAccounts: React.FC = () => {
</span>
</div>
<div className={style["wechat-id"]}>
{account.wechatAccount}
{account.wechatId}
</div>
</div>
</div>
@@ -259,48 +229,6 @@ const WechatAccounts: React.FC = () => {
/>
)}
</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>
</Layout>
);

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -296,6 +296,9 @@ const AutoGroupList: React.FC = () => {
<ClockCircleOutline style={{ marginRight: 4 }} />
{task.lastCreateTime}
</div>
<div className={style.footerRight}>
{task.createTime}
</div>
</div>
</Card>
))

View File

@@ -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;

View File

@@ -1,7 +1,6 @@
import Home from "@/pages/home/index";
import Mine from "@/pages/mine/index";
import WechatAccounts from "@/pages/wechat-accounts/list/index";
import WechatAccountDetail from "@/pages/wechat-accounts/detail/index";
import WechatAccounts from "@/pages/mine/wechat-accounts/list/index";
import WechatAccountDetail from "@/pages/mine/wechat-accounts/detail/index";
import Recharge from "@/pages/mine/recharge/index";
import UserSetting from "@/pages/mine/userSet/index";
@@ -12,11 +11,6 @@ const routes = [
element: <Home />,
auth: true, // 需要登录
},
{
path: "/mine",
element: <Mine />,
auth: true,
},
// 微信号管理路由
{
path: "/wechat-accounts",

View File

@@ -1,4 +1,3 @@
import Profile from "@/pages/profile/Profile";
import Plans from "@/pages/plans/Plans";
import PlanDetail from "@/pages/plans/PlanDetail";
import Orders from "@/pages/orders/Orders";
@@ -6,16 +5,6 @@ import ContactImport from "@/pages/contact-import/ContactImport";
import SelectionTest from "@/components/SelectionTest";
const otherRoutes = [
{
path: "/mine",
element: <Profile />,
auth: true,
},
{
path: "/profile",
element: <Profile />,
auth: true,
},
{
path: "/plans",
element: <Plans />,

View 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;

View File

@@ -1,5 +1,5 @@
import WechatAccounts from "@/pages/wechat-accounts/list";
import WechatAccountDetail from "@/pages/wechat-accounts/detail";
import WechatAccounts from "@/pages/mine/wechat-accounts/list";
import WechatAccountDetail from "@/pages/mine/wechat-accounts/detail";
const wechatAccountRoutes = [
{