分销提交
This commit is contained in:
@@ -498,6 +498,166 @@
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
// 渠道登录入口卡片
|
||||
.loginEntryCard {
|
||||
margin: 16px 16px 8px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid #e5e6eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.loginEntryHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loginEntryTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.loginEntryDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.loginEntryText {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.loginEntryTypeTabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #edf2ff;
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.loginEntryTypeTab {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: #ffffff;
|
||||
color: #1890ff;
|
||||
box-shadow: 0 1px 4px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.loginEntryContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.loginEntryDesc {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
// 登录二维码弹窗
|
||||
.loginQrDialog {
|
||||
padding: 16px 16px 24px;
|
||||
background: #fff;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
}
|
||||
|
||||
.loginQrHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.loginQrTypeSelector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loginQrContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loginQrLoading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 32px 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.loginQrImage {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.loginQrLinkSection {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.loginQrLinkLabel {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.loginQrLinkWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.loginQrLinkInput {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loginQrError {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 24px 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
// 加载和空状态
|
||||
.loading {
|
||||
text-align: center;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Select,
|
||||
Modal,
|
||||
} from "antd";
|
||||
import { Tabs, DatePicker, InfiniteScroll, SpinLoading } from "antd-mobile";
|
||||
import { Tabs, DatePicker, InfiniteScroll, SpinLoading, Popup } from "antd-mobile";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
|
||||
const { TextArea } = Input;
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
fetchWithdrawalList,
|
||||
reviewWithdrawal,
|
||||
markAsPaid,
|
||||
generateLoginQRCode,
|
||||
} from "./api";
|
||||
import type {
|
||||
Channel,
|
||||
@@ -130,6 +131,16 @@ const DistributionManagement: React.FC = () => {
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [withdrawalKeyword, setWithdrawalKeyword] = useState("");
|
||||
|
||||
// 渠道登录入口二维码相关状态
|
||||
const [loginQrVisible, setLoginQrVisible] = useState(false);
|
||||
const [loginQrType, setLoginQrType] = useState<"h5" | "miniprogram">("h5");
|
||||
const [loginQrLoading, setLoginQrLoading] = useState(false);
|
||||
const [loginQrData, setLoginQrData] = useState<{
|
||||
qrCode: string;
|
||||
url: string;
|
||||
type: "h5" | "miniprogram";
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
@@ -164,6 +175,35 @@ const DistributionManagement: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 生成渠道登录二维码(每次调用都重新请求)
|
||||
const handleGenerateLoginQRCode = async (type: "h5" | "miniprogram") => {
|
||||
setLoginQrLoading(true);
|
||||
try {
|
||||
const res = await generateLoginQRCode(type);
|
||||
setLoginQrData(res);
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.message || e?.msg || "生成登录二维码失败";
|
||||
message.error(errorMsg);
|
||||
setLoginQrData(null);
|
||||
} finally {
|
||||
setLoginQrLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开登录入口弹窗(默认用当前类型重新请求一次)
|
||||
const handleOpenLoginQrDialog = async () => {
|
||||
setLoginQrVisible(true);
|
||||
setLoginQrData(null);
|
||||
await handleGenerateLoginQRCode(loginQrType);
|
||||
};
|
||||
|
||||
// 切换登录二维码类型(H5 / 小程序),每次都重新请求接口生成
|
||||
const handleLoginQrTypeChange = async (type: "h5" | "miniprogram") => {
|
||||
setLoginQrType(type);
|
||||
setLoginQrData(null);
|
||||
await handleGenerateLoginQRCode(type);
|
||||
};
|
||||
|
||||
const loadChannelList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -551,6 +591,46 @@ const DistributionManagement: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 渠道登录入口 */}
|
||||
<div className={styles.loginEntryCard}>
|
||||
<div className={styles.loginEntryHeader}>
|
||||
<div className={styles.loginEntryTitle}>
|
||||
<span className={styles.loginEntryDot}></span>
|
||||
<span className={styles.loginEntryText}>渠道登录入口</span>
|
||||
</div>
|
||||
<div className={styles.loginEntryTypeTabs}>
|
||||
<button
|
||||
className={`${styles.loginEntryTypeTab} ${
|
||||
loginQrType === "h5" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLoginQrTypeChange("h5")}
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.loginEntryTypeTab} ${
|
||||
loginQrType === "miniprogram" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLoginQrTypeChange("miniprogram")}
|
||||
>
|
||||
小程序
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.loginEntryContent}>
|
||||
<div className={styles.loginEntryDesc}>
|
||||
生成登录入口二维码,分发给渠道方扫码登录管理后台
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={handleOpenLoginQrDialog}
|
||||
>
|
||||
查看登录二维码
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div className={styles.searchBar}>
|
||||
<Input
|
||||
@@ -1044,6 +1124,91 @@ const DistributionManagement: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 渠道登录入口二维码弹窗 */}
|
||||
<Popup
|
||||
visible={loginQrVisible}
|
||||
onMaskClick={() => setLoginQrVisible(false)}
|
||||
position="bottom"
|
||||
bodyStyle={{ borderTopLeftRadius: 16, borderTopRightRadius: 16 }}
|
||||
>
|
||||
<div className={styles.loginQrDialog}>
|
||||
<div className={styles.loginQrHeader}>
|
||||
<h3>渠道登录入口</h3>
|
||||
<Button size="small" onClick={() => setLoginQrVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.loginQrTypeSelector}>
|
||||
<button
|
||||
className={`${styles.loginEntryTypeTab} ${
|
||||
loginQrType === "h5" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLoginQrTypeChange("h5")}
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.loginEntryTypeTab} ${
|
||||
loginQrType === "miniprogram" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleLoginQrTypeChange("miniprogram")}
|
||||
>
|
||||
小程序
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.loginQrContent}>
|
||||
{loginQrLoading ? (
|
||||
<div className={styles.loginQrLoading}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 28 }} />
|
||||
<div>生成二维码中...</div>
|
||||
</div>
|
||||
) : loginQrData?.qrCode ? (
|
||||
<>
|
||||
<img
|
||||
src={loginQrData.qrCode}
|
||||
alt="登录二维码"
|
||||
className={styles.loginQrImage}
|
||||
/>
|
||||
{loginQrData.url && (
|
||||
<div className={styles.loginQrLinkSection}>
|
||||
<div className={styles.loginQrLinkLabel}>
|
||||
{loginQrType === "h5" ? "H5 链接" : "小程序链接"}
|
||||
</div>
|
||||
<div className={styles.loginQrLinkWrapper}>
|
||||
<Input
|
||||
value={loginQrData.url}
|
||||
readOnly
|
||||
className={styles.loginQrLinkInput}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(loginQrData.url);
|
||||
message.success("链接已复制到剪贴板");
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.loginQrError}>
|
||||
<div>二维码生成失败,请重试</div>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => handleGenerateLoginQRCode(loginQrType)}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* 新增/编辑渠道弹窗 */}
|
||||
<AddChannelModal
|
||||
visible={addChannelModalVisible}
|
||||
|
||||
@@ -993,20 +993,13 @@ class ChannelController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
// 生成登录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);
|
||||
// 登录入口只需要携带 companyId 参数
|
||||
$h5Url = $h5BaseUrl . '/pages/channel/login?companyId=' . urlencode($companyId);
|
||||
|
||||
// 生成二维码
|
||||
$qrCode = new QrCode($h5Url);
|
||||
@@ -1042,8 +1035,11 @@ class ChannelController extends BaseController
|
||||
|
||||
$app = Factory::miniProgram($miniProgramConfig);
|
||||
|
||||
// scene参数长度限制为32位,使用token的MD5值
|
||||
$scene = substr(md5($token), 0, 32);
|
||||
// scene 参数直接使用 companyId(字符串),并确保长度不超过32
|
||||
$scene = (string)$companyId;
|
||||
if (strlen($scene) > 32) {
|
||||
$scene = substr($scene, 0, 32);
|
||||
}
|
||||
|
||||
// 调用接口生成小程序码
|
||||
// 小程序登录页面路径,需要根据实际项目调整
|
||||
@@ -1064,8 +1060,7 @@ class ChannelController extends BaseController
|
||||
'data' => [
|
||||
'type' => 'miniprogram',
|
||||
'qrCode' => $imgBase64,
|
||||
'scene' => $scene,
|
||||
'token' => $token
|
||||
'scene' => $scene
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user