分销提交

This commit is contained in:
wong
2025-12-18 16:23:01 +08:00
parent 66c8623138
commit 2f00ee176b
3 changed files with 334 additions and 14 deletions

View File

@@ -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;

View File

@@ -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}

View File

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