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

存一下进度
This commit is contained in:
笔记本里的永平
2025-07-23 15:14:01 +08:00
parent 7b30f98ab9
commit f3f2e8db41
11 changed files with 2210 additions and 85 deletions

View File

@@ -1,4 +1,4 @@
# 基础环境变量示例
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
VITE_API_BASE_URL=http://www.yishi.com
VITE_APP_TITLE=Nkebao Base

View File

@@ -1,30 +0,0 @@
import React from "react";
import { NavBar } from "antd-mobile";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const WechatAccountDetail: React.FC = () => {
return (
<Layout
header={
<NavBar
backArrow
style={{ background: "#fff" }}
onBack={() => window.history.back()}
>
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
</NavBar>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default WechatAccountDetail;

View File

@@ -1,37 +0,0 @@
import React from "react";
import { NavBar, Button } from "antd-mobile";
import { PlusOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
const WechatAccounts: React.FC = () => {
return (
<Layout
header={
<NavBar
back={null}
style={{ background: "#fff" }}
left={
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
</div>
}
right={
<Button size="small" color="primary">
<PlusOutlined />
<span style={{ marginLeft: 4, fontSize: 12 }}></span>
</Button>
}
/>
}
footer={<MeauMobile />}
>
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
<h3></h3>
<p>...</p>
</div>
</Layout>
);
};
export default WechatAccounts;

View File

@@ -0,0 +1,21 @@
import request from "@/api/request";
// 获取微信号详情
export function getWechatAccountDetail(id: string) {
return request("/api/WechatAccount/detail", { id }, "GET");
}
// 获取微信号好友列表
export function getWechatFriends(params: {
wechatAccountKeyword: string;
pageIndex: number;
pageSize: number;
friendKeyword?: string;
}) {
return request("/api/WechatFriend/friendlistData", params, "POST");
}
// 获取微信好友详情
export function getWechatFriendDetail(id: string) {
return request("/api/WechatFriend/detail", { id }, "GET");
}

View File

@@ -0,0 +1,720 @@
.wechat-account-detail-page {
padding: 16px;
background: linear-gradient(to bottom, #f0f8ff, #ffffff);
min-height: 100vh;
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.account-card {
margin-bottom: 16px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e8f4fd;
.account-info {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
.avatar-section {
position: relative;
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
border: 4px solid #e8f4fd;
}
.status-dot {
position: absolute;
bottom: 2px;
right: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #fff;
&.status-normal {
background: #52c41a;
}
&.status-abnormal {
background: #ff4d4f;
}
}
}
.info-section {
flex: 1;
min-width: 0;
.name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.nickname {
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
}
.wechat-id {
font-size: 14px;
color: #666;
margin: 0 0 12px 0;
}
.action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
.action-btn {
font-size: 12px;
padding: 6px 12px;
border-radius: 8px;
border: 1px solid #d9d9d9;
background: #fff;
color: #666;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
border-color: #bfbfbf;
}
}
}
}
}
}
.tabs-card {
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e8f4fd;
.tabs {
.adm-tabs-header {
border-bottom: 1px solid #e8e8e8;
background: #fff;
border-radius: 16px 16px 0 0;
.adm-tabs-tab {
font-size: 14px;
font-weight: 500;
color: #666;
transition: all 0.2s;
&.adm-tabs-tab-active {
color: #1677ff;
font-weight: 600;
}
}
.adm-tabs-tab-line {
background: #1677ff;
height: 2px;
}
}
.adm-tabs-content {
padding: 16px;
}
}
}
.overview-content {
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
.info-card {
background: linear-gradient(135deg, #e6f7ff, #f0f8ff);
padding: 16px;
border-radius: 12px;
border: 1px solid #bae7ff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.info-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.info-icon {
font-size: 16px;
color: #1677ff;
padding: 6px;
background: #e6f7ff;
border-radius: 8px;
}
.info-title {
flex: 1;
.title-text {
font-size: 12px;
font-weight: 600;
color: #1677ff;
margin-bottom: 2px;
}
.title-sub {
font-size: 10px;
color: #666;
}
}
}
.info-value {
text-align: right;
font-size: 18px;
font-weight: 700;
color: #1677ff;
.value-unit {
font-size: 12px;
color: #666;
margin-left: 4px;
}
}
}
}
.weight-card {
background: linear-gradient(135deg, #fff7e6, #fff2d9);
padding: 20px;
border-radius: 12px;
border: 1px solid #ffd591;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.weight-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.weight-icon {
font-size: 16px;
color: #fa8c16;
margin-right: 8px;
}
.weight-title {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #fa8c16;
}
.weight-score {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: 16px;
font-weight: 600;
&.text-green-600 {
background: #f6ffed;
color: #52c41a;
}
&.text-yellow-600 {
background: #fffbe6;
color: #fa8c16;
}
&.text-red-600 {
background: #fff2f0;
color: #ff4d4f;
}
.score-value {
font-size: 20px;
font-weight: 700;
}
.score-unit {
font-size: 12px;
}
}
}
.weight-description {
font-size: 12px;
color: #fa8c16;
background: #fff7e6;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid #ffd591;
margin-bottom: 16px;
}
.weight-items {
.weight-item {
display: flex;
align-items: center;
margin-bottom: 12px;
.item-label {
flex-shrink: 0;
width: 64px;
font-size: 12px;
font-weight: 500;
color: #fa8c16;
}
.progress-bar {
flex: 1;
margin: 0 12px;
height: 8px;
background: #ffd591;
border-radius: 4px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #fa8c16, #ffa940);
border-radius: 4px;
transition: width 0.5s ease;
}
}
.item-value {
flex-shrink: 0;
width: 40px;
font-size: 12px;
font-weight: 500;
color: #fa8c16;
text-align: right;
}
}
}
}
.restrictions-card {
background: linear-gradient(135deg, #fff2f0, #fff1f0);
padding: 16px;
border-radius: 12px;
border: 1px solid #ffccc7;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
.restrictions-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.restrictions-icon {
font-size: 16px;
color: #ff4d4f;
margin-right: 8px;
}
.restrictions-title {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #ff4d4f;
}
.restrictions-btn {
font-size: 12px;
padding: 4px 8px;
border-radius: 6px;
}
}
.restrictions-list {
.restriction-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #ffccc7;
&:last-child {
border-bottom: none;
}
.restriction-info {
flex: 1;
.restriction-reason {
display: block;
font-size: 12px;
color: #333;
margin-bottom: 2px;
}
.restriction-date {
font-size: 10px;
color: #666;
}
}
.restriction-level {
font-size: 10px;
padding: 2px 6px;
border-radius: 8px;
font-weight: 500;
&.text-red-600 {
background: #fff2f0;
color: #ff4d4f;
}
&.text-yellow-600 {
background: #fffbe6;
color: #fa8c16;
}
&.text-gray-600 {
background: #f5f5f5;
color: #666;
}
}
}
}
}
}
.friends-content {
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 16px;
.search-input-wrapper {
flex: 1;
.adm-input {
border-radius: 8px;
border: 1px solid #d9d9d9;
}
}
.search-btn {
padding: 8px 12px;
border-radius: 8px;
}
}
.friends-list {
.empty {
text-align: center;
color: #999;
padding: 40px 0;
font-size: 14px;
}
.error {
text-align: center;
color: #ff4d4f;
padding: 40px 0;
p {
margin-bottom: 12px;
}
}
.friend-item {
display: flex;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f5f5f5;
border-color: #d9d9d9;
}
.friend-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 12px;
}
.friend-info {
flex: 1;
min-width: 0;
.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;
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;
}
}
}
}
.popup-content {
padding: 20px;
max-height: 80vh;
overflow-y: auto;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
h3 {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.popup-description {
font-size: 14px;
color: #666;
margin-bottom: 16px;
line-height: 1.5;
}
.popup-actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 20px;
}
.restrictions-detail {
.restriction-detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.restriction-detail-info {
flex: 1;
.restriction-detail-reason {
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.restriction-detail-date {
font-size: 12px;
color: #666;
}
}
.restriction-detail-level {
font-size: 12px;
padding: 4px 8px;
border-radius: 8px;
font-weight: 500;
&.text-red-600 {
background: #fff2f0;
color: #ff4d4f;
}
&.text-yellow-600 {
background: #fffbe6;
color: #fa8c16;
}
&.text-gray-600 {
background: #f5f5f5;
color: #666;
}
}
}
}
.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;
}
}
}
}
}
}

