From 4f8a69a511af2ad79792246e66cd5af5d90ee2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Thu, 11 Dec 2025 09:40:54 +0800 Subject: [PATCH 01/11] 1 --- Touchkebao/提示词/技术栈.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Touchkebao/提示词/技术栈.md b/Touchkebao/提示词/技术栈.md index 006ec1a7..86f06576 100644 --- a/Touchkebao/提示词/技术栈.md +++ b/Touchkebao/提示词/技术栈.md @@ -32,17 +32,3 @@ TanStack Query Sentry - -123 -3 - -STEP:1 -STEP:2 -STEP:3 -STEP:4 -STEP:5 -STEP:6 -STEP:7 -STEP:8 -STEP:9 -STEP:10 From 84a51b8f91fa5ec45b58821c97b413f91b4d0cd5 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 11 Dec 2025 17:30:40 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E8=BD=AC=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/wechat-accounts/detail/api.ts | 14 + .../mine/wechat-accounts/detail/data.ts | 3 + .../wechat-accounts/detail/detail.module.scss | 232 +++++++++++----- .../mine/wechat-accounts/detail/index.tsx | 260 ++++++++++++++++-- 4 files changed, 415 insertions(+), 94 deletions(-) diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 8c14f3e9..0165179a 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -55,6 +55,20 @@ export function transferWechatFriends(params: { return request("/v1/wechats/transfer-friends", params, "POST"); } +// 获取客服账号列表 +export function getKefuAccountsList() { + return request("/v1/kefu/accounts/list", {}, "GET"); +} + +// 转移好友到客服账号 +export function transferFriend(params: { + friendId: string; + toAccountId: string; + comment?: string; +}) { + return request("/v1/friend/transfer", params, "POST"); +} + // 导出朋友圈接口(直接下载文件) export async function exportWechatMoments(params: { wechatId: string; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts index 9245ba9f..6a374dba 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/data.ts @@ -102,9 +102,12 @@ export interface WechatAccountSummary { export interface Friend { id: string; + friendId?: string; avatar: string; nickname: string; wechatId: string; + accountUserName: string; + accountRealName: string; remark: string; addTime: string; lastInteraction: string; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index 95c33d8a..41530573 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -684,99 +684,114 @@ .friend-card { display: flex; - align-items: center; - padding: 14px; + align-items: flex-start; + padding: 16px; background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; - margin-bottom: 10px; + margin-bottom: 12px; gap: 12px; - transition: box-shadow 0.2s, border-color 0.2s; + transition: all 0.2s; + cursor: pointer; - &:hover { - border-color: #cfe2ff; - box-shadow: 0 6px 16px rgba(24, 144, 255, 0.15); + &:active { + background: #f8f9fa; + border-color: #1677ff; + transform: scale(0.98); } } .friend-avatar { - width: 48px; - height: 48px; + flex-shrink: 0; + width: 52px; + height: 52px; .adm-avatar { - width: 48px; - height: 48px; - border-radius: 50%; + width: 52px; + height: 52px; + border-radius: 50%; + border: 2px solid #f0f0f0; } } .friend-main { flex: 1; min-width: 0; - } - - .friend-name-row { - display: flex; - align-items: center; + display: flex; + flex-direction: column; gap: 8px; - margin-bottom: 4px; } - .friend-name { - font-size: 15px; + .friend-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .friend-name { + font-size: 16px; font-weight: 600; color: #111; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .friend-value { flex-shrink: 0; + + .value-amount { + font-size: 15px; + font-weight: 600; + color: #fa541c; + white-space: nowrap; + } + } + + .friend-info { + display: flex; + flex-direction: column; + gap: 4px; + } + + .friend-info-item { + font-size: 13px; + color: #666; + display: flex; + align-items: center; + gap: 4px; + + .info-label { + color: #999; + flex-shrink: 0; + } + + .info-value { + color: #666; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .friend-tags { display: flex; flex-wrap: wrap; - gap: 4px; - } + gap: 6px; + margin-top: 4px; + } .friend-tag { font-size: 11px; - padding: 2px 8px; - border-radius: 999px; - background: #f5f5f5; - color: #666; - } - - .friend-id-row { - font-size: 12px; - color: #999; - margin-bottom: 6px; - } - - .friend-status-row { - display: flex; - flex-wrap: wrap; - gap: 6px; - } - - .friend-status-chip { + padding: 4px 10px; + border-radius: 12px; background: #f0f7ff; color: #1677ff; - font-size: 11px; - padding: 2px 8px; - border-radius: 8px; - } - - .friend-value { - text-align: right; - - .value-label { - font-size: 11px; - color: #999; - margin-bottom: 4px; - } - - .value-amount { - font-size: 14px; - font-weight: 600; - color: #fa541c; - } + font-weight: 500; + white-space: nowrap; } } @@ -851,11 +866,98 @@ margin-top: 20px; } - .popup-footer { - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; - } + .popup-footer { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } + + .friend-info-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: #f5f5f5; + border-radius: 8px; + + .friend-info-text { + flex: 1; + + .friend-info-name { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + } + + .friend-info-id { + font-size: 12px; + color: #999; + } + } + } + + .loading-accounts, + .empty-accounts { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: #999; + font-size: 14px; + } + + .kefu-accounts-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; + + .kefu-account-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + border: 1px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + background: white; + + &:hover { + border-color: #1677ff; + background: #f0f7ff; + } + + &.selected { + border-color: #1677ff; + background: #e6f4ff; + } + + .account-info { + flex: 1; + + .account-name { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + } + + .account-id { + font-size: 12px; + color: #999; + } + } + + .selected-icon { + color: #1677ff; + font-size: 18px; + font-weight: bold; + } + } + } .export-form { margin-top: 20px; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 06609940..a985f182 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -14,7 +14,7 @@ import { DatePicker, InfiniteScroll, } from "antd-mobile"; -import { Input } from "antd"; +import { Input, Select } from "antd"; import NavCommon from "@/components/NavCommon"; import { SearchOutlined, @@ -34,6 +34,8 @@ import { getWechatAccountOverview, getWechatMoments, exportWechatMoments, + getKefuAccountsList, + transferFriend, } from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -84,6 +86,15 @@ const WechatAccountDetail: React.FC = () => { const [showEndTimePicker, setShowEndTimePicker] = useState(false); const [exportLoading, setExportLoading] = useState(false); + // 迁移好友相关状态 + const [showTransferFriendPopup, setShowTransferFriendPopup] = useState(false); + const [selectedFriend, setSelectedFriend] = useState(null); + const [kefuAccounts, setKefuAccounts] = useState([]); + const [selectedKefuAccountId, setSelectedKefuAccountId] = useState(""); + const [transferComment, setTransferComment] = useState(""); + const [transferFriendLoading, setTransferFriendLoading] = useState(false); + const [loadingKefuAccounts, setLoadingKefuAccounts] = useState(false); + // 获取基础信息 const fetchAccountInfo = useCallback(async () => { if (!id) return; @@ -237,9 +248,12 @@ const WechatAccountDetail: React.FC = () => { return { id: friend.id.toString(), + friendId: friend.friendId || friend.id?.toString() || "", avatar: friend.avatar || "/placeholder.svg", nickname: friend.nickname || "未知用户", wechatId: friend.wechatId || "", + accountUserName: friend.accountUserName || "", + accountRealName: friend.accountRealName || "", remark: friend.notes || "", addTime: friend.createTime || new Date().toISOString().split("T")[0], @@ -466,7 +480,66 @@ const WechatAccountDetail: React.FC = () => { }; const handleFriendClick = (friend: Friend) => { - navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`); + setSelectedFriend(friend); + setShowTransferFriendPopup(true); + // 加载客服账号列表 + fetchKefuAccounts(); + }; + + // 获取客服账号列表 + const fetchKefuAccounts = useCallback(async () => { + setLoadingKefuAccounts(true); + try { + const response = await getKefuAccountsList(); + // 数据结构:{ code: 200, msg: "success", data: { total: 7, list: [...] } } + const accountsList = response?.data?.list || response?.list || (Array.isArray(response) ? response : []); + setKefuAccounts(accountsList); + } catch (error) { + console.error("获取客服账号列表失败:", error); + Toast.show({ + content: "获取客服账号列表失败", + position: "top", + }); + } finally { + setLoadingKefuAccounts(false); + } + }, []); + + // 确认转移好友 + const handleConfirmTransferFriend = async () => { + if (!selectedFriend) { + Toast.show({ content: "请选择好友", position: "top" }); + return; + } + if (!selectedKefuAccountId) { + Toast.show({ content: "请选择目标客服账号", position: "top" }); + return; + } + + setTransferFriendLoading(true); + try { + await transferFriend({ + friendId: selectedFriend.friendId || selectedFriend.id, + toAccountId: selectedKefuAccountId, + comment: transferComment || undefined, + }); + + Toast.show({ content: "转移成功", position: "top" }); + setShowTransferFriendPopup(false); + setSelectedFriend(null); + setSelectedKefuAccountId(""); + setTransferComment(""); + // 刷新好友列表 + fetchFriendsList(1, searchQuery, false); + } catch (error: any) { + console.error("转移好友失败:", error); + Toast.show({ + content: error?.message || "转移失败,请重试", + position: "top", + }); + } finally { + setTransferFriendLoading(false); + } }; const handleLoadMoreMoments = async () => { @@ -903,38 +976,53 @@ const WechatAccountDetail: React.FC = () => {
-
+
{friend.nickname || "未知好友"}
- +
+
+ {friend.valueFormatted + || (typeof friend.value === "number" + ? `¥${friend.value.toLocaleString()}` + : "¥3")} +
+
-
- ID: {friend.wechatId || "-"} -
-
- {friend.statusTags?.map((tag, idx) => ( - - {tag} - - ))} - {friend.remark && ( - - {friend.remark} +
+
+ 微信号: + + {friend.wechatId || "-"} +
+ {(friend.accountUserName || friend.accountRealName) && ( +
+ 归属: + + {friend.accountUserName || ""} + {friend.accountRealName && `(${friend.accountRealName})`} + +
)}
-
-
-
- {friend.valueFormatted - || (typeof friend.value === "number" - ? `¥${friend.value.toLocaleString()}` - : "估值 -")} -
+ {(friend.statusTags?.length > 0 || friend.remark) && ( +
+ {friend.statusTags?.map((tag, idx) => ( + + {tag} + + ))} + {friend.remark && ( + + {friend.remark} + + )} +
+ )}
))} @@ -1405,8 +1493,122 @@ const WechatAccountDetail: React.FC = () => {
- {/* 好友详情弹窗 */} - {/* Removed */} + {/* 迁移好友弹窗 */} + { + setShowTransferFriendPopup(false); + setSelectedFriend(null); + setSelectedKefuAccountId(""); + setTransferComment(""); + }} + bodyStyle={{ borderRadius: "16px 16px 0 0" }} + > +
+
+

