代码优化

This commit is contained in:
wong
2025-12-10 17:56:55 +08:00
parent 3e145ca123
commit 55fe2b46df
4 changed files with 351 additions and 165 deletions

View File

@@ -94,6 +94,20 @@ export async function exportWechatMoments(params: {
}
);
// 检查响应类型如果是JSON错误响应需要解析错误信息
const contentType = response.headers["content-type"] || "";
if (contentType.includes("application/json")) {
// 如果是JSON响应说明可能是错误信息
const text = await response.data.text();
const errorData = JSON.parse(text);
throw new Error(errorData.message || errorData.msg || "导出失败");
}
// 检查响应状态
if (response.status !== 200) {
throw new Error(`导出失败,状态码: ${response.status}`);
}
// 创建下载链接
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
@@ -116,6 +130,32 @@ export async function exportWechatMoments(params: {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || "导出失败");
// 如果是我们抛出的错误,直接抛出
if (error.message && error.message !== "导出失败") {
throw error;
}
// 处理axios错误响应
if (error.response) {
// 如果响应是blob类型尝试读取为文本
if (error.response.data instanceof Blob) {
try {
const text = await error.response.data.text();
const errorData = JSON.parse(text);
throw new Error(errorData.message || errorData.msg || "导出失败");
} catch (parseError) {
throw new Error("导出失败,请重试");
}
} else {
throw new Error(
error.response.data?.message ||
error.response.data?.msg ||
error.message ||
"导出失败"
);
}
} else {
throw new Error(error.message || "导出失败");
}
}
}

View File

@@ -373,6 +373,12 @@
font-weight: 600;
color: #52c41a;
}
.stat-value-negative {
font-size: 20px;
font-weight: 600;
color: #ff4d4f;
}
}
}
@@ -868,9 +874,10 @@
.type-selector {
display: flex;
gap: 8px;
flex-wrap: wrap;
width: 100%;
.type-option {
flex: 1;
padding: 8px 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
@@ -879,6 +886,7 @@
cursor: pointer;
transition: all 0.2s;
background: white;
text-align: center;
&:hover {
border-color: #1677ff;
@@ -1331,82 +1339,54 @@
}
}
.moments-action-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #f0f0f0;
.action-button, .action-button-dark {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 8px;
background: #1677ff;
border: none;
cursor: pointer;
transition: all 0.2s;
color: white;
&:active {
background: #0958d9;
transform: scale(0.95);
}
svg {
font-size: 20px;
color: white;
}
}
.action-button-dark {
background: #1677ff;
color: white;
&:active {
background: #0958d9;
}
}
}
.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;
@@ -1459,7 +1439,7 @@
.image-grid {
display: grid;
gap: 8px;
gap: 4px;
width: 100%;
// 1张图片宽度拉伸高度自适应
@@ -1469,56 +1449,57 @@
img {
width: 100%;
height: auto;
max-height: 400px;
object-fit: cover;
border-radius: 8px;
border-radius: 4px;
}
}
// 2张图片左右并列
// 2张图片左右并列1:1比例
&.double {
grid-template-columns: 1fr 1fr;
img {
width: 100%;
height: 120px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
border-radius: 4px;
}
}
// 3张图片三张并列
// 3张图片三张并列1:1比例
&.triple {
grid-template-columns: 1fr 1fr 1fr;
img {
width: 100%;
height: 100px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
border-radius: 4px;
}
}
// 4张图片2x2网格布局
// 4张图片2x2网格布局1:1比例
&.quad {
grid-template-columns: repeat(2, 1fr);
img {
width: 100%;
height: 140px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
border-radius: 4px;
}
}
// 5张及以上网格布局9宫格
// 5张及以上网格布局9宫格1:1比例
&.grid {
grid-template-columns: repeat(3, 1fr);
img {
width: 100%;
height: 100px;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
border-radius: 4px;
}
.image-more {
@@ -1526,11 +1507,11 @@
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
border-radius: 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: 500;
height: 100px;
aspect-ratio: 1 / 1;
}
}
}
@@ -1547,6 +1528,34 @@
}
}
}
.moments-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
.moments-no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
}
.friends-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
.friends-no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 16px 0;
}
}

View File

