From f3f2e8db412cb4e6f2e7d58d99add3614274c312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Wed, 23 Jul 2025 15:14:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B=20?= =?UTF-8?q?=E5=AD=98=E4=B8=80=E4=B8=8B=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- .../wechat-accounts/WechatAccountDetail.tsx | 30 - .../pages/wechat-accounts/WechatAccounts.tsx | 37 - .../src/pages/wechat-accounts/detail/api.ts | 21 + .../wechat-accounts/detail/detail.module.scss | 720 ++++++++++++++ .../pages/wechat-accounts/detail/index.tsx | 928 ++++++++++++++++++ nkebao/src/pages/wechat-accounts/list/api.ts | 30 + .../wechat-accounts/list/index.module.scss | 171 ++++ .../src/pages/wechat-accounts/list/index.tsx | 309 ++++++ nkebao/src/router/module/index.tsx | 13 + nkebao/src/router/module/wechat-accounts.tsx | 34 +- 11 files changed, 2210 insertions(+), 85 deletions(-) delete mode 100644 nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx delete mode 100644 nkebao/src/pages/wechat-accounts/WechatAccounts.tsx create mode 100644 nkebao/src/pages/wechat-accounts/detail/api.ts create mode 100644 nkebao/src/pages/wechat-accounts/detail/detail.module.scss create mode 100644 nkebao/src/pages/wechat-accounts/detail/index.tsx create mode 100644 nkebao/src/pages/wechat-accounts/list/api.ts create mode 100644 nkebao/src/pages/wechat-accounts/list/index.module.scss create mode 100644 nkebao/src/pages/wechat-accounts/list/index.tsx diff --git a/nkebao/.env.development b/nkebao/.env.development index fe189d22..da6a111b 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -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 diff --git a/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx b/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx deleted file mode 100644 index ac5fa6ec..00000000 --- a/nkebao/src/pages/wechat-accounts/WechatAccountDetail.tsx +++ /dev/null @@ -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 ( - window.history.back()} - > -
- 微信号详情 -
- - } - footer={} - > -
-

微信号详情页面

-

此页面正在开发中...

-
-
- ); -}; - -export default WechatAccountDetail; diff --git a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx b/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx deleted file mode 100644 index 57b00863..00000000 --- a/nkebao/src/pages/wechat-accounts/WechatAccounts.tsx +++ /dev/null @@ -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 ( - - 微信号管理 - - } - right={ - - } - /> - } - footer={} - > -
-

微信号管理页面

-

此页面正在开发中...

-
-
- ); -}; - -export default WechatAccounts; diff --git a/nkebao/src/pages/wechat-accounts/detail/api.ts b/nkebao/src/pages/wechat-accounts/detail/api.ts new file mode 100644 index 00000000..6fad702e --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/api.ts @@ -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"); +} diff --git a/nkebao/src/pages/wechat-accounts/detail/detail.module.scss b/nkebao/src/pages/wechat-accounts/detail/detail.module.scss new file mode 100644 index 00000000..bd0ca84c --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/detail.module.scss @@ -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; + } + } + } + } + } +} diff --git a/nkebao/src/pages/wechat-accounts/detail/index.tsx b/nkebao/src/pages/wechat-accounts/detail/index.tsx new file mode 100644 index 00000000..eab5f4f1 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/detail/index.tsx @@ -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(null); + const [accountInfo, setAccountInfo] = useState(null); + const [showRestrictions, setShowRestrictions] = useState(false); + const [showTransferConfirm, setShowTransferConfirm] = useState(false); + const [showFriendDetail, setShowFriendDetail] = useState(false); + const [selectedFriend, setSelectedFriend] = useState(null); + const [friendDetail, setFriendDetail] = useState( + null + ); + const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false); + const [friendDetailError, setFriendDetailError] = useState( + null + ); + const [searchQuery, setSearchQuery] = useState(""); + const [activeTab, setActiveTab] = useState("overview"); + const [isLoading, setIsLoading] = useState(false); + + // 好友列表相关状态 + const [friends, setFriends] = useState([]); + 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(null); + const friendsLoadingRef = useRef(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 ( + + 微信号详情 + + } + > +
+ +
+
+ ); + } + + return ( + + 微信号详情 + + } + > +
+ {/* 账号基本信息卡片 */} + +
+
+ +
+
+
+
+