View File

@@ -0,0 +1,928 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
NavBar,
Card,
Tabs,
List,
Button,
SpinLoading,
Popup,
Toast,
Input,
Avatar,
Tag,
} from "antd-mobile";
import {
ArrowLeftOutlined,
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,
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 [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 fetchAccountSummary = useCallback(async () => {
if (!id) return;
try {
setIsLoading(true);
const response = await getWechatAccountDetail(id);
if (response && response.data) {
setAccountSummary(response.data);
setAccountInfo(response.data);
} else {
Toast.show({
content: response?.msg || "获取账号概览失败",
position: "top",
});
}
} catch (error) {
console.error("获取账号概览失败:", error);
Toast.show({
content: "获取账号概览失败,请检查网络连接",
position: "top",
});
} finally {
setIsLoading(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) {
fetchAccountSummary();
if (activeTab === "friends") {
fetchFriends(1, true);
}
}
}, [id, fetchAccountSummary]);
// 监听标签切换
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 (isLoading) {
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={
<NavBar back={null} style={{ background: "#fff" }}>
<span className={style["nav-title"]}></span>
</NavBar>
}
>
<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

@@ -0,0 +1,30 @@
import request from "@/api/request";
// 获取微信号列表
export function getWechatAccounts(params: {
page: number;
page_size: number;
keyword?: string;
}) {
return request("v1/wechats", params, "GET");
}
// 获取微信号详情
export function getWechatAccountDetail(id: string) {
return request("v1/WechatAccount/detail", { id }, "GET");
}
// 获取微信号好友列表
export function getWechatFriends(params: {
wechatAccountKeyword: string;
pageIndex: number;
pageSize: number;
friendKeyword?: string;
}) {
return request("v1/WechatFriend/friendlistData", params, "POST");
}
// 获取微信好友详情
export function getWechatFriendDetail(id: string) {
return request("v1/WechatFriend/detail", { id }, "GET");
}

View File

@@ -0,0 +1,171 @@
.wechat-accounts-page {
padding: 0 12px;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #222;
}
.card-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.account-card {
background: #fff;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 14px 14px 10px 14px;
transition: box-shadow 0.2s;
cursor: pointer;
border: 1px solid #f0f0f0;
&:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
border-color: #e6f7ff;
}
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.avatar-wrapper {
position: relative;
margin-right: 12px;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 3px solid #e6f0fa;
box-shadow: 0 0 0 2px #1677ff33;
object-fit: cover;
}
.status-dot-normal {
position: absolute;
right: -2px;
bottom: -2px;
width: 14px;
height: 14px;
background: #52c41a;
border: 2px solid #fff;
border-radius: 50%;
}
.status-dot-abnormal {
position: absolute;
right: -2px;
bottom: -2px;
width: 14px;
height: 14px;
background: #ff4d4f;
border: 2px solid #fff;
border-radius: 50%;
}
.header-info {
flex: 1;
min-width: 0;
}
.nickname-row {
display: flex;
align-items: center;
gap: 8px;
}
.nickname {
font-weight: 600;
font-size: 16px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-label-normal {
background: #e6fffb;
color: #13c2c2;
font-size: 12px;
border-radius: 8px;
padding: 2px 8px;
margin-left: 4px;
}
.status-label-abnormal {
background: #fff1f0;
color: #ff4d4f;
font-size: 12px;
border-radius: 8px;
padding: 2px 8px;
margin-left: 4px;
}
.wechat-id {
color: #888;
font-size: 13px;
margin-top: 2px;
}
.card-action {
margin-left: 8px;
}
.card-body {
margin-top: 2px;
}
.row-group {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
gap: 8px;
}
.row-item {
font-size: 13px;
color: #555;
display: flex;
align-items: center;
gap: 2px;
}
.strong {
font-weight: 600;
color: #222;
}
.strong-green {
font-weight: 600;
color: #52c41a;
}
.progress-bar {
margin: 6px 0 8px 0;
}
.progress-bg {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 8px;
background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%);
border-radius: 6px;
transition: width 0.3s;
}
.pagination {
margin: 16px 0 0 0;
display: flex;
justify-content: center;
}
.popup-content {
padding: 16px 0 8px 0;
}
.popup-content img {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.empty {
text-align: center;
color: #999;
padding: 48px 0 32px 0;
font-size: 15px;
}

View File

@@ -0,0 +1,309 @@
import React, { useState, useEffect } from "react";
import {
NavBar,
List,
Card,
Button,
SpinLoading,
Popup,
Toast,
} from "antd-mobile";
import { Pagination, Input, Tooltip } from "antd";
import {
ArrowLeftOutlined,
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";
interface WechatAccount {
id: number;
nickname: string;
avatar: string;
wechatId: string;
wechatAccount: string;
deviceId: number;
times: number; // 今日可添加
addedCount: number; // 今日新增
wechatStatus: number; // 1正常 0异常
totalFriend: number;
deviceMemo: string; // 设备名
activeTime: string; // 最后活跃
}
const PAGE_SIZE = 10;
const WechatAccounts: React.FC = () => {
const navigate = useNavigate();
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
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);
try {
const res = await getWechatAccounts({
page,
page_size: PAGE_SIZE,
keyword,
});
if (res && res.list) {
setAccounts(res.list);
setTotalAccounts(res.total || 0);
} else {
setAccounts([]);
setTotalAccounts(0);
}
} catch (e) {
Toast.show({ content: "获取微信号失败", position: "top" });
setAccounts([]);
setTotalAccounts(0);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchAccounts(currentPage, searchTerm);
// eslint-disable-next-line
}, [currentPage]);
const handleSearch = () => {
setCurrentPage(1);
fetchAccounts(1, searchTerm);
};
const handleRefresh = async () => {
setIsRefreshing(true);
await fetchAccounts(currentPage, searchTerm);
setIsRefreshing(false);
Toast.show({ content: "刷新成功", position: "top" });
};
const handleAccountClick = (account: WechatAccount) => {
setSelectedAccount(account);
setPopupVisible(true);
};
const handleTransferFriends = (account: WechatAccount) => {
// TODO: 实现好友转移弹窗或跳转
Toast.show({ content: `好友转移:${account.nickname}` });
};
return (
<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>
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索微信号/昵称"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
onPressEnter={handleSearch}
/>
</div>
<Button
size="small"
onClick={handleRefresh}
loading={isRefreshing}
className="refresh-btn"
>
<ReloadOutlined />
</Button>
</div>
</>
}
>
<div className={style["wechat-accounts-page"]}>
{isLoading ? (
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
</div>
) : accounts.length === 0 ? (
<div className={style["empty"]}></div>
) : (
<div className={style["card-list"]}>
{accounts.map((account) => {
const percent =
account.times > 0
? Math.min((account.addedCount / account.times) * 100, 100)
: 0;
return (
<div
key={account.id}
className={style["account-card"]}
onClick={() => handleAccountClick(account)}
>
<div className={style["card-header"]}>
<div className={style["avatar-wrapper"]}>
<img
src={account.avatar}
alt={account.nickname}
className={style["avatar"]}
/>
<span
className={
account.wechatStatus === 1
? style["status-dot-normal"]
: style["status-dot-abnormal"]
}
/>
</div>
<div className={style["header-info"]}>
<div className={style["nickname-row"]}>
<span className={style["nickname"]}>
{account.nickname}
</span>
<span
className={
account.wechatStatus === 1
? style["status-label-normal"]
: style["status-label-abnormal"]
}
>
{account.wechatStatus === 1 ? "正常" : "异常"}
</span>
</div>
<div className={style["wechat-id"]}>
{account.wechatAccount}
</div>
</div>
</div>
<div className={style["card-body"]}>
<div className={style["row-group"]}>
<div className={style["row-item"]}>
<span></span>
<span className={style["strong"]}>
{account.totalFriend}
</span>
</div>
<div className={style["row-item"]}>
<span></span>
<span className={style["strong-green"]}>
+{account.addedCount}
</span>
</div>
</div>
<div className={style["row-group"]}>
<div className={style["row-item"]}>
<span></span>
<span>{account.times}</span>
</div>
<div className={style["row-item"]}>
<Tooltip title={`每日最多添加 ${account.times} 个好友`}>
<span></span>
<span>
{account.addedCount}/{account.times}
</span>
</Tooltip>
</div>
</div>
<div className={style["progress-bar"]}>
<div className={style["progress-bg"]}>
<div
className={style["progress-fill"]}
style={{ width: `${percent}%` }}
/>
</div>
</div>
<div className={style["row-group"]}>
<div className={style["row-item"]}>
<span></span>
<span>{account.deviceMemo || "-"}</span>
</div>
<div className={style["row-item"]}>
<span></span>
<span>{account.activeTime}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
)}
<div className={style["pagination"]}>
{totalAccounts > PAGE_SIZE && (
<Pagination
total={Math.ceil(totalAccounts / PAGE_SIZE)}
current={currentPage}
onChange={setCurrentPage}
/>
)}
</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>
);
};
export default WechatAccounts;

View File

@@ -1,5 +1,7 @@
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";
const routes = [
// 基础路由
@@ -13,6 +15,17 @@ const routes = [
element: <Mine />,
auth: true,
},
// 微信号管理路由
{
path: "/wechat-accounts",
element: <WechatAccounts />,
auth: true,
},
{
path: "/wechat-accounts/detail/:id",
element: <WechatAccountDetail />,
auth: true,
},
];
export default routes;

View File

@@ -1,17 +1,17 @@
import WechatAccounts from "@/pages/wechat-accounts/WechatAccounts";
import WechatAccountDetail from "@/pages/wechat-accounts/WechatAccountDetail";
const wechatAccountRoutes = [
{
path: "/wechat-accounts",
element: <WechatAccounts />,
auth: true,
},
{
path: "/wechat-accounts/:id",
element: <WechatAccountDetail />,
auth: true,
},
];
export default wechatAccountRoutes;
import WechatAccounts from "@/pages/wechat-accounts/list";
import WechatAccountDetail from "@/pages/wechat-accounts/detail";
const wechatAccountRoutes = [
{
path: "/wechat-accounts",
element: <WechatAccounts />,
auth: true,
},
{
path: "/wechat-accounts/detail/:id",
element: <WechatAccountDetail />,
auth: true,
},
];
export default wechatAccountRoutes;