自动建群提交 + 触客宝发布朋友圈优化
This commit is contained in:
@@ -219,6 +219,48 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.searchInputWrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
flex: 1;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clearSearchBtn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
padding: 0;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchBtn {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.memberList {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SearchOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { SearchOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Popup } from "antd-mobile";
|
||||
import { Avatar } from "antd-mobile";
|
||||
import style from "./index.module.scss";
|
||||
@@ -82,6 +82,9 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
const [allMembers, setAllMembers] = useState<Record<string, GroupMember[]>>({});
|
||||
const [selectedMembers, setSelectedMembers] = useState<Record<string, GroupMember[]>>({});
|
||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||
const [memberSearchKeyword, setMemberSearchKeyword] = useState("");
|
||||
// 存储完整成员列表(用于搜索时切换回完整列表)
|
||||
const [fullMembersCache, setFullMembersCache] = useState<Record<string, GroupMember[]>>({});
|
||||
|
||||
// 处理群选择
|
||||
const handleGroupSelect = (groups: GroupSelectionItem[]) => {
|
||||
@@ -110,24 +113,66 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
const handleOpenMemberSelection = async (groupId: string) => {
|
||||
if (readonly) return;
|
||||
setMemberSelectionVisible({ visible: true, groupId });
|
||||
setMemberSearchKeyword(""); // 重置搜索关键词
|
||||
|
||||
// 如果还没有加载过该群的成员列表,则加载
|
||||
if (!allMembers[groupId]) {
|
||||
// 如果还没有加载过该群的成员列表,则加载所有成员(不使用搜索关键词)
|
||||
if (!allMembers[groupId] && !fullMembersCache[groupId]) {
|
||||
setLoadingMembers(true);
|
||||
try {
|
||||
const members = await getGroupMembers(groupId);
|
||||
const members = await getGroupMembers(groupId, 1, 100, "");
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: members }));
|
||||
setFullMembersCache(prev => ({ ...prev, [groupId]: members })); // 缓存完整列表
|
||||
} catch (error) {
|
||||
console.error("加载群成员失败:", error);
|
||||
} finally {
|
||||
setLoadingMembers(false);
|
||||
}
|
||||
} else if (fullMembersCache[groupId] && !allMembers[groupId]) {
|
||||
// 如果有缓存但没有显示列表,恢复完整列表
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭成员选择弹窗
|
||||
const handleCloseMemberSelection = () => {
|
||||
setMemberSelectionVisible({ visible: false, groupId: "" });
|
||||
setMemberSearchKeyword(""); // 重置搜索关键词
|
||||
};
|
||||
|
||||
// 手动触发搜索
|
||||
const handleSearchMembers = async () => {
|
||||
const groupId = memberSelectionVisible.groupId;
|
||||
if (!groupId) return;
|
||||
|
||||
const keyword = memberSearchKeyword.trim();
|
||||
|
||||
// 如果搜索关键词为空,使用缓存的完整列表
|
||||
if (!keyword) {
|
||||
if (fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 有搜索关键词时,调用 API 搜索
|
||||
setLoadingMembers(true);
|
||||
try {
|
||||
const members = await getGroupMembers(groupId, 1, 100, keyword);
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: members }));
|
||||
} catch (error) {
|
||||
console.error("搜索群成员失败:", error);
|
||||
} finally {
|
||||
setLoadingMembers(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空搜索
|
||||
const handleClearSearch = () => {
|
||||
setMemberSearchKeyword("");
|
||||
const groupId = memberSelectionVisible.groupId;
|
||||
if (groupId && fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
|
||||
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
|
||||
}
|
||||
};
|
||||
|
||||
// 选择成员
|
||||
@@ -328,6 +373,36 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style.searchBox}>
|
||||
<div className={style.searchInputWrapper}>
|
||||
<Input
|
||||
placeholder="搜索成员昵称或微信号"
|
||||
value={memberSearchKeyword}
|
||||
onChange={val => setMemberSearchKeyword(val)}
|
||||
onEnterPress={handleSearchMembers}
|
||||
className={style.searchInput}
|
||||
/>
|
||||
{memberSearchKeyword && (
|
||||
<Button
|
||||
fill="none"
|
||||
size="small"
|
||||
className={style.clearSearchBtn}
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={handleSearchMembers}
|
||||
loading={loadingMembers}
|
||||
className={style.searchBtn}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<div className={style.memberList}>
|
||||
{loadingMembers ? (
|
||||
<div className={style.loadingBox}>
|
||||
|
||||
@@ -99,13 +99,18 @@ export default function ContentForm() {
|
||||
: []);
|
||||
setSelectedDevices(deviceOptions || []);
|
||||
setSelectedFriends(data.sourceFriends || []);
|
||||
setSelectedGroups(data.selectedGroups || []);
|
||||
// 使用 wechatGroupsOptions 作为群列表数据
|
||||
setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []);
|
||||
const groupsOptions = data.wechatGroupsOptions || data.selectedGroupsOptions || [];
|
||||
setSelectedGroupsOptions(groupsOptions);
|
||||
// 从 groupsOptions 中提取群 ID 列表,如果没有 selectedGroups 的话
|
||||
const groupIds = data.selectedGroups && data.selectedGroups.length > 0
|
||||
? data.selectedGroups
|
||||
: groupsOptions.map((g: any) => String(g.id));
|
||||
setSelectedGroups(groupIds);
|
||||
// 处理带成员的群数据
|
||||
// groupMembersOptions 是一个对象,key是群ID(字符串),value是成员数组
|
||||
const groupMembersMap = data.groupMembersOptions || {};
|
||||
const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map(
|
||||
const groupsWithMembers: GroupWithMembers[] = groupsOptions.map(
|
||||
(group: any) => {
|
||||
const groupIdStr = String(group.id);
|
||||
const members = groupMembersMap[groupIdStr] || [];
|
||||
|
||||
@@ -75,7 +75,7 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
useEffect(() => {
|
||||
if (!visible || !ruleId) return;
|
||||
|
||||
setLoading(true);
|
||||
setLoading(true);
|
||||
const params: { startTime?: string; endTime?: string } = {};
|
||||
if (startTime) {
|
||||
params.startTime = formatDate(startTime);
|
||||
@@ -85,26 +85,26 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
}
|
||||
|
||||
getFriendRequestTaskStats(ruleId.toString(), params)
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
setXData(res.dateArray);
|
||||
setYData([
|
||||
res.allNumArray,
|
||||
res.errorNumArray,
|
||||
res.passNumArray,
|
||||
res.passRateArray,
|
||||
res.successNumArray,
|
||||
res.successRateArray,
|
||||
]);
|
||||
setStatistics(res.totalStats);
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
setXData(res.dateArray);
|
||||
setYData([
|
||||
res.allNumArray,
|
||||
res.errorNumArray,
|
||||
res.passNumArray,
|
||||
res.passRateArray,
|
||||
res.successNumArray,
|
||||
res.successRateArray,
|
||||
]);
|
||||
setStatistics(res.totalStats);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("获取统计数据失败:", error);
|
||||
message.error("获取统计数据失败");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [visible, ruleId, startTime, endTime, formatDate]);
|
||||
|
||||
const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据";
|
||||
|
||||
@@ -221,7 +221,7 @@ const AddChannelModal: React.FC<AddChannelModalProps> = ({
|
||||
setQrCodeData(null);
|
||||
setScanning(false);
|
||||
const timer = setTimeout(() => {
|
||||
handleGenerateQRCode();
|
||||
handleGenerateQRCode();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.syncOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.syncContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
padding: 32px 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.syncText {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loadingContainer,
|
||||
.emptyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
padding: 8px;
|
||||
margin-right: -8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
// 群组信息卡片
|
||||
.groupInfoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.groupIconLarge {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 auto 16px;
|
||||
object-fit: cover;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.groupTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
text-align: center;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.createTimeInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.createTimeIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.actionButtonIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
// 基本信息
|
||||
.basicInfoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sectionTitleDot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.basicInfoList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.basicInfoItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.basicInfoLabel {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.basicInfoValue {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.planIcon {
|
||||
font-size: 16px;
|
||||
color: #3b82f6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.adminAvatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.adminAvatarText {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.basicInfoDivider {
|
||||
height: 1px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.noAnnouncement {
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chevronIcon {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
// 群成员
|
||||
.membersCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.membersHeader {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.memberCountBadge {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
padding: 6px;
|
||||
color: #6b7280;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.membersList {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.memberItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f9fafb;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.memberInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memberInfoText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.memberNameRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memberAvatarWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memberAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.memberAvatarPlaceholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #e9d5ff;
|
||||
color: #7c3aed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.adminBadge {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #fbbf24;
|
||||
border: 2px solid #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.adminBadgeIcon {
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.memberInfoText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.memberNameRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memberName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.memberWechatId {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.quitTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.joinStatusTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memberExtraInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.memberJoinTime {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.memberActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.adminTag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid rgba(146, 64, 14, 0.3);
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
color: #9ca3af;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:active {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyMembers {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyMembersText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.actionsSection {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.actionCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.actionCardIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.actionCard:hover .actionCardIcon {
|
||||
background: #dbeafe;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.actionCardText {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.actionCardDanger {
|
||||
&:active {
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.actionCardIconDanger {
|
||||
background: #fee2e2;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.actionCardTextDanger {
|
||||
color: #dc2626;
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Toast, SpinLoading, Dialog, Input, TextArea } from "antd-mobile";
|
||||
import {
|
||||
EditOutlined,
|
||||
QrcodeOutlined,
|
||||
SearchOutlined,
|
||||
SyncOutlined,
|
||||
LogoutOutlined,
|
||||
StarOutlined,
|
||||
CloseCircleOutlined,
|
||||
ScheduleOutlined,
|
||||
FileTextOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getCreatedGroupDetail, syncGroupInfo, modifyGroupInfo, quitGroup } from "../form/api";
|
||||
import style from "./group-detail.module.scss";
|
||||
|
||||
interface GroupMember {
|
||||
id?: string;
|
||||
friendId?: number;
|
||||
nickname?: string;
|
||||
wechatId?: string;
|
||||
avatar?: string;
|
||||
isOwner?: number; // 1表示是群主,0表示不是
|
||||
isGroupAdmin?: boolean;
|
||||
joinStatus?: string; // "auto" | "manual" - 入群状态
|
||||
isQuit?: number; // 0/1 - 是否已退群
|
||||
joinTime?: string; // 入群时间
|
||||
alias?: string; // 成员别名
|
||||
remark?: string; // 成员备注
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GroupDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
createTime?: string;
|
||||
planName?: string;
|
||||
groupAdmin?: {
|
||||
id: string;
|
||||
nickname?: string;
|
||||
wechatId?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
announcement?: string;
|
||||
memberCount?: number;
|
||||
members?: GroupMember[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupDetailPage: React.FC = () => {
|
||||
const { id, groupId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groupDetail, setGroupDetail] = useState<GroupDetail | null>(null);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [editNameVisible, setEditNameVisible] = useState(false);
|
||||
const [editNameValue, setEditNameValue] = useState("");
|
||||
const [editAnnouncementVisible, setEditAnnouncementVisible] = useState(false);
|
||||
const [editAnnouncementValue, setEditAnnouncementValue] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !groupId) return;
|
||||
fetchGroupDetail();
|
||||
}, [id, groupId]);
|
||||
|
||||
const fetchGroupDetail = async () => {
|
||||
if (!id || !groupId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getCreatedGroupDetail({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
const data = res?.data || res || {};
|
||||
|
||||
// 处理成员数据,确保有id字段
|
||||
const members = (data.members || []).map((member: any) => ({
|
||||
...member,
|
||||
id: String(member.friendId || member.id || member.wechatId || ''),
|
||||
}));
|
||||
|
||||
const detailData: GroupDetail = {
|
||||
id: String(data.id || groupId),
|
||||
name: data.groupName || data.name || "未命名群组",
|
||||
createTime: data.createTime || "",
|
||||
planName: data.workbenchName || data.planName || "",
|
||||
groupAdmin: data.ownerWechatId || data.ownerNickname
|
||||
? {
|
||||
id: String(data.ownerWechatId || ""),
|
||||
nickname: data.ownerNickname,
|
||||
wechatId: data.ownerWechatId,
|
||||
avatar: data.ownerAvatar,
|
||||
}
|
||||
: undefined,
|
||||
announcement: data.announce || data.announcement, // 接口返回的字段是 announce
|
||||
memberCount: data.memberCount || members.length || 0,
|
||||
members: members,
|
||||
avatar: data.avatar || data.groupAvatar,
|
||||
groupAvatar: data.groupAvatar,
|
||||
};
|
||||
|
||||
setGroupDetail(detailData);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取群组详情失败", position: "top" });
|
||||
navigate(-1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditName = () => {
|
||||
if (!groupDetail) return;
|
||||
setEditNameValue(groupDetail.name);
|
||||
setEditNameVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmEditName = async () => {
|
||||
if (!id || !groupId || !editNameValue.trim()) {
|
||||
Toast.show({ content: "群名称不能为空", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await modifyGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
chatroomName: editNameValue.trim(),
|
||||
});
|
||||
Toast.show({ content: "修改成功", position: "top" });
|
||||
setEditNameVisible(false);
|
||||
// 刷新群详情
|
||||
fetchGroupDetail();
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "修改群名称失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAnnouncement = () => {
|
||||
if (!groupDetail) return;
|
||||
setEditAnnouncementValue(groupDetail.announcement || "");
|
||||
setEditAnnouncementVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmEditAnnouncement = async () => {
|
||||
if (!id || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await modifyGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
announce: editAnnouncementValue.trim() || undefined,
|
||||
});
|
||||
Toast.show({ content: "修改成功", position: "top" });
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
// 刷新群详情
|
||||
fetchGroupDetail();
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "修改群公告失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowQRCode = () => {
|
||||
Toast.show({ content: "群二维码功能待实现", position: "top" });
|
||||
};
|
||||
|
||||
const handleSyncGroup = async () => {
|
||||
if (!id || !groupId) {
|
||||
Toast.show({ content: "参数错误", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncing(true);
|
||||
try {
|
||||
const res = await syncGroupInfo({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
const data = res?.data || res || {};
|
||||
|
||||
const successMessages: string[] = [];
|
||||
if (data.groupInfoSynced) {
|
||||
successMessages.push("群信息同步成功");
|
||||
}
|
||||
if (data.memberInfoSynced) {
|
||||
successMessages.push("群成员信息同步成功");
|
||||
}
|
||||
|
||||
if (successMessages.length > 0) {
|
||||
Toast.show({
|
||||
content: successMessages.join(","),
|
||||
position: "top"
|
||||
});
|
||||
// 同步成功后刷新群详情
|
||||
await fetchGroupDetail();
|
||||
} else {
|
||||
Toast.show({
|
||||
content: res?.msg || "同步完成",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "同步群信息失败",
|
||||
position: "top"
|
||||
});
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisbandGroup = () => {
|
||||
Dialog.confirm({
|
||||
content: "确定要退出该群组吗?",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
onConfirm: async () => {
|
||||
if (!id || !groupId) {
|
||||
Toast.show({ content: "参数错误", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await quitGroup({
|
||||
workbenchId: id,
|
||||
groupId: groupId,
|
||||
});
|
||||
Toast.show({ content: "退出群组成功", position: "top" });
|
||||
// 退出成功后返回上一页
|
||||
navigate(-1);
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || "退出群组失败",
|
||||
position: "top"
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveMember = (memberId: string) => {
|
||||
Dialog.confirm({
|
||||
content: "确定要移除此成员吗?",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
onConfirm: () => {
|
||||
Toast.show({ content: "移除成员功能待实现", position: "top" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤成员
|
||||
const filteredMembers = groupDetail?.members?.filter((member) => {
|
||||
if (!searchKeyword) return true;
|
||||
const keyword = searchKeyword.toLowerCase();
|
||||
return (
|
||||
member.nickname?.toLowerCase().includes(keyword) ||
|
||||
member.wechatId?.toLowerCase().includes(keyword)
|
||||
);
|
||||
}) || [];
|
||||
|
||||
// 群组图标颜色
|
||||
const iconColors = {
|
||||
from: "#3b82f6",
|
||||
to: "#4f46e5",
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!groupDetail) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyText}>未找到该群组</div>
|
||||
<button
|
||||
className={style.backButton}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="群详情"
|
||||
backFn={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 同步遮罩层 */}
|
||||
{syncing && (
|
||||
<div className={style.syncOverlay}>
|
||||
<div className={style.syncContent}>
|
||||
<SpinLoading style={{ "--size": "32px" }} />
|
||||
<div className={style.syncText}>同步中...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 群组信息卡片 */}
|
||||
<section className={style.groupInfoCard}>
|
||||
{groupDetail.avatar || groupDetail.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconLarge}
|
||||
src={groupDetail.avatar || groupDetail.groupAvatar}
|
||||
alt={groupDetail.name || ""}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.nextElementSibling as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconLarge}
|
||||
style={{
|
||||
display: groupDetail.avatar || groupDetail.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
|
||||
}}
|
||||
>
|
||||
{(groupDetail.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
<h2 className={style.groupTitle}>{groupDetail.name}</h2>
|
||||
<div className={style.createTimeInfo}>
|
||||
<ScheduleOutlined className={style.createTimeIcon} />
|
||||
创建于 {groupDetail.createTime || "-"}
|
||||
</div>
|
||||
<div className={style.actionButtons}>
|
||||
<button className={style.actionButton} onClick={handleEditName}>
|
||||
<EditOutlined className={style.actionButtonIcon} />
|
||||
修改名称
|
||||
</button>
|
||||
{/* 群二维码功能暂时隐藏 */}
|
||||
{/* <button className={style.actionButton} onClick={handleShowQRCode}>
|
||||
<QrcodeOutlined className={style.actionButtonIcon} />
|
||||
群二维码
|
||||
</button> */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<section className={style.basicInfoCard}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
基本信息
|
||||
</h3>
|
||||
<div className={style.basicInfoList}>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>所属计划</span>
|
||||
<div className={style.basicInfoValue}>
|
||||
<span className={style.planIcon}>📋</span>
|
||||
{groupDetail.planName || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.basicInfoDivider}></div>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>群主</span>
|
||||
<div className={style.basicInfoValue}>
|
||||
{groupDetail.groupAdmin ? (
|
||||
<>
|
||||
<span className={style.adminAvatar}>
|
||||
{groupDetail.groupAdmin.avatar ? (
|
||||
<img
|
||||
src={groupDetail.groupAdmin.avatar}
|
||||
alt={groupDetail.groupAdmin.nickname || ""}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
|
||||
style.hidden
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span className={style.adminAvatarText}>
|
||||
{groupDetail.groupAdmin.nickname?.charAt(0)?.toUpperCase() || "A"}
|
||||
</span>
|
||||
</span>
|
||||
{groupDetail.groupAdmin.wechatId || "-"}
|
||||
</>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.basicInfoDivider}></div>
|
||||
<div className={style.basicInfoItem}>
|
||||
<span className={style.basicInfoLabel}>群公告</span>
|
||||
<div
|
||||
className={style.basicInfoValue}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={handleEditAnnouncement}
|
||||
>
|
||||
{groupDetail.announcement ? (
|
||||
groupDetail.announcement
|
||||
) : (
|
||||
<span className={style.noAnnouncement}>
|
||||
未设置
|
||||
<span className={style.chevronIcon}>›</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 群成员 */}
|
||||
<section className={style.membersCard}>
|
||||
<div className={style.membersHeader}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
群成员
|
||||
<span className={style.memberCountBadge}>
|
||||
{groupDetail.memberCount || 0}人
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
className={style.searchButton}
|
||||
onClick={() => setShowSearch(!showSearch)}
|
||||
title="搜索成员"
|
||||
>
|
||||
<SearchOutlined />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<div className={style.searchBox}>
|
||||
<input
|
||||
className={style.searchInput}
|
||||
placeholder="搜索成员..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style.membersList}>
|
||||
{filteredMembers.length > 0 ? (
|
||||
filteredMembers.map((member) => {
|
||||
const isGroupAdmin = member.isOwner === 1 || member.isGroupAdmin || member.id === groupDetail.groupAdmin?.id;
|
||||
return (
|
||||
<div key={member.friendId || member.id || member.wechatId} className={style.memberItem}>
|
||||
<div className={style.memberInfo}>
|
||||
<div className={style.memberAvatarWrapper}>
|
||||
{member.avatar ? (
|
||||
<img
|
||||
className={style.memberAvatar}
|
||||
src={member.avatar}
|
||||
alt={member.nickname || ""}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={style.memberAvatarPlaceholder}>
|
||||
{member.nickname?.charAt(0)?.toUpperCase() || "?"}
|
||||
</div>
|
||||
)}
|
||||
{isGroupAdmin && (
|
||||
<div className={style.adminBadge}>
|
||||
<StarOutlined className={style.adminBadgeIcon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.memberInfoText}>
|
||||
<div className={style.memberNameRow}>
|
||||
<span className={style.memberName}>
|
||||
{member.nickname || "-"}
|
||||
</span>
|
||||
{member.isQuit === 1 && (
|
||||
<span className={style.quitTag}>已退群</span>
|
||||
)}
|
||||
{member.joinStatus && (
|
||||
<span className={style.joinStatusTag}>
|
||||
{member.joinStatus === "auto" ? "自动" : "手动"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.memberWechatId}>
|
||||
{member.wechatId || "-"}
|
||||
</div>
|
||||
{(member.alias || member.remark) && (
|
||||
<div className={style.memberExtraInfo}>
|
||||
{member.alias && <span>别名:{member.alias}</span>}
|
||||
{member.alias && member.remark && <span className={style.divider}>|</span>}
|
||||
{member.remark && <span>备注:{member.remark}</span>}
|
||||
</div>
|
||||
)}
|
||||
{member.joinTime && (
|
||||
<div className={style.memberJoinTime}>
|
||||
入群时间:{member.joinTime}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.memberActions}>
|
||||
{isGroupAdmin ? (
|
||||
<span className={style.adminTag}>群主</span>
|
||||
) : (
|
||||
// 移除成员功能暂时隐藏
|
||||
// <button
|
||||
// className={style.removeButton}
|
||||
// onClick={() => handleRemoveMember(String(member.friendId || member.id || member.wechatId || ''))}
|
||||
// >
|
||||
// <CloseCircleOutlined />
|
||||
// </button>
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={style.emptyMembers}>
|
||||
<div className={style.emptyMembersText}>
|
||||
{searchKeyword ? "未找到匹配的成员" : "暂无成员"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<section className={style.actionsSection}>
|
||||
<button className={style.actionCard} onClick={handleSyncGroup}>
|
||||
<div className={style.actionCardIcon}>
|
||||
<SyncOutlined />
|
||||
</div>
|
||||
<span className={style.actionCardText}>同步群信息</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${style.actionCard} ${style.actionCardDanger}`}
|
||||
onClick={handleDisbandGroup}
|
||||
>
|
||||
<div className={`${style.actionCardIcon} ${style.actionCardIconDanger}`}>
|
||||
<LogoutOutlined />
|
||||
</div>
|
||||
<span className={`${style.actionCardText} ${style.actionCardTextDanger}`}>
|
||||
解散/退出群组
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 修改群名称弹窗 */}
|
||||
<Dialog
|
||||
visible={editNameVisible}
|
||||
content={
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
|
||||
群名称
|
||||
</div>
|
||||
<Input
|
||||
placeholder="请输入群名称"
|
||||
value={editNameValue}
|
||||
onChange={(val) => setEditNameValue(val)}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
padding: "10px 12px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#fff",
|
||||
}}
|
||||
maxLength={30}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
closeOnAction
|
||||
onClose={() => {
|
||||
setEditNameVisible(false);
|
||||
setEditNameValue("");
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
key: "cancel",
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
setEditNameVisible(false);
|
||||
setEditNameValue("");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "confirm",
|
||||
text: "确定",
|
||||
primary: true,
|
||||
onClick: handleConfirmEditName,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 修改群公告弹窗 */}
|
||||
<Dialog
|
||||
visible={editAnnouncementVisible}
|
||||
content={
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
|
||||
群公告
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder="请输入群公告"
|
||||
value={editAnnouncementValue}
|
||||
onChange={(val) => setEditAnnouncementValue(val)}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
padding: "10px 12px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#fff",
|
||||
minHeight: "120px",
|
||||
}}
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
closeOnAction
|
||||
onClose={() => {
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
key: "cancel",
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
setEditAnnouncementVisible(false);
|
||||
setEditAnnouncementValue("");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "confirm",
|
||||
text: "确定",
|
||||
primary: true,
|
||||
onClick: handleConfirmEditAnnouncement,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDetailPage;
|
||||
@@ -0,0 +1,275 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
padding: 8px;
|
||||
margin-right: -8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索和筛选区域
|
||||
.filterSection {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
background: #f3f4f6;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
font-size: 18px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
&:focus + .searchIcon,
|
||||
&:focus ~ .searchIcon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #4b5563;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButtonActive {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 1px 2px 0 rgba(59, 130, 246, 0.3);
|
||||
|
||||
&:active {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 群组列表
|
||||
.groupsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.groupCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.groupCardContent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.groupIconWrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupIconImg,
|
||||
.groupIconFallback {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.groupIconImg {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.groupIconFallback {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.groupInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.groupName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.groupMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.memberCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #4b5563;
|
||||
background: #f3f4f6;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.createTime {
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 32px;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.footerText {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Toast, SpinLoading, InfiniteScroll } from "antd-mobile";
|
||||
import { SearchOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getCreatedGroupsList } from "../form/api";
|
||||
import style from "./groups-list.module.scss";
|
||||
|
||||
interface Group {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
groupName?: string;
|
||||
memberCount: number;
|
||||
memberCountText?: string;
|
||||
createTime: string;
|
||||
avatar?: string;
|
||||
groupAvatar?: string;
|
||||
memberAvatars?: Array<{
|
||||
avatar?: string;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
ownerNickname?: string;
|
||||
ownerWechatId?: string;
|
||||
ownerAvatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GroupListPageProps {
|
||||
planId?: string;
|
||||
}
|
||||
|
||||
const GroupListPage: React.FC<GroupListPageProps> = ({ planId: propPlanId }) => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const planId = propPlanId || id;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [searchKeyword, setSearchKeyword] = useState("");
|
||||
const [sortBy, setSortBy] = useState<"all" | "createTime" | "memberCount">("all");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!planId) return;
|
||||
fetchGroups(true);
|
||||
}, [planId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!planId) return;
|
||||
const timer = setTimeout(() => {
|
||||
fetchGroups(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchKeyword]);
|
||||
|
||||
const fetchGroups = async (reset = false) => {
|
||||
if (!planId) return;
|
||||
const page = reset ? 1 : currentPage;
|
||||
if (reset) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const res = await getCreatedGroupsList({
|
||||
workbenchId: planId,
|
||||
page,
|
||||
limit: 20,
|
||||
keyword: searchKeyword || undefined,
|
||||
});
|
||||
// 注意:request 拦截器已经提取了 data 字段,所以 res 就是 data 对象
|
||||
// 接口返回结构:{ list: [...], total: 2, page: "1", limit: "10" }
|
||||
const groupsData = res?.list || (Array.isArray(res) ? res : []);
|
||||
if (reset) {
|
||||
setGroups(groupsData);
|
||||
setCurrentPage(2); // 重置后下一页是2
|
||||
} else {
|
||||
setGroups(prev => [...prev, ...groupsData]);
|
||||
setCurrentPage(prev => prev + 1);
|
||||
}
|
||||
setHasMore(groupsData.length >= 20);
|
||||
} catch (e: any) {
|
||||
// request拦截器在code !== 200时会reject并显示Toast
|
||||
// 如果拦截器没有显示错误(比如网络错误),这里才显示
|
||||
// 注意:拦截器已经在错误时显示了Toast,所以这里通常不需要再显示
|
||||
} finally {
|
||||
if (reset) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (hasMore && !loading) {
|
||||
await fetchGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 排序(搜索和过滤由接口完成)
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
if (sortBy === "createTime") {
|
||||
const timeA = new Date(a.createTime || 0).getTime();
|
||||
const timeB = new Date(b.createTime || 0).getTime();
|
||||
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
|
||||
} else if (sortBy === "memberCount") {
|
||||
return sortOrder === "asc" ? a.memberCount - b.memberCount : b.memberCount - a.memberCount;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleSortClick = (type: "createTime" | "memberCount") => {
|
||||
if (sortBy === type) {
|
||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortBy(type);
|
||||
setSortOrder("desc");
|
||||
}
|
||||
};
|
||||
|
||||
const getSortIcon = (type: "createTime" | "memberCount") => {
|
||||
if (sortBy !== type) return <span style={{ fontSize: '12px' }}>⇅</span>;
|
||||
return sortOrder === "asc" ? <UpOutlined /> : <DownOutlined />;
|
||||
};
|
||||
|
||||
// 群组图标颜色配置
|
||||
const iconColors = [
|
||||
{ from: "#3b82f6", to: "#4f46e5" }, // blue to indigo
|
||||
{ from: "#6366f1", to: "#8b5cf6" }, // indigo to purple
|
||||
{ from: "#a855f7", to: "#ec4899" }, // purple to pink
|
||||
{ from: "#f472b6", to: "#f43f5e" }, // pink to rose
|
||||
{ from: "#fb923c", to: "#ef4444" }, // orange to red
|
||||
{ from: "#14b8a6", to: "#10b981" }, // teal to emerald
|
||||
{ from: "#22d3ee", to: "#3b82f6" }, // cyan to blue
|
||||
];
|
||||
|
||||
const getGroupIcon = (index: number) => {
|
||||
const colors = iconColors[index % iconColors.length];
|
||||
return { from: colors.from, to: colors.to };
|
||||
};
|
||||
|
||||
const getGroupIconEmoji = (group: Group, index: number) => {
|
||||
// 可以根据群组名称或其他属性返回不同的图标
|
||||
const icons = ["groups", "star", "fiber_new", "local_fire_department", "campaign", "forum", "school"];
|
||||
return icons[index % icons.length];
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="所有群组列表" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="所有群组列表"
|
||||
backFn={() => navigate(-1)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 搜索和筛选区域 */}
|
||||
<div className={style.filterSection}>
|
||||
<div className={style.searchBox}>
|
||||
<SearchOutlined className={style.searchIcon} />
|
||||
<input
|
||||
className={style.searchInput}
|
||||
placeholder="搜索群名称、群ID、群主昵称..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.filterButtons}>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "all" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => {
|
||||
setSortBy("all");
|
||||
setSortOrder("desc");
|
||||
}}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "createTime" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => handleSortClick("createTime")}
|
||||
>
|
||||
创建时间
|
||||
<span className={style.filterIcon}>{getSortIcon("createTime")}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${style.filterButton} ${sortBy === "memberCount" ? style.filterButtonActive : ""}`}
|
||||
onClick={() => handleSortClick("memberCount")}
|
||||
>
|
||||
成员数量
|
||||
<span className={style.filterIcon}>{getSortIcon("memberCount")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 群组列表 */}
|
||||
<div className={style.groupsList}>
|
||||
{sortedGroups.length > 0 ? (
|
||||
sortedGroups.map((group, index) => {
|
||||
const iconColors = getGroupIcon(index);
|
||||
return (
|
||||
<div
|
||||
key={String(group.id)}
|
||||
className={style.groupCard}
|
||||
onClick={() => navigate(`/workspace/group-create/${planId}/groups/${String(group.id)}`)}
|
||||
>
|
||||
<div className={style.groupCardContent}>
|
||||
<div className={style.groupIconWrapper}>
|
||||
{group.avatar || group.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconImg}
|
||||
src={group.avatar || group.groupAvatar}
|
||||
alt={group.groupName || group.name || ""}
|
||||
onError={(e) => {
|
||||
// 加载失败时显示默认图标
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconFallback}
|
||||
style={{
|
||||
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
|
||||
}}
|
||||
>
|
||||
{(group.groupName || group.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.groupInfo}>
|
||||
<h4 className={style.groupName}>{group.groupName || group.name || `群组 ${index + 1}`}</h4>
|
||||
<div className={style.groupMeta}>
|
||||
<span className={style.memberCount}>
|
||||
👤 {group.memberCountText || `${group.memberCount || 0}人`}
|
||||
</span>
|
||||
<span className={style.createTime}>{group.createTime || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={style.emptyState}>
|
||||
<div className={style.emptyText}>暂无群组</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 无限滚动加载 */}
|
||||
{sortedGroups.length > 0 && (
|
||||
<InfiniteScroll
|
||||
loadMore={handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
threshold={100}
|
||||
>
|
||||
{hasMore ? (
|
||||
<div className={style.footer}>
|
||||
<SpinLoading style={{ "--size": "24px" }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.footer}>
|
||||
<p className={style.footerText}>已显示全部 {sortedGroups.length} 个群组</p>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupListPage;
|
||||
@@ -0,0 +1,482 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f3f4f6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loadingContainer,
|
||||
.emptyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 基本信息卡片
|
||||
.infoCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.infoHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.infoTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.infoSubtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.statusRunning {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border-color: #a7f3d0;
|
||||
}
|
||||
|
||||
.statusStopped {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.infoMeta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.metaItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metaLabel {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.metaValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.metaIcon {
|
||||
font-size: 16px;
|
||||
color: #9ca3af;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
.statsSection {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sectionTitleDot {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3b82f6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statIconBlue {
|
||||
background: #dbeafe;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.statIconGreen {
|
||||
background: #d1fae5;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.statIconPurple {
|
||||
background: #f3e8ff;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.statNumber {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
// 配置信息
|
||||
.configCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.configList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.configItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.configValue {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deviceIcon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.configDivider {
|
||||
height: 1px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.wechatTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wechatTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.wechatTagAdmin {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-color: #fde68a;
|
||||
}
|
||||
|
||||
.adminBadge {
|
||||
margin-left: 4px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
border: 1px solid rgba(146, 64, 14, 0.3);
|
||||
background: rgba(254, 243, 199, 0.5);
|
||||
}
|
||||
|
||||
// 群列表
|
||||
.groupsSection {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.groupsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.groupsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.groupCard {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.groupCardHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.groupCardLeft {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.groupIconWrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupIconImg,
|
||||
.groupIconFallback {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.groupIconImg {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.groupIconFallback {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.groupName {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.groupMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.groupMemberCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #f3f4f6;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
color: #4b5563;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.groupDate {
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.chevronIcon {
|
||||
font-size: 24px;
|
||||
color: #d1d5db;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groupMembers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.memberAvatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.memberAvatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
margin-left: -10px;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memberMore {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
background: #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
margin-left: -10px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.viewDetailText {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.emptyGroups {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyGroupsText {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { Button, Toast, SpinLoading } from "antd-mobile";
|
||||
import { EditOutlined, ScheduleOutlined, HistoryOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getGroupCreateDetail, getCreatedGroupsList } from "../form/api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface GroupCreateDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
planType: number; // 0-全局计划, 1-独立计划
|
||||
status: number; // 1-启用, 0-禁用
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
createdGroupsCount?: number; // 已建群数
|
||||
totalMembersCount?: number; // 总人数
|
||||
groupSizeMax?: number; // 群组最大人数
|
||||
config?: {
|
||||
deviceGroupsOptions?: Array<{
|
||||
id: number;
|
||||
nickname?: string;
|
||||
memo?: string;
|
||||
wechatId?: string;
|
||||
}>;
|
||||
wechatGroupsOptions?: Array<{
|
||||
id: number;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
groupAdminWechatId?: number;
|
||||
groupNameTemplate?: string;
|
||||
};
|
||||
groups?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
memberCount: number;
|
||||
createTime: string;
|
||||
members?: Array<{
|
||||
id: string;
|
||||
avatar?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupCreateDetail: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detail, setDetail] = useState<GroupCreateDetail | null>(null);
|
||||
const [groups, setGroups] = useState<Array<{
|
||||
id: number | string;
|
||||
name?: string;
|
||||
groupName?: string;
|
||||
memberCount: number;
|
||||
memberCountText?: string;
|
||||
createTime: string;
|
||||
avatar?: string;
|
||||
groupAvatar?: string;
|
||||
memberAvatars?: Array<{
|
||||
avatar?: string;
|
||||
wechatId?: string;
|
||||
nickname?: string;
|
||||
}>;
|
||||
ownerNickname?: string;
|
||||
ownerWechatId?: string;
|
||||
ownerAvatar?: string;
|
||||
}>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchDetail();
|
||||
fetchGroups();
|
||||
}, [id]);
|
||||
|
||||
const fetchGroups = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await getCreatedGroupsList({
|
||||
workbenchId: id,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
const groupsData = res?.list || res?.data?.list || res?.data || [];
|
||||
setGroups(groupsData);
|
||||
} catch (e: any) {
|
||||
// 静默失败,不影响主详情展示
|
||||
console.error("获取群组列表失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getGroupCreateDetail(id!);
|
||||
const config = res.config || {};
|
||||
const stats = config.stats || {};
|
||||
|
||||
const detailData: GroupCreateDetail = {
|
||||
id: String(res.id),
|
||||
name: res.name || "",
|
||||
planType: config.planType ?? res.planType ?? 1,
|
||||
status: res.status ?? 1,
|
||||
createTime: res.createTime || "",
|
||||
updateTime: res.updateTime || res.createTime || "",
|
||||
createdGroupsCount: stats.createdGroupsCount ?? res.createdGroupsCount ?? 0,
|
||||
totalMembersCount: stats.totalMembersCount ?? res.totalMembersCount ?? 0,
|
||||
groupSizeMax: config.groupSizeMax || 38,
|
||||
config: {
|
||||
deviceGroupsOptions: config.deviceGroupsOptions || [],
|
||||
wechatGroupsOptions: config.wechatGroupsOptions || [],
|
||||
groupAdminWechatId: config.groupAdminWechatId,
|
||||
groupNameTemplate: config.groupNameTemplate || "",
|
||||
},
|
||||
groups: [], // 群列表通过单独的接口获取
|
||||
};
|
||||
setDetail(detailData);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取详情失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
navigate(`/workspace/group-create/${id}/edit`);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.loadingContainer}>
|
||||
<SpinLoading style={{ "--size": "48px" }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
|
||||
>
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyText}>未找到该计划</div>
|
||||
<Button color="primary" onClick={() => navigate("/workspace/group-create")}>
|
||||
返回列表
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const isRunning = detail.status === 1;
|
||||
const planTypeText = detail.planType === 0 ? "全局计划" : "独立计划";
|
||||
const executorDevice = detail.config?.deviceGroupsOptions?.[0];
|
||||
const executorName = executorDevice?.nickname || executorDevice?.memo || executorDevice?.wechatId || "-";
|
||||
const fixedWechatIds = detail.config?.wechatGroupsOptions || [];
|
||||
const groupAdminId = detail.config?.groupAdminWechatId;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="计划详情"
|
||||
backFn={() => navigate(-1)}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleEdit}
|
||||
style={{ marginRight: "-8px" }}
|
||||
>
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 基本信息卡片 */}
|
||||
<section className={style.infoCard}>
|
||||
<div className={style.infoHeader}>
|
||||
<div>
|
||||
<h2 className={style.infoTitle}>{detail.name}</h2>
|
||||
<p className={style.infoSubtitle}>{planTypeText}</p>
|
||||
</div>
|
||||
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
|
||||
{isRunning && <span className={style.statusDot}></span>}
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.infoMeta}>
|
||||
<div className={style.metaItem}>
|
||||
<span className={style.metaLabel}>创建时间</span>
|
||||
<div className={style.metaValue}>
|
||||
<ScheduleOutlined className={style.metaIcon} />
|
||||
{detail.createTime || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.metaItem}>
|
||||
<span className={style.metaLabel}>更新时间</span>
|
||||
<div className={style.metaValue}>
|
||||
<HistoryOutlined className={style.metaIcon} />
|
||||
{detail.updateTime || "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<section className={style.statsSection}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
统计信息
|
||||
</h3>
|
||||
<div className={style.statsGrid}>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconBlue}`}>
|
||||
👥
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.createdGroupsCount || 0}</span>
|
||||
<span className={style.statLabel}>已建群数</span>
|
||||
</div>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconGreen}`}>
|
||||
👥
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.totalMembersCount || 0}</span>
|
||||
<span className={style.statLabel}>总人数</span>
|
||||
</div>
|
||||
<div className={style.statCard}>
|
||||
<div className={`${style.statIcon} ${style.statIconPurple}`}>
|
||||
📊
|
||||
</div>
|
||||
<span className={style.statNumber}>{detail.groupSizeMax || 38}</span>
|
||||
<span className={style.statLabel}>人/群</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 配置信息 */}
|
||||
<section className={style.configCard}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
配置信息
|
||||
</h3>
|
||||
<div className={style.configList}>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>分组方式</span>
|
||||
<span className={style.configValue}>所有好友自动分组</span>
|
||||
</div>
|
||||
<div className={style.configDivider}></div>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>执行设备</span>
|
||||
<div className={style.configValue}>
|
||||
<span className={style.deviceIcon}>📱</span>
|
||||
{executorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.configDivider}></div>
|
||||
<div className={style.configItem}>
|
||||
<span className={style.configLabel}>固定微信号</span>
|
||||
<div className={style.wechatTags}>
|
||||
{fixedWechatIds.length > 0 ? (
|
||||
fixedWechatIds.map((wechat: any) => {
|
||||
const isGroupAdmin = wechat.id === groupAdminId;
|
||||
const displayText = wechat.nickname && wechat.wechatId
|
||||
? `${wechat.nickname}(${wechat.wechatId})`
|
||||
: wechat.wechatId || wechat.nickname || "-";
|
||||
return (
|
||||
<span
|
||||
key={wechat.id}
|
||||
className={`${style.wechatTag} ${isGroupAdmin ? style.wechatTagAdmin : ""}`}
|
||||
>
|
||||
{displayText}
|
||||
{isGroupAdmin && (
|
||||
<span className={style.adminBadge}>群主</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className={style.configValue}>-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 群列表 */}
|
||||
<section className={style.groupsSection}>
|
||||
<div className={style.groupsHeader}>
|
||||
<h3 className={style.sectionTitle}>
|
||||
<span className={style.sectionTitleDot}></span>
|
||||
群列表
|
||||
</h3>
|
||||
<button
|
||||
className={style.viewAllButton}
|
||||
onClick={() => navigate(`/workspace/group-create/${id}/groups`)}
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
<div className={style.groupsList}>
|
||||
{groups.length > 0 ? (
|
||||
groups.map((group, index) => (
|
||||
<div
|
||||
key={String(group.id)}
|
||||
className={style.groupCard}
|
||||
onClick={() => navigate(`/workspace/group-create/${id}/groups/${String(group.id)}`)}
|
||||
>
|
||||
<div className={style.groupCardHeader}>
|
||||
<div className={style.groupCardLeft}>
|
||||
<div className={style.groupIconWrapper}>
|
||||
{group.avatar || group.groupAvatar ? (
|
||||
<img
|
||||
className={style.groupIconImg}
|
||||
src={group.avatar || group.groupAvatar}
|
||||
alt={group.groupName || group.name || ""}
|
||||
onError={(e) => {
|
||||
// 加载失败时显示默认图标
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
|
||||
if (fallback) {
|
||||
fallback.style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={style.groupIconFallback}
|
||||
style={{
|
||||
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
|
||||
background: `linear-gradient(to bottom right, ${
|
||||
['#60a5fa', '#818cf8', '#a78bfa', '#f472b6'][index % 4]
|
||||
}, ${
|
||||
['#4f46e5', '#6366f1', '#8b5cf6', '#ec4899'][index % 4]
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
{(group.groupName || group.name || '').charAt(0) || '👥'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className={style.groupName}>{group.groupName || group.name || detail.config?.groupNameTemplate || `群组 ${index + 1}`}</h4>
|
||||
<div className={style.groupMeta}>
|
||||
<span className={style.groupMemberCount}>
|
||||
👤 {group.memberCountText || `${group.memberCount || 0}人`}
|
||||
</span>
|
||||
<span className={style.groupDate}>{group.createTime || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={style.chevronIcon}>›</span>
|
||||
</div>
|
||||
{group.memberAvatars && group.memberAvatars.length > 0 && (
|
||||
<div className={style.groupMembers}>
|
||||
<div className={style.memberAvatars}>
|
||||
{group.memberAvatars.slice(0, 6).map((member, memberIndex) => (
|
||||
<img
|
||||
key={member.wechatId || memberIndex}
|
||||
className={style.memberAvatar}
|
||||
src={member.avatar || "https://via.placeholder.com/24"}
|
||||
alt={member.nickname || ""}
|
||||
style={{ zIndex: 6 - memberIndex }}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "https://via.placeholder.com/24";
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{group.memberAvatars.length > 6 && (
|
||||
<span className={style.memberMore}>
|
||||
+{group.memberAvatars.length - 6}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={style.viewDetailText}>点击查看详情</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={style.emptyGroups}>
|
||||
<div className={style.emptyGroupsText}>暂无群组</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupCreateDetail;
|
||||
@@ -16,3 +16,59 @@ export const getGroupCreateDetail = (id: string) =>
|
||||
export const getGroupCreateList = (params: any) =>
|
||||
request("/v1/workbench/list", params, "GET");
|
||||
|
||||
// 删除自动建群任务
|
||||
export const deleteGroupCreate = (id: string) =>
|
||||
request("/v1/workbench/delete", { id }, "DELETE");
|
||||
|
||||
// 切换任务状态
|
||||
export const toggleGroupCreateStatus = (data: { id: string | number; status: number }) =>
|
||||
request("/v1/workbench/update-status", { ...data }, "POST");
|
||||
|
||||
// 获取群列表
|
||||
export const getCreatedGroupsList = (params: {
|
||||
workbenchId: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
keyword?: string;
|
||||
}) => {
|
||||
// 如果没有指定 limit,默认使用 20
|
||||
const finalParams = {
|
||||
...params,
|
||||
limit: params.limit ?? 20,
|
||||
};
|
||||
return request("/v1/workbench/created-groups-list", finalParams, "GET");
|
||||
};
|
||||
|
||||
// 获取群详情
|
||||
export const getCreatedGroupDetail = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/created-group-detail", params, "GET");
|
||||
};
|
||||
|
||||
// 同步群信息
|
||||
export const syncGroupInfo = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/sync-group-info", params, "POST");
|
||||
};
|
||||
|
||||
// 修改群名称/群公告
|
||||
export const modifyGroupInfo = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
chatroomName?: string;
|
||||
announce?: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/modify-group-info", params, "POST");
|
||||
};
|
||||
|
||||
// 退出群组
|
||||
export const quitGroup = (params: {
|
||||
workbenchId: string;
|
||||
groupId: string;
|
||||
}) => {
|
||||
return request("/v1/workbench/quit-group", params, "POST");
|
||||
};
|
||||
|
||||
@@ -692,3 +692,9 @@
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.statusSwitchContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -37,12 +37,13 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
onChange({
|
||||
executor: selectedDevice,
|
||||
executorId: selectedDevice.id,
|
||||
groupName: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
|
||||
groupNameTemplate: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
executor: undefined,
|
||||
executorId: undefined,
|
||||
groupNameTemplate: "", // 清空群名称
|
||||
});
|
||||
}
|
||||
setExecutorSelectionVisible(false);
|
||||
@@ -51,7 +52,7 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
// 处理固定微信号选择(必须3个)
|
||||
const handleFixedWechatIdsSelect = (friends: FriendSelectionItem[]) => {
|
||||
// 检查总数是否超过3个(包括已添加的手动微信号)
|
||||
const currentManualCount = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual).length;
|
||||
const currentManualCount = (formData.wechatGroupsOptions || []).filter(f => f.isManual).length;
|
||||
const newSelectedCount = friends.length;
|
||||
if (currentManualCount + newSelectedCount > 3) {
|
||||
Toast.show({ content: "固定微信号最多只能选择3个", position: "top" });
|
||||
@@ -60,10 +61,10 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
// 标记为选择的(非手动添加),确保所有从选择弹窗来的都标记为非手动
|
||||
const selectedFriends = friends.map(f => ({ ...f, isManual: false }));
|
||||
// 合并已添加的手动微信号和新的选择
|
||||
const manualFriends = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
|
||||
const manualFriends = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
|
||||
onChange({
|
||||
fixedWechatIds: [...manualFriends, ...selectedFriends].map(f => f.id),
|
||||
fixedWechatIdsOptions: [...manualFriends, ...selectedFriends],
|
||||
wechatGroups: [...manualFriends, ...selectedFriends].map(f => f.id),
|
||||
wechatGroupsOptions: [...manualFriends, ...selectedFriends],
|
||||
});
|
||||
setFixedWechatIdsSelectionVisible(false);
|
||||
};
|
||||
@@ -77,6 +78,27 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
setFixedWechatIdsSelectionVisible(true);
|
||||
};
|
||||
|
||||
// 打开群管理员选择弹窗
|
||||
const handleOpenGroupAdminSelection = () => {
|
||||
if (selectedWechatIds.length === 0) {
|
||||
Toast.show({
|
||||
content: manualAddedWechatIds.length > 0
|
||||
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
|
||||
: "请先选择固定微信号",
|
||||
position: "top"
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 如果当前选择的群管理员是手动添加的,清空选择
|
||||
if (formData.groupAdminWechatIdOption && formData.groupAdminWechatIdOption.isManual) {
|
||||
onChange({
|
||||
groupAdminWechatIdOption: undefined,
|
||||
groupAdminWechatId: undefined,
|
||||
});
|
||||
}
|
||||
setGroupAdminSelectionVisible(true);
|
||||
};
|
||||
|
||||
// 处理群管理员选择(单选)
|
||||
const handleGroupAdminSelect = (friends: FriendSelectionItem[]) => {
|
||||
if (friends.length > 0) {
|
||||
@@ -99,12 +121,12 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
Toast.show({ content: "请输入微信号", position: "top" });
|
||||
return;
|
||||
}
|
||||
const existingIds = formData.fixedWechatIdsOptions.map(f => f.wechatId.toLowerCase());
|
||||
const existingIds = formData.wechatGroupsOptions.map(f => f.wechatId.toLowerCase());
|
||||
if (existingIds.includes(manualWechatIdInput.trim().toLowerCase())) {
|
||||
Toast.show({ content: "该微信号已添加", position: "top" });
|
||||
return;
|
||||
}
|
||||
if (formData.fixedWechatIdsOptions.length >= 3) {
|
||||
if (formData.wechatGroupsOptions.length >= 3) {
|
||||
Toast.show({ content: "固定微信号最多只能添加3个", position: "top" });
|
||||
return;
|
||||
}
|
||||
@@ -117,18 +139,26 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
isManual: true, // 标记为手动添加
|
||||
};
|
||||
onChange({
|
||||
fixedWechatIds: [...formData.fixedWechatIds, newFriend.id],
|
||||
fixedWechatIdsOptions: [...formData.fixedWechatIdsOptions, newFriend],
|
||||
wechatGroups: [...formData.wechatGroups, newFriend.id],
|
||||
wechatGroupsOptions: [...formData.wechatGroupsOptions, newFriend],
|
||||
});
|
||||
setManualWechatIdInput("");
|
||||
};
|
||||
|
||||
// 移除固定微信号
|
||||
const handleRemoveFixedWechatId = (id: number) => {
|
||||
onChange({
|
||||
fixedWechatIds: formData.fixedWechatIds.filter(fid => fid !== id),
|
||||
fixedWechatIdsOptions: formData.fixedWechatIdsOptions.filter(f => f.id !== id),
|
||||
});
|
||||
const removedFriend = formData.wechatGroupsOptions.find(f => f.id === id);
|
||||
const newOptions = formData.wechatGroupsOptions.filter(f => f.id !== id);
|
||||
const updateData: Partial<GroupCreateFormData> = {
|
||||
wechatGroups: formData.wechatGroups.filter(fid => fid !== id),
|
||||
wechatGroupsOptions: newOptions,
|
||||
};
|
||||
// 如果移除的是群管理员,也要清除群管理员设置
|
||||
if (formData.groupAdminWechatId === id) {
|
||||
updateData.groupAdminWechatId = undefined;
|
||||
updateData.groupAdminWechatIdOption = undefined;
|
||||
}
|
||||
onChange(updateData);
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -144,14 +174,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
return false;
|
||||
}
|
||||
// 固定微信号不是必填的,移除验证
|
||||
if (!formData.groupName?.trim()) {
|
||||
Toast.show({ content: "请输入群名称", position: "top" });
|
||||
if (!formData.groupNameTemplate?.trim()) {
|
||||
Toast.show({ content: "请输入群名称模板", position: "top" });
|
||||
return false;
|
||||
}
|
||||
// 群名称长度验证(2-100个字符)
|
||||
const groupNameLength = formData.groupName.trim().length;
|
||||
if (groupNameLength < 2 || groupNameLength > 100) {
|
||||
Toast.show({ content: "群名称长度应在2-100个字符之间", position: "top" });
|
||||
// 群名称模板长度验证(2-100个字符)
|
||||
const groupNameTemplateLength = formData.groupNameTemplate.trim().length;
|
||||
if (groupNameTemplateLength < 2 || groupNameTemplateLength > 100) {
|
||||
Toast.show({ content: "群名称模板长度应在2-100个字符之间", position: "top" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -163,8 +193,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
|
||||
// 区分已选择的微信号(从下拉选择)和已添加的微信号(手动输入)
|
||||
// 如果 isManual 未定义,默认为 false(即选择的)
|
||||
const selectedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => !f.isManual);
|
||||
const manualAddedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
|
||||
const selectedWechatIds = (formData.wechatGroupsOptions || []).filter(f => !f.isManual);
|
||||
const manualAddedWechatIds = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
@@ -355,14 +385,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
</div>
|
||||
{formData.groupAdminEnabled && (
|
||||
<div
|
||||
className={`${style.wechatSelectInput} ${(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? style.disabled : ''}`}
|
||||
onClick={() => {
|
||||
if ((selectedWechatIds.length + manualAddedWechatIds.length) === 0) {
|
||||
Toast.show({ content: "请先选择固定微信号", position: "top" });
|
||||
return;
|
||||
}
|
||||
setGroupAdminSelectionVisible(true);
|
||||
}}
|
||||
className={`${style.wechatSelectInput} ${selectedWechatIds.length === 0 ? style.disabled : ''}`}
|
||||
onClick={handleOpenGroupAdminSelection}
|
||||
>
|
||||
<div className={style.selectInputWrapper}>
|
||||
<span className={style.selectInputPlaceholder}>
|
||||
@@ -383,8 +407,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="text"
|
||||
className={style.input}
|
||||
value={formData.groupName}
|
||||
onChange={e => onChange({ groupName: e.target.value })}
|
||||
value={formData.groupNameTemplate}
|
||||
onChange={e => onChange({ groupNameTemplate: e.target.value })}
|
||||
placeholder="请输入群名称"
|
||||
/>
|
||||
</div>
|
||||
@@ -448,8 +472,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="time"
|
||||
className={style.timeInput}
|
||||
value={formData.executeTime || "09:00"}
|
||||
onChange={e => onChange({ executeTime: e.target.value || "09:00" })}
|
||||
value={formData.startTime || "09:00"}
|
||||
onChange={e => onChange({ startTime: e.target.value || "09:00" })}
|
||||
/>
|
||||
<ClockCircleOutlined className={style.timeIcon} />
|
||||
</div>
|
||||
@@ -458,21 +482,25 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
<input
|
||||
type="time"
|
||||
className={style.timeInput}
|
||||
value={formData.executeEndTime || "18:00"}
|
||||
onChange={e => onChange({ executeEndTime: e.target.value || "18:00" })}
|
||||
value={formData.endTime || "21:00"}
|
||||
onChange={e => onChange({ endTime: e.target.value || "21:00" })}
|
||||
/>
|
||||
<ClockCircleOutlined className={style.timeIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{(selectedWechatIds.length + manualAddedWechatIds.length) !== 3 && (
|
||||
<div className={style.errorTip}>
|
||||
<ExclamationCircleOutlined className={style.errorIcon} />
|
||||
<span className={style.errorText}>固定微信号必须选择3个</span>
|
||||
{/* 是否启用 */}
|
||||
<div className={style.card}>
|
||||
<div className={style.statusSwitchContainer}>
|
||||
<label className={style.label} style={{ marginBottom: 0 }}>是否启用</label>
|
||||
<Switch
|
||||
checked={formData.status === 1}
|
||||
onChange={(checked) => onChange({ status: checked ? 1 : 0 })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 隐藏的选择组件 */}
|
||||
<div style={{ display: "none" }}>
|
||||
@@ -518,12 +546,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||
{(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? (
|
||||
{selectedWechatIds.length === 0 ? (
|
||||
<div style={{ textAlign: "center", padding: "40px", color: "#94a3b8" }}>
|
||||
暂无固定微信号可选
|
||||
{manualAddedWechatIds.length > 0
|
||||
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
|
||||
: "暂无固定微信号可选"}
|
||||
</div>
|
||||
) : (
|
||||
[...selectedWechatIds, ...manualAddedWechatIds].map(friend => {
|
||||
selectedWechatIds.map(friend => {
|
||||
const isSelected = formData.groupAdminWechatIdOption?.id === friend.id;
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -10,25 +10,25 @@ export interface DeviceSelectionStepRef {
|
||||
}
|
||||
|
||||
interface DeviceSelectionStepProps {
|
||||
selectedDevices?: DeviceSelectionItem[];
|
||||
selectedDeviceIds?: number[];
|
||||
deviceGroupsOptions?: DeviceSelectionItem[];
|
||||
deviceGroups?: number[];
|
||||
onSelect: (devices: DeviceSelectionItem[]) => void;
|
||||
}
|
||||
|
||||
const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionStepProps>(
|
||||
({ selectedDevices = [], selectedDeviceIds = [], onSelect }, ref) => {
|
||||
({ deviceGroupsOptions = [], deviceGroups = [], onSelect }, ref) => {
|
||||
const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false);
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate: async () => {
|
||||
if (selectedDeviceIds.length === 0) {
|
||||
if (deviceGroups.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getValues: () => {
|
||||
return { selectedDevices, selectedDeviceIds };
|
||||
return { deviceGroupsOptions, deviceGroups };
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -48,10 +48,10 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
className={style.deviceSelector}
|
||||
onClick={() => setDeviceSelectionVisible(true)}
|
||||
>
|
||||
{selectedDevices.length > 0 ? (
|
||||
{deviceGroupsOptions.length > 0 ? (
|
||||
<div className={style.selectedDevicesInfo}>
|
||||
<span className={style.selectedCountText}>
|
||||
已选择 {selectedDevices.length} 个设备
|
||||
已选择 {deviceGroupsOptions.length} 个设备
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -64,9 +64,9 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
</div>
|
||||
|
||||
{/* 已选设备列表 */}
|
||||
{selectedDevices.length > 0 && (
|
||||
{deviceGroupsOptions.length > 0 && (
|
||||
<div className={style.selectedDevicesGrid}>
|
||||
{selectedDevices.map((device) => (
|
||||
{deviceGroupsOptions.map((device) => (
|
||||
<div key={device.id} className={style.selectedDeviceCard}>
|
||||
<div className={style.deviceCardHeader}>
|
||||
<div className={`${style.deviceCardIcon} ${device.status === "online" ? style.deviceIconOnline : style.deviceIconOffline}`}>
|
||||
@@ -88,8 +88,8 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
className={style.deviceCardDeleteButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newSelectedDevices = selectedDevices.filter(d => d.id !== device.id);
|
||||
onSelect(newSelectedDevices);
|
||||
const newDeviceGroupsOptions = deviceGroupsOptions.filter(d => d.id !== device.id);
|
||||
onSelect(newDeviceGroupsOptions);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined className={style.deviceCardDeleteIcon} />
|
||||
@@ -102,7 +102,7 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
|
||||
{/* 设备选择弹窗 */}
|
||||
<div style={{ display: "none" }}>
|
||||
<DeviceSelection
|
||||
selectedOptions={selectedDevices}
|
||||
selectedOptions={deviceGroupsOptions}
|
||||
onSelect={handleDeviceSelect}
|
||||
placeholder="选择设备"
|
||||
showInput={false}
|
||||
|
||||
@@ -19,24 +19,22 @@ const steps = [
|
||||
|
||||
const defaultForm: GroupCreateFormData = {
|
||||
planType: 1, // 默认独立计划
|
||||
name: "新建群计划",
|
||||
name: "",
|
||||
executorId: undefined,
|
||||
executor: undefined,
|
||||
selectedDevices: [],
|
||||
selectedDeviceIds: [],
|
||||
fixedWechatIds: [],
|
||||
fixedWechatIdsOptions: [],
|
||||
deviceGroupsOptions: [],
|
||||
deviceGroups: [],
|
||||
wechatGroups: [],
|
||||
wechatGroupsOptions: [],
|
||||
groupAdminEnabled: false,
|
||||
groupAdminWechatId: undefined,
|
||||
groupAdminWechatIdOption: undefined,
|
||||
groupName: "卡若001",
|
||||
groupMethod: 0, // 默认所有好友自动分组
|
||||
maxGroupsPerDay: 10,
|
||||
groupNameTemplate: "",
|
||||
maxGroupsPerDay: 20,
|
||||
groupSizeMin: 3,
|
||||
groupSizeMax: 38,
|
||||
executeType: 1, // 默认定时执行
|
||||
executeTime: "09:00", // 默认开始时间
|
||||
executeEndTime: "18:00", // 默认结束时间
|
||||
startTime: "09:00", // 默认开始时间
|
||||
endTime: "21:00", // 默认结束时间
|
||||
poolGroups: [],
|
||||
poolGroupsOptions: [],
|
||||
status: 1, // 默认启用
|
||||
@@ -61,30 +59,41 @@ const GroupCreateForm: React.FC = () => {
|
||||
// 获取详情并回填表单
|
||||
getGroupCreateDetail(id)
|
||||
.then(res => {
|
||||
const config = res.config || {};
|
||||
// 转换 deviceGroups 从字符串数组到数字数组
|
||||
const deviceGroups = (config.deviceGroups || []).map((id: string | number) => Number(id));
|
||||
// 转换 poolGroups 保持字符串数组
|
||||
const poolGroups = (config.poolGroups || []).map((id: string | number) => String(id));
|
||||
// 转换 wechatGroups 到数字数组
|
||||
const wechatGroups = (config.wechatGroups || []).map((id: string | number) => Number(id));
|
||||
|
||||
// 查找群管理员选项(如果有)
|
||||
const groupAdminWechatIdOption = config.groupAdminWechatId && config.wechatGroupsOptions
|
||||
? config.wechatGroupsOptions.find((f: any) => f.id === config.groupAdminWechatId)
|
||||
: undefined;
|
||||
|
||||
const updatedForm: GroupCreateFormData = {
|
||||
...defaultForm,
|
||||
id: res.id,
|
||||
planType: res.planType ?? 1,
|
||||
id: String(res.id),
|
||||
planType: config.planType ?? res.planType ?? 1,
|
||||
name: res.name ?? "",
|
||||
executorId: res.executorId,
|
||||
executor: res.executor,
|
||||
selectedDevices: res.selectedDevices || [],
|
||||
selectedDeviceIds: res.selectedDeviceIds || [],
|
||||
fixedWechatIds: res.fixedWechatIds || [],
|
||||
fixedWechatIdsOptions: res.fixedWechatIdsOptions || [],
|
||||
groupAdminEnabled: res.groupAdminEnabled ?? false,
|
||||
groupAdminWechatId: res.groupAdminWechatId,
|
||||
groupAdminWechatIdOption: res.groupAdminWechatIdOption,
|
||||
groupName: res.groupName ?? "",
|
||||
groupMethod: res.groupMethod ?? 0,
|
||||
maxGroupsPerDay: res.maxGroupsPerDay ?? 10,
|
||||
groupSizeMin: res.groupSizeMin ?? 3,
|
||||
groupSizeMax: res.groupSizeMax ?? 38,
|
||||
executeType: res.executeType ?? 1,
|
||||
executeTime: res.executeTime ?? "09:00",
|
||||
executeEndTime: res.executeEndTime ?? "18:00",
|
||||
poolGroups: res.poolGroups || [],
|
||||
poolGroupsOptions: res.poolGroupsOptions || [],
|
||||
executorId: config.executorId,
|
||||
executor: config.deviceGroupsOptions?.[0], // executor 使用第一个设备(如果需要)
|
||||
deviceGroupsOptions: config.deviceGroupsOptions || [],
|
||||
deviceGroups: deviceGroups,
|
||||
wechatGroups: wechatGroups,
|
||||
wechatGroupsOptions: config.wechatGroupsOptions || config.wechatFriendsOptions || [],
|
||||
groupAdminEnabled: config.groupAdminEnabled === 1,
|
||||
groupAdminWechatId: config.groupAdminWechatId || undefined,
|
||||
groupAdminWechatIdOption: groupAdminWechatIdOption,
|
||||
groupNameTemplate: config.groupNameTemplate || "",
|
||||
maxGroupsPerDay: config.maxGroupsPerDay ?? 20,
|
||||
groupSizeMin: config.groupSizeMin ?? 3,
|
||||
groupSizeMax: config.groupSizeMax ?? 38,
|
||||
startTime: config.startTime || "09:00",
|
||||
endTime: config.endTime || "21:00",
|
||||
poolGroups: poolGroups,
|
||||
poolGroupsOptions: config.poolGroupsOptions || [],
|
||||
status: res.status ?? 1,
|
||||
};
|
||||
setFormData(updatedForm);
|
||||
@@ -163,9 +172,7 @@ const GroupCreateForm: React.FC = () => {
|
||||
await createGroupCreate(submitData);
|
||||
Toast.show({ content: "创建成功" });
|
||||
}
|
||||
// 注意:需要导航到列表页面,但目前列表页面还未创建
|
||||
// navigate("/workspace/group-create");
|
||||
navigate("/workspace");
|
||||
navigate("/workspace/group-create");
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "提交失败" });
|
||||
} finally {
|
||||
@@ -194,13 +201,13 @@ const GroupCreateForm: React.FC = () => {
|
||||
return (
|
||||
<DeviceSelectionStep
|
||||
ref={deviceSelectionStepRef}
|
||||
selectedDevices={formData.selectedDevices || []}
|
||||
selectedDeviceIds={formData.selectedDeviceIds || []}
|
||||
deviceGroupsOptions={formData.deviceGroupsOptions || []}
|
||||
deviceGroups={formData.deviceGroups || []}
|
||||
onSelect={(devices) => {
|
||||
const deviceIds = devices.map(d => d.id);
|
||||
handleFormDataChange({
|
||||
selectedDevices: devices,
|
||||
selectedDeviceIds: deviceIds,
|
||||
deviceGroupsOptions: devices,
|
||||
deviceGroups: deviceIds,
|
||||
// 如果只有一个设备,也设置 executor 和 executorId 用于兼容
|
||||
executor: devices.length === 1 ? devices[0] : undefined,
|
||||
executorId: devices.length === 1 ? devices[0].id : undefined,
|
||||
|
||||
@@ -9,21 +9,19 @@ export interface GroupCreateFormData {
|
||||
name: string; // 计划名称
|
||||
executor?: DeviceSelectionItem; // 执行智能体(执行者)- 单个设备(保留用于兼容)
|
||||
executorId?: number; // 执行智能体ID(设备ID)(保留用于兼容)
|
||||
selectedDevices?: DeviceSelectionItem[]; // 选中的设备列表
|
||||
selectedDeviceIds?: number[]; // 选中的设备ID列表
|
||||
fixedWechatIds: number[]; // 固定微信号ID列表(必须3个)
|
||||
fixedWechatIdsOptions: FriendSelectionItem[]; // 固定微信号选项
|
||||
deviceGroupsOptions?: DeviceSelectionItem[]; // 选中的设备列表
|
||||
deviceGroups?: number[]; // 选中的设备ID列表
|
||||
wechatGroups: number[]; // 固定微信号ID列表(必须3个)
|
||||
wechatGroupsOptions: FriendSelectionItem[]; // 固定微信号选项
|
||||
groupAdminEnabled: boolean; // 群管理员开关
|
||||
groupAdminWechatId?: number; // 群管理员微信号ID
|
||||
groupAdminWechatIdOption?: FriendSelectionItem; // 群管理员微信号选项
|
||||
groupName: string; // 群名称
|
||||
groupMethod: number; // 分组方式:0-所有好友自动分组,1-指定群数量
|
||||
maxGroupsPerDay: number; // 每日最大建群数(当groupMethod为1时使用)
|
||||
groupNameTemplate: string; // 群名称模板
|
||||
maxGroupsPerDay: number; // 每日最大建群数
|
||||
groupSizeMin: number; // 群组最小人数
|
||||
groupSizeMax: number; // 群组最大人数
|
||||
executeType: number; // 执行类型:0-立即执行,1-定时执行
|
||||
executeTime?: string; // 执行开始时间(HH:mm),默认 09:00
|
||||
executeEndTime?: string; // 执行结束时间(HH:mm),默认 18:00
|
||||
startTime?: string; // 执行开始时间(HH:mm),默认 09:00
|
||||
endTime?: string; // 执行结束时间(HH:mm),默认 21:00
|
||||
poolGroups?: string[]; // 流量池ID列表
|
||||
poolGroupsOptions?: PoolSelectionItem[]; // 流量池选项列表
|
||||
status: number; // 是否启用 (1: 启用, 0: 禁用)
|
||||
@@ -44,7 +42,7 @@ export const formValidationRules = {
|
||||
{ min: 2, max: 50, message: "计划名称长度应在2-50个字符之间" },
|
||||
],
|
||||
executorId: [{ required: true, message: "请选择执行智能体" }],
|
||||
fixedWechatIds: [
|
||||
wechatGroups: [
|
||||
{ required: true, message: "请选择固定微信号" },
|
||||
{
|
||||
validator: (_: any, value: number[]) => {
|
||||
@@ -55,9 +53,9 @@ export const formValidationRules = {
|
||||
},
|
||||
},
|
||||
],
|
||||
groupName: [
|
||||
{ required: true, message: "请输入群名称" },
|
||||
{ min: 2, max: 100, message: "群名称长度应在2-100个字符之间" },
|
||||
groupNameTemplate: [
|
||||
{ required: true, message: "请输入群名称模板" },
|
||||
{ min: 2, max: 100, message: "群名称模板长度应在2-100个字符之间" },
|
||||
],
|
||||
maxGroupsPerDay: [
|
||||
{ required: true, message: "请输入每日最大建群数" },
|
||||
@@ -76,28 +74,4 @@ export const formValidationRules = {
|
||||
{ required: true, message: "请输入群组最大人数" },
|
||||
{ type: "number", min: 1, max: 500, message: "群组最大人数应在1-500之间" },
|
||||
],
|
||||
executeDate: [
|
||||
{
|
||||
validator: (_: any, value: string, callback: any) => {
|
||||
// 如果执行类型是定时执行,则必填
|
||||
const executeType = callback?.executeType ?? 1;
|
||||
if (executeType === 1 && !value) {
|
||||
return Promise.reject(new Error("请选择执行日期"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
executeTime: [
|
||||
{
|
||||
validator: (_: any, value: string, callback: any) => {
|
||||
// 如果执行类型是定时执行,则必填
|
||||
const executeType = callback?.executeType ?? 1;
|
||||
if (executeType === 1 && !value) {
|
||||
return Promise.reject(new Error("请选择执行时间"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
padding-bottom: 100px;
|
||||
background: #f8fafc;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
.infoBox {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: #2563eb;
|
||||
font-size: 20px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.infoText {
|
||||
font-size: 13px;
|
||||
color: #1e40af;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.sectionDot {
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
background: #2563eb;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 8px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.sectionTitleIndependent {
|
||||
.sectionDot {
|
||||
background: #fb923c;
|
||||
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.sectionDotIndependent {
|
||||
background: #fb923c;
|
||||
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
|
||||
}
|
||||
|
||||
.planList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.planCard {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: linear-gradient(to bottom right, rgba(239, 246, 255, 0.5), transparent);
|
||||
border-radius: 0 0 0 80px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cardTitleSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.statusRunning {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border-color: rgba(22, 101, 52, 0.1);
|
||||
}
|
||||
|
||||
.statusStopped {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
border-color: rgba(100, 116, 139, 0.1);
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statusSwitch {
|
||||
--checked-color: #2563eb;
|
||||
}
|
||||
|
||||
.moreButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardStats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 4px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.statIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statNumber {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
font-family: "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
.statDivider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.cardDetails {
|
||||
border-top: 1px solid #f1f5f9;
|
||||
padding-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.detailIcon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.detailTime {
|
||||
margin-left: auto;
|
||||
font-weight: 500;
|
||||
font-family: monospace;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.emptyButton {
|
||||
min-width: 140px;
|
||||
}
|
||||
344
Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx
Normal file
344
Cunkebao/src/pages/mobile/workspace/group-create/list/index.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Toast, Switch, Dialog } from "antd-mobile";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import { MoreOutlined, EditOutlined, DeleteOutlined, PlusOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getGroupCreateList, toggleGroupCreateStatus, deleteGroupCreate } from "../form/api";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface GroupCreatePlan {
|
||||
id: string;
|
||||
name: string;
|
||||
planType: number; // 0-全局计划, 1-独立计划
|
||||
status: number; // 1-启用, 0-禁用
|
||||
groupNameTemplate?: string;
|
||||
groupSizeMax?: number;
|
||||
groupSizeMin?: number;
|
||||
updateTime?: string;
|
||||
createTime?: string;
|
||||
createdGroupsCount?: number; // 已建群数
|
||||
totalMembersCount?: number; // 总人数
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const GroupCreateList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [plans, setPlans] = useState<GroupCreatePlan[]>([]);
|
||||
const [menuLoadingId, setMenuLoadingId] = useState<string | null>(null);
|
||||
|
||||
// 获取列表数据
|
||||
const fetchList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getGroupCreateList({ type: 4 });
|
||||
const list = res?.list || res?.data?.list || res?.data || [];
|
||||
const normalized: GroupCreatePlan[] = (list as any[]).map((item: any) => {
|
||||
const stats = item.config?.stats || {};
|
||||
return {
|
||||
id: String(item.id),
|
||||
name: item.name || "",
|
||||
planType: item.config?.planType ?? item.planType ?? 1,
|
||||
status: item.status === 1 ? 1 : 0,
|
||||
groupNameTemplate: item.config?.groupNameTemplate || item.groupNameTemplate || item.groupName || "",
|
||||
groupSizeMax: item.config?.groupSizeMax || item.groupSizeMax || 38,
|
||||
groupSizeMin: item.config?.groupSizeMin || item.groupSizeMin || 3,
|
||||
updateTime: item.updateTime || item.createTime || "",
|
||||
createTime: item.createTime || "",
|
||||
createdGroupsCount: stats.createdGroupsCount ?? item.createdGroupsCount ?? 0,
|
||||
totalMembersCount: stats.totalMembersCount ?? item.totalMembersCount ?? 0,
|
||||
};
|
||||
});
|
||||
setPlans(normalized);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "获取列表失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, []);
|
||||
|
||||
// 切换状态
|
||||
const handleStatusToggle = async (plan: GroupCreatePlan) => {
|
||||
try {
|
||||
const newStatus = plan.status === 1 ? 0 : 1;
|
||||
await toggleGroupCreateStatus({
|
||||
id: plan.id,
|
||||
status: newStatus,
|
||||
});
|
||||
Toast.show({ content: newStatus === 1 ? "已启用" : "已停止", position: "top" });
|
||||
// 直接更新本地状态
|
||||
setPlans(prev =>
|
||||
prev.map(p => (p.id === plan.id ? { ...p, status: newStatus } : p))
|
||||
);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "操作失败", position: "top" });
|
||||
}
|
||||
};
|
||||
|
||||
// 删除计划
|
||||
const handleDelete = async (plan: GroupCreatePlan) => {
|
||||
const result = await Dialog.confirm({
|
||||
content: "确定要删除该计划吗?",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
setMenuLoadingId(plan.id);
|
||||
try {
|
||||
await deleteGroupCreate(plan.id);
|
||||
Toast.show({ content: "删除成功", position: "top" });
|
||||
fetchList();
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "删除失败", position: "top" });
|
||||
} finally {
|
||||
setMenuLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 菜单点击
|
||||
const handleMenuClick = ({ key }: { key: string }, plan: GroupCreatePlan) => {
|
||||
if (key === "detail") {
|
||||
navigate(`/workspace/group-create/${plan.id}`);
|
||||
} else if (key === "edit") {
|
||||
navigate(`/workspace/group-create/${plan.id}/edit`);
|
||||
} else if (key === "delete") {
|
||||
handleDelete(plan);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新计划
|
||||
const handleCreate = () => {
|
||||
navigate("/workspace/group-create/new");
|
||||
};
|
||||
|
||||
// 分隔全局计划和独立计划
|
||||
const globalPlans = plans.filter(p => p.planType === 0);
|
||||
const independentPlans = plans.filter(p => p.planType === 1);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="自动建群"
|
||||
right={
|
||||
<div style={{ marginRight: "-16px" }}>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleCreate}
|
||||
>
|
||||
<PlusOutlined /> 新建任务
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
loading={loading}
|
||||
>
|
||||
<div className={style.container}>
|
||||
{/* 全局计划提示 */}
|
||||
{globalPlans.length > 0 && (
|
||||
<div className={style.infoBox}>
|
||||
<span className={style.infoIcon}>ℹ</span>
|
||||
<p className={style.infoText}>
|
||||
全局建群计划将应用与所有设备,包含新添加的设备,请确保设置合理的规则。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 全局建群计划 */}
|
||||
{globalPlans.length > 0 && (
|
||||
<section className={style.section}>
|
||||
<h2 className={style.sectionTitle}>
|
||||
<div className={style.sectionDot}></div>
|
||||
全局建群计划
|
||||
</h2>
|
||||
{globalPlans.map(plan => (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onMenuClick={handleMenuClick}
|
||||
menuLoading={menuLoadingId === plan.id}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 独立建群计划 */}
|
||||
{independentPlans.length > 0 && (
|
||||
<section className={style.section}>
|
||||
<h2 className={`${style.sectionTitle} ${style.sectionTitleIndependent}`}>
|
||||
<div className={`${style.sectionDot} ${style.sectionDotIndependent}`}></div>
|
||||
独立建群计划
|
||||
</h2>
|
||||
<div className={style.planList}>
|
||||
{independentPlans.map(plan => (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
onStatusToggle={handleStatusToggle}
|
||||
onMenuClick={handleMenuClick}
|
||||
menuLoading={menuLoadingId === plan.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{plans.length === 0 && !loading && (
|
||||
<div className={style.emptyState}>
|
||||
<div className={style.emptyIcon}>📋</div>
|
||||
<div className={style.emptyText}>暂无建群计划</div>
|
||||
<Button color="primary" onClick={handleCreate} className={style.emptyButton}>
|
||||
创建第一个计划
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
// 计划卡片组件
|
||||
interface PlanCardProps {
|
||||
plan: GroupCreatePlan;
|
||||
onStatusToggle: (plan: GroupCreatePlan) => void;
|
||||
onMenuClick: (params: { key: string }, plan: GroupCreatePlan) => void;
|
||||
menuLoading: boolean;
|
||||
}
|
||||
|
||||
const PlanCard: React.FC<PlanCardProps> = ({
|
||||
plan,
|
||||
onStatusToggle,
|
||||
onMenuClick,
|
||||
menuLoading,
|
||||
}) => {
|
||||
const isRunning = plan.status === 1;
|
||||
|
||||
return (
|
||||
<div className={style.planCard}>
|
||||
{/* 卡片头部 */}
|
||||
<div className={style.cardHeader}>
|
||||
<div className={style.cardTitleSection}>
|
||||
<h3 className={style.cardTitle}>{plan.name}</h3>
|
||||
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
|
||||
{isRunning ? "运行中" : "已停止"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.cardActions}>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onChange={() => onStatusToggle(plan)}
|
||||
className={style.statusSwitch}
|
||||
/>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "detail",
|
||||
icon: <EyeOutlined />,
|
||||
label: "计划详情",
|
||||
disabled: menuLoading,
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
icon: <EditOutlined />,
|
||||
label: "编辑",
|
||||
disabled: menuLoading,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "删除",
|
||||
disabled: menuLoading,
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
onClick: (params) => onMenuClick(params, plan),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
disabled={menuLoading}
|
||||
>
|
||||
<button className={style.moreButton}>
|
||||
<MoreOutlined />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className={style.cardStats}>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statHeader}>
|
||||
<span className={style.statIcon}>👥</span>
|
||||
<p className={style.statLabel}>已建群数</p>
|
||||
</div>
|
||||
<div className={style.statValue}>
|
||||
<span className={style.statNumber}>{plan.createdGroupsCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.statDivider}></div>
|
||||
<div className={style.statItem}>
|
||||
<div className={style.statHeader}>
|
||||
<span className={style.statIcon}>👥</span>
|
||||
<p className={style.statLabel}>总人数</p>
|
||||
</div>
|
||||
<div className={style.statValue}>
|
||||
<span className={style.statNumber}>{plan.totalMembersCount || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细信息 */}
|
||||
<div className={style.cardDetails}>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>📝</span>
|
||||
<span className={style.detailLabel}>群名称</span>
|
||||
<span className={style.detailValue}>{plan.groupNameTemplate || "-"}</span>
|
||||
</div>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>⚙️</span>
|
||||
<span className={style.detailLabel}>群规模</span>
|
||||
<span className={style.detailValue}>
|
||||
{plan.groupSizeMax || 38}人/群
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.detailItem}>
|
||||
<span className={style.detailIcon}>🕐</span>
|
||||
<span className={style.detailLabel}>更新时间</span>
|
||||
<span className={style.detailTime}>
|
||||
{plan.updateTime
|
||||
? new Date(plan.updateTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).replace(/\//g, "-")
|
||||
: plan.createTime
|
||||
? new Date(plan.createTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).replace(/\//g, "-")
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupCreateList;
|
||||
@@ -62,6 +62,17 @@ const Workspace: React.FC = () => {
|
||||
path: "/workspace/auto-group",
|
||||
bgColor: "#f6ffed",
|
||||
},
|
||||
{
|
||||
id: "group-create",
|
||||
name: "自动建群(新版)",
|
||||
description: "智能拉好友建群",
|
||||
icon: (
|
||||
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
|
||||
),
|
||||
path: "/workspace/group-create",
|
||||
bgColor: "#f6ffed",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "traffic-distribution",
|
||||
name: "流量分发",
|
||||
|
||||
@@ -52,13 +52,13 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
|
||||
}
|
||||
|
||||
return {
|
||||
id: pool.id || pool.poolId,
|
||||
name: pool.name || pool.poolName || `流量池${pool.id}`,
|
||||
description: pool.description || pool.desc || "",
|
||||
id: pool.id || pool.poolId,
|
||||
name: pool.name || pool.poolName || `流量池${pool.id}`,
|
||||
description: pool.description || pool.desc || "",
|
||||
userCount: pool.num || pool.userCount || pool.count || 0,
|
||||
tags: pool.tags || [],
|
||||
tags: pool.tags || [],
|
||||
createdAt: createdAt,
|
||||
deviceIds: pool.deviceIds || [],
|
||||
deviceIds: pool.deviceIds || [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -40,8 +40,12 @@ export const routeGroups = {
|
||||
"/workspace/auto-like/:id/edit",
|
||||
"/workspace/auto-group",
|
||||
"/workspace/auto-group/:id",
|
||||
"/workspace/group-create",
|
||||
"/workspace/group-create/new",
|
||||
"/workspace/group-create/:id",
|
||||
"/workspace/group-create/:id/groups",
|
||||
"/workspace/group-create/:id/groups/:groupId",
|
||||
"/workspace/group-create/:id/edit",
|
||||
"/workspace/group-push",
|
||||
"/workspace/group-push/new",
|
||||
"/workspace/group-push/:id",
|
||||
|
||||
@@ -5,7 +5,11 @@ import RecordAutoLike from "@/pages/mobile/workspace/auto-like/record";
|
||||
import AutoGroupList from "@/pages/mobile/workspace/auto-group/list";
|
||||
import AutoGroupDetail from "@/pages/mobile/workspace/auto-group/detail";
|
||||
import AutoGroupForm from "@/pages/mobile/workspace/auto-group/form";
|
||||
import GroupCreateList from "@/pages/mobile/workspace/group-create/list";
|
||||
import GroupCreateForm from "@/pages/mobile/workspace/group-create/form";
|
||||
import GroupCreateDetail from "@/pages/mobile/workspace/group-create/detail";
|
||||
import GroupListPage from "@/pages/mobile/workspace/group-create/detail/groups-list";
|
||||
import GroupDetailPage from "@/pages/mobile/workspace/group-create/detail/group-detail";
|
||||
import GroupPush from "@/pages/mobile/workspace/group-push/list";
|
||||
import FormGroupPush from "@/pages/mobile/workspace/group-push/form";
|
||||
import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail";
|
||||
@@ -76,6 +80,11 @@ const workspaceRoutes = [
|
||||
auth: true,
|
||||
},
|
||||
// 自动建群(新版)
|
||||
{
|
||||
path: "/workspace/group-create",
|
||||
element: <GroupCreateList />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/new",
|
||||
element: <GroupCreateForm />,
|
||||
@@ -83,6 +92,21 @@ const workspaceRoutes = [
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id",
|
||||
element: <GroupCreateDetail />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/groups",
|
||||
element: <GroupListPage />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/groups/:groupId",
|
||||
element: <GroupDetailPage />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/workspace/group-create/:id/edit",
|
||||
element: <GroupCreateForm />,
|
||||
auth: true,
|
||||
},
|
||||
|
||||
@@ -1020,7 +1020,6 @@ class WebSocketController extends BaseController
|
||||
// "wechatFriendIds" => $data['wechatFriendIds']
|
||||
"wechatFriendIds" => [17453051,17453058]
|
||||
];
|
||||
|
||||
$message = $this->sendMessage($params,false);
|
||||
return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
|
||||
} catch (\Exception $e) {
|
||||
@@ -1116,6 +1115,7 @@ class WebSocketController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
//chatroomOperateType 4退群 6群公告 5群名称
|
||||
$params = [
|
||||
"chatroomOperateType" => !empty($data['chatroomName']) ? 6 : 5,
|
||||
"cmdType" => "CmdChatroomOperate",
|
||||
@@ -1125,7 +1125,7 @@ class WebSocketController extends BaseController
|
||||
"wechatChatroomId" => $data['wechatChatroomId']
|
||||
];
|
||||
$message = $this->sendMessage($params,false);
|
||||
return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
|
||||
return json_encode(['code' => 200, 'msg' => '修改群信息成功', 'data' => $message]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('修改群信息异常: ' . $e->getMessage());
|
||||
return json_encode(['code' => 500, 'msg' => '修改群信息失败: ' . $e->getMessage()]);
|
||||
|
||||
@@ -39,7 +39,7 @@ class WechatChatroomController extends BaseController
|
||||
'groupId' => $data['groupId'] ?? '',
|
||||
'wechatChatroomId' => $data['wechatChatroomId'] ?? '',
|
||||
'memberKeyword' => $data['memberKeyword'] ?? '',
|
||||
'pageIndex' => $data['pageIndex'] ?? 1,
|
||||
'pageIndex' => $data['pageIndex'] ?? 0,
|
||||
'pageSize' => $data['pageSize'] ?? 20
|
||||
];
|
||||
|
||||
|
||||
@@ -93,27 +93,32 @@ Route::group('v1/', function () {
|
||||
|
||||
// 工作台相关
|
||||
Route::group('workbench', function () {
|
||||
Route::post('create', 'app\cunkebao\controller\WorkbenchController@create'); // 创建工作台
|
||||
Route::get('list', 'app\cunkebao\controller\WorkbenchController@getList'); // 获取工作台列表
|
||||
Route::post('update-status', 'app\cunkebao\controller\WorkbenchController@updateStatus'); // 更新工作台状态
|
||||
Route::delete('delete', 'app\cunkebao\controller\WorkbenchController@delete'); // 删除工作台
|
||||
Route::post('copy', 'app\cunkebao\controller\WorkbenchController@copy'); // 拷贝工作台
|
||||
Route::get('detail', 'app\cunkebao\controller\WorkbenchController@detail'); // 获取工作台详情
|
||||
Route::post('update', 'app\cunkebao\controller\WorkbenchController@update'); // 更新工作台
|
||||
Route::get('like-records', 'app\cunkebao\controller\WorkbenchController@getLikeRecords'); // 获取点赞记录列表
|
||||
Route::get('moments-records', 'app\cunkebao\controller\WorkbenchController@getMomentsRecords'); // 获取朋友圈发布记录列表
|
||||
Route::get('device-labels', 'app\cunkebao\controller\WorkbenchController@getDeviceLabels'); // 获取设备微信好友标签统计
|
||||
Route::get('group-list', 'app\cunkebao\controller\WorkbenchController@getGroupList'); // 获取群列表
|
||||
Route::get('account-list', 'app\cunkebao\controller\WorkbenchController@getAccountList'); // 获取账号列表
|
||||
Route::get('transfer-friends', 'app\cunkebao\controller\WorkbenchController@getTrafficList'); // 获取账号列表
|
||||
Route::get('import-contact', 'app\cunkebao\controller\WorkbenchController@getImportContact'); // 获取通讯录导入记录列表
|
||||
Route::post('create', 'app\cunkebao\controller\workbench\WorkbenchController@create'); // 创建工作台
|
||||
Route::get('list', 'app\cunkebao\controller\workbench\WorkbenchController@getList'); // 获取工作台列表
|
||||
Route::post('update-status', 'app\cunkebao\controller\workbench\WorkbenchController@updateStatus'); // 更新工作台状态
|
||||
Route::delete('delete', 'app\cunkebao\controller\workbench\WorkbenchController@delete'); // 删除工作台
|
||||
Route::post('copy', 'app\cunkebao\controller\workbench\WorkbenchController@copy'); // 拷贝工作台
|
||||
Route::get('detail', 'app\cunkebao\controller\workbench\WorkbenchController@detail'); // 获取工作台详情
|
||||
Route::post('update', 'app\cunkebao\controller\workbench\WorkbenchController@update'); // 更新工作台
|
||||
Route::get('like-records', 'app\cunkebao\controller\workbench\WorkbenchController@getLikeRecords'); // 获取点赞记录列表
|
||||
Route::get('moments-records', 'app\cunkebao\controller\workbench\WorkbenchController@getMomentsRecords'); // 获取朋友圈发布记录列表
|
||||
Route::get('device-labels', 'app\cunkebao\controller\workbench\WorkbenchController@getDeviceLabels'); // 获取设备微信好友标签统计
|
||||
Route::get('group-list', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupList'); // 获取群列表
|
||||
Route::get('created-groups-list', 'app\cunkebao\controller\workbench\WorkbenchController@getCreatedGroupsList'); // 获取已创建的群列表(自动建群)
|
||||
Route::get('created-group-detail', 'app\cunkebao\controller\workbench\WorkbenchController@getCreatedGroupDetail'); // 获取已创建群的详情(自动建群)
|
||||
Route::post('sync-group-info', 'app\cunkebao\controller\workbench\WorkbenchController@syncGroupInfo'); // 同步群最新信息(包括群成员)
|
||||
Route::post('modify-group-info', 'app\cunkebao\controller\workbench\WorkbenchController@modifyGroupInfo'); // 修改群名称、群公告
|
||||
Route::post('quit-group', 'app\cunkebao\controller\workbench\WorkbenchController@quitGroup'); // 退群(自动建群)
|
||||
Route::get('account-list', 'app\cunkebao\controller\workbench\WorkbenchController@getAccountList'); // 获取账号列表
|
||||
Route::get('transfer-friends', 'app\cunkebao\controller\workbench\WorkbenchController@getTrafficList'); // 获取账号列表
|
||||
Route::get('import-contact', 'app\cunkebao\controller\workbench\WorkbenchController@getImportContact'); // 获取通讯录导入记录列表
|
||||
|
||||
Route::get('getJdSocialMedia', 'app\cunkebao\controller\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体
|
||||
Route::get('getJdPromotionSite', 'app\cunkebao\controller\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位
|
||||
Route::get('changeLink', 'app\cunkebao\controller\WorkbenchController@changeLink'); // 获取京东联盟广告位
|
||||
Route::get('getJdSocialMedia', 'app\cunkebao\controller\workbench\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体
|
||||
Route::get('getJdPromotionSite', 'app\cunkebao\controller\workbench\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位
|
||||
Route::get('changeLink', 'app\cunkebao\controller\workbench\WorkbenchController@changeLink'); // 获取京东联盟广告位
|
||||
|
||||
Route::get('group-push-stats', 'app\cunkebao\controller\WorkbenchController@getGroupPushStats'); // 获取群发统计数据
|
||||
Route::get('group-push-history', 'app\cunkebao\controller\WorkbenchController@getGroupPushHistory'); // 获取推送历史记录列表
|
||||
Route::get('group-push-stats', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupPushStats'); // 获取群发统计数据
|
||||
Route::get('group-push-history', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupPushHistory'); // 获取推送历史记录列表
|
||||
});
|
||||
|
||||
// 内容库相关
|
||||
|
||||
@@ -643,7 +643,7 @@ class ContentLibraryController extends Controller
|
||||
// 查询数据
|
||||
$list = ContentItem::where($where)
|
||||
->field('id,libraryId,type,title,content,contentAi,contentType,resUrls,urls,friendId,wechatId,wechatChatroomId,createTime,createMomentTime,createMessageTime,coverImage,ossUrls')
|
||||
->order('createTime DESC,createMomentTime DESC')
|
||||
->order('createMomentTime DESC,createMessageTime DESC,createTime DESC')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
|
||||
@@ -782,8 +782,8 @@ class ChannelController extends BaseController
|
||||
if (!empty($channelIds)) {
|
||||
// 构建提现查询条件
|
||||
$withdrawalWhere = [
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', 'in', $channelIds]
|
||||
['companyId', '=', $companyId],
|
||||
['channelId', 'in', $channelIds]
|
||||
];
|
||||
|
||||
// 如果不是管理员,只能查看自己创建的提现申请
|
||||
@@ -817,7 +817,7 @@ class ChannelController extends BaseController
|
||||
|
||||
// totalRevenue 不包括驳回(rejected)状态的金额
|
||||
if ($status !== DistributionWithdrawal::STATUS_REJECTED) {
|
||||
$withdrawalStats[$cid]['totalRevenue'] += $amount;
|
||||
$withdrawalStats[$cid]['totalRevenue'] += $amount;
|
||||
}
|
||||
|
||||
if ($status === DistributionWithdrawal::STATUS_PAID) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\workbench;
|
||||
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 工作台 - 自动点赞相关功能
|
||||
*/
|
||||
class WorkbenchAutoLikeController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取点赞记录列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getLikeRecords()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
|
||||
$where = [
|
||||
['wali.workbenchId', '=', $workbenchId]
|
||||
];
|
||||
|
||||
// 查询点赞记录
|
||||
$list = Db::name('workbench_auto_like_item')->alias('wali')
|
||||
->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId')
|
||||
->field([
|
||||
'wali.id',
|
||||
'wali.workbenchId',
|
||||
'wali.momentsId',
|
||||
'wali.snsId',
|
||||
'wali.wechatAccountId',
|
||||
'wali.wechatFriendId',
|
||||
'wali.createTime as likeTime',
|
||||
'wm.content',
|
||||
'wm.resUrls',
|
||||
'wm.createTime as momentTime',
|
||||
'wm.userName',
|
||||
])
|
||||
->where($where)
|
||||
->order('wali.createTime', 'desc')
|
||||
->group('wali.id')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
|
||||
// 处理数据
|
||||
foreach ($list as &$item) {
|
||||
//处理用户信息
|
||||
$friend = Db::table('s2_wechat_friend')
|
||||
->where(['id' => $item['wechatFriendId']])
|
||||
->field('nickName,avatar')
|
||||
->find();
|
||||
if (!empty($friend)) {
|
||||
$item['friendName'] = $friend['nickName'];
|
||||
$item['friendAvatar'] = $friend['avatar'];
|
||||
} else {
|
||||
$item['friendName'] = '';
|
||||
$item['friendAvatar'] = '';
|
||||
}
|
||||
|
||||
|
||||
//处理客服
|
||||
$friend = Db::table('s2_wechat_account')
|
||||
->where(['id' => $item['wechatAccountId']])
|
||||
->field('nickName,avatar')
|
||||
->find();
|
||||
if (!empty($friend)) {
|
||||
$item['operatorName'] = $friend['nickName'];
|
||||
$item['operatorAvatar'] = $friend['avatar'];
|
||||
} else {
|
||||
$item['operatorName'] = '';
|
||||
$item['operatorAvatar'] = '';
|
||||
}
|
||||
|
||||
// 处理时间格式
|
||||
$item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']);
|
||||
$item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : '';
|
||||
|
||||
// 处理资源链接
|
||||
if (!empty($item['resUrls'])) {
|
||||
$item['resUrls'] = json_decode($item['resUrls'], true);
|
||||
} else {
|
||||
$item['resUrls'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取总记录数
|
||||
$total = Db::name('workbench_auto_like_item')->alias('wali')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\workbench;
|
||||
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
use think\facade\Env;
|
||||
|
||||
/**
|
||||
* 工作台 - 辅助功能
|
||||
*/
|
||||
class WorkbenchHelperController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取所有微信好友标签及数量统计
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getDeviceLabels()
|
||||
{
|
||||
$deviceIds = $this->request->param('deviceIds', '');
|
||||
$companyId = $this->request->userInfo['companyId'];
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
|
||||
$where = [
|
||||
['wc.companyId', '=', $companyId],
|
||||
];
|
||||
|
||||
if (!empty($deviceIds)) {
|
||||
$deviceIds = explode(',', $deviceIds);
|
||||
$where[] = ['dwl.deviceId', 'in', $deviceIds];
|
||||
}
|
||||
|
||||
$wechatAccounts = Db::name('wechat_customer')->alias('wc')
|
||||
->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId')
|
||||
->where($where)
|
||||
->field('wa.id,wa.wechatId,wa.nickName,wa.labels')
|
||||
->select();
|
||||
$labels = [];
|
||||
$wechatIds = [];
|
||||
foreach ($wechatAccounts as $account) {
|
||||
$labelArr = json_decode($account['labels'], true);
|
||||
if (is_array($labelArr)) {
|
||||
foreach ($labelArr as $label) {
|
||||
if ($label !== '' && $label !== null) {
|
||||
$labels[] = $label;
|
||||
}
|
||||
}
|
||||
}
|
||||
$wechatIds[] = $account['wechatId'];
|
||||
}
|
||||
// 去重(只保留一个)
|
||||
$labels = array_values(array_unique($labels));
|
||||
$wechatIds = array_unique($wechatIds);
|
||||
|
||||
// 搜索过滤
|
||||
if (!empty($keyword)) {
|
||||
$labels = array_filter($labels, function ($label) use ($keyword) {
|
||||
return mb_stripos($label, $keyword) !== false;
|
||||
});
|
||||
$labels = array_values($labels); // 重新索引数组
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
$labels2 = array_slice($labels, ($page - 1) * $limit, $limit);
|
||||
|
||||
// 统计数量
|
||||
$newLabel = [];
|
||||
foreach ($labels2 as $label) {
|
||||
$friendCount = Db::table('s2_wechat_friend')
|
||||
->whereIn('ownerWechatId', $wechatIds)
|
||||
->where('labels', 'like', '%"' . $label . '"%')
|
||||
->count();
|
||||
$newLabel[] = [
|
||||
'label' => $label,
|
||||
'count' => $friendCount
|
||||
];
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $newLabel,
|
||||
'total' => count($labels),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getGroupList()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
|
||||
$where = [
|
||||
['wg.deleteTime', '=', 0],
|
||||
['wg.companyId', '=', $this->request->userInfo['companyId']],
|
||||
];
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$where[] = ['wg.name', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
$query = Db::name('wechat_group')->alias('wg')
|
||||
->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
|
||||
->where($where);
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->order('wg.id', 'desc')
|
||||
->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 优化:格式化时间,头像兜底
|
||||
$defaultGroupAvatar = '';
|
||||
$defaultAvatar = '';
|
||||
foreach ($list as &$item) {
|
||||
$item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
|
||||
$item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar;
|
||||
$item['avatar'] = $item['avatar'] ?: $defaultAvatar;
|
||||
}
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量池列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTrafficPoolList()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$companyId = $this->request->userInfo['companyId'];
|
||||
|
||||
$baseQuery = Db::name('traffic_source_package')->alias('tsp')
|
||||
->where('tsp.isDel', 0)
|
||||
->whereIn('tsp.companyId', [$companyId, 0]);
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$baseQuery->whereLike('tsp.name', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$total = (clone $baseQuery)->count();
|
||||
|
||||
$list = $baseQuery
|
||||
->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
|
||||
->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime')
|
||||
->group('tsp.id')
|
||||
->order('tsp.id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : '';
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getAccountList()
|
||||
{
|
||||
$companyId = $this->request->userInfo['companyId'];
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$query = Db::table('s2_company_account')
|
||||
->alias('a')
|
||||
->where(['a.departmentId' => $companyId, 'a.status' => 0])
|
||||
->whereNotLike('a.userName', '%_offline%')
|
||||
->whereNotLike('a.userName', '%_delete%');
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取京东联盟导购媒体
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getJdSocialMedia()
|
||||
{
|
||||
$data = Db::name('jd_social_media')->order('id DESC')->select();
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取京东联盟广告位
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getJdPromotionSite()
|
||||
{
|
||||
$id = $this->request->param('id', '');
|
||||
if (empty($id)) {
|
||||
return json(['code' => 500, 'msg' => '参数缺失']);
|
||||
}
|
||||
|
||||
$data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select();
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 京东转链-京推推
|
||||
* @param string $content
|
||||
* @param string $positionid
|
||||
* @return string
|
||||
*/
|
||||
public function changeLink($content = '', $positionid = '')
|
||||
{
|
||||
$unionId = Env::get('jd.unionId', '');
|
||||
$jttAppId = Env::get('jd.jttAppId', '');
|
||||
$appKey = Env::get('jd.appKey', '');
|
||||
$apiUrl = Env::get('jd.apiUrl', '');
|
||||
|
||||
$content = !empty($content) ? $content : $this->request->param('content', '');
|
||||
$positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', '');
|
||||
|
||||
if (empty($content)) {
|
||||
return json_encode(['code' => 500, 'msg' => '转链的内容为空']);
|
||||
}
|
||||
|
||||
// 验证是否包含链接
|
||||
if (!$this->containsLink($content)) {
|
||||
return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']);
|
||||
}
|
||||
|
||||
if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) {
|
||||
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||
}
|
||||
$params = [
|
||||
'unionid' => $unionId,
|
||||
'content' => $content,
|
||||
'appid' => $jttAppId,
|
||||
'appkey' => $appKey,
|
||||
'v' => 'v2'
|
||||
];
|
||||
|
||||
if (!empty($positionid)) {
|
||||
$params['positionid'] = $positionid;
|
||||
}
|
||||
|
||||
$res = requestCurl($apiUrl, $params, 'GET', [], 'json');
|
||||
$res = json_decode($res, true);
|
||||
if (empty($res)) {
|
||||
return json_encode(['code' => 500, 'msg' => '未知错误']);
|
||||
}
|
||||
$result = $res['result'];
|
||||
if ($res['return'] == 0) {
|
||||
return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]);
|
||||
} else {
|
||||
return json_encode(['code' => 500, 'msg' => $result['msg']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证内容是否包含链接
|
||||
* @param string $content 要检测的内容
|
||||
* @return bool
|
||||
*/
|
||||
private function containsLink($content)
|
||||
{
|
||||
// 定义各种链接的正则表达式模式
|
||||
$patterns = [
|
||||
// HTTP/HTTPS链接
|
||||
'/https?:\/\/[^\s]+/i',
|
||||
// 京东商品链接
|
||||
'/item\.jd\.com\/\d+/i',
|
||||
// 京东短链接
|
||||
'/u\.jd\.com\/[a-zA-Z0-9]+/i',
|
||||
// 淘宝商品链接
|
||||
'/item\.taobao\.com\/item\.htm\?id=\d+/i',
|
||||
// 天猫商品链接
|
||||
'/detail\.tmall\.com\/item\.htm\?id=\d+/i',
|
||||
// 淘宝短链接
|
||||
'/m\.tb\.cn\/[a-zA-Z0-9]+/i',
|
||||
// 拼多多链接
|
||||
'/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
|
||||
// 苏宁易购链接
|
||||
'/product\.suning\.com\/\d+\/\d+\.html/i',
|
||||
// 通用域名模式(包含常见电商域名)
|
||||
'/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
|
||||
// 通用短链接模式
|
||||
'/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
|
||||
];
|
||||
|
||||
// 遍历所有模式进行匹配
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $content)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\workbench;
|
||||
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 工作台 - 联系人导入相关功能
|
||||
*/
|
||||
class WorkbenchImportContactController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取通讯录导入记录列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getImportContact()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
|
||||
$where = [
|
||||
['wici.workbenchId', '=', $workbenchId]
|
||||
];
|
||||
|
||||
// 查询发布记录
|
||||
$list = Db::name('workbench_import_contact_item')->alias('wici')
|
||||
->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
|
||||
->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
|
||||
->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
|
||||
->field([
|
||||
'wici.id',
|
||||
'wici.workbenchId',
|
||||
'wici.createTime',
|
||||
'tp.identifier',
|
||||
'tp.mobile',
|
||||
'tp.wechatId',
|
||||
'tc.name',
|
||||
'wa.nickName',
|
||||
'wa.avatar',
|
||||
'wa.alias',
|
||||
])
|
||||
->where($where)
|
||||
->order('tc.name DESC,wici.createTime DESC')
|
||||
->group('tp.identifier')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||
}
|
||||
|
||||
// 获取总记录数
|
||||
$total = Db::name('workbench_import_contact_item')->alias('wici')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\workbench;
|
||||
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 工作台 - 朋友圈同步相关功能
|
||||
*/
|
||||
class WorkbenchMomentsController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取朋友圈发布记录列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getMomentsRecords()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
|
||||
$where = [
|
||||
['wmsi.workbenchId', '=', $workbenchId]
|
||||
];
|
||||
|
||||
// 查询发布记录
|
||||
$list = Db::name('workbench_moments_sync_item')->alias('wmsi')
|
||||
->join('content_item ci', 'ci.id = wmsi.contentId', 'left')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left')
|
||||
->field([
|
||||
'wmsi.id',
|
||||
'wmsi.workbenchId',
|
||||
'wmsi.createTime as publishTime',
|
||||
'ci.contentType',
|
||||
'ci.content',
|
||||
'ci.resUrls',
|
||||
'ci.urls',
|
||||
'wa.nickName as operatorName',
|
||||
'wa.avatar as operatorAvatar'
|
||||
])
|
||||
->where($where)
|
||||
->order('wmsi.createTime', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['resUrls'] = json_decode($item['resUrls'], true);
|
||||
$item['urls'] = json_decode($item['urls'], true);
|
||||
}
|
||||
|
||||
|
||||
// 获取总记录数
|
||||
$total = Db::name('workbench_moments_sync_item')->alias('wmsi')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取朋友圈发布统计
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getMomentsStats()
|
||||
{
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
if (empty($workbenchId)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
// 获取今日数据
|
||||
$todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
|
||||
$todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
|
||||
|
||||
$todayStats = Db::name('workbench_moments_sync_item')
|
||||
->where([
|
||||
['workbenchId', '=', $workbenchId],
|
||||
['createTime', 'between', [$todayStart, $todayEnd]]
|
||||
])
|
||||
->field([
|
||||
'COUNT(*) as total',
|
||||
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
|
||||
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
|
||||
])
|
||||
->find();
|
||||
|
||||
// 获取总数据
|
||||
$totalStats = Db::name('workbench_moments_sync_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->field([
|
||||
'COUNT(*) as total',
|
||||
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
|
||||
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
|
||||
])
|
||||
->find();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'today' => [
|
||||
'total' => intval($todayStats['total']),
|
||||
'success' => intval($todayStats['success']),
|
||||
'failed' => intval($todayStats['failed'])
|
||||
],
|
||||
'total' => [
|
||||
'total' => intval($totalStats['total']),
|
||||
'success' => intval($totalStats['success']),
|
||||
'failed' => intval($totalStats['failed'])
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\workbench;
|
||||
|
||||
use think\Controller;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 工作台 - 流量分发相关功能
|
||||
*/
|
||||
class WorkbenchTrafficController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取流量分发记录列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTrafficDistributionRecords()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
|
||||
$where = [
|
||||
['wtdi.workbenchId', '=', $workbenchId]
|
||||
];
|
||||
|
||||
// 查询分发记录
|
||||
$list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
|
||||
->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
|
||||
->field([
|
||||
'wtdi.id',
|
||||
'wtdi.workbenchId',
|
||||
'wtdi.wechatAccountId',
|
||||
'wtdi.wechatFriendId',
|
||||
'wtdi.createTime as distributeTime',
|
||||
'wtdi.status',
|
||||
'wtdi.errorMsg',
|
||||
'wa.nickName as operatorName',
|
||||
'wa.avatar as operatorAvatar',
|
||||
'wf.nickName as friendName',
|
||||
'wf.avatar as friendAvatar',
|
||||
'wf.gender',
|
||||
'wf.province',
|
||||
'wf.city'
|
||||
])
|
||||
->where($where)
|
||||
->order('wtdi.createTime', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 处理数据
|
||||
foreach ($list as &$item) {
|
||||
// 处理时间格式
|
||||
$item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
|
||||
|
||||
// 处理性别
|
||||
$genderMap = [
|
||||
0 => '未知',
|
||||
1 => '男',
|
||||
2 => '女'
|
||||
];
|
||||
$item['genderText'] = $genderMap[$item['gender']] ?? '未知';
|
||||
|
||||
// 处理状态文字
|
||||
$statusMap = [
|
||||
0 => '待分发',
|
||||
1 => '分发成功',
|
||||
2 => '分发失败'
|
||||
];
|
||||
$item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
|
||||
}
|
||||
|
||||
// 获取总记录数
|
||||
$total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量分发统计
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTrafficDistributionStats()
|
||||
{
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
if (empty($workbenchId)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
// 获取今日数据
|
||||
$todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
|
||||
$todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
|
||||
|
||||
$todayStats = Db::name('workbench_traffic_distribution_item')
|
||||
->where([
|
||||
['workbenchId', '=', $workbenchId],
|
||||
['createTime', 'between', [$todayStart, $todayEnd]]
|
||||
])
|
||||
->field([
|
||||
'COUNT(*) as total',
|
||||
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
|
||||
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
|
||||
])
|
||||
->find();
|
||||
|
||||
// 获取总数据
|
||||
$totalStats = Db::name('workbench_traffic_distribution_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->field([
|
||||
'COUNT(*) as total',
|
||||
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
|
||||
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
|
||||
])
|
||||
->find();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'today' => [
|
||||
'total' => intval($todayStats['total']),
|
||||
'success' => intval($todayStats['success']),
|
||||
'failed' => intval($todayStats['failed'])
|
||||
],
|
||||
'total' => [
|
||||
'total' => intval($totalStats['total']),
|
||||
'success' => intval($totalStats['success']),
|
||||
'failed' => intval($totalStats['failed'])
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量分发详情
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTrafficDistributionDetail()
|
||||
{
|
||||
$id = $this->request->param('id', 0);
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
|
||||
->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
|
||||
->field([
|
||||
'wtdi.id',
|
||||
'wtdi.workbenchId',
|
||||
'wtdi.wechatAccountId',
|
||||
'wtdi.wechatFriendId',
|
||||
'wtdi.createTime as distributeTime',
|
||||
'wtdi.status',
|
||||
'wtdi.errorMsg',
|
||||
'wa.nickName as operatorName',
|
||||
'wa.avatar as operatorAvatar',
|
||||
'wf.nickName as friendName',
|
||||
'wf.avatar as friendAvatar',
|
||||
'wf.gender',
|
||||
'wf.province',
|
||||
'wf.city',
|
||||
'wf.signature',
|
||||
'wf.remark'
|
||||
])
|
||||
->where('wtdi.id', $id)
|
||||
->find();
|
||||
|
||||
if (empty($detail)) {
|
||||
return json(['code' => 404, 'msg' => '记录不存在']);
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
$detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
|
||||
|
||||
// 处理性别
|
||||
$genderMap = [
|
||||
0 => '未知',
|
||||
1 => '男',
|
||||
2 => '女'
|
||||
];
|
||||
$detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
|
||||
|
||||
// 处理状态文字
|
||||
$statusMap = [
|
||||
0 => '待分发',
|
||||
1 => '分发成功',
|
||||
2 => '分发失败'
|
||||
];
|
||||
$detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $detail
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建流量分发计划
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function createTrafficPlan()
|
||||
{
|
||||
$param = $this->request->post();
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 1. 创建主表
|
||||
$planId = Db::name('ck_workbench')->insertGetId([
|
||||
'name' => $param['name'],
|
||||
'type' => 5, // TYPE_TRAFFIC_DISTRIBUTION
|
||||
'status' => 1,
|
||||
'autoStart' => $param['autoStart'] ?? 0,
|
||||
'userId' => $this->request->userInfo['id'],
|
||||
'companyId' => $this->request->userInfo['companyId'],
|
||||
'createTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
// 2. 创建扩展表
|
||||
Db::name('ck_workbench_traffic_config')->insert([
|
||||
'workbenchId' => $planId,
|
||||
'distributeType' => $param['distributeType'],
|
||||
'maxPerDay' => $param['maxPerDay'],
|
||||
'timeType' => $param['timeType'],
|
||||
'startTime' => $param['startTime'],
|
||||
'endTime' => $param['endTime'],
|
||||
'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
|
||||
'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
|
||||
'createTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'msg' => '创建成功']);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTrafficList()
|
||||
{
|
||||
$companyId = $this->request->userInfo['companyId'];
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$workbenchId = $this->request->param('workbenchId', '');
|
||||
$isRecycle = $this->request->param('isRecycle', '');
|
||||
if (empty($workbenchId)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find();
|
||||
|
||||
if (empty($workbench)) {
|
||||
return json(['code' => 400, 'msg' => '该任务不存在或已删除']);
|
||||
}
|
||||
$query = Db::name('workbench_traffic_config_item')->alias('wtc')
|
||||
->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
|
||||
->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
|
||||
->field([
|
||||
'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
|
||||
'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
|
||||
'u.account', 'u.username'
|
||||
])
|
||||
->where(['wtc.workbenchId' => $workbenchId])
|
||||
->order('wtc.id DESC');
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
if ($isRecycle != '' || $isRecycle != null) {
|
||||
$query->where('isRecycle',$isRecycle);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $limit)->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||
$item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$data = [
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
];
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,24 @@ namespace app\job;
|
||||
|
||||
use app\chukebao\model\Reply;
|
||||
use app\chukebao\model\ReplyGroup;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
|
||||
class SyncContentJob
|
||||
{
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
|
||||
$ddd= Db::table('s2_wechat_friend')->where('ownerWechatId','wxid_h7nsh7vxseyn29')->select();
|
||||
foreach ($ddd as $v) {
|
||||
$d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find();
|
||||
if (!empty($d) && !in_array($d['status'],[4,5])) {
|
||||
Db::table('ck_task_customer')->where('id',$d['id'])->update(['status'=>5]);
|
||||
}
|
||||
}
|
||||
|
||||
exit_data(111);
|
||||
|
||||
return true;
|
||||
/*$ddd= '[{"id":21909,"groupName":"私域运营招聘","sortIndex":240426546,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8074,"groupName":"存客宝新客户介绍","sortIndex":230,"parentId":0,"replyType":1,"children":[{"id":8081,"groupName":"客户系统上线准备","sortIndex":1,"parentId":8074,"replyType":1,"children":[],"replys":null}],"replys":null},{"id":10441,"groupName":"BOSS直聘","sortIndex":229,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":15111,"groupName":"点了码新客户了解","sortIndex":228,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":12732,"groupName":"测试","sortIndex":219,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8814,"groupName":"封单话术","sortIndex":176,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8216,"groupName":"私域合伙人","sortIndex":172,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":6476,"groupName":"新客户了解","sortIndex":119,"parentId":0,"replyType":1,"children":[],"replys":null}]';
|
||||
$ddd=json_decode($ddd,1);*/
|
||||
|
||||
@@ -77,7 +77,8 @@ class WorkbenchGroupCreateJob
|
||||
{
|
||||
try {
|
||||
// 1. 查询启用了建群功能的数据
|
||||
$workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0,'id' => 315])->order('id desc')->select();
|
||||
$workbenches = Workbench::where(['status' => 0, 'type' => 4, 'isDel' => 0,'id' => 354])->order('id desc')->select();
|
||||
|
||||
foreach ($workbenches as $workbench) {
|
||||
// 获取工作台配置
|
||||
$config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
|
||||
@@ -86,22 +87,49 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
$config['poolGroups'] = json_decode($config['poolGroups'], true);
|
||||
$config['devices'] = json_decode($config['devices'], true);
|
||||
$config['poolGroups'] = json_decode($config['poolGroups'] ?? '[]', true) ?: [];
|
||||
$config['devices'] = json_decode($config['devices'] ?? '[]', true) ?: [];
|
||||
$config['wechatGroups'] = json_decode($config['wechatGroups'] ?? '[]', true) ?: [];
|
||||
$config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
|
||||
|
||||
if (empty($config['poolGroups']) || empty($config['devices'])) {
|
||||
// 检查时间限制
|
||||
if (!$this->isWithinTimeRange($config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查每日建群数量限制
|
||||
if (!$this->checkDailyLimit($workbench->id, $config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有正在创建中的群,如果有则跳过(避免重复创建)
|
||||
$creatingCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('status', self::STATUS_CREATING)
|
||||
->where('groupId', '<>', null) // 有groupId的记录
|
||||
->group('groupId')
|
||||
->count();
|
||||
if ($creatingCount > 0) {
|
||||
Log::info("工作台ID: {$workbench->id} 有正在创建中的群({$creatingCount}个),跳过本次执行");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($config['devices'])) {
|
||||
continue;
|
||||
}
|
||||
// 获取群主成员(从设备中获取)
|
||||
$groupMember = [];
|
||||
$wechatId = Db::name('device_wechat_login')
|
||||
->whereIn('deviceId',$config['devices'])
|
||||
$wechatIds = Db::name('device_wechat_login')
|
||||
->whereIn('deviceId', $config['devices'])
|
||||
->where('alive', DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE)
|
||||
->order('id desc')
|
||||
->value('wechatId');
|
||||
if (empty($wechatId)) {
|
||||
->column('wechatId');
|
||||
|
||||
if (empty($wechatIds)) {
|
||||
continue;
|
||||
}
|
||||
$groupMember[] = $wechatId;
|
||||
$groupMember = array_unique($wechatIds);
|
||||
|
||||
// 获取群主好友ID映射(所有群主的好友)
|
||||
$groupMemberWechatId = [];
|
||||
$groupMemberId = [];
|
||||
@@ -110,6 +138,7 @@ class WorkbenchGroupCreateJob
|
||||
$friends = Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $ownerWechatId)
|
||||
->whereIn('wechatId', $groupMember)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,wechatId')
|
||||
->select();
|
||||
|
||||
@@ -120,44 +149,78 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($groupMemberWechatId)) {
|
||||
|
||||
// 如果配置了wechatGroups,从指定的群组中获取成员
|
||||
if (!empty($config['wechatGroups'])) {
|
||||
$this->addGroupMembersFromWechatGroups($config['wechatGroups'], $groupMember, $groupMemberId, $groupMemberWechatId);
|
||||
}
|
||||
|
||||
if (empty($groupMemberId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取流量池用户
|
||||
$poolItem = Db::name('traffic_source_package_item')
|
||||
->whereIn('packageId', $config['poolGroups'])
|
||||
->group('identifier')
|
||||
->column('identifier');
|
||||
|
||||
|
||||
if (empty($poolItem)) {
|
||||
// 获取流量池用户(如果配置了流量池)
|
||||
$poolItem = [];
|
||||
if (!empty($config['poolGroups'])) {
|
||||
$poolItem = Db::name('traffic_source_package_item')
|
||||
->whereIn('packageId', $config['poolGroups'])
|
||||
->where('isDel', 0)
|
||||
->group('identifier')
|
||||
->column('identifier');
|
||||
}
|
||||
|
||||
// 如果既没有流量池也没有指定群组,跳过
|
||||
if (empty($poolItem) && empty($config['wechatGroups'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取已入群的用户(排除已成功入群的)111
|
||||
$groupUser = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING])
|
||||
->whereIn('wechatId', $poolItem)
|
||||
->group('wechatId')
|
||||
->column('wechatId');
|
||||
// 待入群的用户
|
||||
$joinUser = array_diff($poolItem, $groupUser);
|
||||
if (empty($joinUser)) {
|
||||
// 获取已入群的用户(排除已成功入群的)
|
||||
$groupUser = [];
|
||||
if (!empty($poolItem)) {
|
||||
$groupUser = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING])
|
||||
->whereIn('wechatId', $poolItem)
|
||||
->group('wechatId')
|
||||
->column('wechatId');
|
||||
}
|
||||
|
||||
// 待入群的用户(从流量池中筛选)
|
||||
$joinUser = !empty($poolItem) ? array_diff($poolItem, $groupUser) : [];
|
||||
|
||||
// 如果流量池用户已用完或没有配置流量池,但配置了wechatGroups,至少创建一次(使用群主成员)
|
||||
if (empty($joinUser) && !empty($config['wechatGroups'])) {
|
||||
// 如果没有流量池用户,创建一个空批次,让processBatchUsers处理只有群主成员的情况
|
||||
$joinUser = []; // 空数组,但会继续执行
|
||||
}
|
||||
|
||||
// 如果既没有流量池用户也没有配置wechatGroups,跳过
|
||||
if (empty($joinUser) && empty($config['wechatGroups'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算随机群人数(不包含管理员,只减去群主成员数)
|
||||
$groupRandNum = mt_rand($config['groupSizeMin'], $config['groupSizeMax']) - count($groupMember);
|
||||
// 群主成员数 = 群主好友ID数量
|
||||
$minGroupSize = max(2, $config['groupSizeMin']); // 至少2人才能建群
|
||||
$maxGroupSize = max($minGroupSize, $config['groupSizeMax']);
|
||||
$groupRandNum = mt_rand($minGroupSize, $maxGroupSize) - count($groupMemberId);
|
||||
if ($groupRandNum <= 0) {
|
||||
$groupRandNum = 1; // 至少需要1个成员
|
||||
}
|
||||
|
||||
// 分批处理待入群用户
|
||||
$addGroupUser = [];
|
||||
$totalRows = count($joinUser);
|
||||
for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
|
||||
$batchRows = array_slice($joinUser, $i, $groupRandNum);
|
||||
if (!empty($batchRows)) {
|
||||
$addGroupUser[] = $batchRows;
|
||||
if (!empty($joinUser)) {
|
||||
$totalRows = count($joinUser);
|
||||
for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
|
||||
$batchRows = array_slice($joinUser, $i, $groupRandNum);
|
||||
if (!empty($batchRows)) {
|
||||
$addGroupUser[] = $batchRows;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有流量池用户但配置了wechatGroups,创建一个空批次
|
||||
$addGroupUser[] = [];
|
||||
}
|
||||
// 初始化WebSocket
|
||||
$toAccountId = '';
|
||||
@@ -229,28 +292,59 @@ class WorkbenchGroupCreateJob
|
||||
$ownerFriendIdsByAccount = [];
|
||||
$wechatIds = [];
|
||||
|
||||
// 获取群主的好友关系(从流量池中筛选)
|
||||
$ownerFriends = Db::table('s2_wechat_friend')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('a.wechatId', $groupOwnerWechatIds)
|
||||
->where('f.isDeleted', 0)
|
||||
->field('f.id,f.wechatId,a.id as wechatAccountId')
|
||||
->select();
|
||||
|
||||
if (empty($ownerFriends)) {
|
||||
Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按微信账号分组群主好友
|
||||
foreach ($ownerFriends as $friend) {
|
||||
$wechatAccountId = $friend['wechatAccountId'];
|
||||
if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
|
||||
$ownerFriendIdsByAccount[$wechatAccountId] = [];
|
||||
// 如果batchUsers为空,说明没有流量池用户,但可能配置了wechatGroups
|
||||
// 这种情况下,使用群主成员作为基础,按账号分组
|
||||
if (empty($batchUsers)) {
|
||||
// 按账号分组群主成员
|
||||
foreach ($groupMemberId as $memberId) {
|
||||
$member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
|
||||
if ($member) {
|
||||
$accountWechatId = $member['ownerWechatId'];
|
||||
$account = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $accountWechatId)
|
||||
->field('id')
|
||||
->find();
|
||||
|
||||
if ($account) {
|
||||
$wechatAccountId = $account['id'];
|
||||
if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
|
||||
$ownerFriendIdsByAccount[$wechatAccountId] = [];
|
||||
}
|
||||
$ownerFriendIdsByAccount[$wechatAccountId][] = $memberId;
|
||||
$wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
$ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
} else {
|
||||
// 获取群主的好友关系(从流量池中筛选)
|
||||
$ownerFriends = Db::table('s2_wechat_friend')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('a.wechatId', $groupOwnerWechatIds)
|
||||
->where('f.isDeleted', 0)
|
||||
->field('f.id,f.wechatId,a.id as wechatAccountId')
|
||||
->select();
|
||||
|
||||
if (empty($ownerFriends)) {
|
||||
Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按微信账号分组群主好友
|
||||
foreach ($ownerFriends as $friend) {
|
||||
$wechatAccountId = $friend['wechatAccountId'];
|
||||
if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
|
||||
$ownerFriendIdsByAccount[$wechatAccountId] = [];
|
||||
}
|
||||
$ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到任何好友,跳过
|
||||
if (empty($ownerFriendIdsByAccount)) {
|
||||
Log::warning("未找到任何群主好友或成员,跳过。工作台ID: {$workbench->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 遍历每个微信账号,创建群
|
||||
@@ -279,21 +373,47 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
|
||||
// 4.3 限制群主好友数量(按随机群人数)
|
||||
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
|
||||
// 如果ownerFriendIds只包含群主成员(没有流量池用户),则不需要限制
|
||||
$limitedOwnerFriendIds = $ownerFriendIds;
|
||||
if (count($ownerFriendIds) > $groupRandNum) {
|
||||
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
|
||||
}
|
||||
|
||||
// 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选)
|
||||
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds, $limitedOwnerFriendIds);
|
||||
// 合并时去重,避免重复添加群主成员
|
||||
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds);
|
||||
foreach ($limitedOwnerFriendIds as $friendId) {
|
||||
if (!in_array($friendId, $createFriendIds)) {
|
||||
$createFriendIds[] = $friendId;
|
||||
}
|
||||
}
|
||||
|
||||
// 微信建群至少需要2个人
|
||||
if (count($createFriendIds) < 2) {
|
||||
Log::warning("建群好友数量不足,跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}");
|
||||
Log::warning("建群好友数量不足(至少需要2人),跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}, 当前人数: " . count($createFriendIds));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.5 生成群名称
|
||||
// 4.5 检查当前账号是否有正在创建中的群,如果有则跳过
|
||||
$creatingGroupCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_CREATING)
|
||||
->where('groupId', '<>', null)
|
||||
->group('groupId')
|
||||
->count();
|
||||
|
||||
if ($creatingGroupCount > 0) {
|
||||
Log::info("工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId} 有正在创建中的群({$creatingGroupCount}个),跳过本次创建");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.6 生成群名称
|
||||
$existingGroupCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_SUCCESS)
|
||||
->where('groupId', '<>', null) // 排除groupId为NULL的记录
|
||||
->group('groupId')
|
||||
->count();
|
||||
|
||||
@@ -301,7 +421,7 @@ class WorkbenchGroupCreateJob
|
||||
? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
|
||||
: $config['groupNameTemplate'];
|
||||
|
||||
// 4.6 调用建群接口
|
||||
// 4.7 调用建群接口
|
||||
$createTime = time();
|
||||
$createResult = $webSocket->CmdChatroomCreate([
|
||||
'chatroomName' => $chatroomName,
|
||||
@@ -311,18 +431,65 @@ class WorkbenchGroupCreateJob
|
||||
|
||||
$createResultData = json_decode($createResult, true);
|
||||
|
||||
// 4.7 解析建群结果,获取群ID
|
||||
$chatroomId = 0;
|
||||
// 4.8 解析建群结果,获取群ID
|
||||
// chatroomId: varchar(64) - 微信的群聊ID(字符串)
|
||||
// groupId: int(10) - 数据库中的群组ID(整数)
|
||||
$chatroomId = null; // 微信群聊ID(字符串)
|
||||
$groupId = 0; // 数据库群组ID(整数)
|
||||
$tempGroupId = null; // 临时群标识,用于轮询查询
|
||||
|
||||
if (!empty($createResultData) && isset($createResultData['code']) && $createResultData['code'] == 200) {
|
||||
// 尝试从返回数据中获取群ID(根据实际API返回格式调整)
|
||||
if (isset($createResultData['data']['chatroomId'])) {
|
||||
$chatroomId = $createResultData['data']['chatroomId'];
|
||||
// API返回的是chatroomId(字符串)
|
||||
$chatroomId = (string)$createResultData['data']['chatroomId'];
|
||||
// 通过chatroomId查询数据库获取groupId
|
||||
$group = Db::name('wechat_group')
|
||||
->where('chatroomId', $chatroomId)
|
||||
->where('deleteTime', 0)
|
||||
->find();
|
||||
if ($group) {
|
||||
$groupId = intval($group['id']);
|
||||
}
|
||||
} elseif (isset($createResultData['data']['id'])) {
|
||||
$chatroomId = $createResultData['data']['id'];
|
||||
// API返回的是数据库ID(整数)
|
||||
$groupId = intval($createResultData['data']['id']);
|
||||
// 通过groupId查询chatroomId
|
||||
$group = Db::name('wechat_group')
|
||||
->where('id', $groupId)
|
||||
->where('deleteTime', 0)
|
||||
->find();
|
||||
if ($group && !empty($group['chatroomId'])) {
|
||||
$chatroomId = (string)$group['chatroomId'];
|
||||
}
|
||||
}
|
||||
// 如果有临时标识,保存用于轮询
|
||||
if (isset($createResultData['data']['tempId'])) {
|
||||
$tempGroupId = $createResultData['data']['tempId'];
|
||||
}
|
||||
}
|
||||
|
||||
// 4.8 记录创建请求
|
||||
// 4.9 如果建群接口没有立即返回群ID,进行同步轮询检查
|
||||
if ($groupId == 0) {
|
||||
// 获取账号的微信ID(群主微信ID)
|
||||
$accountWechatId = Db::table('s2_wechat_account')
|
||||
->where('id', $wechatAccountId)
|
||||
->value('wechatId');
|
||||
|
||||
if (!empty($accountWechatId)) {
|
||||
$pollResult = $this->pollGroupCreation($chatroomName, $accountWechatId, $wechatAccountId, $tempGroupId);
|
||||
if ($pollResult && is_array($pollResult)) {
|
||||
$groupId = intval($pollResult['groupId'] ?? 0);
|
||||
$chatroomId = !empty($pollResult['chatroomId']) ? (string)$pollResult['chatroomId'] : null;
|
||||
} elseif ($pollResult > 0) {
|
||||
// 兼容旧返回值(只返回groupId)
|
||||
$groupId =0;
|
||||
$chatroomId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.10 记录创建请求
|
||||
$installData = [];
|
||||
foreach ($createFriendIds as $friendId) {
|
||||
$memberType = in_array($friendId, $currentAdminFriendIds)
|
||||
@@ -333,20 +500,21 @@ class WorkbenchGroupCreateJob
|
||||
'workbenchId' => $workbench->id,
|
||||
'friendId' => $friendId,
|
||||
'wechatId' => $wechatIds[$friendId] ?? ($groupMemberWechatId[$friendId] ?? ''),
|
||||
'groupId' => $chatroomId,
|
||||
'groupId' => $groupId > 0 ? $groupId : null, // int类型
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'status' => $chatroomId > 0 ? self::STATUS_SUCCESS : self::STATUS_CREATING,
|
||||
'status' => $groupId > 0 ? self::STATUS_SUCCESS : self::STATUS_FAILED,
|
||||
'memberType' => $memberType,
|
||||
'retryCount' => 0,
|
||||
'chatroomId' => $chatroomId > 0 ? $chatroomId : null,
|
||||
'chatroomId' => $chatroomId, // varchar类型
|
||||
'createTime' => $createTime,
|
||||
];
|
||||
}
|
||||
Db::name('workbench_group_create_item')->insertAll($installData);
|
||||
|
||||
// 5. 如果群创建成功,拉管理员的好友进群
|
||||
if ($chatroomId > 0 && !empty($currentAdminFriendIds)) {
|
||||
$this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $wechatAccountId, $wechatIds, $createTime, $webSocket);
|
||||
// 注意:拉人接口需要chatroomId(字符串),而不是groupId(整数)
|
||||
if (!empty($chatroomId) && !empty($currentAdminFriendIds)) {
|
||||
$this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,13 +525,14 @@ class WorkbenchGroupCreateJob
|
||||
* @param array $config 配置
|
||||
* @param array $batchUsers 批次用户(流量池微信ID数组)
|
||||
* @param array $adminFriendIds 管理员好友ID数组
|
||||
* @param int $chatroomId 群ID
|
||||
* @param string $chatroomId 群聊ID(字符串,用于API调用)
|
||||
* @param int $groupId 数据库群组ID(整数)
|
||||
* @param int $wechatAccountId 微信账号ID
|
||||
* @param array $wechatIds 好友ID到微信ID的映射
|
||||
* @param int $createTime 创建时间
|
||||
* @param WebSocketController $webSocket WebSocket实例
|
||||
*/
|
||||
protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $wechatAccountId, $wechatIds, $createTime, $webSocket)
|
||||
protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket)
|
||||
{
|
||||
// 获取管理员的微信ID列表
|
||||
$adminWechatIds = [];
|
||||
@@ -399,7 +568,7 @@ class WorkbenchGroupCreateJob
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
}
|
||||
|
||||
// 调用拉人接口
|
||||
// 调用拉人接口(使用chatroomId字符串)
|
||||
$inviteResult = $webSocket->CmdChatroomInvite([
|
||||
'wechatChatroomId' => $chatroomId,
|
||||
'wechatFriendIds' => $adminFriendIdsToInvite
|
||||
@@ -415,12 +584,12 @@ class WorkbenchGroupCreateJob
|
||||
'workbenchId' => $workbench->id,
|
||||
'friendId' => $friendId,
|
||||
'wechatId' => $wechatIds[$friendId] ?? '',
|
||||
'groupId' => $chatroomId,
|
||||
'groupId' => $groupId > 0 ? $groupId : null, // int类型
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'status' => $inviteSuccess ? self::STATUS_ADMIN_FRIEND_ADDED : self::STATUS_FAILED,
|
||||
'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
|
||||
'retryCount' => 0,
|
||||
'chatroomId' => $chatroomId,
|
||||
'chatroomId' => $chatroomId, // varchar类型
|
||||
'createTime' => $createTime,
|
||||
];
|
||||
}
|
||||
@@ -429,40 +598,209 @@ class WorkbenchGroupCreateJob
|
||||
if ($inviteSuccess) {
|
||||
// 去除成功日志,减少日志空间消耗
|
||||
} else {
|
||||
Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群ID: {$chatroomId}");
|
||||
Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群组ID: {$groupId}, 群聊ID: {$chatroomId}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
* @param Workbench $workbench 工作台
|
||||
* @param WorkbenchGroupPush $config 配置
|
||||
* @return array|bool
|
||||
* 轮询检查群是否创建成功
|
||||
* @param string $chatroomName 群名称
|
||||
* @param string $ownerWechatId 群主微信ID
|
||||
* @param int $wechatAccountId 微信账号ID
|
||||
* @param string|null $tempGroupId 临时群标识(如果有)
|
||||
* @return array|int 返回数组包含groupId和chatroomId,或只返回groupId(兼容旧代码),如果未找到返回0
|
||||
*/
|
||||
protected function isCreate($workbench, $config, $groupIds = [])
|
||||
protected function pollGroupCreation($chatroomName, $ownerWechatId, $wechatAccountId, $tempGroupId = null)
|
||||
{
|
||||
// 检查发送间隔(新逻辑:根据startTime、endTime、maxPerDay动态计算)
|
||||
$maxAttempts = 10; // 最多查询10次
|
||||
$interval = 5; // 每次间隔5秒
|
||||
|
||||
// 获取账号ID(accountId)和微信账号的微信ID(wechatAccountWechatId),用于查询s2_wechat_chatroom表
|
||||
$accountInfo = Db::table('s2_wechat_account')
|
||||
->where('id', $wechatAccountId)
|
||||
->field('id,wechatId')
|
||||
->find();
|
||||
|
||||
$accountId = $accountInfo['id'] ?? null;
|
||||
$wechatAccountWechatId = $accountInfo['wechatId'] ?? null;
|
||||
|
||||
if (empty($accountId) && empty($wechatAccountWechatId)) {
|
||||
Log::warning("无法获取账号ID和微信账号ID,跳过轮询。微信账号ID: {$wechatAccountId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 获取授权信息(用于调用同步接口)
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
// 等待5秒(第一次立即查询,后续等待)
|
||||
if ($attempt > 1) {
|
||||
sleep($interval);
|
||||
}
|
||||
|
||||
// 1. 先调用接口同步最新的群组信息
|
||||
try {
|
||||
$chatroomController = new \app\api\controller\WechatChatroomController();
|
||||
// 构建同步参数
|
||||
$syncData = [
|
||||
'wechatAccountKeyword' => $ownerWechatId, // 通过群主微信ID筛选
|
||||
'isDeleted' => false,
|
||||
'pageIndex' => 0,
|
||||
'pageSize' => 5 // 获取足够多的数据
|
||||
];
|
||||
// 调用getlist方法同步数据(内部调用,isInner=true)
|
||||
$chatroomController->getlist($syncData, true, 0);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning("同步群组信息失败: " . $e->getMessage());
|
||||
// 即使同步失败,也继续查询本地数据
|
||||
}
|
||||
|
||||
// 2. 查询本地表 s2_wechat_chatroom
|
||||
// 计算5分钟前的时间戳
|
||||
$fiveMinutesAgo = time() - 300; // 5分钟 = 300秒
|
||||
$now = time();
|
||||
|
||||
// 查询群聊:通过群名称、账号ID或微信账号ID和创建时间查询
|
||||
// 如果accountId不为空,优先使用accountId查询;如果accountId为空,则使用wechatAccountWechatId查询
|
||||
$chatroom = Db::table('s2_wechat_chatroom')
|
||||
->where('nickname', $chatroomName)
|
||||
->where('isDeleted', 0)
|
||||
->where('createTime', '>=', $fiveMinutesAgo) // 创建时间在5分钟内
|
||||
->where('createTime', '<=', $now)
|
||||
->where('wechatAccountWechatId', $wechatAccountWechatId)
|
||||
->order('createTime', 'desc')
|
||||
->find();
|
||||
|
||||
|
||||
// 如果找到了群聊,返回群ID和chatroomId
|
||||
if ($chatroom && !empty($chatroom['id'])) {
|
||||
$chatroomId = !empty($chatroom['chatroomId']) ? (string)$chatroom['chatroomId'] : null;
|
||||
// 如果有chatroomId,尝试查询wechat_group表获取groupId
|
||||
$groupId = $chatroom['id'];
|
||||
Log::info("轮询检查群创建成功。群名称: {$chatroomName}, 群聊ID: {$chatroom['id']}, chatroomId: {$chatroomId}, 群组ID: {$groupId}, 尝试次数: {$attempt}");
|
||||
return [
|
||||
'groupId' => $groupId > 0 ? $groupId : intval($chatroom['id']), // 如果没有groupId,使用chatroom的id
|
||||
'chatroomId' => $chatroomId ?: (string)$chatroom['id']
|
||||
];
|
||||
}
|
||||
|
||||
Log::debug("轮询检查群创建中。群名称: {$chatroomName}, 尝试次数: {$attempt}/{$maxAttempts}");
|
||||
}
|
||||
|
||||
// 10次查询后仍未找到,返回0表示失败
|
||||
Log::warning("轮询检查群创建失败,已查询{$maxAttempts}次仍未找到群组。群名称: {$chatroomName}, 群主微信ID: {$ownerWechatId}, 账号ID: {$accountId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在时间范围内
|
||||
* @param array $config 配置
|
||||
* @return bool
|
||||
*/
|
||||
protected function isWithinTimeRange($config)
|
||||
{
|
||||
if (empty($config['startTime']) || empty($config['endTime'])) {
|
||||
return true; // 如果没有配置时间,则允许执行
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
|
||||
$endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
|
||||
|
||||
// 如果时间不符,则跳过
|
||||
if ($startTimestamp > time() || $endTimestamp < time() || empty($groupIds)) {
|
||||
$currentTime = time();
|
||||
|
||||
// 如果开始时间大于当前时间,还未到执行时间
|
||||
if ($startTimestamp > $currentTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询今日建群数量
|
||||
$count = Db::name('wechat_group')
|
||||
->whereIn('id', $groupIds)
|
||||
->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp])
|
||||
->count();
|
||||
if ($count >= $config['maxGroupsPerDay']) {
|
||||
|
||||
// 如果结束时间小于当前时间,已过执行时间
|
||||
if ($endTimestamp < $currentTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查每日建群数量限制
|
||||
* @param int $workbenchId 工作台ID
|
||||
* @param array $config 配置
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkDailyLimit($workbenchId, $config)
|
||||
{
|
||||
if (empty($config['maxGroupsPerDay']) || $config['maxGroupsPerDay'] <= 0) {
|
||||
return true; // 如果没有配置限制,则允许执行
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$startTimestamp = strtotime($today . ' 00:00:00');
|
||||
$endTimestamp = strtotime($today . ' 23:59:59');
|
||||
|
||||
// 查询今日已创建的群数量(状态为成功)
|
||||
$todayCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('status', self::STATUS_SUCCESS)
|
||||
->where('groupId', '<>', null) // 排除groupId为NULL的记录
|
||||
->where('createTime', 'between', [$startTimestamp, $endTimestamp])
|
||||
->group('groupId')
|
||||
->count();
|
||||
|
||||
return $todayCount < $config['maxGroupsPerDay'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定的微信群组中获取成员
|
||||
* @param array $wechatGroups 群组ID数组(可能是好友ID或群组ID)
|
||||
* @param array $groupMember 群主成员微信ID数组(引用传递)
|
||||
* @param array $groupMemberId 群主成员好友ID数组(引用传递)
|
||||
* @param array $groupMemberWechatId 群主成员微信ID映射(引用传递)
|
||||
*/
|
||||
protected function addGroupMembersFromWechatGroups($wechatGroups, &$groupMember, &$groupMemberId, &$groupMemberWechatId)
|
||||
{
|
||||
foreach ($wechatGroups as $groupId) {
|
||||
if (is_numeric($groupId)) {
|
||||
// 数字ID:可能是好友ID,查询好友信息
|
||||
$friend = Db::table('s2_wechat_friend')
|
||||
->where('id', $groupId)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,wechatId,ownerWechatId')
|
||||
->find();
|
||||
|
||||
if ($friend) {
|
||||
// 添加到群主成员
|
||||
if (!in_array($friend['ownerWechatId'], $groupMember)) {
|
||||
$groupMember[] = $friend['ownerWechatId'];
|
||||
}
|
||||
|
||||
if (!isset($groupMemberWechatId[$friend['id']])) {
|
||||
$groupMemberWechatId[$friend['id']] = $friend['wechatId'];
|
||||
$groupMemberId[] = $friend['id'];
|
||||
}
|
||||
} else {
|
||||
// 如果不是好友ID,可能是群组ID,查询群组信息
|
||||
$group = Db::name('wechat_group')
|
||||
->where('id', $groupId)
|
||||
->where('deleteTime', 0)
|
||||
->field('ownerWechatId')
|
||||
->find();
|
||||
|
||||
if ($group && !in_array($group['ownerWechatId'], $groupMember)) {
|
||||
$groupMember[] = $group['ownerWechatId'];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 字符串ID:手动创建的群组,可能是wechatId
|
||||
if (!in_array($groupId, $groupMember)) {
|
||||
$groupMember[] = $groupId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录任务开始
|
||||
@@ -510,4 +848,5 @@ class WorkbenchGroupCreateJob
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user