+ {accountInfo?.nickname || "未知昵称"} +

+ + {accountInfo?.wechatStatus === 1 ? "正常" : "异常"} + +
+

+ 微信号:{accountInfo?.wechatAccount || "未知"} +

+
+ + +
+
+
+ + + {/* 标签页 */} + + + +
+ {/* 账号基础信息 */} +
+
+
+ +
+
账号年龄
+ {accountSummary && ( +
+ 注册于{" "} + {new Date( + accountSummary.accountAge + ).toLocaleDateString()} +
+ )} +
+
+ {accountSummary && ( +
+ {formatAccountAge( + calculateAccountAge(accountSummary.accountAge) + )} +
+ )} +
+ +
+
+ +
+
活跃程度
+ {accountSummary && ( +
+ 总聊天{" "} + {accountSummary.activityLevel.allTimes.toLocaleString()}{" "} + 次 +
+ )} +
+
+ {accountSummary && ( +
+ {accountSummary.activityLevel.dayTimes.toLocaleString()} + 次/天 +
+ )} +
+
+ + {/* 账号权重评估 */} + {accountSummary && ( +
+
+ + + 账号权重评估 + +
+ + {accountSummary.accountWeight.scope} + + +
+
+

+ {getWeightDescription(accountSummary.accountWeight.scope)} +

+
+
+ 账号年龄 +
+
+
+ + {accountSummary.accountWeight.ageWeight}% + +
+
+ 活跃度 +
+
+
+ + {accountSummary.accountWeight.activityWeigth}% + +
+
+
+ )} + + {/* 限制记录 */} + {accountSummary && + accountSummary.restrictions && + accountSummary.restrictions.length > 0 && ( +
+
+ + + 限制记录 + + +
+
+ {accountSummary.restrictions + .slice(0, 3) + .map((restriction) => ( +
+
+ + {restriction.reason} + + + {formatDateTime(restriction.date)} + +
+ + {restriction.level === "high" + ? "高风险" + : restriction.level === "medium" + ? "中风险" + : "低风险"} + +
+ ))} +
+
+ )} +
+ + + 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`} + key="friends" + > +
+ {/* 搜索栏 */} +
+
+ setSearchQuery(e.target.value)} + prefix={} + allowClear + size="large" + onPressEnter={handleSearch} + /> +
+ +
+ + {/* 好友列表 */} +
+ {isFriendsEmpty ? ( +
暂无好友数据
+ ) : hasFriendLoadError ? ( +
+

加载失败,请重试

+ +
+ ) : ( + <> + {friends.map((friend) => ( +
handleFriendClick(friend)} + > + +
+
+
+ {friend.nickname} + {friend.remark && ( + + ({friend.remark}) + + )} +
+ +
+
+ {friend.wechatId} +
+
+ {friend.tags?.map((tag, index) => ( + + {typeof tag === "string" ? tag : tag.name} + + ))} +
+
+
+ ))} + {hasMoreFriends && !isFriendsEmpty && ( +
+ +
+ )} + + )} +
+
+
+ + +
+ + {/* 限制记录详情弹窗 */} + setShowRestrictions(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

限制记录详情

+ +
+

每次限制恢复时间为24小时

+ {accountSummary && accountSummary.restrictions && ( +
+ {accountSummary.restrictions.map((restriction) => ( +
+
+
+ {restriction.reason} +
+
+ {formatDateTime(restriction.date)} +
+
+ + {restriction.level === "high" + ? "高风险" + : restriction.level === "medium" + ? "中风险" + : "低风险"} + +
+ ))} +
+ )} +
+
+ + {/* 好友转移确认弹窗 */} + setShowTransferConfirm(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

确认好友转移

+
+

+ 确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。 +

+
+ + +
+
+
+ + {/* 好友详情弹窗 */} + setShowFriendDetail(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

好友详情

+ +
+ + {isLoadingFriendDetail ? ( +
+ +
+ ) : friendDetailError ? ( +
+

{friendDetailError}

+ +
+ ) : friendDetail && selectedFriend ? ( +
+
+ +
+

+ {selectedFriend.nickname} +

+

+ 微信号:{selectedFriend.wechatId} +

+
+
+ +
+
+ 地区 + + {friendDetail.region || "未知"} + +
+
+ 添加时间 + + {friendDetail.addDate} + +
+
+ 来源 + + {friendDetail.source || "未知"} + +
+ {friendDetail.memo && ( +
+ 备注 + + {friendDetail.memo} + +
+ )} + {friendDetail.tags && friendDetail.tags.length > 0 && ( +
+ 标签 +
+ {friendDetail.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} +
+
+ ) : null} +
+
+ + ); +}; + +export default WechatAccountDetail; diff --git a/nkebao/src/pages/wechat-accounts/list/api.ts b/nkebao/src/pages/wechat-accounts/list/api.ts new file mode 100644 index 00000000..08ff193e --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/api.ts @@ -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"); +} diff --git a/nkebao/src/pages/wechat-accounts/list/index.module.scss b/nkebao/src/pages/wechat-accounts/list/index.module.scss new file mode 100644 index 00000000..3e657fb4 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/index.module.scss @@ -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; +} diff --git a/nkebao/src/pages/wechat-accounts/list/index.tsx b/nkebao/src/pages/wechat-accounts/list/index.tsx new file mode 100644 index 00000000..e1a306a6 --- /dev/null +++ b/nkebao/src/pages/wechat-accounts/list/index.tsx @@ -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([]); + 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( + 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 ( + + + navigate(-1)} + /> +
+ } + > + 微信号管理 + +
+
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + onPressEnter={handleSearch} + /> +
+ +
+ + } + > +
+ {isLoading ? ( +
+ +
+ ) : accounts.length === 0 ? ( +
暂无微信账号数据
+ ) : ( +
+ {accounts.map((account) => { + const percent = + account.times > 0 + ? Math.min((account.addedCount / account.times) * 100, 100) + : 0; + return ( +
handleAccountClick(account)} + > +
+
+ {account.nickname} + +
+
+
+ + {account.nickname} + + + {account.wechatStatus === 1 ? "正常" : "异常"} + +
+
+ 微信号:{account.wechatAccount} +
+
+
+
+
+
+ 好友数量: + + {account.totalFriend} + +
+
+ 今日新增: + + +{account.addedCount} + +
+
+
+
+ 今日可添加: + {account.times} +
+
+ + 进度: + + {account.addedCount}/{account.times} + + +
+
+
+
+
+
+
+
+
+ 所属设备: + {account.deviceMemo || "-"} +
+
+ 最后活跃: + {account.activeTime} +
+
+
+
+ ); + })} +
+ )} +
+ {totalAccounts > PAGE_SIZE && ( + + )} +
+ setPopupVisible(false)} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > + {selectedAccount && ( +
+
+ avatar +
+ {selectedAccount.nickname} +
+
+ 微信号:{selectedAccount.wechatAccount} +
+
+
+ +
+ +
+ )} +
+
+ + ); +}; + +export default WechatAccounts; diff --git a/nkebao/src/router/module/index.tsx b/nkebao/src/router/module/index.tsx index 749cf176..c68fdffd 100644 --- a/nkebao/src/router/module/index.tsx +++ b/nkebao/src/router/module/index.tsx @@ -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: , auth: true, }, + // 微信号管理路由 + { + path: "/wechat-accounts", + element: , + auth: true, + }, + { + path: "/wechat-accounts/detail/:id", + element: , + auth: true, + }, ]; export default routes; diff --git a/nkebao/src/router/module/wechat-accounts.tsx b/nkebao/src/router/module/wechat-accounts.tsx index 496ab006..378e49db 100644 --- a/nkebao/src/router/module/wechat-accounts.tsx +++ b/nkebao/src/router/module/wechat-accounts.tsx @@ -1,17 +1,17 @@ -import WechatAccounts from "@/pages/wechat-accounts/WechatAccounts"; -import WechatAccountDetail from "@/pages/wechat-accounts/WechatAccountDetail"; - -const wechatAccountRoutes = [ - { - path: "/wechat-accounts", - element: , - auth: true, - }, - { - path: "/wechat-accounts/:id", - element: , - 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: , + auth: true, + }, + { + path: "/wechat-accounts/detail/:id", + element: , + auth: true, + }, +]; + +export default wechatAccountRoutes;