From 66c86231383dcce847e3d8d9afee98d6d5b77d03 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 18 Dec 2025 15:36:25 +0800 Subject: [PATCH] 11111 --- .../mobile/scenarios/plan/list/index.tsx | 81 +++++---- .../plan/new/steps/DistributionSettings.tsx | 172 +++++++++++++----- .../workspace/distribution-management/api.ts | 17 +- .../components/AddChannelModal.tsx | 55 +++--- Server/application/cunkebao/config/route.php | 3 +- .../distribution/ChannelController.php | 140 +++++++++++++- 6 files changed, 353 insertions(+), 115 deletions(-) diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 903b86fb..367187e9 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -594,43 +594,52 @@ const ScenarioList: React.FC = () => {
生成二维码中...
- ) : qrImg ? ( - <> - 小程序二维码 - {/* 链接复制区域 */} -
-
小程序链接
-
- - -
-
- ) : ( -
二维码生成失败
+ <> + {/* 二维码显示区域 */} + {qrImg ? ( + 小程序二维码 + ) : ( +
+ +
二维码生成失败
+
+ )} + + {/* H5链接展示 - 无论二维码是否成功都要显示 */} + {currentTaskId && ( +
+
H5链接
+
+ + +
+
+ )} + )} diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/DistributionSettings.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/DistributionSettings.tsx index cccafd88..60e9d18e 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/DistributionSettings.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/new/steps/DistributionSettings.tsx @@ -5,6 +5,7 @@ import { SearchOutlined, DeleteOutlined, QrcodeOutlined, + CopyOutlined, } from "@ant-design/icons"; import { Checkbox, Popup } from "antd-mobile"; import styles from "./base.module.scss"; @@ -60,11 +61,46 @@ const DistributionSettings: React.FC = ({ const PAGE_SIZE = 20; + // 生成H5链接(独立生成,不依赖二维码) + const generateH5Url = (channelId: string | number, channelCode?: string): string => { + // 生成H5链接,参考列表中的实现 + // 格式: https://h5.ckb.quwanzhi.com/#/pages/form/input2?id={planId}&channelId={channelId} + if (planId) { + return `https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${planId}&channelId=${channelId}`; + } else if (channelCode) { + // 新建状态,使用渠道code + return `https://h5.ckb.quwanzhi.com/#/pages/form/input2?channelCode=${channelCode}`; + } + return ""; + }; + // 生成渠道二维码 const generateChannelQRCode = async (channelId: string | number, channelCode?: string) => { - // 如果已经有二维码,直接返回 - if (qrCodeMap[channelId]?.qrCode) { - return; + // 先生成H5链接(无论二维码是否成功都要显示) + const h5Url = generateH5Url(channelId, channelCode); + + // 如果已经有二维码数据,只更新H5链接(如果还没有) + if (qrCodeMap[channelId]) { + if (!qrCodeMap[channelId].url) { + setQrCodeMap(prev => ({ + ...prev, + [channelId]: { ...prev[channelId], url: h5Url }, + })); + } + // 如果已经有二维码,直接返回 + if (qrCodeMap[channelId].qrCode) { + return; + } + } else { + // 初始化,先设置H5链接 + setQrCodeMap(prev => ({ + ...prev, + [channelId]: { + qrCode: "", + url: h5Url, + loading: true, + }, + })); } // 设置加载状态 @@ -85,18 +121,20 @@ const DistributionSettings: React.FC = ({ } // 调用API生成二维码,参考列表中的实现 + // 接口返回格式: { code: 200, msg: "获取小程序码成功", data: "data:image/png;base64,..." } const response = await request( `/v1/plan/getWxMinAppCode`, params, "GET" ); - if (response && response.qrCode) { + // response 已经是 base64 字符串(因为 request 拦截器返回了 res.data.data) + if (response && typeof response === 'string' && response.startsWith('data:image')) { setQrCodeMap(prev => ({ ...prev, [channelId]: { - qrCode: response.qrCode, - url: response.url || "", + qrCode: response, // base64 图片数据 + url: h5Url, // H5链接(确保即使失败也有) loading: false, }, })); @@ -104,13 +142,19 @@ const DistributionSettings: React.FC = ({ throw new Error("二维码生成失败"); } } catch (error: any) { + // 即使二维码生成失败,也要保留H5链接 Toast.show({ content: error.message || "生成二维码失败", position: "top", }); setQrCodeMap(prev => ({ ...prev, - [channelId]: { ...prev[channelId], loading: false }, + [channelId]: { + ...prev[channelId], + qrCode: "", // 二维码为空 + url: h5Url, // H5链接保留 + loading: false, + }, })); } }; @@ -673,63 +717,93 @@ const DistributionSettings: React.FC = ({
生成二维码中...
- ) : qrCodeMap[currentQrChannel.id]?.qrCode ? ( + ) : ( <> - 渠道二维码 - {qrCodeMap[currentQrChannel.id].url && ( + {/* 二维码显示区域 */} + {qrCodeMap[currentQrChannel.id]?.qrCode ? ( + 渠道二维码 + ) : ( +
+ +
二维码生成失败
+ +
+ )} + + {/* H5链接展示 - 无论二维码是否成功都要显示 */} + {qrCodeMap[currentQrChannel.id]?.url && (
- 链接地址 + H5链接
- {qrCodeMap[currentQrChannel.id].url} + +
)} - ) : ( -
- -
二维码生成失败
- -
)} )} diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts b/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts index 6f637d33..ba9f8acb 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/api.ts @@ -133,7 +133,7 @@ export const markAsPaid = async ( return request(`/v1/distribution/withdrawals/${id}/mark-paid`, data, "POST"); }; -// 生成二维码 +// 生成渠道扫码创建二维码 export const generateQRCode = async ( type: "h5" | "miniprogram", ): Promise<{ @@ -143,3 +143,18 @@ export const generateQRCode = async ( }> => { return request("/v1/distribution/channel/generate-qrcode", { type }, "POST"); }; + +// 生成渠道登录二维码 +export const generateLoginQRCode = async ( + type: "h5" | "miniprogram" = "h5", +): Promise<{ + type: "h5" | "miniprogram"; + qrCode: string; // base64 或图片URL + url: string; // H5 或小程序落地页URL +}> => { + return request( + "/v1/distribution/channel/generate-login-qrcode", + { type }, + "POST", + ); +}; diff --git a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx index 6f2fe465..26ab1b35 100644 --- a/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx +++ b/Cunkebao/src/pages/mobile/workspace/distribution-management/components/AddChannelModal.tsx @@ -70,23 +70,11 @@ const AddChannelModal: React.FC = ({ } }, [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) { - // 重置状态后重新生成 + // 弹窗打开时,如果是扫码创建模式,无论之前是否有数据都重新生成 + if (visible && !isEdit && createMethod === "scan" && !qrCodeLoading && !generatingRef.current) { + // 重置状态后重新生成(无论之前是否有数据) setQrCodeData(null); setScanning(false); // 使用 setTimeout 确保状态更新完成 @@ -96,7 +84,22 @@ const AddChannelModal: React.FC = ({ return () => clearTimeout(timer); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [qrCodeType]); + }, [visible]); + + // 当二维码类型变化时,重新生成二维码(无论之前是否成功或失败都要重新生成) + useEffect(() => { + if (visible && !isEdit && createMethod === "scan" && !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, visible, createMethod]); // 验证手机号格式 const validatePhone = (phone: string): boolean => { @@ -213,20 +216,18 @@ const AddChannelModal: React.FC = ({ // 当切换到扫码创建时,自动生成二维码 useEffect(() => { - if (visible && createMethod === "scan" && !isEdit && !qrCodeData && !qrCodeLoading) { - handleGenerateQRCode(); + if (visible && createMethod === "scan" && !isEdit && !qrCodeLoading && !generatingRef.current) { + // 重置状态后重新生成(无论之前是否有数据) + setQrCodeData(null); + setScanning(false); + const timer = setTimeout(() => { + handleGenerateQRCode(); + }, 100); + return () => clearTimeout(timer); } // 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 ( request->domain()); + $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#'); // 确保URL格式正确(去除末尾斜杠) $h5BaseUrl = rtrim($h5BaseUrl, '/'); $h5Url = $h5BaseUrl . '/pages/channel/add?token=' . urlencode($token); @@ -970,6 +970,144 @@ class ChannelController extends BaseController } } + /** + * 生成渠道登录二维码(H5或小程序码) + * 通用登录二维码,不绑定特定渠道,用户扫码后输入手机号和密码登录 + * @return \think\response\Json + */ + public function generateLoginQrCode() + { + try { + // 获取参数 + $type = $this->request->param('type', 'h5'); // h5 或 miniprogram + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (!in_array($type, ['h5', 'miniprogram'])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '类型参数错误,必须为 h5 或 miniprogram', + 'data' => null + ]); + } + + // 生成登录token(只包含公司ID,有效期1小时) + // 用户扫码后需要自己输入手机号和密码 + $tokenData = [ + 'companyId' => $companyId, + 'expireTime' => time() + 3600 // 1小时后过期 + ]; + $token = base64_encode(json_encode($tokenData)); + + if ($type === 'h5') { + // 生成H5登录二维码 + $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#'); + $h5BaseUrl = rtrim($h5BaseUrl, '/'); + // H5登录页面路径,需要根据实际项目调整 + $h5Url = $h5BaseUrl . '/pages/channel/login?token=' . urlencode($token); + + // 生成二维码 + $qrCode = new QrCode($h5Url); + $qrCode->setSize(300); + $qrCode->setMargin(10); + $qrCode->setWriterByName('png'); + $qrCode->setEncoding('UTF-8'); + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH); + + // 转换为base64 + $qrCodeBase64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString()); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成H5登录二维码成功', + 'data' => [ + 'type' => 'h5', + 'qrCode' => $qrCodeBase64, + 'url' => $h5Url + ] + ]); + + } else { + // 生成小程序登录码 + try { + // 从环境变量获取小程序配置 + $miniProgramConfig = [ + 'app_id' => Env::get('weChat.appidMiniApp', 'wx789850448e26c91d'), + 'secret' => Env::get('weChat.secretMiniApp', 'd18f75b3a3623cb40da05648b08365a1'), + 'response_type' => 'array' + ]; + + $app = Factory::miniProgram($miniProgramConfig); + + // scene参数长度限制为32位,使用token的MD5值 + $scene = substr(md5($token), 0, 32); + + // 调用接口生成小程序码 + // 小程序登录页面路径,需要根据实际项目调整 + $response = $app->app_code->getUnlimit($scene, [ + 'page' => 'pages/channel/login', // 请确保小程序里存在该页面 + 'width' => 430, + ]); + + // 成功时返回的是 StreamResponse + if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) { + $img = $response->getBody()->getContents(); + $imgBase64 = 'data:image/png;base64,' . base64_encode($img); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成小程序登录码成功', + 'data' => [ + 'type' => 'miniprogram', + 'qrCode' => $imgBase64, + 'scene' => $scene, + 'token' => $token + ] + ]); + } + + // 如果不是流响应,而是数组(错误信息),则解析错误返回 + if (is_array($response) && isset($response['errcode']) && $response['errcode'] != 0) { + $errMsg = isset($response['errmsg']) ? $response['errmsg'] : '微信接口返回错误'; + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:' . $errMsg, + 'data' => $response + ]); + } + + // 其他未知格式 + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:响应格式错误', + 'data' => $response + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '生成登录二维码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + /** * 扫码提交渠道信息(H5和小程序共用) * GET请求:返回预填信息