迁移好友

+ +
+ +
+ {/* 好友信息 */} + {selectedFriend && ( +
+ +
+ +
+
+ {selectedFriend.nickname || "未知好友"} +
+
+ {selectedFriend.wechatId || "-"} +
+
+
+
+ )} + + {/* 选择客服账号 */} +
+ + {loadingKefuAccounts ? ( +
+ + 加载中... +
+ ) : kefuAccounts.length === 0 ? ( +
暂无客服账号
+ ) : ( + + )} +
+ + {/* 备注 */} +
+ + setTransferComment(e.target.value)} + allowClear + /> +
+
+ +
+ + +
+
+
); }; From 61926503cfbece997e1ec1a3e5526e3bd57d66bd Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 11 Dec 2025 17:31:33 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E6=B5=81=E9=87=8F=E5=88=86=E5=8F=91?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E7=8A=B6=E6=80=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../traffic-distribution/list/api.ts | 1 + .../list/components/SendRcrodModal.tsx | 51 +++++++++++++++++-- .../list/index.module.scss | 46 +++++++++++++++++ .../api/controller/AutomaticAssign.php | 7 +-- 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts index fddb19bc..c8042414 100644 --- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/api.ts @@ -38,6 +38,7 @@ export function fetchTransferFriends(params: { limit?: number; keyword?: string; workbenchId: number; + isRecycle?: number; // 0=未回收, 1=已回收, undefined=全部 }) { return request("/v1/workbench/transfer-friends", params, "GET"); } diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/components/SendRcrodModal.tsx b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/components/SendRcrodModal.tsx index e9d82452..e0d5548b 100644 --- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/components/SendRcrodModal.tsx +++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/components/SendRcrodModal.tsx @@ -35,10 +35,11 @@ const SendRcrodModal: React.FC = ({ const [searchKeyword, setSearchKeyword] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [total, setTotal] = useState(0); + const [recycleFilter, setRecycleFilter] = useState(undefined); // undefined=全部, 0=未回收, 1=已回收 const pageSize = 20; // 获取分发记录数据 - const fetchSendRecords = async (page = 1, keyword = "") => { + const fetchSendRecords = async (page = 1, keyword = "", isRecycle?: number) => { if (!ruleId) return; setLoading(true); @@ -48,6 +49,7 @@ const SendRcrodModal: React.FC = ({ page, limit: pageSize, keyword, + isRecycle, }); console.log(detailRes); @@ -68,21 +70,29 @@ const SendRcrodModal: React.FC = ({ setCurrentPage(1); setSearchQuery(""); setSearchKeyword(""); - fetchSendRecords(1, ""); + setRecycleFilter(undefined); + fetchSendRecords(1, "", undefined); } }, [visible, ruleId]); // 搜索关键词变化时触发搜索 useEffect(() => { - if (!visible || !ruleId || searchKeyword === "") return; + if (!visible || !ruleId) return; setCurrentPage(1); - fetchSendRecords(1, searchKeyword); + fetchSendRecords(1, searchKeyword, recycleFilter); }, [searchKeyword]); + // 筛选条件变化时触发搜索 + useEffect(() => { + if (!visible || !ruleId) return; + setCurrentPage(1); + fetchSendRecords(1, searchKeyword, recycleFilter); + }, [recycleFilter]); + // 页码变化 useEffect(() => { if (!visible || !ruleId) return; - fetchSendRecords(currentPage, searchKeyword); + fetchSendRecords(currentPage, searchKeyword, recycleFilter); }, [currentPage]); // 处理页码变化 @@ -160,6 +170,37 @@ const SendRcrodModal: React.FC = ({ + {/* 回收状态筛选 */} +
+
回收状态:
+
+
setRecycleFilter(undefined)} + > + 全部 +
+
setRecycleFilter(0)} + > + 未回收 +
+
setRecycleFilter(1)} + > + 已回收 +
+
+
+ {/* 分发记录列表 */}
{loading ? ( diff --git a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss index 1415242c..91923e5d 100644 --- a/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/workspace/traffic-distribution/list/index.module.scss @@ -251,6 +251,52 @@ font-weight: 500; } +.filterBar { + display: flex; + align-items: center; + padding: 12px 16px; + background: white; + border-bottom: 1px solid #f0f0f0; + + .filterLabel { + font-size: 14px; + font-weight: 500; + color: #333; + margin-right: 12px; + white-space: nowrap; + } + + .filterOptions { + display: flex; + gap: 8px; + flex: 1; + + .filterOption { + flex: 1; + padding: 8px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s; + background: white; + text-align: center; + + &:hover { + border-color: #1677ff; + color: #1677ff; + } + + &.active { + background: #1677ff; + border-color: #1677ff; + color: white; + } + } + } +} + .accountModalFooter { padding: 16px 20px; border-top: 1px solid #f0f0f0; diff --git a/Server/application/api/controller/AutomaticAssign.php b/Server/application/api/controller/AutomaticAssign.php index a9e24102..6a0953cf 100644 --- a/Server/application/api/controller/AutomaticAssign.php +++ b/Server/application/api/controller/AutomaticAssign.php @@ -174,7 +174,8 @@ class AutomaticAssign extends BaseController public function allotWechatFriend($data = [],$isInner = false,$errorNum = 0) { // 获取授权token - $authorization = trim($this->request->header('authorization', $this->authorization)); + $authorization = $this->authorization; + if (empty($authorization)) { if($isInner){ return json_encode(['code'=>500,'msg'=>'缺少授权信息']); @@ -209,8 +210,8 @@ class AutomaticAssign extends BaseController // 发送请求 $url = $this->baseUrl . 'api/WechatFriend/allot?wechatFriendId='.$wechatFriendId.'¬ifyReceiver='.$notifyReceiver.'&comment='.$comment.'&toAccountId='.$toAccountId.'&optFrom='.$optFrom; $result = requestCurl($url, [], 'PUT', $header, 'json'); - - if (empty($result)) { + $response = handleApiResponse($result); + if (empty($response)) { if($isInner){ return json_encode(['code'=>200,'msg'=>'微信好友分配成功']); }else{ From b2760cde0e964140206632e2c94f2d3e8291a612 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 11 Dec 2025 17:32:27 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E7=BE=A4=E3=80=81=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=A2=E6=9C=8D=E5=BE=AE=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chukebao/controller/WechatChatroomController.php | 7 ++++++- .../chukebao/controller/WechatFriendController.php | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Server/application/chukebao/controller/WechatChatroomController.php b/Server/application/chukebao/controller/WechatChatroomController.php index 9c33baed..5d521143 100644 --- a/Server/application/chukebao/controller/WechatChatroomController.php +++ b/Server/application/chukebao/controller/WechatChatroomController.php @@ -15,7 +15,8 @@ class WechatChatroomController extends BaseController $page = $this->request->param('page', 1); $limit = $this->request->param('limit', 10); $keyword = $this->request->param('keyword', ''); - $groupIds = $this->request->param('groupIds', ''); + $groupIds = $this->request->param('groupId', ''); + $ownerWechatId = $this->request->param('ownerWechatId', ''); $accountId = $this->getUserInfo('s2_accountId'); if (empty($accountId)){ return ResponseHelper::error('请先登录'); @@ -37,6 +38,10 @@ class WechatChatroomController extends BaseController $query->where('groupIds', $groupIds); } + if (!empty($ownerWechatId)) { + $query->where('ownerWechatId', $ownerWechatId); + } + $query->order('id desc'); $total = $query->count(); $list = $query->page($page, $limit)->select(); diff --git a/Server/application/chukebao/controller/WechatFriendController.php b/Server/application/chukebao/controller/WechatFriendController.php index da4782c2..aaf541f1 100644 --- a/Server/application/chukebao/controller/WechatFriendController.php +++ b/Server/application/chukebao/controller/WechatFriendController.php @@ -14,7 +14,8 @@ class WechatFriendController extends BaseController $page = $this->request->param('page', 1); $limit = $this->request->param('limit', 10); $keyword = $this->request->param('keyword', ''); - $groupIds = $this->request->param('groupIds', ''); + $groupIds = $this->request->param('groupId', ''); + $ownerWechatId = $this->request->param('ownerWechatId', ''); $accountId = $this->getUserInfo('s2_accountId'); if (empty($accountId)) { return ResponseHelper::error('请先登录'); @@ -38,6 +39,10 @@ class WechatFriendController extends BaseController $query->where('groupIds', $groupIds); } + if (!empty($ownerWechatId)) { + $query->where('ownerWechatId', $ownerWechatId); + } + $query->order('id desc'); $total = $query->count(); $list = $query->page($page, $limit)->select(); From 08fc18da8672c73af920ff47e00bf32d19ac07e8 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 11 Dec 2025 17:32:59 +0800 Subject: [PATCH 05/11] =?UTF-8?q?php=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/cunkebao/config/route.php | 1 + .../controller/WorkbenchController.php | 10 +- .../friend/GetFriendListV1Controller.php | 136 ++++++++++++++++-- .../GetWechatOnDeviceFriendsV1Controller.php | 6 +- .../job/WorkbenchGroupCreateJob.php | 35 +++-- 5 files changed, 153 insertions(+), 35 deletions(-) diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 041d596a..0dfa952b 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -136,6 +136,7 @@ Route::group('v1/', function () { // 好友相关 Route::group('friend', function () { Route::get('', 'app\cunkebao\controller\friend\GetFriendListV1Controller@index'); // 获取好友列表 + Route::post('transfer', 'app\cunkebao\controller\friend\GetFriendListV1Controller@transfer'); // 好友转移 }); //群相关 diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 969eaeed..d330b9f0 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -1884,6 +1884,7 @@ class WorkbenchController extends Controller $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' => '参数错误']); } @@ -1897,7 +1898,7 @@ class WorkbenchController extends Controller ->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.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' ]) @@ -1908,11 +1909,18 @@ class WorkbenchController extends Controller $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); diff --git a/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php b/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php index dc7a525f..4ba300f4 100644 --- a/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php +++ b/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php @@ -5,6 +5,7 @@ use app\common\model\Device as DeviceModel; use app\common\model\DeviceUser as DeviceUserModel; use app\common\model\WechatFriendShip as WechatFriendShipModel; use app\cunkebao\controller\BaseController; +use app\api\controller\AutomaticAssign; use think\Db; /** @@ -66,29 +67,45 @@ class GetFriendListV1Controller extends BaseController $where[] = ['ownerWechatId','in',$wechatIds]; $data = Db::table('s2_wechat_friend') - ->field(['nickname','avatar','alias','id','wechatId','ownerNickname','ownerAlias','ownerWechatId','createTime']) + ->field([ + 'id', 'nickname', 'avatar', 'alias', 'wechatId', + 'gender', 'phone', 'createTime', 'updateTime', 'deleteTime', + 'ownerNickname', 'ownerAlias', 'ownerWechatId', + 'accountUserName', 'accountNickname', 'accountRealName' + ]) ->where($where); $total = $data->count(); $list = $data->page($page, $limit)->order('id DESC')->select(); - -// $data = WechatFriendShipModel::alias('wf') -// ->field(['wa1.nickname','wa1.avatar','wa1.alias','wf.id','wf.wechatId','wa2.nickname as ownerNickname','wa2.alias as ownerAlias','wa2.wechatId as ownerWechatId','wf.createTime']) -// ->Join('wechat_account wa1','wf.wechatId = wa1.wechatId') -// ->Join('wechat_account wa2','wf.ownerWechatId = wa2.wechatId') -// ->where($where); -// -// $total = $data->count(); -// $list = $data->page($page, $limit)->order('wf.id DESC')->group('wf.id')->select(); - - - + // 格式化时间字段和处理数据 + $formattedList = []; + foreach ($list as $item) { + $formattedItem = [ + 'id' => $item['id'], + 'nickname' => $item['nickname'] ?? '', + 'avatar' => $item['avatar'] ?? '', + 'alias' => $item['alias'] ?? '', + 'wechatId' => $item['wechatId'] ?? '', + 'gender' => $item['gender'] ?? 0, + 'phone' => $item['phone'] ?? '', + 'account' => $item['accountUserName'] ?? '', + 'username' => $item['accountRealName'] ?? '', + 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '1970-01-01 08:00:00', + 'updateTime' => !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '1970-01-01 08:00:00', + 'deleteTime' => !empty($item['deleteTime']) ? date('Y-m-d H:i:s', $item['deleteTime']) : '1970-01-01 08:00:00', + 'ownerNickname' => $item['ownerNickname'] ?? '', + 'ownerAlias' => $item['ownerAlias'] ?? '', + 'ownerWechatId' => $item['ownerWechatId'] ?? '', + 'accountNickname' => $item['accountNickname'] ?? '' + ]; + $formattedList[] = $formattedItem; + } return json([ 'code' => 200, 'msg' => '获取成功', 'data' => [ - 'list' => $list, + 'list' => $formattedList, 'total' => $total, 'companyId' => $this->getUserInfo('companyId') ] @@ -100,4 +117,95 @@ class GetFriendListV1Controller extends BaseController ]); } } + + /** + * 好友转移 + * @return \think\response\Json + */ + public function transfer() + { + $friendId = $this->request->param('friendId', 0); + $toAccountId = $this->request->param('toAccountId', ''); + $comment = $this->request->param('comment', ''); + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($friendId)) { + return json([ + 'code' => 400, + 'msg' => '好友ID不能为空' + ]); + } + + if (empty($toAccountId)) { + return json([ + 'code' => 400, + 'msg' => '目标账号ID不能为空' + ]); + } + + try { + // 验证目标账号是否存在且属于当前公司 + $accountInfo = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->where('departmentId', $companyId) + ->field('id as accountId, userName as accountUserName, realName as accountRealName, nickname as accountNickname, tenantId') + ->find(); + + if (empty($accountInfo)) { + return json([ + 'code' => 404, + 'msg' => '目标账号不存在' + ]); + } + + + // 调用 AutomaticAssign 进行好友转移 + $automaticAssign = new AutomaticAssign(); + $result = $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friendId, + 'toAccountId' => $toAccountId, + 'comment' => $comment, + 'notifyReceiver' => false, + 'optFrom' => 4 + ], true); + + $resultData = json_decode($result, true); + + if (!empty($resultData) && $resultData['code'] == 200) { + // 转移成功后更新数据库 + $updateData = [ + 'accountId' => $accountInfo['accountId'], + 'accountUserName' => $accountInfo['accountUserName'], + 'accountRealName' => $accountInfo['accountRealName'], + 'accountNickname' => $accountInfo['accountNickname'], + 'updateTime' => time() + ]; + + Db::table('s2_wechat_friend') + ->where('id', $friendId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '好友转移成功', + 'data' => [ + 'friendId' => $friendId, + 'toAccountId' => $toAccountId + ] + ]); + } else { + return json([ + 'code' => 500, + 'msg' => '好友转移失败:' . ($resultData['msg'] ?? '未知错误') + ]); + } + + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '好友转移失败:' . $e->getMessage() + ]); + } + } } \ No newline at end of file diff --git a/Server/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php index 9a95e862..cd544c4d 100644 --- a/Server/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php +++ b/Server/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php @@ -43,10 +43,12 @@ class GetWechatOnDeviceFriendsV1Controller extends BaseController [ 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId', 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', - 'f.memo', 'f.tags' + 'f.memo', 'f.tags', + 'ff.accountUserName', 'ff.accountRealName','ff.id AS friendId' ] ) - ->join('wechat_account w', 'w.wechatId = f.wechatId'); + ->join('wechat_account w', 'w.wechatId = f.wechatId') + ->join(['s2_wechat_friend' => 'ff'], 'ff.id = f.id'); foreach ($where as $key => $value) { if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { diff --git a/Server/application/job/WorkbenchGroupCreateJob.php b/Server/application/job/WorkbenchGroupCreateJob.php index 6225a4e4..bfb77b15 100644 --- a/Server/application/job/WorkbenchGroupCreateJob.php +++ b/Server/application/job/WorkbenchGroupCreateJob.php @@ -77,7 +77,7 @@ class WorkbenchGroupCreateJob { try { // 1. 查询启用了建群功能的数据 - $workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0])->order('id desc')->select(); + $workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0,'id' => 315])->order('id desc')->select(); foreach ($workbenches as $workbench) { // 获取工作台配置 $config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find(); @@ -120,7 +120,6 @@ class WorkbenchGroupCreateJob } } } - if (empty($groupMemberWechatId)) { continue; } @@ -150,7 +149,7 @@ class WorkbenchGroupCreateJob } // 计算随机群人数(不包含管理员,只减去群主成员数) $groupRandNum = mt_rand($config['groupSizeMin'], $config['groupSizeMax']) - count($groupMember); - + // 分批处理待入群用户 $addGroupUser = []; $totalRows = count($joinUser); @@ -168,7 +167,7 @@ class WorkbenchGroupCreateJob $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); } $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); - + // 遍历每批用户 foreach ($addGroupUser as $batchUsers) { $this->processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket); @@ -201,7 +200,7 @@ class WorkbenchGroupCreateJob $groupOwnerWechatIds[] = $member['ownerWechatId']; } } - + // 如果从好友表获取不到,使用群主成员微信ID列表(作为备用) if (empty($groupOwnerWechatIds)) { @@ -225,19 +224,20 @@ class WorkbenchGroupCreateJob } } - exit_data($adminWechatIds); + // 3. 从流量池用户中筛选出是群主好友的用户(按微信账号分组) $ownerFriendIdsByAccount = []; $wechatIds = []; // 获取群主的好友关系(从流量池中筛选) - $ownerFriends = Db::name('wechat_friendship')->alias('f') - ->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId') - ->where('f.companyId', $workbench->companyId) + $ownerFriends = Db::table('s2_wechat_friend')->alias('f') + ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id') ->whereIn('f.wechatId', $batchUsers) - ->whereIn('f.ownerWechatId', $groupOwnerWechatIds) + ->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; @@ -252,13 +252,12 @@ class WorkbenchGroupCreateJob $ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id']; $wechatIds[$friend['id']] = $friend['wechatId']; } - + // 4. 遍历每个微信账号,创建群 foreach ($ownerFriendIdsByAccount as $wechatAccountId => $ownerFriendIds) { // 4.1 获取当前账号的管理员好友ID $currentAdminFriendIds = []; $accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId'); - foreach ($adminFriendIds as $adminFriendId) { $adminFriend = Db::table('s2_wechat_friend')->where('id', $adminFriendId)->find(); if ($adminFriend && $adminFriend['ownerWechatId'] == $accountWechatId) { @@ -278,10 +277,10 @@ class WorkbenchGroupCreateJob } } } - + // 4.3 限制群主好友数量(按随机群人数) $limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum); - + // 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选) $createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds, $limitedOwnerFriendIds); @@ -379,12 +378,12 @@ class WorkbenchGroupCreateJob } // 从流量池用户中筛选出是管理员好友的用户 - $adminFriendsFromPool = Db::name('wechat_friendship')->alias('f') - ->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId') - ->where('f.companyId', $workbench->companyId) + $adminFriendsFromPool = Db::table('s2_wechat_friend')->alias('f') + ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id') ->whereIn('f.wechatId', $batchUsers) - ->whereIn('f.ownerWechatId', $adminWechatIds) + ->whereIn('a.wechatId', $adminWechatIds) ->where('a.id', $wechatAccountId) + ->where('f.isDeleted', 0) ->field('f.id,f.wechatId') ->select(); From 7dda34a7793d83464f576f628141e83adf517bb3 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 17 Dec 2025 16:20:46 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E5=88=86=E9=94=80=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/src/components/NavCommon/index.tsx | 2 +- .../wechat-accounts/detail/detail.module.scss | 60 +- .../mobile/scenarios/plan/new/index.data.ts | 19 + .../pages/mobile/scenarios/plan/new/index.tsx | 89 +- .../plan/new/steps/BasicSettings.tsx | 512 ++++++- .../scenarios/plan/new/steps/base.module.scss | 211 +++ .../workspace/distribution-management/api.ts | 145 ++ .../components/AddChannelModal.module.scss | 290 ++++ .../components/AddChannelModal.tsx | 431 ++++++ .../workspace/distribution-management/data.ts | 67 + .../distribution-management/detail/api.ts | 192 +++ .../distribution-management/detail/data.ts | 52 + .../detail/index.module.scss | 574 ++++++++ .../distribution-management/detail/index.tsx | 623 +++++++++ .../distribution-management/index.module.scss | 1015 ++++++++++++++ .../distribution-management/index.tsx | 1205 +++++++++++++++++ .../src/pages/mobile/workspace/main/index.tsx | 15 + Cunkebao/src/router/module/workspace.tsx | 15 +- Server/application/cunkebao/config/route.php | 41 + .../distribution/ChannelController.php | 1163 ++++++++++++++++ .../distribution/ChannelUserController.php | 590 ++++++++ .../distribution/WithdrawalController.php | 670 +++++++++ .../GetAddFriendPlanDetailV1Controller.php | 34 + .../PostCreateAddFriendPlanV1Controller.php | 84 +- .../plan/PostExternalApiV1Controller.php | 52 +- .../PostUpdateAddFriendPlanV1Controller.php | 85 +- .../plan/PosterWeChatMiniProgram.php | 106 +- .../wechat/GetWechatMomentsV1Controller.php | 3 +- .../cunkebao/model/DistributionChannel.php | 87 ++ .../cunkebao/model/DistributionWithdrawal.php | 71 + .../service/DistributionRewardService.php | 276 ++++ .../cunkebao/validate/DistributionChannel.php | 31 + .../Adapters/ChuKeBao/Adapter.php | 35 + Server/sql.sql | 219 ++- 34 files changed, 8959 insertions(+), 105 deletions(-) create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/detail/api.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/detail/data.ts create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/detail/index.tsx create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/index.module.scss create mode 100644 Cunkebao/src/pages/mobile/workspace/distribution-management/index.tsx create mode 100644 Server/application/cunkebao/controller/distribution/ChannelController.php create mode 100644 Server/application/cunkebao/controller/distribution/ChannelUserController.php create mode 100644 Server/application/cunkebao/controller/distribution/WithdrawalController.php create mode 100644 Server/application/cunkebao/model/DistributionChannel.php create mode 100644 Server/application/cunkebao/model/DistributionWithdrawal.php create mode 100644 Server/application/cunkebao/service/DistributionRewardService.php create mode 100644 Server/application/cunkebao/validate/DistributionChannel.php diff --git a/Cunkebao/src/components/NavCommon/index.tsx b/Cunkebao/src/components/NavCommon/index.tsx index 0ee5ec1b..5085a6ea 100644 --- a/Cunkebao/src/components/NavCommon/index.tsx +++ b/Cunkebao/src/components/NavCommon/index.tsx @@ -4,7 +4,7 @@ import { ArrowLeftOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import { getSafeAreaHeight } from "@/utils/common"; interface NavCommonProps { - title: string; + title: string | React.ReactNode; backFn?: () => void; right?: React.ReactNode; left?: React.ReactNode; diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index 41530573..0f6ed761 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -709,7 +709,7 @@ .adm-avatar { width: 52px; height: 52px; - border-radius: 50%; + border-radius: 50%; border: 2px solid #f0f0f0; } } @@ -723,13 +723,13 @@ } .friend-header { - display: flex; - align-items: center; + display: flex; + align-items: center; justify-content: space-between; gap: 12px; } - .friend-name { + .friend-name { font-size: 16px; font-weight: 600; color: #111; @@ -754,7 +754,7 @@ display: flex; flex-direction: column; gap: 4px; - } + } .friend-info-item { font-size: 13px; @@ -764,7 +764,7 @@ gap: 4px; .info-label { - color: #999; + color: #999; flex-shrink: 0; } @@ -775,11 +775,11 @@ text-overflow: ellipsis; white-space: nowrap; } - } + } .friend-tags { - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; gap: 6px; margin-top: 4px; } @@ -866,11 +866,11 @@ margin-top: 20px; } - .popup-footer { - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid #f0f0f0; - } + .popup-footer { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + } .friend-info-card { display: flex; @@ -1441,22 +1441,22 @@ } } -.moments-action-bar { - display: flex; - justify-content: space-between; + .moments-action-bar { + display: flex; + justify-content: space-between; align-items: center; padding: 12px 16px; background: white; border-bottom: 1px solid #f0f0f0; - .action-button, .action-button-dark { - display: flex; - align-items: center; - justify-content: center; + .action-button, .action-button-dark { + display: flex; + align-items: center; + justify-content: center; width: 40px; - height: 40px; - border-radius: 8px; - background: #1677ff; + height: 40px; + border-radius: 8px; + background: #1677ff; border: none; cursor: pointer; transition: all 0.2s; @@ -1465,22 +1465,22 @@ &:active { background: #0958d9; transform: scale(0.95); - } + } svg { font-size: 20px; - color: white; - } - } + color: white; + } + } - .action-button-dark { + .action-button-dark { background: #1677ff; color: white; &:active { background: #0958d9; + } } - } } .moments-content { diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts index 3cd21563..a3921add 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.data.ts @@ -30,6 +30,20 @@ export interface FormData { wechatGroups: string[]; wechatGroupsOptions: GroupSelectionItem[]; messagePlans: any[]; + // 分销相关 + distributionEnabled?: boolean; + // 选中的分销渠道ID列表(前端使用,提交时转为 distributionChannels) + distributionChannelIds?: Array; + // 选中的分销渠道选项(用于回显名称) + distributionChannelsOptions?: Array<{ + id: string | number; + name: string; + code?: string; + }>; + // 获客奖励金额(元,前端使用,提交时转为 customerRewardAmount) + distributionCustomerReward?: number; + // 添加奖励金额(元,前端使用,提交时转为 addFriendRewardAmount) + distributionAddReward?: number; [key: string]: any; } export const defFormData: FormData = { @@ -56,4 +70,9 @@ export const defFormData: FormData = { wechatGroupsOptions: [], contentGroups: [], contentGroupsOptions: [], + distributionEnabled: false, + distributionChannelIds: [], + distributionChannelsOptions: [], + distributionCustomerReward: undefined, + distributionAddReward: undefined, }; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx index e7032d04..b9c4a20b 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/index.tsx @@ -15,6 +15,7 @@ import { updatePlan, } from "./index.api"; import { FormData, defFormData, steps } from "./index.data"; +import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api"; export default function NewPlan() { const router = useNavigate(); @@ -49,6 +50,42 @@ export default function NewPlan() { //获取计划详情 const detail = await getPlanDetail(planId); + + // 处理分销相关数据回填 + const distributionChannels = detail.distributionChannels || []; + let distributionChannelsOptions: Array<{ id: string | number; name: string; code?: string }> = []; + + if (distributionChannels.length > 0) { + // 判断 distributionChannels 是对象数组还是ID数组 + const isObjectArray = distributionChannels.some((item: any) => typeof item === 'object' && item !== null); + + if (isObjectArray) { + // 如果已经是对象数组,直接使用(包含 id, code, name) + distributionChannelsOptions = distributionChannels.map((channel: any) => ({ + id: channel.id, + name: channel.name || `渠道${channel.id}`, + code: channel.code, + })); + } else { + // 如果是ID数组,需要查询渠道信息 + try { + const channelRes = await fetchChannelList({ page: 1, limit: 200, status: "enabled" }); + distributionChannelsOptions = distributionChannels.map((channelId: number) => { + const channel = channelRes.list.find((c: any) => c.id === channelId); + return channel + ? { id: channelId, name: channel.name, code: channel.code } + : { id: channelId, name: `渠道${channelId}` }; + }); + } catch { + // 如果获取渠道信息失败,使用默认名称 + distributionChannelsOptions = distributionChannels.map((channelId: number) => ({ + id: channelId, + name: `渠道${channelId}`, + })); + } + } + } + setFormData(prev => ({ ...prev, name: detail.name ?? "", @@ -76,6 +113,12 @@ export default function NewPlan() { contentGroupsOptions: detail.contentGroupsOptions ?? [], status: detail.status ?? 0, messagePlans: detail.messagePlans ?? [], + // 分销相关数据回填 + distributionEnabled: detail.distributionEnabled ?? false, + distributionChannelIds: distributionChannelsOptions.map(item => item.id), + distributionChannelsOptions: distributionChannelsOptions, + distributionCustomerReward: detail.customerRewardAmount, + distributionAddReward: detail.addFriendRewardAmount, })); } else { if (scenarioId) { @@ -118,21 +161,45 @@ export default function NewPlan() { setSubmitting(true); try { + // 构建提交数据,转换分销相关字段为接口需要的格式 + const submitData: any = { + ...formData, + sceneId: Number(formData.scenario), + }; + + // 转换分销相关字段为接口需要的格式 + if (formData.distributionEnabled) { + submitData.distributionEnabled = true; + // 转换渠道ID数组,确保都是数字类型 + submitData.distributionChannels = (formData.distributionChannelIds || []).map(id => + typeof id === 'string' ? Number(id) : id + ); + // 转换奖励金额,确保是浮点数,最多2位小数 + submitData.customerRewardAmount = formData.distributionCustomerReward + ? Number(Number(formData.distributionCustomerReward).toFixed(2)) + : 0; + submitData.addFriendRewardAmount = formData.distributionAddReward + ? Number(Number(formData.distributionAddReward).toFixed(2)) + : 0; + } else { + // 如果未开启分销,设置为false + submitData.distributionEnabled = false; + } + + // 移除前端使用的字段,避免提交到后端 + delete submitData.distributionChannelIds; + delete submitData.distributionChannelsOptions; + delete submitData.distributionCustomerReward; + delete submitData.distributionAddReward; + if (isEdit && planId) { // 编辑:拼接后端需要的完整参数 - const editData = { - ...formData, - ...{ sceneId: Number(formData.scenario) }, - id: Number(planId), - planId: Number(planId), - // 兼容后端需要的字段 - // 你可以根据实际需要补充其它字段 - }; - await updatePlan(editData); + submitData.id = Number(planId); + submitData.planId = Number(planId); + await updatePlan(submitData); } else { // 新建 - formData.sceneId = Number(formData.scenario); - await createPlan(formData); + await createPlan(submitData); } message.success(isEdit ? "计划已更新" : "获客计划已创建"); const sceneItem = sceneList.find(v => formData.scenario === v.id); diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 6c61b6da..0573b0d2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,17 +1,24 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Input, Button, Tag, Switch, Modal, Spin } from "antd"; +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { Input, Button, Tag, Switch, Spin, message, Modal } from "antd"; import { PlusOutlined, EyeOutlined, CloseOutlined, DownloadOutlined, + SearchOutlined, + DeleteOutlined, } from "@ant-design/icons"; +import { Checkbox, Popup } from "antd-mobile"; import { uploadFile } from "@/api/common"; import styles from "./base.module.scss"; +import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api"; import { posterTemplates } from "./base.data"; import GroupSelection from "@/components/GroupSelection"; import FileUpload from "@/components/Upload/FileUpload"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; interface BasicSettingsProps { isEdit: boolean; @@ -68,6 +75,26 @@ const BasicSettings: React.FC = ({ questionExtraction: formData.phoneSettings?.questionExtraction ?? true, }); + // 分销相关状态 + const [distributionEnabled, setDistributionEnabled] = useState( + formData.distributionEnabled ?? false, + ); + const [channelModalVisible, setChannelModalVisible] = useState(false); + const [channelLoading, setChannelLoading] = useState(false); + const [channelList, setChannelList] = useState([]); + const [tempSelectedChannelIds, setTempSelectedChannelIds] = useState< + Array + >(formData.distributionChannelIds || []); + const [channelSearchQuery, setChannelSearchQuery] = useState(""); + const [channelCurrentPage, setChannelCurrentPage] = useState(1); + const [channelTotal, setChannelTotal] = useState(0); + const [customerReward, setCustomerReward] = useState( + formData.distributionCustomerReward + ); + const [addReward, setAddReward] = useState( + formData.distributionAddReward + ); + // 新增:自定义海报相关状态 const [customPosters, setCustomPosters] = useState([]); const [previewUrl, setPreviewUrl] = useState(null); @@ -97,6 +124,19 @@ const BasicSettings: React.FC = ({ setTips(formData.tips || ""); }, [formData.tips]); + // 同步分销相关的外部表单数据到本地状态 + useEffect(() => { + setDistributionEnabled(formData.distributionEnabled ?? false); + setTempSelectedChannelIds(formData.distributionChannelIds || []); + setCustomerReward(formData.distributionCustomerReward); + setAddReward(formData.distributionAddReward); + }, [ + formData.distributionEnabled, + formData.distributionChannelIds, + formData.distributionCustomerReward, + formData.distributionAddReward, + ]); + // 选中场景 const handleScenarioSelect = (sceneId: number) => { onChange({ ...formData, scenario: sceneId }); @@ -225,6 +265,181 @@ const BasicSettings: React.FC = ({ wechatGroupsOptions: groups, }); }; + + const PAGE_SIZE = 20; + + // 加载分销渠道列表,支持keyword和分页,强制只获取启用的渠道 + const loadDistributionChannels = useCallback( + async (keyword: string = "", page: number = 1) => { + setChannelLoading(true); + try { + const res = await fetchChannelList({ + page, + limit: PAGE_SIZE, + keyword: keyword.trim() || undefined, + status: "enabled", // 强制只获取启用的渠道 + }); + setChannelList(res.list || []); + setChannelTotal(res.total || 0); + } catch (error: any) { + + } finally { + setChannelLoading(false); + } + }, + [] + ); + + const handleToggleDistribution = (value: boolean) => { + setDistributionEnabled(value); + // 关闭时清空已选渠道和奖励金额 + if (!value) { + setTempSelectedChannelIds([]); + setCustomerReward(undefined); + setAddReward(undefined); + onChange({ + ...formData, + distributionEnabled: false, + distributionChannelIds: [], + distributionChannelsOptions: [], + distributionCustomerReward: undefined, + distributionAddReward: undefined, + }); + } else { + onChange({ + ...formData, + distributionEnabled: true, + }); + } + }; + + // 打开弹窗时获取第一页 + useEffect(() => { + if (channelModalVisible) { + setChannelSearchQuery(""); + setChannelCurrentPage(1); + // 复制一份已选渠道到临时变量 + setTempSelectedChannelIds(formData.distributionChannelIds || []); + loadDistributionChannels("", 1); + } + }, [channelModalVisible, loadDistributionChannels, formData.distributionChannelIds]); + + // 搜索防抖 + useEffect(() => { + if (!channelModalVisible) return; + const timer = setTimeout(() => { + setChannelCurrentPage(1); + loadDistributionChannels(channelSearchQuery, 1); + }, 500); + return () => clearTimeout(timer); + }, [channelSearchQuery, channelModalVisible, loadDistributionChannels]); + + // 翻页时重新请求 + useEffect(() => { + if (!channelModalVisible) return; + loadDistributionChannels(channelSearchQuery, channelCurrentPage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [channelCurrentPage]); + + const handleOpenChannelModal = () => { + setChannelModalVisible(true); + }; + + const handleChannelToggle = (channel: any) => { + const id = channel.id; + setTempSelectedChannelIds(prev => + prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id], + ); + }; + + // 直接使用从API返回的渠道列表(API已过滤为只返回启用的) + const filteredChannels = channelList; + + const channelTotalPages = Math.max(1, Math.ceil(channelTotal / PAGE_SIZE)); + + // 全选当前页 + const handleSelectAllCurrentPage = (checked: boolean) => { + if (checked) { + // 全选:添加当前页面所有未选中的渠道 + const currentPageChannels = filteredChannels.filter( + (channel: any) => !tempSelectedChannelIds.includes(channel.id), + ); + setTempSelectedChannelIds(prev => [ + ...prev, + ...currentPageChannels.map((c: any) => c.id), + ]); + } else { + // 取消全选:移除当前页面的所有渠道 + const currentPageChannelIds = filteredChannels.map((c: any) => c.id); + setTempSelectedChannelIds(prev => + prev.filter(id => !currentPageChannelIds.includes(id)), + ); + } + }; + + // 检查当前页是否全选 + const isCurrentPageAllSelected = + filteredChannels.length > 0 && + filteredChannels.every((channel: any) => + tempSelectedChannelIds.includes(channel.id), + ); + + const handleConfirmChannels = () => { + const selectedOptions = + channelList + .filter(c => tempSelectedChannelIds.includes(c.id)) + .map(c => ({ + id: c.id, + name: c.name, + })) || []; + + onChange({ + ...formData, + distributionEnabled: true, + distributionChannelIds: tempSelectedChannelIds, + distributionChannelsOptions: selectedOptions, + }); + setDistributionEnabled(true); + setChannelModalVisible(false); + }; + + const handleCancelChannels = () => { + setChannelModalVisible(false); + // 取消时恢复为表单中的已有值 + setTempSelectedChannelIds(formData.distributionChannelIds || []); + }; + + // 获取显示文本(参考设备选择) + const getChannelDisplayText = () => { + const selectedChannels = formData.distributionChannelsOptions || []; + if (selectedChannels.length === 0) return ""; + return `已选择 ${selectedChannels.length} 个渠道`; + }; + + // 删除已选渠道 + const handleRemoveChannel = (id: string | number) => { + const newChannelIds = (formData.distributionChannelIds || []).filter( + (cid: string | number) => cid !== id + ); + const newChannelOptions = (formData.distributionChannelsOptions || []).filter( + (item: { id: string | number; name: string }) => item.id !== id + ); + onChange({ + ...formData, + distributionChannelIds: newChannelIds, + distributionChannelsOptions: newChannelOptions, + }); + }; + + // 清除所有已选渠道 + const handleClearAllChannels = () => { + onChange({ + ...formData, + distributionChannelIds: [], + distributionChannelsOptions: [], + }); + }; + return (
{/* 场景选择区块 */} @@ -473,6 +688,190 @@ const BasicSettings: React.FC = ({
)} + {/* 分销设置 */} +
+
+
+
分销设置
+
+ 开启后,可将当前场景的获客用户同步到指定分销渠道 +
+
+ +
+ + {distributionEnabled && ( + <> + {/* 输入框 - 参考设备选择样式 */} +
+ } + allowClear + onClear={handleClearAllChannels} + size="large" + readOnly + style={{ cursor: "pointer" }} + /> +
+ {/* 已选渠道列表 - 参考设备选择样式 */} + {formData.distributionChannelsOptions && + formData.distributionChannelsOptions.length > 0 ? ( +
+ {formData.distributionChannelsOptions.map( + (item: { id: string | number; name: string; code?: string }) => ( +
+ {/* 渠道图标 */} +
+ + {(item.name || "渠")[0]} + +
+ +
+
+ {item.name} +
+ {item.code && ( +
+ 编码: {item.code} +
+ )} +
+
+ ), + )} +
+ ) : null} + {/* 奖励金额设置 */} +
+
获客奖励金额(元)
+ { + const value = e.target.value ? Number(e.target.value) : undefined; + setCustomerReward(value); + onChange({ + ...formData, + distributionCustomerReward: value, + }); + }} + min={0} + step={0.01} + style={{ marginBottom: 12 }} + /> +
添加奖励金额(元)
+ { + const value = e.target.value ? Number(e.target.value) : undefined; + setAddReward(value); + onChange({ + ...formData, + distributionAddReward: value, + }); + }} + min={0} + step={0.01} + /> +
+ + )} +
+ {/* 订单导入区块 - 使用FileUpload组件 */}
订单表格上传
@@ -559,6 +958,115 @@ const BasicSettings: React.FC = ({ onChange={value => onChange({ ...formData, status: value ? 1 : 0 })} />
+ + {/* 分销渠道选择弹框 - 参考设备选择样式 */} + + loadDistributionChannels(channelSearchQuery, channelCurrentPage)} + showTabs={false} + /> + } + footer={ + + } + > +
+ {channelLoading && channelList.length === 0 ? ( +
+
加载中...
+
+ ) : filteredChannels.length === 0 ? ( +
+
+ 暂无分销渠道,请先在「分销管理」中创建渠道 +
+
+ ) : ( +
+ {filteredChannels.map((channel: any) => ( +
+ {/* 顶部行:选择框和编码 */} +
+
+ handleChannelToggle(channel)} + className={styles["channelCheckbox"]} + /> +
+ + 编码: {channel.code} + +
+ + {/* 主要内容区域:渠道信息 */} +
+ {/* 渠道信息 */} +
+
+ + {channel.name} + +
+ {channel.status === "enabled" ? "启用" : "禁用"} +
+
+
+ {channel.phone && ( +
+ 手机号: + + {channel.phone} + +
+ )} + {channel.wechatId && ( +
+ 微信号: + + {channel.wechatId} + +
+ )} +
+
+
+
+ ))} +
+ )} +
+
+
); }; diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss index aac76ca4..a51e743c 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss @@ -161,3 +161,214 @@ justify-content: space-between; margin: 16px 0; } + +// 分销渠道选择弹框样式 - 参考设备选择 +.channelList { + flex: 1; + overflow-y: auto; +} + +.channelListInner { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} + +.channelItem { + display: flex; + flex-direction: column; + padding: 12px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; + border: 1px solid #f5f5f5; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + } +} + +.headerRow { + display: flex; + align-items: center; + gap: 8px; +} + +.checkboxContainer { + flex-shrink: 0; +} + +.codeText { + font-size: 13px; + color: #666; + font-family: monospace; + flex: 1; +} + +.mainContent { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: background-color 0.2s ease; + + &:hover { + background-color: #f8f9fa; + } +} + +.channelCheckbox { + flex-shrink: 0; +} + +.channelContent { + flex: 1; + min-width: 0; +} + +.channelInfoRow { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.channelName { + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statusEnabled { + font-size: 11px; + padding: 1px 6px; + border-radius: 8px; + color: #52c41a; + background: #f6ffed; + border: 1px solid #b7eb8f; + font-weight: 500; +} + +.statusDisabled { + font-size: 11px; + padding: 1px 6px; + border-radius: 8px; + color: #ff4d4f; + background: #fff2f0; + border: 1px solid #ffccc7; + font-weight: 500; +} + +.channelInfoDetail { + display: flex; + flex-direction: column; + gap: 4px; +} + +.infoItem { + display: flex; + align-items: center; + gap: 8px; +} + +.infoLabel { + font-size: 13px; + color: #666; + min-width: 60px; +} + +.infoValue { + font-size: 13px; + color: #333; + + &.customerCount { + font-weight: 500; + } +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.loadingText { + color: #888; + font-size: 15px; +} + +// 分销设置 +.basic-distribution { + margin: 16px 0; + padding: 16px; + background: #f7f8fa; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); + + .basic-distribution-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + + .basic-distribution-title { + font-size: 15px; + font-weight: 500; + color: #333; + } + + .basic-distribution-desc { + font-size: 12px; + color: #999; + margin-top: 4px; + } + } + + .distribution-input-wrapper { + position: relative; + margin-top: 12px; + + .ant-input { + padding-left: 38px !important; + height: 56px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; + } + } + + .distribution-selected-list { + .distribution-selected-item { + &:last-child { + border-bottom: none; + } + } + } + + .distribution-rewards { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #e5e6eb; + } +} + +.basic-distribution-modal-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.basic-distribution-modal-tag { + margin-bottom: 4px; +} diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts b/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts new file mode 100644 index 00000000..6f637d33 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts @@ -0,0 +1,145 @@ +// 分销管理 API + +import request from "@/api/request"; +import type { + Channel, + Statistics, + FundStatistics, + ChannelEarnings, + WithdrawalRequest, + WithdrawalStatus, +} from "./data"; + +// 获取统计数据 +export const fetchStatistics = async (): Promise => { + return request("/v1/distribution/channels/statistics", {}, "GET"); +}; + +// 获取渠道列表 +export const fetchChannelList = async (params: { + page?: number; + limit?: number; + keyword?: string; + status?: "enabled" | "disabled"; // 渠道状态筛选 +}): Promise<{ list: Channel[]; total: number }> => { + return request("/v1/distribution/channels", params, "GET"); +}; + +// 创建渠道 +export const createChannel = async (data: { + name: string; + phone?: string; + wechatId?: string; + remarks?: string; +}): Promise => { + return request("/v1/distribution/channel", data, "POST"); +}; + +// 更新渠道 +export const updateChannel = async ( + id: string, + data: { + name: string; + phone?: string; + wechatId?: string; + remarks?: string; + }, +): Promise => { + return request(`/v1/distribution/channel/${id}`, data, "PUT"); +}; + +// 删除渠道 +export const deleteChannel = async (id: string): Promise => { + return request(`/v1/distribution/channel/${id}`, null, "DELETE"); +}; + +// 禁用/启用渠道 +export const toggleChannelStatus = async ( + id: string, + status: "enabled" | "disabled", +): Promise => { + return request(`/v1/distribution/channel/${id}/status`, { status }, "PUT"); +}; + +// 获取资金统计数据 +export const fetchFundStatistics = async (): Promise => { + return request("/v1/distribution/channels/revenue-statistics", {}, "GET"); +}; + +// 获取渠道收益列表 +export const fetchChannelEarningsList = async (params: { + page?: number; + limit?: number; + keyword?: string; +}): Promise<{ list: ChannelEarnings[]; total: number }> => { + const queryParams: any = {}; + if (params.page) queryParams.page = params.page; + if (params.limit) queryParams.limit = params.limit; + if (params.keyword) queryParams.keyword = params.keyword; + + return request("/v1/distribution/channels/revenue-detail", queryParams, "GET"); +}; + +// 获取提现申请列表 +export const fetchWithdrawalList = async (params: { + page?: number; + limit?: number; + status?: WithdrawalStatus; + date?: string; + keyword?: string; +}): Promise<{ list: WithdrawalRequest[]; total: number }> => { + const queryParams: any = {}; + if (params.page) queryParams.page = params.page; + if (params.limit) queryParams.limit = params.limit; + if (params.status && params.status !== "all") { + queryParams.status = params.status; + } + if (params.date) queryParams.date = params.date; + if (params.keyword) queryParams.keyword = params.keyword; + + return request("/v1/distribution/withdrawals", queryParams, "GET"); +}; + +// 审核提现申请 +export const reviewWithdrawal = async ( + id: string, + action: "approve" | "reject", + remark?: string, +): Promise => { + const data: any = { action }; + // 拒绝时 remark 必填,通过时可选 + if (action === "reject") { + if (!remark || !remark.trim()) { + throw new Error("拒绝时必须填写审核备注"); + } + data.remark = remark.trim(); + } else if (remark) { + // 通过时如果有备注也传递 + data.remark = remark.trim(); + } + return request(`/v1/distribution/withdrawals/${id}/review`, data, "POST"); +}; + +// 标记为已打款 +export const markAsPaid = async ( + id: string, + payType: "wechat" | "alipay" | "bankcard", + remark?: string, +): Promise => { + const data: any = { payType }; + if (remark) { + data.remark = remark.trim(); + } + return request(`/v1/distribution/withdrawals/${id}/mark-paid`, data, "POST"); +}; + +// 生成二维码 +export const generateQRCode = async ( + type: "h5" | "miniprogram", +): Promise<{ + type: "h5" | "miniprogram"; + qrCode: string; + url: string; +}> => { + return request("/v1/distribution/channel/generate-qrcode", { type }, "POST"); +}; diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.module.scss b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.module.scss new file mode 100644 index 00000000..c677408e --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.module.scss @@ -0,0 +1,290 @@ +.modalWrapper { + :global(.ant-modal-content) { + padding: 0; + border-radius: 16px; + overflow: hidden; + } + + :global(.ant-modal-body) { + padding: 0; + max-height: 85vh; + overflow: hidden; + } +} + +.modal { + display: flex; + flex-direction: column; + max-height: 85vh; + background: #fff; +} + +// 头部 +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 20px; + border-bottom: 1px solid #f0f0f0; +} + +.headerLeft { + flex: 1; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #222; + margin: 0 0 8px 0; +} + +.subtitle { + font-size: 12px; + color: #888; + margin: 0; + line-height: 1.4; +} + +.closeBtn { + font-size: 20px; + color: #888; + cursor: pointer; + padding: 4px; + flex-shrink: 0; + margin-left: 12px; + + &:hover { + color: #222; + } +} + +// 创建方式选择 +.methodTabs { + display: flex; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; +} + +.methodTab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background: #f8f9fa; + color: #666; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + + &.active { + background: #fff; + border-color: #1890ff; + color: #222; + font-weight: 500; + } +} + +.tabIcon { + font-size: 16px; +} + +// 内容区域 +.content { + flex: 1; + overflow-y: auto; + padding: 20px; + min-height: 0; +} + +// 表单样式 +.form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.formItem { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: 14px; + color: #222; + font-weight: 500; +} + +.required { + color: #ff4d4f; + margin-left: 2px; +} + +.input { + :global(.ant-input) { + border-radius: 8px; + height: 44px; + font-size: 14px; + } +} + +.phoneHint { + margin-top: 4px; + min-height: 18px; + display: flex; + align-items: center; +} + +.textarea { + :global(.adm-text-area) { + border-radius: 8px; + font-size: 14px; + padding: 12px; + } +} + +// 扫码创建样式 +.scanContent { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 0; +} + +.qrCodeContainer { + margin-bottom: 20px; +} + +.qrCodeBox { + width: 200px; + height: 200px; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.qrCode { + width: 100%; + height: 100%; + object-fit: contain; +} + +.qrCodePlaceholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.qrCodeIcon { + font-size: 80px; + color: #ddd; +} + +.scanInstruction { + font-size: 16px; + color: #222; + margin: 0 0 8px 0; + font-weight: 500; +} + +.scanDescription { + font-size: 13px; + color: #888; + margin: 0 0 20px 0; + text-align: center; +} + +.qrCodeTypeSelector { + width: 100%; + margin-bottom: 24px; +} + +.typeTabs { + display: flex; + gap: 12px; + background: #f5f5f5; + padding: 4px; + border-radius: 8px; +} + +.typeTab { + flex: 1; + padding: 8px 16px; + border: none; + border-radius: 6px; + background: transparent; + color: #666; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: #1890ff; + } + + &.active { + background: #fff; + color: #1890ff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } +} + +// 底部按钮 +.footer { + display: flex; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #f0f0f0; + background: #fff; +} + +.cancelBtn { + flex: 1; + height: 44px; + font-size: 16px; +} + +.submitBtn { + flex: 1; + height: 44px; + font-size: 16px; +} + +// 响应式 +@media (max-width: 375px) { + .header { + padding: 16px; + } + + .methodTabs { + padding: 12px 16px; + } + + .content { + padding: 16px; + } + + .qrCodeBox { + width: 180px; + height: 180px; + } +} diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx new file mode 100644 index 00000000..6f2fe465 --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx @@ -0,0 +1,431 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Button, TextArea, SpinLoading } from "antd-mobile"; +import { Modal, Input, message } from "antd"; +import { CloseOutlined, UserOutlined, QrcodeOutlined } from "@ant-design/icons"; +import { generateQRCode } from "../api"; +import styles from "./AddChannelModal.module.scss"; + +interface AddChannelModalProps { + visible: boolean; + onClose: () => void; + editData?: { + id: string; + name: string; + phone?: string; + wechatId?: string; + remarks?: string; + }; + onSubmit?: (data: { + id?: string; + name: string; + phone?: string; + wechatId?: string; + remarks?: string; + }) => void; +} + +type CreateMethod = "manual" | "scan"; + +const AddChannelModal: React.FC = ({ + visible, + onClose, + editData, + onSubmit, +}) => { + const isEdit = !!editData; + const [createMethod, setCreateMethod] = useState("manual"); + const [formData, setFormData] = useState({ + name: "", + phone: "", + wechatId: "", + remarks: "", + }); + const [loading, setLoading] = useState(false); + const [scanning, setScanning] = useState(false); + const [qrCodeType, setQrCodeType] = useState<"h5" | "miniprogram">("h5"); + const [qrCodeData, setQrCodeData] = useState<{ + qrCode: string; + url: string; + type: "h5" | "miniprogram"; + } | null>(null); + const [qrCodeLoading, setQrCodeLoading] = useState(false); + const generatingRef = useRef(false); // 用于防止重复请求 + + // 当编辑数据变化时,更新表单数据 + useEffect(() => { + if (editData) { + setFormData({ + name: editData.name || "", + phone: editData.phone || "", + wechatId: editData.wechatId || "", + remarks: editData.remarks || "", + }); + } else { + setFormData({ + name: "", + phone: "", + wechatId: "", + remarks: "", + }); + } + }, [editData, visible]); + + // 当弹窗打开或切换到扫码创建时,自动生成二维码 + useEffect(() => { + // 只有在弹窗可见、非编辑模式、选择扫码方式、没有二维码数据、且不在加载中时才生成 + if (visible && !isEdit && createMethod === "scan" && !qrCodeData && !qrCodeLoading && !generatingRef.current) { + // 使用 setTimeout 确保状态更新完成 + const timer = setTimeout(() => { + handleGenerateQRCode(); + }, 100); + return () => clearTimeout(timer); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible, createMethod]); + + // 当二维码类型变化时,重新生成二维码 + useEffect(() => { + if (visible && !isEdit && createMethod === "scan" && qrCodeData && !qrCodeLoading && !generatingRef.current) { + // 重置状态后重新生成 + setQrCodeData(null); + setScanning(false); + // 使用 setTimeout 确保状态更新完成 + const timer = setTimeout(() => { + handleGenerateQRCode(); + }, 100); + return () => clearTimeout(timer); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [qrCodeType]); + + // 验证手机号格式 + const validatePhone = (phone: string): boolean => { + if (!phone) return true; // 手机号是可选的,空值视为有效 + const phoneRegex = /^1[3-9]\d{9}$/; + return phoneRegex.test(phone); + }; + + // 处理手机号输入,只允许输入数字,最多11位 + const handlePhoneChange = (value: string) => { + // 只保留数字 + const numbersOnly = value.replace(/\D/g, ""); + // 限制最多11位 + const limitedValue = numbersOnly.slice(0, 11); + handleInputChange("phone", limitedValue); + }; + + const handleInputChange = (field: string, value: string) => { + setFormData(prev => ({ + ...prev, + [field]: value, + })); + }; + + const handleSubmit = async () => { + if (createMethod === "manual") { + if (!formData.name.trim()) { + message.error("请输入渠道名称"); + return; + } + + // 验证手机号格式 + if (formData.phone && formData.phone.trim()) { + if (!validatePhone(formData.phone.trim())) { + message.error("请输入正确的手机号(11位数字,1开头)"); + return; + } + } + + setLoading(true); + try { + await onSubmit?.({ + id: editData?.id, + name: formData.name.trim(), + phone: formData.phone?.trim() || undefined, + wechatId: formData.wechatId?.trim() || undefined, + remarks: formData.remarks?.trim() || undefined, + }); + // 成功后关闭弹窗(父组件会处理成功提示) + handleClose(); + } catch (e) { + // 错误已在父组件处理,这里不需要再次提示 + // 保持弹窗打开,让用户修改后重试 + } finally { + setLoading(false); + } + } else { + // 扫码创建逻辑 + if (!scanning) { + setScanning(true); + // TODO: 实现扫码创建逻辑 + message.info("扫码创建功能开发中"); + } + } + }; + + const handleClose = () => { + setFormData({ + name: "", + phone: "", + wechatId: "", + remarks: "", + }); + setScanning(false); + setQrCodeData(null); + setQrCodeType("h5"); + onClose(); + }; + + // 生成二维码 + const handleGenerateQRCode = async () => { + // 如果正在生成,直接返回,避免重复请求 + if (generatingRef.current || qrCodeLoading) { + return; + } + generatingRef.current = true; + setQrCodeLoading(true); + try { + const res = await generateQRCode(qrCodeType); + // 确保返回的数据有效 + if (res && res.qrCode) { + setQrCodeData(res); + setScanning(true); + } else { + throw new Error("二维码数据格式错误"); + } + } catch (e: any) { + // 接口拦截器已经显示了错误提示,这里不需要再次显示 + // 请求失败时重置状态,允许重试 + setQrCodeData(null); + setScanning(false); + } finally { + setQrCodeLoading(false); + generatingRef.current = false; + } + }; + + // 重新生成二维码 + const handleRegenerateQR = async () => { + setScanning(false); + setQrCodeData(null); + await handleGenerateQRCode(); + }; + + // 当切换到扫码创建时,自动生成二维码 + useEffect(() => { + if (visible && createMethod === "scan" && !isEdit && !qrCodeData && !qrCodeLoading) { + handleGenerateQRCode(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [createMethod, visible]); + + // 当二维码类型变化时,重新生成二维码 + useEffect(() => { + if (visible && createMethod === "scan" && !isEdit && qrCodeData) { + handleGenerateQRCode(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [qrCodeType, visible]); + + return ( + +
+ {/* 头部 */} +
+
+

{isEdit ? "编辑渠道" : "新增渠道"}

+

+ {isEdit + ? "修改渠道信息" + : "选择创建方式: 手动填写或扫码获取微信信息"} +

+
+ +
+ + {/* 创建方式选择 */} + {!isEdit && ( +
+ + +
+ )} + + {/* 内容区域 */} +
+ {createMethod === "manual" || isEdit ? ( +
+
+ + handleInputChange("name", e.target.value)} + className={styles.input} + /> +
+ +
+ + handlePhoneChange(e.target.value)} + className={styles.input} + maxLength={11} + type="tel" + /> + {formData.phone && formData.phone.length > 0 && ( +
+ {formData.phone.length < 11 ? ( + + 还需输入 {11 - formData.phone.length} 位数字 + + ) : !validatePhone(formData.phone) ? ( + + 手机号格式不正确,请以1开头 + + ) : ( + + ✓ 手机号格式正确 + + )} +
+ )} +
+ +
+ + handleInputChange("wechatId", e.target.value)} + className={styles.input} + /> +
+ +
+ +