@@ -12,13 +12,18 @@ import {
Tag,
Switch,
DatePicker,
InfiniteScroll,
} from "antd-mobile";
import { Input, Pagination } from "antd";
import { Input } from "antd";
import NavCommon from "@/components/NavCommon";
import {
SearchOutlined,
ReloadOutlined,
UserOutlined,
FileTextOutlined,
PictureOutlined,
VideoCameraOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./detail.module.scss";
@@ -32,6 +37,7 @@ import {
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import dayjs from "dayjs";
import { WechatAccountSummary, Friend, MomentItem } from "./data";
@@ -107,6 +113,84 @@ const WechatAccountDetail: React.FC = () => {
}
}, [id]);
// 计算账号价值
// 规则:
// 1. 1个好友3块
// 2. 1个群1块
// 3. 修改过微信号10块
const calculateAccountValue = useCallback(() => {
// 获取好友数量(优先使用概览数据,其次使用好友列表总数,最后使用账号信息)
const friendsCount = overviewData?.totalFriends || friendsTotal || accountInfo?.friendShip?.totalFriend || 0;
// 获取群数量(优先使用概览数据,其次使用账号信息)
const groupsCount = overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || 0;
// 判断是否修改过微信号
// 注意需要根据实际API返回的字段来判断可能的字段名
// - isWechatIdModified (布尔值)
// - wechatIdModified (布尔值)
// - hasModifiedWechatId (布尔值)
// - wechatIdChangeCount (数字大于0表示修改过)
// 如果API没有返回该字段需要后端添加或根据其他逻辑判断
const isWechatIdModified =
accountInfo?.isWechatIdModified ||
accountInfo?.wechatIdModified ||
accountInfo?.hasModifiedWechatId ||
(accountInfo?.wechatIdChangeCount && accountInfo.wechatIdChangeCount > 0) ||
false;
// 计算各部分价值
const friendsValue = friendsCount * 3; // 好友数 * 3
const groupsValue = groupsCount * 1; // 群数 * 1
const wechatIdModifiedValue = isWechatIdModified ? 10 : 0; // 修改过微信号 ? 10 : 0
// 计算总价值
const totalValue = friendsValue + groupsValue + wechatIdModifiedValue;
return {
value: totalValue,
formatted: `¥${totalValue.toLocaleString()}`,
breakdown: {
friends: friendsValue,
groups: groupsValue,
wechatIdModified: wechatIdModifiedValue,
friendsCount,
groupsCount,
isWechatIdModified,
},
};
}, [overviewData, friendsTotal, accountInfo]);
// 计算今日价值变化
// 规则:
// 1. 今日新增好友 * 3块
// 2. 今日新增群 * 1块
const calculateTodayValueChange = useCallback(() => {
// 获取今日新增好友数
const todayNewFriends = overviewData?.todayNewFriends || accountSummary?.statistics?.todayAdded || 0;
// 获取今日新增群数
const todayNewChatrooms = overviewData?.todayNewChatrooms || 0;
// 计算今日价值变化
const friendsValueChange = todayNewFriends * 3; // 今日新增好友数 * 3
const groupsValueChange = todayNewChatrooms * 1; // 今日新增群数 * 1
const totalChange = friendsValueChange + groupsValueChange;
return {
change: totalChange,
formatted: totalChange >= 0 ? `+${totalChange.toLocaleString()}` : `${totalChange.toLocaleString()}`,
isPositive: totalChange >= 0,
breakdown: {
friends: friendsValueChange,
groups: groupsValueChange,
todayNewFriends,
todayNewChatrooms,
},
};
}, [overviewData, accountSummary]);
// 获取概览数据
const fetchOverviewData = useCallback(async () => {
if (!id) return;
@@ -122,7 +206,7 @@ const WechatAccountDetail: React.FC = () => {
// 获取好友列表 - 封装为独立函数
const fetchFriendsList = useCallback(
async (page: number = 1, keyword: string = "") => {
async (page: number = 1, keyword: string = "", append: boolean = false) => {
if (!id) return;
setIsFetchingFriends(true);
@@ -132,7 +216,7 @@ const WechatAccountDetail: React.FC = () => {
const response = await getWechatFriends({
wechatAccount: id,
page: page,
limit: 5,
limit: 20,
keyword: keyword,
});
@@ -175,15 +259,17 @@ const WechatAccountDetail: React.FC = () => {
};
});
setFriends(newFriends);
setFriends(prev => (append ? [...prev, ...newFriends] : newFriends));
setFriendsTotal(response.total);
setFriendsPage(page);
setIsFriendsEmpty(newFriends.length === 0);
setIsFriendsEmpty(newFriends.length === 0 && !append);
} catch (error) {
console.error("获取好友列表失败:", error);
setHasFriendLoadError(true);
setFriends([]);
setIsFriendsEmpty(true);
if (!append) {
setFriends([]);
setIsFriendsEmpty(true);
}
Toast.show({
content: "获取好友列表失败,请检查网络连接",
position: "top",
@@ -238,22 +324,20 @@ const WechatAccountDetail: React.FC = () => {
// 搜索好友
const handleSearch = useCallback(() => {
setFriendsPage(1);
fetchFriendsList(1, searchQuery);
fetchFriendsList(1, searchQuery, false);
}, [searchQuery, fetchFriendsList]);
// 刷新好友列表
const handleRefreshFriends = useCallback(() => {
fetchFriendsList(friendsPage, searchQuery);
fetchFriendsList(friendsPage, searchQuery, false);
}, [friendsPage, searchQuery, fetchFriendsList]);
// 分页切换
const handlePageChange = useCallback(
(page: number) => {
setFriendsPage(page);
fetchFriendsList(page, searchQuery);
},
[searchQuery, fetchFriendsList],
);
// 加载更多好友
const handleLoadMoreFriends = async () => {
if (isFetchingFriends) return;
if (friends.length >= friendsTotal) return;
await fetchFriendsList(friendsPage + 1, searchQuery, true);
};
// 初始化数据
useEffect(() => {
@@ -268,7 +352,7 @@ const WechatAccountDetail: React.FC = () => {
if (activeTab === "friends" && id) {
setIsFriendsEmpty(false);
setHasFriendLoadError(false);
fetchFriendsList(1, searchQuery);
fetchFriendsList(1, searchQuery, false);
}
}, [activeTab, id, fetchFriendsList, searchQuery]);
@@ -298,8 +382,8 @@ const WechatAccountDetail: React.FC = () => {
setInheritInfo(true);
// 设置默认打招呼内容,使用当前微信账号昵称
const nickname = accountInfo?.nickname || "未知";
setGreeting(`这个${nickname}的新号,之前那个号没用了,重新加一下您`);
setFirstMessage("");
setGreeting(`${nickname}的新号,请通过`);
setFirstMessage("这个是我的新号,重新加你一下,以后业务就用这个号!");
setShowTransferConfirm(true);
};
@@ -385,10 +469,10 @@ const WechatAccountDetail: React.FC = () => {
navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`);
};
const handleLoadMoreMoments = () => {
const handleLoadMoreMoments = async () => {
if (isFetchingMoments) return;
if (moments.length >= momentsTotal) return;
fetchMomentsList(momentsPage + 1, true);
await fetchMomentsList(momentsPage + 1, true);
};
// 处理朋友圈导出
@@ -398,6 +482,18 @@ const WechatAccountDetail: React.FC = () => {
return;
}
// 验证时间范围不超过1个月
if (exportStartTime && exportEndTime) {
const maxDate = dayjs(exportStartTime).add(1, "month").toDate();
if (exportEndTime > maxDate) {
Toast.show({
content: "日期范围不能超过1个月",
position: "top",
});
return;
}
}
setExportLoading(true);
try {
// 格式化时间
@@ -418,18 +514,27 @@ const WechatAccountDetail: React.FC = () => {
});
Toast.show({ content: "导出成功", position: "top" });
setShowExportPopup(false);
// 重置筛选条件
// 重置筛选条件(先重置,再关闭弹窗)
setExportKeyword("");
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
setShowStartTimePicker(false);
setShowEndTimePicker(false);
// 延迟关闭弹窗确保Toast显示
setTimeout(() => {
setShowExportPopup(false);
}, 500);
} catch (error: any) {
console.error("导出失败:", error);
const errorMessage = error?.message || "导出失败,请重试";
Toast.show({
content: error.message || "导出失败,请重试",
content: errorMessage,
position: "top",
duration: 2000,
});
// 确保loading状态被重置
setExportLoading(false);
} finally {
setExportLoading(false);
}
@@ -548,7 +653,7 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["stat-icon-up"]}></div>
</div>
<div className={style["stat-value"]}>
{overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`}
{calculateAccountValue().formatted}
</div>
</div>
@@ -558,8 +663,8 @@ const WechatAccountDetail: React.FC = () => {
<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 className={calculateTodayValueChange().isPositive ? style["stat-value-positive"] : style["stat-value-negative"]}>
{calculateTodayValueChange().formatted}
</div>
</div>
</div>
@@ -761,7 +866,7 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["summary-item"]}>
<div className={style["summary-label"]}></div>
<div className={style["summary-value-highlight"]}>
{overviewData?.accountValue?.formatted || "¥1,500,000"}
{calculateAccountValue().formatted}
</div>
</div>
</div>
@@ -780,7 +885,7 @@ const WechatAccountDetail: React.FC = () => {
<Button
size="small"
onClick={() =>
fetchFriendsList(friendsPage, searchQuery)
fetchFriendsList(friendsPage, searchQuery, false)
}
>
@@ -837,47 +942,58 @@ const WechatAccountDetail: React.FC = () => {
)}
</div>
{/* 分页组件 */}
{friendsTotal > 20 &&
!isFriendsEmpty &&
!hasFriendLoadError && (
<div className={style["pagination-wrapper"]}>
<Pagination
total={Math.ceil(friendsTotal / 20)}
current={friendsPage}
onChange={handlePageChange}
/>
{/* 无限滚动加载 */}
<InfiniteScroll
loadMore={handleLoadMoreFriends}
hasMore={friends.length < friendsTotal}
threshold={100}
>
{isFetchingFriends && friends.length > 0 && (
<div className={style["friends-loading"]}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
...
</span>
</div>
)}
{friends.length >= friendsTotal && friends.length > 0 && (
<div className={style["friends-no-more"]}>
<span style={{ color: "#999", fontSize: 12 }}></span>
</div>
)}
</InfiniteScroll>
</div>
</Tabs.Tab>
<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"]}
onClick={() => setShowExportPopup(true)}
>
<span className={style["action-icon-export"]}></span>
<span className={style["action-text-light"]}></span>
</div>
{/* 功能按钮栏 - 移到白色背景上 */}
<div className={style["moments-action-bar"]}>
<div className={style["action-button"]}>
<FileTextOutlined />
</div>
<div className={style["action-button"]}>
<PictureOutlined />
</div>
<div className={style["action-button"]}>
<VideoCameraOutlined />
</div>
<div
className={style["action-button-dark"]}
onClick={() => {
// 默认设置近7天
const today = new Date();
const sevenDaysAgo = dayjs(today).subtract(7, "day").toDate();
setExportStartTime(sevenDaysAgo);
setExportEndTime(today);
setShowExportPopup(true);
}}
>
<DownloadOutlined />
</div>
</div>
<div className={style["moments-content"]}>
{/* 朋友圈列表 */}
<div className={style["moments-list"]}>
{isFetchingMoments && moments.length === 0 ? (
@@ -949,18 +1065,25 @@ const WechatAccountDetail: React.FC = () => {
)}
</div>
{moments.length < momentsTotal && (
<div className={style["moments-load-more"]}>
<Button
size="small"
onClick={handleLoadMoreMoments}
loading={isFetchingMoments}
disabled={isFetchingMoments}
>
</Button>
</div>
)}
<InfiniteScroll
loadMore={handleLoadMoreMoments}
hasMore={moments.length < momentsTotal}
threshold={100}
>
{isFetchingMoments && moments.length > 0 && (
<div className={style["moments-loading"]}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
...
</span>
</div>
)}
{moments.length >= momentsTotal && moments.length > 0 && (
<div className={style["moments-no-more"]}>
<span style={{ color: "#999", fontSize: 12 }}></span>
</div>
)}
</InfiniteScroll>
</div>
</Tabs.Tab>
@@ -1124,7 +1247,11 @@ const WechatAccountDetail: React.FC = () => {
{/* 朋友圈导出弹窗 */}
<Popup
visible={showExportPopup}
onMaskClick={() => setShowExportPopup(false)}
onMaskClick={() => {
setShowExportPopup(false);
setShowStartTimePicker(false);
setShowEndTimePicker(false);
}}
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
>
<div className={style["popup-content"]}>
@@ -1133,7 +1260,11 @@ const WechatAccountDetail: React.FC = () => {
<Button
size="small"
fill="outline"
onClick={() => setShowExportPopup(false)}
onClick={() => {
setShowExportPopup(false);
setShowStartTimePicker(false);
setShowEndTimePicker(false);
}}
>
</Button>
@@ -1207,11 +1338,13 @@ const WechatAccountDetail: React.FC = () => {
visible={showStartTimePicker}
title="开始时间"
value={exportStartTime}
max={exportEndTime || new Date()}
onClose={() => setShowStartTimePicker(false)}
onConfirm={val => {
setExportStartTime(val);
setShowStartTimePicker(false);
}}
onCancel={() => setShowStartTimePicker(false)}
/>
</div>
@@ -1230,6 +1363,8 @@ const WechatAccountDetail: React.FC = () => {
visible={showEndTimePicker}
title="结束时间"
value={exportEndTime}
min={exportStartTime || undefined}
max={new Date()}
onClose={() => setShowEndTimePicker(false)}
onConfirm={val => {
setExportEndTime(val);
@@ -1259,6 +1394,8 @@ const WechatAccountDetail: React.FC = () => {
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
setShowStartTimePicker(false);
setShowEndTimePicker(false);
}}
style={{ marginTop: 12 }}
>

View File

@@ -67,7 +67,7 @@ class PostTransferFriends extends BaseController
'endTime' => '18:00',
'remarkType' => 'phone',
'addFriendInterval' => 60,
'greeting' => !empty($greeting) ? $greeting :'这个是'. $wechat['nickname'] .'的新号,之前那个号没用了,重新加一下您'
'greeting' => !empty($greeting) ? $greeting :'是'. $wechat['nickname'] .'的新号,请通过'
];
if (!empty($firstMessage)){