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 && (
)}
>
- ) : (
-
-
-
二维码生成失败
-
-
)}
>
)}
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请求:返回预填信息