This commit is contained in:
wong
2025-12-18 15:36:25 +08:00
parent 183df2cac3
commit 66c8623138
6 changed files with 353 additions and 115 deletions

View File

@@ -594,43 +594,52 @@ const ScenarioList: React.FC = () => {
<SpinLoading color="primary" />
<div>...</div>
</div>
) : qrImg ? (
<>
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
{/* 链接复制区域 */}
<div className={style["qr-link-section"]}>
<div className={style["link-label"]}></div>
<div className={style["link-input-wrapper"]}>
<Input
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`}
readOnly
className={style["link-input"]}
placeholder="小程序链接"
/>
<Button
size="small"
onClick={() => {
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",
position: "top",
});
}}
className={style["copy-button"]}
>
<CopyOutlined />
</Button>
</div>
</div>
</>
) : (
<div className={style["qr-error"]}></div>
<>
{/* 二维码显示区域 */}
{qrImg ? (
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
) : (
<div className={style["qr-error"]}>
<QrcodeOutlined style={{ fontSize: 48, color: "#999", marginBottom: 12 }} />
<div></div>
</div>
)}
{/* H5链接展示 - 无论二维码是否成功都要显示 */}
{currentTaskId && (
<div className={style["qr-link-section"]}>
<div className={style["link-label"]}>H5链接</div>
<div className={style["link-input-wrapper"]}>
<Input
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`}
readOnly
className={style["link-input"]}
placeholder="H5链接"
/>
<Button
size="small"
onClick={() => {
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input2?id=${currentTaskId}`;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",
position: "top",
});
}}
className={style["copy-button"]}
>
<CopyOutlined />
</Button>
</div>
</div>
)}
</>
)}
</div>
</div>

View File

@@ -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<DistributionSettingsProps> = ({
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<DistributionSettingsProps> = ({
}
// 调用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<DistributionSettingsProps> = ({
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<DistributionSettingsProps> = ({
<SpinLoading color="primary" style={{ fontSize: 32 }} />
<div style={{ fontSize: 14, color: "#666" }}>...</div>
</div>
) : qrCodeMap[currentQrChannel.id]?.qrCode ? (
) : (
<>
<img
src={qrCodeMap[currentQrChannel.id].qrCode}
alt="渠道二维码"
style={{
width: 200,
height: 200,
border: "1px solid #e5e6eb",
borderRadius: 8,
padding: 8,
background: "#fff",
}}
/>
{qrCodeMap[currentQrChannel.id].url && (
{/* 二维码显示区域 */}
{qrCodeMap[currentQrChannel.id]?.qrCode ? (
<img
src={qrCodeMap[currentQrChannel.id].qrCode}
alt="渠道二维码"
style={{
width: 200,
height: 200,
border: "1px solid #e5e6eb",
borderRadius: 8,
padding: 8,
background: "#fff",
}}
/>
) : (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
padding: "40px 20px",
color: "#999",
}}>
<QrcodeOutlined style={{ fontSize: 48 }} />
<div style={{ fontSize: 14 }}></div>
<Button
size="small"
color="primary"
onClick={() => currentQrChannel && generateChannelQRCode(currentQrChannel.id, currentQrChannel.code)}
>
</Button>
</div>
)}
{/* H5链接展示 - 无论二维码是否成功都要显示 */}
{qrCodeMap[currentQrChannel.id]?.url && (
<div style={{
width: "100%",
padding: "12px",
background: "#f5f5f5",
borderRadius: 8,
marginTop: 16,
}}>
<div style={{
fontSize: 12,
fontSize: 14,
color: "#666",
marginBottom: 8,
fontWeight: 500,
}}>
H5链接
</div>
<div style={{
fontSize: 12,
color: "#333",
wordBreak: "break-all",
display: "flex",
gap: 8,
alignItems: "center",
}}>
{qrCodeMap[currentQrChannel.id].url}
<Input
value={qrCodeMap[currentQrChannel.id].url}
readOnly
style={{
flex: 1,
fontSize: 12,
}}
/>
<Button
size="small"
onClick={() => {
const link = qrCodeMap[currentQrChannel.id].url;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",
position: "top",
});
}}
style={{
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<CopyOutlined />
</Button>
</div>
</div>
)}
</>
) : (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
padding: "40px 20px",
color: "#999",
}}>
<QrcodeOutlined style={{ fontSize: 48 }} />
<div style={{ fontSize: 14 }}></div>
<Button
size="small"
color="primary"
onClick={() => currentQrChannel && generateChannelQRCode(currentQrChannel.id, currentQrChannel.code)}
>
</Button>
</div>
)}
</>
)}

View File

@@ -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",
);
};

View File

@@ -70,23 +70,11 @@ const AddChannelModal: React.FC<AddChannelModalProps> = ({
}
}, [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<AddChannelModalProps> = ({
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<AddChannelModalProps> = ({
// 当切换到扫码创建时,自动生成二维码
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 (
<Modal
open={visible}