分销提交
This commit is contained in:
@@ -498,6 +498,166 @@
|
|||||||
color: #fa8c16;
|
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 {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Modal,
|
Modal,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { Tabs, DatePicker, InfiniteScroll, SpinLoading } from "antd-mobile";
|
import { Tabs, DatePicker, InfiniteScroll, SpinLoading, Popup } from "antd-mobile";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
@@ -70,6 +70,7 @@ import {
|
|||||||
fetchWithdrawalList,
|
fetchWithdrawalList,
|
||||||
reviewWithdrawal,
|
reviewWithdrawal,
|
||||||
markAsPaid,
|
markAsPaid,
|
||||||
|
generateLoginQRCode,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import type {
|
import type {
|
||||||
Channel,
|
Channel,
|
||||||
@@ -130,6 +131,16 @@ const DistributionManagement: React.FC = () => {
|
|||||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
const [withdrawalKeyword, setWithdrawalKeyword] = useState("");
|
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(() => {
|
useEffect(() => {
|
||||||
loadData();
|
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 () => {
|
const loadChannelList = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -551,6 +591,46 @@ const DistributionManagement: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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}>
|
<div className={styles.searchBar}>
|
||||||
<Input
|
<Input
|
||||||
@@ -1044,6 +1124,91 @@ const DistributionManagement: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<AddChannelModal
|
||||||
visible={addChannelModalVisible}
|
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') {
|
if ($type === 'h5') {
|
||||||
// 生成H5登录二维码
|
// 生成H5登录二维码
|
||||||
$h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#');
|
$h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#');
|
||||||
$h5BaseUrl = rtrim($h5BaseUrl, '/');
|
$h5BaseUrl = rtrim($h5BaseUrl, '/');
|
||||||
// H5登录页面路径,需要根据实际项目调整
|
// H5登录页面路径,需要根据实际项目调整
|
||||||
$h5Url = $h5BaseUrl . '/pages/channel/login?token=' . urlencode($token);
|
// 登录入口只需要携带 companyId 参数
|
||||||
|
$h5Url = $h5BaseUrl . '/pages/channel/login?companyId=' . urlencode($companyId);
|
||||||
|
|
||||||
// 生成二维码
|
// 生成二维码
|
||||||
$qrCode = new QrCode($h5Url);
|
$qrCode = new QrCode($h5Url);
|
||||||
@@ -1042,8 +1035,11 @@ class ChannelController extends BaseController
|
|||||||
|
|
||||||
$app = Factory::miniProgram($miniProgramConfig);
|
$app = Factory::miniProgram($miniProgramConfig);
|
||||||
|
|
||||||
// scene参数长度限制为32位,使用token的MD5值
|
// scene 参数直接使用 companyId(字符串),并确保长度不超过32
|
||||||
$scene = substr(md5($token), 0, 32);
|
$scene = (string)$companyId;
|
||||||
|
if (strlen($scene) > 32) {
|
||||||
|
$scene = substr($scene, 0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
// 调用接口生成小程序码
|
// 调用接口生成小程序码
|
||||||
// 小程序登录页面路径,需要根据实际项目调整
|
// 小程序登录页面路径,需要根据实际项目调整
|
||||||
@@ -1064,8 +1060,7 @@ class ChannelController extends BaseController
|
|||||||
'data' => [
|
'data' => [
|
||||||
'type' => 'miniprogram',
|
'type' => 'miniprogram',
|
||||||
'qrCode' => $imgBase64,
|
'qrCode' => $imgBase64,
|
||||||
'scene' => $scene,
|
'scene' => $scene
|
||||||
'token' => $token
|
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user