健康分功能提交 + 微信客服页面改版
This commit is contained in:
@@ -5,6 +5,20 @@ export function getWechatAccountDetail(id: string) {
|
||||
return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号概览数据
|
||||
export function getWechatAccountOverview(id: string) {
|
||||
return request("/v1/wechats/overview", { wechatId: id }, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号朋友圈列表
|
||||
export function getWechatMoments(params: {
|
||||
wechatId: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
return request("/v1/wechats/moments", params, "GET");
|
||||
}
|
||||
|
||||
// 获取微信号好友列表
|
||||
export function getWechatFriends(params: {
|
||||
wechatAccount: string;
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
// 概览数据接口
|
||||
export interface WechatAccountOverview {
|
||||
healthScoreAssessment: {
|
||||
score: number;
|
||||
dailyLimit: number;
|
||||
todayAdded: number;
|
||||
lastAddTime: string;
|
||||
statusTag: string;
|
||||
baseComposition?: Array<{
|
||||
name: string;
|
||||
score: number;
|
||||
formatted: string;
|
||||
friendCount?: number;
|
||||
}>;
|
||||
dynamicRecords?: Array<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
time?: string;
|
||||
score?: number;
|
||||
formatted?: string;
|
||||
statusTag?: string;
|
||||
}>;
|
||||
};
|
||||
accountValue: {
|
||||
value: number;
|
||||
formatted: string;
|
||||
};
|
||||
todayValueChange: {
|
||||
change: number;
|
||||
formatted: string;
|
||||
isPositive: boolean;
|
||||
};
|
||||
totalFriends: number;
|
||||
todayNewFriends: number;
|
||||
highValueChatrooms: number;
|
||||
todayNewChatrooms: number;
|
||||
}
|
||||
|
||||
export interface WechatAccountSummary {
|
||||
accountAge: string;
|
||||
activityLevel: {
|
||||
@@ -15,12 +53,51 @@ export interface WechatAccountSummary {
|
||||
todayAdded: number;
|
||||
addLimit: number;
|
||||
};
|
||||
healthScore?: {
|
||||
score: number;
|
||||
lastUpdate?: string;
|
||||
lastAddTime?: string;
|
||||
baseScore?: number;
|
||||
verifiedScore?: number;
|
||||
friendsScore?: number;
|
||||
activities?: {
|
||||
type: string;
|
||||
time?: string;
|
||||
score: number;
|
||||
description?: string;
|
||||
status?: string;
|
||||
}[];
|
||||
};
|
||||
moments?: {
|
||||
id: string;
|
||||
date: string;
|
||||
month: string;
|
||||
day: string;
|
||||
content: string;
|
||||
images?: string[];
|
||||
timeAgo?: string;
|
||||
hasEmoji?: boolean;
|
||||
}[];
|
||||
accountValue?: {
|
||||
value: number;
|
||||
todayChange?: number;
|
||||
};
|
||||
friendsCount?: {
|
||||
total: number;
|
||||
todayAdded?: number;
|
||||
};
|
||||
groupsCount?: {
|
||||
total: number;
|
||||
todayAdded?: number;
|
||||
};
|
||||
restrictions: {
|
||||
id: number;
|
||||
level: number;
|
||||
reason: string;
|
||||
date: string;
|
||||
}[];
|
||||
// 新增概览数据
|
||||
overview?: WechatAccountOverview;
|
||||
}
|
||||
|
||||
export interface Friend {
|
||||
@@ -39,6 +116,27 @@ export interface Friend {
|
||||
region: string;
|
||||
source: string;
|
||||
notes: string;
|
||||
value?: number;
|
||||
valueFormatted?: string;
|
||||
statusTags?: string[];
|
||||
}
|
||||
|
||||
export interface MomentItem {
|
||||
id: string;
|
||||
snsId: string;
|
||||
type: number;
|
||||
content: string;
|
||||
resUrls: string[];
|
||||
commentList?: any[];
|
||||
likeList?: any[];
|
||||
createTime: string;
|
||||
momentEntity?: {
|
||||
lat?: string;
|
||||
lng?: string;
|
||||
location?: string;
|
||||
picSize?: number;
|
||||
userName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WechatFriendDetail {
|
||||
|
||||
@@ -143,67 +143,235 @@
|
||||
}
|
||||
|
||||
.overview-content {
|
||||
.info-grid {
|
||||
// 健康分评估区域
|
||||
.health-score-section {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.health-score-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.health-score-info {
|
||||
.health-score-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.status-tag {
|
||||
background: #ffebeb;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.health-score-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.score-circle-wrapper {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 24px;
|
||||
position: relative;
|
||||
|
||||
.score-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 8px solid #ff4d4f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.score-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-score-stats {
|
||||
flex: 1;
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 账号统计卡片网格
|
||||
.account-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-card {
|
||||
background: linear-gradient(135deg, #e6f7ff, #f0f8ff);
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #bae7ff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.info-header {
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
color: #1677ff;
|
||||
padding: 6px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 8px;
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
flex: 1;
|
||||
.stat-icon-up {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
.title-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
margin-bottom: 2px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-top: 2px solid #722ed1;
|
||||
border-right: 2px solid #722ed1;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon-plus {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 2px;
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 10px;
|
||||
background: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon-people {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 7px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 5px;
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
background: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-icon-chat {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: #fa8c16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-value {
|
||||
text-align: right;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1677ff;
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.stat-value-positive {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,6 +617,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
.friends-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f5f9ff;
|
||||
border: 1px solid #e0edff;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.summary-value-highlight {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fa541c;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: #e6e6e6;
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
.empty {
|
||||
text-align: center;
|
||||
@@ -467,83 +676,100 @@
|
||||
}
|
||||
}
|
||||
|
||||
.friend-item {
|
||||
.friend-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
gap: 12px;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
|
||||
.friend-item-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
&:hover {
|
||||
border-color: #cfe2ff;
|
||||
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
.adm-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.friend-info {
|
||||
.friend-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.friend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.friend-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.friend-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.friend-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.friend-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.friend-id-row {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.friend-status-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.friend-status-chip {
|
||||
background: #f0f7ff;
|
||||
color: #1677ff;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.friend-value {
|
||||
text-align: right;
|
||||
|
||||
.value-label {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.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;
|
||||
}
|
||||
.value-amount {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fa541c;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -769,6 +995,416 @@
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.health-content {
|
||||
padding: 16px 0;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
.health-score-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.health-score-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.status-tag {
|
||||
background: #ffebeb;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.health-score-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.score-circle-wrapper {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 24px;
|
||||
position: relative;
|
||||
|
||||
.score-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 8px solid #ff4d4f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.score-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-score-stats {
|
||||
flex: 1;
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-section {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.health-section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ff8800;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #ff8800;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.health-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.health-item-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.health-item-icon-warning {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #ffebeb;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '!';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.health-item-tag {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.health-item-value-positive {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.health-item-value-negative {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.health-item-value-empty {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.health-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moments-content {
|
||||
padding: 16px 0;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
background: #f5f5f5;
|
||||
|
||||
.moments-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 16px;
|
||||
|
||||
.action-button, .action-button-dark {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #1677ff;
|
||||
|
||||
.action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon-image::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.action-icon-video::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent white;
|
||||
}
|
||||
|
||||
.action-text, .action-text-light {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-dark {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.moments-list {
|
||||
padding: 0 16px;
|
||||
|
||||
.moment-item {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.moment-date {
|
||||
margin-right: 12px;
|
||||
text-align: center;
|
||||
|
||||
.date-day {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-month {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.moment-content {
|
||||
flex: 1;
|
||||
|
||||
.moment-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
white-space: pre-wrap; // 保留换行和空格,确保文本完整显示
|
||||
word-wrap: break-word; // 长单词自动换行
|
||||
|
||||
.moment-emoji {
|
||||
display: inline;
|
||||
font-size: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.moment-images {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
// 1张图片:宽度拉伸,高度自适应
|
||||
&.single {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 2张图片:左右并列
|
||||
&.double {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 3张图片:三张并列
|
||||
&.triple {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 4张图片:2x2网格布局
|
||||
&.quad {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 5张及以上:网格布局(9宫格)
|
||||
&.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moment-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.moment-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.risk-content {
|
||||
padding: 16px 0;
|
||||
height: 500px;
|
||||
|
||||
@@ -21,11 +21,17 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./detail.module.scss";
|
||||
import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api";
|
||||
import {
|
||||
getWechatAccountDetail,
|
||||
getWechatFriends,
|
||||
transferWechatFriends,
|
||||
getWechatAccountOverview,
|
||||
getWechatMoments,
|
||||
} from "./api";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
|
||||
import { WechatAccountSummary, Friend } from "./data";
|
||||
import { WechatAccountSummary, Friend, MomentItem } from "./data";
|
||||
|
||||
const WechatAccountDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -34,6 +40,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const [accountSummary, setAccountSummary] =
|
||||
useState<WechatAccountSummary | null>(null);
|
||||
const [accountInfo, setAccountInfo] = useState<any>(null);
|
||||
const [overviewData, setOverviewData] = useState<any>(null);
|
||||
const [showRestrictions, setShowRestrictions] = useState(false);
|
||||
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
||||
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
|
||||
@@ -50,6 +57,12 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
|
||||
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
|
||||
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
|
||||
const [moments, setMoments] = useState<MomentItem[]>([]);
|
||||
const [momentsPage, setMomentsPage] = useState(1);
|
||||
const [momentsTotal, setMomentsTotal] = useState(0);
|
||||
const [isFetchingMoments, setIsFetchingMoments] = useState(false);
|
||||
const [momentsError, setMomentsError] = useState<string | null>(null);
|
||||
const MOMENTS_LIMIT = 10;
|
||||
|
||||
// 获取基础信息
|
||||
const fetchAccountInfo = useCallback(async () => {
|
||||
@@ -80,6 +93,19 @@ const WechatAccountDetail: React.FC = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 获取概览数据
|
||||
const fetchOverviewData = useCallback(async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const response = await getWechatAccountOverview(id);
|
||||
if (response) {
|
||||
setOverviewData(response);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("获取概览数据失败:", e);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 获取好友列表 - 封装为独立函数
|
||||
const fetchFriendsList = useCallback(
|
||||
async (page: number = 1, keyword: string = "") => {
|
||||
@@ -96,26 +122,44 @@ const WechatAccountDetail: React.FC = () => {
|
||||
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 || "",
|
||||
}));
|
||||
const newFriends = response.list.map((friend: any) => {
|
||||
const memoTags = Array.isArray(friend.memo)
|
||||
? friend.memo
|
||||
: friend.memo
|
||||
? String(friend.memo)
|
||||
.split(/[,\s,、]+/)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const tagList = Array.isArray(friend.tags)
|
||||
? friend.tags
|
||||
: friend.tags
|
||||
? [friend.tags]
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: friend.id.toString(),
|
||||
avatar: friend.avatar || "/placeholder.svg",
|
||||
nickname: friend.nickname || "未知用户",
|
||||
wechatId: friend.wechatId || "",
|
||||
remark: friend.notes || "",
|
||||
addTime:
|
||||
friend.createTime || new Date().toISOString().split("T")[0],
|
||||
lastInteraction:
|
||||
friend.lastInteraction || new Date().toISOString().split("T")[0],
|
||||
tags: memoTags.map((tag: string, index: number) => ({
|
||||
id: `tag-${index}`,
|
||||
name: tag,
|
||||
color: getRandomTagColor(),
|
||||
})),
|
||||
statusTags: tagList,
|
||||
region: friend.region || "未知",
|
||||
source: friend.source || "未知",
|
||||
notes: friend.notes || "",
|
||||
value: friend.value,
|
||||
valueFormatted: friend.valueFormatted,
|
||||
};
|
||||
});
|
||||
|
||||
setFriends(newFriends);
|
||||
setFriendsTotal(response.total);
|
||||
@@ -137,6 +181,46 @@ const WechatAccountDetail: React.FC = () => {
|
||||
[id],
|
||||
);
|
||||
|
||||
const fetchMomentsList = useCallback(
|
||||
async (page: number = 1, append: boolean = false) => {
|
||||
if (!id) return;
|
||||
setIsFetchingMoments(true);
|
||||
setMomentsError(null);
|
||||
try {
|
||||
const response = await getWechatMoments({
|
||||
wechatId: id,
|
||||
page,
|
||||
limit: MOMENTS_LIMIT,
|
||||
});
|
||||
|
||||
const list: MomentItem[] = (response.list || []).map((moment: any) => ({
|
||||
id: moment.id?.toString() || Math.random().toString(),
|
||||
snsId: moment.snsId,
|
||||
type: moment.type,
|
||||
content: moment.content || "",
|
||||
resUrls: moment.resUrls || [],
|
||||
commentList: moment.commentList || [],
|
||||
likeList: moment.likeList || [],
|
||||
createTime: moment.createTime || "",
|
||||
momentEntity: moment.momentEntity || {},
|
||||
}));
|
||||
|
||||
setMoments(prev => (append ? [...prev, ...list] : list));
|
||||
setMomentsTotal(response.total || list.length);
|
||||
setMomentsPage(page);
|
||||
} catch (error) {
|
||||
console.error("获取朋友圈数据失败:", error);
|
||||
setMomentsError("获取朋友圈数据失败");
|
||||
if (!append) {
|
||||
setMoments([]);
|
||||
}
|
||||
} finally {
|
||||
setIsFetchingMoments(false);
|
||||
}
|
||||
},
|
||||
[id],
|
||||
);
|
||||
|
||||
// 搜索好友
|
||||
const handleSearch = useCallback(() => {
|
||||
setFriendsPage(1);
|
||||
@@ -161,8 +245,9 @@ const WechatAccountDetail: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchAccountInfo();
|
||||
fetchOverviewData();
|
||||
}
|
||||
}, [id, fetchAccountInfo]);
|
||||
}, [id, fetchAccountInfo, fetchOverviewData]);
|
||||
|
||||
// 监听标签切换 - 只在切换到好友列表时请求一次
|
||||
useEffect(() => {
|
||||
@@ -173,6 +258,14 @@ const WechatAccountDetail: React.FC = () => {
|
||||
}
|
||||
}, [activeTab, id, fetchFriendsList, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "moments" && id) {
|
||||
if (moments.length === 0) {
|
||||
fetchMomentsList(1, false);
|
||||
}
|
||||
}
|
||||
}, [activeTab, id, fetchMomentsList, moments.length]);
|
||||
|
||||
// 工具函数
|
||||
const getRandomTagColor = (): string => {
|
||||
const colors = [
|
||||
@@ -271,6 +364,41 @@ const WechatAccountDetail: React.FC = () => {
|
||||
navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`);
|
||||
};
|
||||
|
||||
const handleLoadMoreMoments = () => {
|
||||
if (isFetchingMoments) return;
|
||||
if (moments.length >= momentsTotal) return;
|
||||
fetchMomentsList(momentsPage + 1, true);
|
||||
};
|
||||
|
||||
const formatMomentDateParts = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return { day: "--", month: "--" };
|
||||
}
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = `${date.getMonth() + 1}月`;
|
||||
return { day, month };
|
||||
};
|
||||
|
||||
const formatMomentTimeAgo = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return dateString || "--";
|
||||
}
|
||||
const diff = Date.now() - date.getTime();
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
if (minutes < 1) return "刚刚";
|
||||
if (minutes < 60) return `${minutes}分钟前`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 7) return `${days}天前`;
|
||||
return date.toLocaleDateString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="微信号详情" />} loading={loadingInfo}>
|
||||
<div className={style["wechat-account-detail-page"]}>
|
||||
@@ -313,73 +441,220 @@ const WechatAccountDetail: React.FC = () => {
|
||||
onChange={handleTabChange}
|
||||
className={style["tabs"]}
|
||||
>
|
||||
<Tabs.Tab title="账号概览" key="overview">
|
||||
<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 className={style["health-score-section"]}>
|
||||
<div className={style["health-score-title"]}>健康分评估</div>
|
||||
<div className={style["health-score-info"]}>
|
||||
<div className={style["health-score-status"]}>
|
||||
<span className={style["status-tag"]}>{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"}</span>
|
||||
<span className={style["status-time"]}>最后添加时间: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}</span>
|
||||
</div>
|
||||
<div className={style["summary-label"]}>好友数量</div>
|
||||
</div>
|
||||
<div className={style["summary-item"]}>
|
||||
<div className={style["summary-value-green"]}>
|
||||
+{accountSummary?.statistics.todayAdded ?? "-"}
|
||||
<div className={style["health-score-display"]}>
|
||||
<div className={style["score-circle-wrapper"]}>
|
||||
<div className={style["score-circle"]}>
|
||||
<div className={style["score-number"]}>
|
||||
{overviewData?.healthScoreAssessment?.score || 67}
|
||||
</div>
|
||||
<div className={style["score-label"]}>SCORE</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["health-score-stats"]}>
|
||||
<div className={style["stats-row"]}>
|
||||
<div className={style["stats-label"]}>每日限额</div>
|
||||
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.dailyLimit || 0} 人</div>
|
||||
</div>
|
||||
<div className={style["stats-row"]}>
|
||||
<div className={style["stats-label"]}>今日已加</div>
|
||||
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.todayAdded || 0} 人</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 className={style["account-stats-grid"]}>
|
||||
{/* 账号价值 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>账号价值</div>
|
||||
<div className={style["stat-icon-up"]}></div>
|
||||
</div>
|
||||
<div className={style["summary-label"]}>群聊数量</div>
|
||||
</div>
|
||||
<div className={style["summary-item"]}>
|
||||
<div className={style["summary-value-green"]}>
|
||||
{accountInfo?.activity?.yesterdayMsgCount ?? "-"}
|
||||
<div className={style["stat-value"]}>
|
||||
{overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 今日价值变化 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>今日价值变化</div>
|
||||
<div className={style["stat-icon-plus"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value-positive"]}>
|
||||
{overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
|
||||
</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 className={style["account-stats-grid"]}>
|
||||
{/* 好友总数 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>好友总数</div>
|
||||
<div className={style["stat-icon-people"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{overviewData?.totalFriends || accountInfo?.friendShip?.totalFriend || "0"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["device-row"]}>
|
||||
<span className={style["device-label"]}>系统类型:</span>
|
||||
<span>{accountInfo?.deviceType ?? "-"}</span>
|
||||
|
||||
{/* 今日新增好友 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>今日新增好友</div>
|
||||
<div className={style["stat-icon-plus"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value-positive"]}>
|
||||
+{overviewData?.todayNewFriends || accountSummary?.statistics.todayAdded || "0"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["device-row"]}>
|
||||
<span className={style["device-label"]}>系统版本:</span>
|
||||
<span>{accountInfo?.deviceVersion ?? "-"}</span>
|
||||
</div>
|
||||
|
||||
{/* 高价群聊区域 */}
|
||||
<div className={style["account-stats-grid"]}>
|
||||
{/* 高价群聊 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>高价群聊</div>
|
||||
<div className={style["stat-icon-chat"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || "0"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 今日新增群聊 */}
|
||||
<div className={style["stat-card"]}>
|
||||
<div className={style["stat-header"]}>
|
||||
<div className={style["stat-title"]}>今日新增群聊</div>
|
||||
<div className={style["stat-icon-plus"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value-positive"]}>
|
||||
+{overviewData?.todayNewChatrooms || "0"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab title="健康分" key="health">
|
||||
<div className={style["health-content"]}>
|
||||
{/* 健康分数圆环 */}
|
||||
<div className={style["health-score-card"]}>
|
||||
<div className={style["health-score-status"]}>
|
||||
<span className={style["status-tag"]}>已添加加人</span>
|
||||
<span className={style["status-time"]}>最后添加时间: {accountSummary?.healthScore?.lastAddTime || "18:36:06"}</span>
|
||||
</div>
|
||||
<div className={style["health-score-display"]}>
|
||||
<div className={style["score-circle-wrapper"]}>
|
||||
<div className={style["score-circle"]}>
|
||||
<div className={style["score-number"]}>
|
||||
{accountSummary?.healthScore?.score || 67}
|
||||
</div>
|
||||
<div className={style["score-label"]}>SCORE</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["health-score-stats"]}>
|
||||
<div className={style["stats-row"]}>
|
||||
<div className={style["stats-label"]}>每日限额</div>
|
||||
<div className={style["stats-value"]}>{accountSummary?.statistics.addLimit || 0} 人</div>
|
||||
</div>
|
||||
<div className={style["stats-row"]}>
|
||||
<div className={style["stats-label"]}>今日已加</div>
|
||||
<div className={style["stats-value"]}>{accountSummary?.statistics.todayAdded || 0} 人</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 基础构成 */}
|
||||
<div className={style["health-section"]}>
|
||||
<div className={style["health-section-title"]}>基础构成</div>
|
||||
{(overviewData?.healthScoreAssessment?.baseComposition &&
|
||||
overviewData.healthScoreAssessment.baseComposition.length > 0
|
||||
? overviewData.healthScoreAssessment.baseComposition
|
||||
: [
|
||||
{ name: "账号基础分", formatted: "+60" },
|
||||
{ name: "已修改微信号", formatted: "+10" },
|
||||
{ name: "好友数量加成", formatted: "+12", friendCount: 5595 },
|
||||
]
|
||||
).map((item, index) => (
|
||||
<div className={style["health-item"]} key={`${item.name}-${index}`}>
|
||||
<div className={style["health-item-label"]}>
|
||||
{item.name}
|
||||
{item.friendCount ? ` (${item.friendCount})` : ""}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
(item.score ?? 0) >= 0
|
||||
? style["health-item-value-positive"]
|
||||
: style["health-item-value-negative"]
|
||||
}
|
||||
>
|
||||
{item.formatted || `${item.score ?? 0}`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 动态记录 */}
|
||||
<div className={style["health-section"]}>
|
||||
<div className={style["health-section-title"]}>动态记录</div>
|
||||
{overviewData?.healthScoreAssessment?.dynamicRecords &&
|
||||
overviewData.healthScoreAssessment.dynamicRecords.length > 0 ? (
|
||||
overviewData.healthScoreAssessment.dynamicRecords.map(
|
||||
(record, index) => (
|
||||
<div className={style["health-item"]} key={`record-${index}`}>
|
||||
<div className={style["health-item-label"]}>
|
||||
<span className={style["health-item-icon-warning"]}></span>
|
||||
{record.title || record.description || "记录"}
|
||||
{record.statusTag && (
|
||||
<span className={style["health-item-tag"]}>
|
||||
{record.statusTag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
(record.score ?? 0) >= 0
|
||||
? style["health-item-value-positive"]
|
||||
: style["health-item-value-negative"]
|
||||
}
|
||||
>
|
||||
{record.formatted ||
|
||||
(record.score && record.score > 0
|
||||
? `+${record.score}`
|
||||
: record.score || "-")}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<div className={style["health-empty"]}>暂无动态记录</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
|
||||
<Tabs.Tab
|
||||
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
||||
title={`好友${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
||||
key="friends"
|
||||
>
|
||||
<div className={style["friends-content"]}>
|
||||
@@ -406,6 +681,23 @@ const WechatAccountDetail: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 好友概要 */}
|
||||
<div className={style["friends-summary"]}>
|
||||
<div className={style["summary-item"]}>
|
||||
<div className={style["summary-label"]}>好友总数</div>
|
||||
<div className={style["summary-value"]}>
|
||||
{friendsTotal || overviewData?.totalFriends || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["summary-divider"]} />
|
||||
<div className={style["summary-item"]}>
|
||||
<div className={style["summary-label"]}>好友总估值</div>
|
||||
<div className={style["summary-value-highlight"]}>
|
||||
{overviewData?.accountValue?.formatted || "¥1,500,000"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 好友列表 */}
|
||||
<div className={style["friends-list"]}>
|
||||
{isFetchingFriends && friends.length === 0 ? (
|
||||
@@ -431,36 +723,53 @@ const WechatAccountDetail: React.FC = () => {
|
||||
{friends.map(friend => (
|
||||
<div
|
||||
key={friend.id}
|
||||
className={style["friend-item"]}
|
||||
className={style["friend-card"]}
|
||||
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-avatar"]}>
|
||||
<Avatar src={friend.avatar} />
|
||||
</div>
|
||||
<div className={style["friend-main"]}>
|
||||
<div className={style["friend-name-row"]}>
|
||||
<div className={style["friend-name"]}>
|
||||
{friend.nickname}
|
||||
{friend.remark && (
|
||||
<span className={style["friend-remark"]}>
|
||||
({friend.remark})
|
||||
{friend.nickname || "未知好友"}
|
||||
</div>
|
||||
<div className={style["friend-tags"]}>
|
||||
{friend.tags?.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={style["friend-tag"]}
|
||||
>
|
||||
{typeof tag === "string" ? tag : tag.name}
|
||||
</span>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["friend-wechat-id"]}>
|
||||
{friend.wechatId}
|
||||
<div className={style["friend-id-row"]}>
|
||||
ID: {friend.wechatId || "-"}
|
||||
</div>
|
||||
<div className={style["friend-tags"]}>
|
||||
{friend.tags?.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
className={style["friend-tag"]}
|
||||
<div className={style["friend-status-row"]}>
|
||||
{friend.statusTags?.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={style["friend-status-chip"]}
|
||||
>
|
||||
{typeof tag === "string" ? tag : tag.name}
|
||||
</Tag>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{friend.remark && (
|
||||
<span className={style["friend-status-chip"]}>
|
||||
{friend.remark}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["friend-value"]}>
|
||||
<div className={style["value-amount"]}>
|
||||
{friend.valueFormatted
|
||||
|| (typeof friend.value === "number"
|
||||
? `¥${friend.value.toLocaleString()}`
|
||||
: "估值 -")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -484,45 +793,115 @@ const WechatAccountDetail: React.FC = () => {
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
<Tabs.Tab title="风险评估" key="risk">
|
||||
<div className={style["risk-content"]}>
|
||||
{accountSummary?.restrictions &&
|
||||
accountSummary.restrictions.length > 0 ? (
|
||||
<div className={style["restrictions-list"]}>
|
||||
{accountSummary.restrictions.map(restriction => (
|
||||
<div
|
||||
key={restriction.id}
|
||||
className={style["restriction-item"]}
|
||||
>
|
||||
<div className={style["restriction-info"]}>
|
||||
<div className={style["restriction-reason"]}>
|
||||
{restriction.reason}
|
||||
</div>
|
||||
<div className={style["restriction-date"]}>
|
||||
{restriction.date
|
||||
? formatDateTime(restriction.date)
|
||||
: "暂无时间"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["restriction-level"]}>
|
||||
<span
|
||||
className={`${style["level-badge"]} ${style[`level-${restriction.level}`]}`}
|
||||
>
|
||||
{restriction.level === 1
|
||||
? "低风险"
|
||||
: restriction.level === 2
|
||||
? "中风险"
|
||||
: "高风险"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Tabs.Tab title="朋友圈" key="moments">
|
||||
<div className={style["moments-content"]}>
|
||||
{/* 功能按钮栏 */}
|
||||
<div className={style["moments-action-bar"]}>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-text"]}></span>
|
||||
<span className={style["action-text"]}>文本</span>
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-image"]}></span>
|
||||
<span className={style["action-text"]}>图片</span>
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-video"]}></span>
|
||||
<span className={style["action-text"]}>视频</span>
|
||||
</div>
|
||||
<div className={style["action-button-dark"]}>
|
||||
<span className={style["action-icon-export"]}></span>
|
||||
<span className={style["action-text-light"]}>导出</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 朋友圈列表 */}
|
||||
<div className={style["moments-list"]}>
|
||||
{isFetchingMoments && moments.length === 0 ? (
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
</div>
|
||||
) : momentsError ? (
|
||||
<div className={style["error"]}>{momentsError}</div>
|
||||
) : moments.length === 0 ? (
|
||||
<div className={style["empty"]}>暂无朋友圈内容</div>
|
||||
) : (
|
||||
moments.map(moment => {
|
||||
const { day, month } = formatMomentDateParts(
|
||||
moment.createTime,
|
||||
);
|
||||
const timeAgo = formatMomentTimeAgo(moment.createTime);
|
||||
const imageCount = moment.resUrls?.length || 0;
|
||||
// 根据图片数量选择对应的grid类,参考素材管理的实现
|
||||
let gridClass = "";
|
||||
if (imageCount === 1) gridClass = style["single"];
|
||||
else if (imageCount === 2) gridClass = style["double"];
|
||||
else if (imageCount === 3) gridClass = style["triple"];
|
||||
else if (imageCount === 4) gridClass = style["quad"];
|
||||
else if (imageCount > 4) gridClass = style["grid"];
|
||||
|
||||
return (
|
||||
<div className={style["moment-item"]} key={moment.id}>
|
||||
<div className={style["moment-date"]}>
|
||||
<div className={style["date-day"]}>{day}</div>
|
||||
<div className={style["date-month"]}>{month}</div>
|
||||
</div>
|
||||
<div className={style["moment-content"]}>
|
||||
{moment.content && (
|
||||
<div className={style["moment-text"]}>
|
||||
{moment.content}
|
||||
</div>
|
||||
)}
|
||||
{imageCount > 0 && (
|
||||
<div className={style["moment-images"]}>
|
||||
<div
|
||||
className={`${style["image-grid"]} ${gridClass}`}
|
||||
>
|
||||
{moment.resUrls
|
||||
.slice(0, 9)
|
||||
.map((url, index) => (
|
||||
<img
|
||||
key={`${moment.id}-img-${index}`}
|
||||
src={url}
|
||||
alt="朋友圈图片"
|
||||
/>
|
||||
))}
|
||||
{imageCount > 9 && (
|
||||
<div className={style["image-more"]}>
|
||||
+{imageCount - 9}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={style["moment-footer"]}>
|
||||
<span className={style["moment-time"]}>
|
||||
{timeAgo}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{moments.length < momentsTotal && (
|
||||
<div className={style["moments-load-more"]}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleLoadMoreMoments}
|
||||
loading={isFetchingMoments}
|
||||
disabled={isFetchingMoments}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style["empty"]}>暂无风险记录</div>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,39 @@
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.filter-button {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
&.filter-button-active {
|
||||
background: #1677ff;
|
||||
border-color: #1677ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -33,11 +33,12 @@ const WechatAccounts: React.FC = () => {
|
||||
const [totalAccounts, setTotalAccounts] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all");
|
||||
|
||||
// 获取路由参数 wechatStatus
|
||||
const wechatStatus = searchParams.get("wechatStatus");
|
||||
|
||||
const fetchAccounts = async (page = 1, keyword = "") => {
|
||||
const fetchAccounts = async (page = 1, keyword = "", status?: "all" | "online" | "offline") => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const params: any = {
|
||||
@@ -46,8 +47,12 @@ const WechatAccounts: React.FC = () => {
|
||||
keyword,
|
||||
};
|
||||
|
||||
// 如果有 wechatStatus 参数,添加到请求参数中
|
||||
if (wechatStatus) {
|
||||
// 优先使用传入的status参数,否则使用路由参数,最后使用状态中的筛选
|
||||
const filterStatus = status || wechatStatus || statusFilter;
|
||||
|
||||
if (filterStatus && filterStatus !== "all") {
|
||||
params.wechatStatus = filterStatus === "online" ? "1" : "0";
|
||||
} else if (wechatStatus) {
|
||||
params.wechatStatus = wechatStatus;
|
||||
}
|
||||
|
||||
@@ -60,7 +65,7 @@ const WechatAccounts: React.FC = () => {
|
||||
setTotalAccounts(0);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show({ content: "获取微信号失败", position: "top" });
|
||||
|
||||
setAccounts([]);
|
||||
setTotalAccounts(0);
|
||||
} finally {
|
||||
@@ -69,18 +74,24 @@ const WechatAccounts: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts(currentPage, searchTerm);
|
||||
fetchAccounts(currentPage, searchTerm, statusFilter);
|
||||
// eslint-disable-next-line
|
||||
}, [currentPage]);
|
||||
}, [currentPage, statusFilter]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchAccounts(1, searchTerm);
|
||||
fetchAccounts(1, searchTerm, statusFilter);
|
||||
};
|
||||
|
||||
const handleStatusFilterChange = (status: "all" | "online" | "offline") => {
|
||||
setStatusFilter(status);
|
||||
setCurrentPage(1);
|
||||
fetchAccounts(1, searchTerm, status);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await fetchAccounts(currentPage, searchTerm);
|
||||
await fetchAccounts(currentPage, searchTerm, statusFilter);
|
||||
setIsRefreshing(false);
|
||||
Toast.show({ content: "刷新成功", position: "top" });
|
||||
};
|
||||
@@ -122,6 +133,31 @@ const WechatAccounts: React.FC = () => {
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style["filter-bar"]}>
|
||||
<div className={style["filter-buttons"]}>
|
||||
<Button
|
||||
size="small"
|
||||
className={`${style["filter-button"]} ${statusFilter === "all" ? style["filter-button-active"] : ""}`}
|
||||
onClick={() => handleStatusFilterChange("all")}
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
className={`${style["filter-button"]} ${statusFilter === "online" ? style["filter-button-active"] : ""}`}
|
||||
onClick={() => handleStatusFilterChange("online")}
|
||||
>
|
||||
在线
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
className={`${style["filter-button"]} ${statusFilter === "offline" ? style["filter-button-active"] : ""}`}
|
||||
onClick={() => handleStatusFilterChange("offline")}
|
||||
>
|
||||
离线
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user