feat(add): 新增多页面新增和编辑表单功能

- 添加编辑和新增收货地址页面,支持表单数据加载和提交
- 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置
- 实现文章新增及编辑页面,包含图片上传及多种文章属性配置
- 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理
- 引入统一表单提交成功和失败处理,支持编辑模式数据回显
- 配置统一eslint和editorconfig规则,增强代码规范和编辑体验
- 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
This commit is contained in:
2026-04-11 12:22:29 +08:00
commit 07f5c92f4b
627 changed files with 85725 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '服务协议与隐私政策',
navigationBarTextStyle: 'black'
})

View File

@@ -0,0 +1,30 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {View, RichText} from '@tarojs/components'
const Agreement = () => {
const [content, setContent] = useState<any>('')
const reload = () => {
Taro.hideTabBar()
setContent('<p>' +
'<span style="font-size: 14px;">欢迎使用</span>' +
'<span style="font-size: 14px;">&nbsp;</span>' +
'<span style="font-size: 14px;"><strong><span style="color: rgb(255, 0, 0);">【WebSoft】</span></strong></span>' +
'<span style="font-size: 14px;">服务协议&nbsp;</span>' +
'</p>')
}
useEffect(() => {
reload()
}, [])
return (
<>
<View className={'content text-gray-700 text-sm p-4'}>
<RichText nodes={content}/>
</View>
</>
)
}
export default Agreement

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '忘记密码',
navigationBarTextStyle: 'black'
})

36
src/passport/forget.tsx Normal file
View File

@@ -0,0 +1,36 @@
import {useEffect} from "react";
import Taro from '@tarojs/taro'
import {Input, Button} from '@nutui/nutui-react-taro'
import {copyText} from "@/utils/common";
const Register = () => {
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'flex flex-col justify-center px-5 pt-3'}>
<div className={'text-sm py-2'}></div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="新的密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
<Input type="text" placeholder="短信验证码" style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}/>
<Button onClick={() => copyText('https://site-10398.shoplnk.cn?v=1.33')}></Button>
</div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'}></Button>
</div>
</div>
</>
)
}
export default Register

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '登录',
navigationBarTextStyle: 'black'
})

56
src/passport/login.tsx Normal file
View File

@@ -0,0 +1,56 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
const Login = () => {
const [isAgree, setIsAgree] = useState(false)
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'flex flex-col justify-center px-5'}>
<div className={'text-3xl text-center py-5 font-normal my-10'}></div>
<>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="text" placeholder="手机号" maxLength={11}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex justify-between my-2 text-left px-1'}>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/forget'})}></a>
<a href={'#'} className={'text-blue-600 text-sm'}
onClick={() => Taro.navigateTo({url: '/passport/register'})}></a>
</div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}></Button>
</div>
<div className={'my-2 flex fixed justify-center bottom-20 left-0 text-sm items-center text-center w-full'}>
<Button onClick={() => Taro.navigateTo({url: '/passport/setting'})}></Button>
</div>
{/*<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>*/}
{/* 没有账号?<a href={''} onClick={() => Taro.navigateTo({url: '/passport/register'})}*/}
{/* className={'text-blue-600'}>立即注册</a>*/}
{/*</div>*/}
</>
<div className={'my-2 flex text-sm items-center px-1'}>
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}></span><a
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
className={'text-blue-600'}></a>
</div>
</div>
</>
)
}
export default Login

View File

@@ -0,0 +1,355 @@
import { useEffect, useState } from "react";
import Taro, { useRouter } from '@tarojs/taro'
import { Button, Popup } from '@nutui/nutui-react-taro'
import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite";
import { TenantId } from "@/config/app";
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
/**
* 手机号授权登录页面(扫码登录场景专用)
* 使用微信手机号快速验证组件
*
* 流程:
* 1. 用户勾选同意服务协议和隐私政策
* 2. 点击微信手机号授权按钮
* 3. 微信弹出授权确认框
* 4. 获取 code 后调用后端接口完成登录
* 5. 登录成功后返回扫码确认页面
*/
// 微信获取手机号回调参数类型
interface GetPhoneNumberDetail {
code?: string;
encryptedData?: string;
iv?: string;
errMsg: string;
}
interface GetPhoneNumberEvent {
detail: GetPhoneNumberDetail;
}
// 登录接口返回数据类型
interface LoginResponse {
data: {
access_token: string;
user: any;
};
code: number;
message: string;
}
const AgreementPopup = ({ visible, onClose }: { visible: boolean; onClose: () => void }) => (
<Popup
visible={visible}
position="bottom"
closeable
onClose={onClose}
style={{ height: '70%' }}
>
<div className="p-5 overflow-y-auto" style={{ height: '100%' }}>
<h3 className="text-lg font-bold mb-4"></h3>
<div className="text-sm text-gray-600 leading-relaxed">
<p className="mb-4">
<br/>
使 WebSoft <br/>
</p>
<p className="mb-4">
<br/>
WebSoft
</p>
<p className="mb-4">
<br/>
使
</p>
<p className="mb-4">
使<br/>
</p>
<p className="mb-4">
<br/>
</p>
<p>
<br/>
</p>
</div>
<div className="mt-4">
<Button type="primary" onClick={onClose}></Button>
</div>
</div>
</Popup>
);
const PrivacyPopup = ({ visible, onClose }: { visible: boolean; onClose: () => void }) => (
<Popup
visible={visible}
position="bottom"
closeable
onClose={onClose}
style={{ height: '70%' }}
>
<div className="p-5 overflow-y-auto" style={{ height: '100%' }}>
<h3 className="text-lg font-bold mb-4"></h3>
<div className="text-sm text-gray-600 leading-relaxed">
<p className="mb-4">
<br/>
使
</p>
<p className="mb-4">
<br/>
使访
</p>
<p className="mb-4">
使<br/>
使
</p>
<p className="mb-4">
<br/>
</p>
<p className="mb-4">
<br/>
</p>
<p className="mb-4">
<br/>
访
</p>
<p>
<br/>
</p>
</div>
<div className="mt-4">
<Button type="primary" onClick={onClose}></Button>
</div>
</div>
</Popup>
);
const PhoneAuthLogin = () => {
const router = useRouter();
const [loading, setLoading] = useState<boolean>(false)
const [agreed, setAgreed] = useState<boolean>(false)
const [showAgreement, setShowAgreement] = useState<boolean>(false)
const [showPrivacy, setShowPrivacy] = useState<boolean>(false)
// 获取 redirect 参数(扫码登录场景)
const redirectUrl = router.params.redirect || '';
useEffect(() => {
Taro.hideTabBar()
}, [])
// 处理微信手机号授权
const handleGetPhoneNumber = ({ detail }: GetPhoneNumberEvent) => {
const { code, encryptedData, iv, errMsg } = detail
// 用户拒绝授权
if (errMsg && errMsg.includes('fail')) {
Taro.showToast({
title: '需要授权手机号才能登录',
icon: 'none'
})
return
}
// 未同意协议
if (!agreed) {
Taro.showToast({
title: '请先阅读并同意服务协议和隐私政策',
icon: 'none'
})
return
}
if (!code) {
Taro.showToast({
title: '获取授权信息失败,请重试',
icon: 'none'
})
return
}
// 执行登录
handleLogin(code, encryptedData, iv)
}
// 执行登录
const handleLogin = async (phoneCode: string, encryptedData?: string, iv?: string) => {
try {
setLoading(true)
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
const res = await Taro.request<LoginResponse>({
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
method: 'POST',
data: {
code: phoneCode,
encryptedData,
iv,
tenantId: TenantId,
notVerifyPhone: true, // 用户未注册时自动注册
refereeId: refereeId, // 推荐人ID
sceneType: 'save_referee'
},
header: {
'content-type': 'application/json',
'TenantId': TenantId
}
})
if (res.data.code !== 0) {
throw new Error(res.data.message || '登录失败')
}
// 保存登录信息
if (res.data.data?.user) {
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
}
// 登录成功后,检查是否存在待处理的邀请关系并尝试绑定
if (hasPendingInvite()) {
try {
await checkAndHandleInviteRelation()
} catch (e) {
console.error('授权登录后处理邀请关系失败:', e)
}
}
Taro.showToast({
title: '授权成功',
icon: 'success'
})
// 延迟跳转
setTimeout(() => {
if (redirectUrl) {
// 扫码登录场景:返回扫码确认页面
Taro.navigateBack();
} else {
// 普通场景:跳转到首页
Taro.reLaunch({
url: '/pages/index/index'
})
}
}, 1500)
} catch (error: any) {
Taro.showToast({
title: error.message || '授权失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
return (
<>
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-blue-50 to-white px-6">
{/* Logo/Icon 区域 */}
<div className="mb-10">
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg shadow-blue-200">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none">
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</div>
{/* 标题 */}
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-800 mb-2"></h1>
<p className="text-sm text-gray-500"></p>
</div>
{/* 微信授权按钮 */}
<div className="w-full mb-8">
<Button
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white font-medium py-4 rounded-xl flex items-center justify-center gap-2 shadow-lg shadow-green-200 transition-transform"
open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
disabled={loading}
>
{loading ? (
<>
<div className="w-5 h-5 border-2 border-t-white rounded-full animate-spin" />
<span>...</span>
</>
) : (
<>
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
</svg>
<span></span>
</>
)}
</Button>
</div>
{/* 协议勾选 */}
<div className="flex items-start mb-6 px-2">
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center mr-3 mt-1 flex-shrink-0 cursor-pointer transition-colors`}
onClick={() => setAgreed(!agreed)}
>
{agreed && (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 6L5 9L10 3" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</div>
<div className="text-sm text-gray-600">
<span
className="text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
setShowAgreement(true);
}}
>
</span>
<span
className="text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
setShowPrivacy(true);
}}
>
</span>
</div>
</div>
{/* 底部提示 */}
<div className="text-center text-xs text-gray-400 mt-auto pb-8">
<p></p>
<p className="mt-1"></p>
</div>
</div>
{/* 服务协议弹窗 */}
<AgreementPopup
visible={showAgreement}
onClose={() => setShowAgreement(false)}
/>
{/* 隐私政策弹窗 */}
<PrivacyPopup
visible={showPrivacy}
onClose={() => setShowPrivacy(false)}
/>
</>
);
}
export default PhoneAuthLogin;

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '确认登录',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
}

View File

@@ -0,0 +1,348 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Button } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite";
/**
* 扫码登录确认页面 - 科技风格授权页
*
* 用户扫描 PC 端二维码后,打开此小程序页面进行微信手机号授权登录
*/
// 微信获取手机号回调参数类型
interface GetPhoneNumberDetail {
code?: string;
encryptedData?: string;
iv?: string;
errMsg: string;
}
interface GetPhoneNumberEvent {
detail: GetPhoneNumberDetail;
}
// 登录接口返回数据类型
interface LoginResponse {
data: {
access_token: string;
user: any;
};
code: number;
message: string;
}
// 协议类型
type AgreementType = 'service' | 'privacy';
const QRConfirmPage: React.FC = () => {
const router = useRouter();
const [authLoading, setAuthLoading] = useState(false); // 授权中状态
const [agreementChecked, setAgreementChecked] = useState(false); // 协议勾选状态
const [token, setToken] = useState<string>(''); // 登录 token
useEffect(() => {
// 从 URL 参数中获取 token
const params = router.params;
let loginToken = params.scene || params.token || params.qrCodeKey || '';
// 兼容 q 参数URL 编码的完整 URL
if (params.q && !loginToken) {
try {
const decodedUrl = decodeURIComponent(params.q);
const url = new URL(decodedUrl);
loginToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || '';
} catch (e) {
loginToken = decodeURIComponent(params.q);
}
}
setToken(loginToken);
}, [router.params]);
/**
* 处理微信手机号授权
*/
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
const { code, encryptedData, iv, errMsg } = detail;
// 检查协议是否勾选
if (!agreementChecked) {
Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
return;
}
// 用户拒绝授权
if (errMsg && errMsg.includes('fail')) {
Taro.showToast({ title: '需要授权手机号才能完成登录', icon: 'none' });
return;
}
if (!code) {
Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' });
return;
}
await handleAuthLogin(code, encryptedData, iv);
};
/**
* 授权登录
*/
const handleAuthLogin = async (phoneCode: string, encryptedData?: string, iv?: string) => {
try {
setAuthLoading(true);
// 获取邀请参数
const inviteParams = getStoredInviteParams();
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0;
const res = await Taro.request<LoginResponse>({
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
method: 'POST',
data: {
code: phoneCode,
encryptedData,
iv,
tenantId: 5,
notVerifyPhone: true,
refereeId,
sceneType: 'save_referee'
},
header: { 'content-type': 'application/json', 'TenantId': 5 }
});
if (res.data.code !== 0) {
throw new Error(res.data.message || '登录失败');
}
if (res.data.data?.user) {
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user);
// 处理邀请关系
if (hasPendingInvite()) {
try {
await checkAndHandleInviteRelation();
} catch (e) {
console.error('处理邀请关系失败:', e);
}
}
Taro.showToast({ title: '授权成功,正在确认登录...', icon: 'none' });
// 延迟确认扫码登录
setTimeout(() => handleConfirmQRLogin(res.data.data.user), 1500);
}
} catch (error: any) {
Taro.showToast({ title: error.message || '授权失败', icon: 'error' });
} finally {
setAuthLoading(false);
}
};
/**
* 确认扫码登录
*/
const handleConfirmQRLogin = async (userInfo: any) => {
if (!token) {
Taro.showToast({ title: '缺少登录token', icon: 'none' });
return;
}
try {
const res = await Taro.request({
url: `${SERVER_API_URL}/qr-login/confirm`,
method: 'POST',
data: {
token,
userId: userInfo.userId,
platform: 'wechat',
wechatInfo: {
nickname: userInfo.nickname || userInfo.username,
avatar: userInfo.avatar
}
},
header: { 'content-type': 'application/json', 'TenantId': 5 }
});
if (res.data.success || res.data.status === 'confirmed') {
Taro.showToast({ title: '登录确认成功', icon: 'success', duration: 1500 });
setTimeout(() => {
Taro.switchTab({ url: '/pages/user/user' });
}, 1500);
} else {
Taro.showToast({ title: res.data.message || '登录确认失败', icon: 'none' });
}
} catch (err: any) {
Taro.showToast({ title: err.message || '确认登录失败', icon: 'error' });
}
};
/**
* 取消登录
*/
const handleCancel = () => {
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
Taro.navigateBack();
} else {
Taro.switchTab({ url: '/pages/user/user' });
}
};
// 打开协议页面
const openAgreement = (type: AgreementType) => {
const urlMap = {
service: 'https://websopy.websoft.top/agreement',
privacy: 'https://websopy.websoft.top/privacy',
};
const targetUrl = encodeURIComponent(urlMap[type]);
Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` });
};
// 科技风格授权页面 - 蓝色主题
return (
<View className="min-h-screen relative overflow-hidden" style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}>
{/* 背景科技元素 */}
{/* 网格背景 */}
<View style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
backgroundImage: `linear-gradient(rgba(59, 130, 246, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.08) 1px, transparent 1px)`,
backgroundSize: '50px 50px', opacity: 0.5,
}} />
{/* 渐变光晕 - 左上 */}
<View style={{
position: 'absolute', top: '-30%', left: '-20%', width: '60%', height: '60%',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)',
filter: 'blur(40px)',
}} />
{/* 渐变光晕 - 右下 */}
<View style={{
position: 'absolute', bottom: '-20%', right: '-20%', width: '50%', height: '50%',
background: 'radial-gradient(circle, rgba(99, 102, 241, 0.2) 0%, transparent 70%)',
filter: 'blur(50px)',
}} />
{/* 动态粒子光点 */}
{[
{ top: '15%', left: '20%', size: 4, delay: '0s' },
{ top: '25%', left: '80%', size: 3, delay: '0.5s' },
{ top: '40%', left: '15%', size: 5, delay: '1s' },
{ top: '35%', left: '85%', size: 3, delay: '1.5s' },
{ top: '55%', left: '10%', size: 4, delay: '2s' },
{ top: '60%', left: '90%', size: 3, delay: '0.3s' },
{ top: '75%', left: '25%', size: 4, delay: '1.2s' },
{ top: '80%', left: '75%', size: 5, delay: '0.8s' },
].map((particle, index) => (
<View key={index} className="particle-blue" style={{
position: 'absolute', top: particle.top, left: particle.left,
width: particle.size, height: particle.size, borderRadius: '50%',
background: index % 2 === 0 ? '#3b82f6' : '#6366f1',
boxShadow: index % 2 === 0
? '0 0 10px rgba(59, 130, 246, 0.8), 0 0 20px rgba(59, 130, 246, 0.4)'
: '0 0 10px rgba(99, 102, 241, 0.8), 0 0 20px rgba(99, 102, 241, 0.4)',
animationDelay: particle.delay,
}} />
))}
{/* 扫描线效果 */}
<View style={{
position: 'absolute', top: 0, left: 0, right: 0, height: '2px',
background: 'linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.6), transparent)',
animation: 'scanline-blue 3s ease-in-out infinite',
}} />
{/* 主内容区域 */}
<View className="relative z-10 flex flex-col items-center justify-center min-h-screen px-8">
{/* Logo 区域 */}
<View className="flex flex-col items-center mb-12">
<View style={{
width: '100px', height: '100px', borderRadius: '24px',
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.2))',
border: '1px solid rgba(59, 130, 246, 0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: '20px', position: 'relative', overflow: 'hidden',
}}>
{/* Logo 内光效 */}
<View style={{
position: 'absolute', top: '-50%', left: '-50%', width: '200%', height: '200%',
background: 'conic-gradient(from 0deg, transparent, rgba(59, 130, 246, 0.15), transparent, rgba(99, 102, 241, 0.15), transparent)',
animation: 'rotate 4s linear infinite',
}} />
<Text style={{ fontSize: '48px', position: 'relative', zIndex: 1 }}>🔐</Text>
</View>
<Text style={{
fontSize: '28px', fontWeight: '700',
background: 'linear-gradient(90deg, #3b82f6, #6366f1, #8b5cf6)',
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
backgroundClip: 'text', marginBottom: '8px',
}}>websopy</Text>
<View style={{
width: '60px', height: '3px',
background: 'linear-gradient(90deg, transparent, #3b82f6, transparent)',
marginTop: '20px', borderRadius: '2px',
}} />
</View>
{/* 主按钮 - 渐变发光按钮 */}
<View style={{
width: '100%', maxWidth: '320px',
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 50%, #6366f1 100%)',
borderRadius: '30px', padding: '2px',
boxShadow: '0 0 30px rgba(59, 130, 246, 0.4), 0 0 60px rgba(59, 130, 246, 0.2)',
}}>
<Button
open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
disabled={authLoading}
style={{
width: '100%', height: '52px', fontSize: '17px', fontWeight: '600',
color: '#fff', background: 'transparent', borderRadius: '28px',
border: 'none', padding: '0', boxSizing: 'border-box', lineHeight: '52px',
}}
>
{authLoading ? '授权中...' : '微信手机号登录'}
</Button>
</View>
{/* 取消按钮 */}
<View style={{ width: '100%', maxWidth: '320px', height: '44px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: '16px' }} onClick={handleCancel}>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '14px' }}></Text>
</View>
{/* 协议勾选 */}
<View className="flex items-center justify-center mt-6">
<View style={{ padding: '8px', marginRight: '4px' }} onClick={() => setAgreementChecked(!agreementChecked)}>
<View style={{
width: '18px', height: '18px', borderRadius: '4px',
border: agreementChecked ? 'none' : '1px solid rgba(255, 255, 255, 0.3)',
background: agreementChecked ? 'linear-gradient(135deg, #1d4ed8, #3b82f6)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{agreementChecked && <Text style={{ color: '#fff', fontSize: '12px' }}></Text>}
</View>
</View>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px' }}></Text>
<Text style={{ color: '#3b82f6', fontSize: '12px', padding: '4px 2px' }} onClick={() => openAgreement('service')}></Text>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px' }}></Text>
<Text style={{ color: '#3b82f6', fontSize: '12px', padding: '4px 2px' }} onClick={() => openAgreement('privacy')}></Text>
</View>
{/* 底部装饰 */}
<View style={{ position: 'absolute', bottom: '40px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<View style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#3b82f6', boxShadow: '0 0 10px rgba(59, 130, 246, 0.8)' }} />
<Text style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: '11px' }}></Text>
<View style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#6366f1', boxShadow: '0 0 10px rgba(99, 102, 241, 0.8)' }} />
</View>
</View>
</View>
);
};
export default QRConfirmPage;

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '扫码登录',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
}

View File

@@ -0,0 +1,193 @@
import React, { useState } from 'react';
import { View, Text } from '@tarojs/components';
import { Card, Divider, Button } from '@nutui/nutui-react-taro';
import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import QRLoginScanner from '@/components/QRLoginScanner';
import { useUser } from '@/hooks/useUser';
/**
* 扫码登录页面
*/
const QRLoginPage: React.FC = () => {
const [loginHistory, setLoginHistory] = useState<any[]>([]);
const { getDisplayName } = useUser();
// 处理扫码成功
const handleScanSuccess = (result: any) => {
console.log('扫码登录成功:', result);
// 添加到登录历史
const newRecord = {
id: Date.now(),
time: new Date().toLocaleString(),
userInfo: result.userInfo,
success: true
};
setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
// 显示成功提示
Taro.showToast({
title: '登录确认成功',
icon: 'success',
duration: 2000
});
};
// 处理扫码失败
const handleScanError = (error: string) => {
console.error('扫码登录失败:', error);
// 添加到登录历史
const newRecord = {
id: Date.now(),
time: new Date().toLocaleString(),
error,
success: false
};
setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]);
};
// 返回上一页
// const handleBack = () => {
// Taro.navigateBack();
// };
// 清除历史记录
const clearHistory = () => {
setLoginHistory([]);
Taro.showToast({
title: '已清除历史记录',
icon: 'success'
});
};
return (
<View className="qr-login-page min-h-screen bg-gray-50">
{/* 导航栏 */}
{/*<NavBar*/}
{/* title="扫码登录"*/}
{/* leftShow*/}
{/* onBackClick={handleBack}*/}
{/* leftText={<ArrowLeft />}*/}
{/* className="bg-white"*/}
{/*/>*/}
{/* 主要内容 */}
<View className="p-4">
{/* 用户信息卡片 */}
<Card className="bg-white rounded-lg shadow-sm">
<View className="p-4">
<View className="flex items-center mb-4">
<View className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<Scan className="text-blue-500" size="24" />
</View>
<View>
<Text className="text-lg font-bold text-gray-800">
{getDisplayName()}
</Text>
<Text className="text-sm text-gray-500">
使
</Text>
</View>
</View>
{/* 扫码登录组件 */}
<QRLoginScanner
onSuccess={handleScanSuccess}
onError={handleScanError}
buttonText="开始扫码登录"
showStatus={true}
/>
</View>
</Card>
{/* 使用说明 */}
<Card className="bg-white rounded-lg shadow-sm">
<View className="p-4">
<View className="flex items-center mb-3">
<Tips className="text-orange-500 mr-2" />
<Text className="font-medium text-gray-800">使</Text>
</View>
<View className="text-sm text-gray-600">
<Text className="block">1. </Text>
<Text className="block">2. "扫码登录"</Text>
<Text className="block">3. 使</Text>
<Text className="block">4. </Text>
</View>
</View>
</Card>
{/* 登录历史 */}
{loginHistory.length > 0 && (
<Card className="bg-white rounded-lg shadow-sm">
<View className="p-4">
<View className="flex items-center justify-between mb-3">
<Text className="font-medium text-gray-800"></Text>
<Button
size="small"
type="default"
onClick={clearHistory}
className="text-xs"
>
</Button>
</View>
<View>
{loginHistory.map((record, index) => (
<View key={record.id}>
<View className="flex items-center justify-between">
<View className="flex items-center">
{record.success ? (
<Success className="text-green-500 mr-2" size="16" />
) : (
<Failure className="text-red-500 mr-2" size="16" />
)}
<View>
<Text className="text-sm text-gray-800">
{record.success ? '登录成功' : '登录失败'}
</Text>
{record.error && (
<Text className="text-xs text-red-500">
{record.error}
</Text>
)}
</View>
</View>
<Text className="text-xs text-gray-500">
{record.time}
</Text>
</View>
{index < loginHistory.length - 1 && (
<Divider className="my-2" />
)}
</View>
))}
</View>
</View>
</Card>
)}
{/* 安全提示 */}
<Card className="bg-yellow-50 border border-yellow-200 rounded-lg">
<View className="p-4">
<View className="flex items-start">
<Tips className="text-yellow-600 mr-2 mt-1" size="16" />
<View>
<Text className="text-sm font-medium text-yellow-800 mb-1">
</Text>
<Text className="text-xs text-yellow-700">
</Text>
</View>
</View>
</View>
</Card>
</View>
</View>
);
};
export default QRLoginPage;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '服务配置',
navigationBarTextStyle: 'black'
})

82
src/passport/setting.tsx Normal file
View File

@@ -0,0 +1,82 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Button,Form} from '@nutui/nutui-react-taro'
const Setting = () => {
const [FormData, setFormData] = useState<any>(
{
domain: undefined
}
)
// 提交表单
const submitSucceed = (values: any) => {
if(values.domain){
Taro.setStorageSync('ServerUrl',values.domain)
setFormData({
domain: values.domain
})
Taro.showToast({
title: '保存成功',
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack()
},500)
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
// Taro.showToast({ title: error[0].message, icon: 'error' })
}
const reload = () => {
Taro.hideTabBar()
if (Taro.getStorageSync('ServerUrl')) {
setFormData({
domain: Taro.getStorageSync('ServerUrl')
})
}
}
useEffect(() => {
reload()
}, [])
return (
<>
<Form
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block type="info" size={'large'}>
</Button>
</div>
}
>
<div className={'flex flex-col justify-center pt-3'}>
<div className={'text-sm py-1 px-4'}></div>
<Form.Item
name="domain"
initialValue={FormData.domain}
rules={[{message: '请输入服务域名'}]}
>
<Input placeholder="https://domain.com/api" type="text" style={{backgroundColor: '#f5f5f5', borderRadius: '8px', padding: '5px 10px'}}/>
</Form.Item>
</div>
</Form>
</>
)
}
export default Setting

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '验证码登录',
navigationBarTextStyle: 'black'
})

236
src/passport/sms-login.tsx Normal file
View File

@@ -0,0 +1,236 @@
import {useEffect, useState} from "react";
import Taro, { useRouter } from '@tarojs/taro'
import {Input, Button} from '@nutui/nutui-react-taro'
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
import {LoginParam} from "@/api/passport/login/model";
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
const SmsLogin = () => {
const router = useRouter();
const [loading, setLoading] = useState<boolean>(false)
const [sendingCode, setSendingCode] = useState<boolean>(false)
const [countdown, setCountdown] = useState<number>(0)
const [formData, setFormData] = useState<LoginParam>({
phone: '',
code: ''
})
// 获取 redirect 参数(扫码登录场景)
const redirectUrl = router.params.redirect || '';
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout
if (countdown > 0) {
timer = setTimeout(() => {
setCountdown(countdown - 1)
}, 1000)
}
return () => {
if (timer) clearTimeout(timer)
}
}, [countdown])
// 验证手机号格式
const validatePhone = (phone: string): boolean => {
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
// 发送短信验证码
const handleSendCode = async () => {
if (!formData.phone) {
Taro.showToast({
title: '请输入手机号码',
icon: 'none'
})
return
}
if (!validatePhone(formData.phone)) {
Taro.showToast({
title: '请输入正确的手机号码',
icon: 'none'
})
return
}
if (sendingCode || countdown > 0) {
return
}
try {
setSendingCode(true)
await sendSmsCaptcha({ phone: formData.phone })
Taro.showToast({
title: '验证码已发送',
icon: 'success'
})
// 开始60秒倒计时
setCountdown(60)
} catch (error: any) {
Taro.showToast({
title: error.message || '发送失败',
icon: 'error'
})
} finally {
setSendingCode(false)
}
}
// 处理登录
const handleLogin = async () => {
// 防止重复提交
if (loading) {
return
}
// 表单验证
if (!formData.phone) {
Taro.showToast({
title: '请输入手机号码',
icon: 'none'
})
return
}
if (!validatePhone(formData.phone)) {
Taro.showToast({
title: '请输入正确的手机号码',
icon: 'none'
})
return
}
if (!formData.code) {
Taro.showToast({
title: '请输入验证码',
icon: 'none'
})
return
}
if (formData.code.length !== 6) {
Taro.showToast({
title: '请输入6位验证码',
icon: 'none'
})
return
}
try {
setLoading(true)
await loginBySms({
phone: formData.phone,
code: formData.code
})
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
if (hasPendingInvite()) {
try {
await checkAndHandleInviteRelation()
} catch (e) {
console.error('短信登录后处理邀请关系失败:', e)
}
}
Taro.showToast({
title: '登录成功',
icon: 'success'
})
// 延迟跳转
setTimeout(() => {
if (redirectUrl) {
// 扫码登录场景:返回扫码确认页面
// 构建带原参数的完整 URL
const currentPages = Taro.getCurrentPages();
const prevPage = currentPages[currentPages.length - 2];
if (prevPage && prevPage.route && prevPage.route.includes('qr-confirm')) {
// 如果上一页是扫码确认页面,直接返回
Taro.navigateBack();
} else {
// 否则跳转到指定页面
Taro.redirectTo({
url: redirectUrl
});
}
} else {
// 普通场景:跳转到首页
Taro.reLaunch({
url: '/pages/index/index'
})
}
}, 1500)
} catch (error: any) {
Taro.showToast({
title: error.message || '登录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
return (
<>
<div className={'flex flex-col justify-center px-5 pt-3'}>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input
type="number"
placeholder="请输入手机号码"
maxLength={11}
value={formData.phone}
onChange={(value) => setFormData({...formData, phone: value})}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
</div>
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
<Input
type="number"
placeholder="请输入6位验证码"
maxLength={6}
value={formData.code}
onChange={(value) => setFormData({...formData, code: value})}
style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}
/>
<Button
size="small"
type={countdown > 0 ? "default" : "primary"}
loading={sendingCode}
disabled={sendingCode || countdown > 0}
onClick={handleSendCode}
>
{countdown > 0 ? `${countdown}s` : sendingCode ? '发送中...' : '获取验证码'}
</Button>
</div>
<div className={'flex justify-center my-5'}>
<Button
type="info"
size={'large'}
className={'w-full rounded-lg p-2'}
loading={loading}
disabled={loading}
onClick={handleLogin}
>
{loading ? '登录中...' : '登录'}
</Button>
</div>
</div>
</>
)
}
export default SmsLogin

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '统一扫码',
navigationBarTextStyle: 'black'
}

View File

@@ -0,0 +1,320 @@
import React, { useState } from 'react';
import { View, Text } from '@tarojs/components';
import { Card, Button, Tag } from '@nutui/nutui-react-taro';
import { Scan, Success, Failure, Tips, ArrowLeft } from '@nutui/icons-react-taro';
import Taro from '@tarojs/taro';
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
/**
* 统一扫码页面
* 支持登录和核销两种类型的二维码扫描
*/
const UnifiedQRPage: React.FC = () => {
const [scanHistory, setScanHistory] = useState<any[]>([]);
const {
startScan,
isLoading,
canScan,
state,
result,
error,
scanType,
reset
} = useUnifiedQRScan();
// 处理扫码成功
const handleScanSuccess = (result: UnifiedScanResult) => {
console.log('扫码成功:', result);
// 添加到扫码历史
const newRecord = {
id: Date.now(),
time: new Date().toLocaleString(),
type: result.type,
data: result.data,
message: result.message,
success: true
};
setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
// 根据类型给出不同提示
if (result.type === ScanType.VERIFICATION) {
// 核销成功后询问是否继续扫码
setTimeout(() => {
Taro.showModal({
title: '核销成功',
content: '是否继续扫码核销其他礼品卡?',
success: (res) => {
if (res.confirm) {
handleStartScan();
}
}
});
}, 2000);
}
};
// 处理扫码失败
const handleScanError = (error: string) => {
console.error('扫码失败:', error);
// 添加到扫码历史
const newRecord = {
id: Date.now(),
time: new Date().toLocaleString(),
error,
success: false
};
setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录
};
// 开始扫码
const handleStartScan = async () => {
try {
const scanResult = await startScan();
if (scanResult) {
handleScanSuccess(scanResult);
}
} catch (error: any) {
handleScanError(error.message || '扫码失败');
}
};
// 返回上一页
const handleGoBack = () => {
Taro.navigateBack();
};
// 获取状态图标
const getStatusIcon = (success: boolean, type?: ScanType) => {
console.log(type,'获取状态图标')
if (success) {
return <Success className="text-green-500" size="16" />;
} else {
return <Failure className="text-red-500" size="16" />;
}
};
// 获取类型标签
const getTypeTag = (type: ScanType) => {
switch (type) {
case ScanType.LOGIN:
return <Tag type="success"></Tag>;
case ScanType.VERIFICATION:
return <Tag type="warning"></Tag>;
default:
return <Tag type="default"></Tag>;
}
};
return (
<View className="unified-qr-page min-h-screen bg-gray-50">
{/* 页面头部 */}
<View className="bg-white px-4 py-3 border-b border-gray-100 flex items-center">
<ArrowLeft
className="text-gray-600 mr-3"
size="20"
onClick={handleGoBack}
/>
<View className="flex-1">
<Text className="text-lg font-bold"></Text>
<Text className="text-sm text-gray-600 block">
</Text>
</View>
</View>
{/* 主要扫码区域 */}
<Card className="m-4">
<View className="text-center py-6">
{/* 状态显示 */}
{state === 'idle' && (
<>
<Scan className="text-blue-500 mx-auto mb-4" size="48" />
<Text className="text-lg font-medium text-gray-800 mb-2 block">
</Text>
<Text className="text-gray-600 mb-6 block">
</Text>
<Button
type="primary"
size="large"
onClick={handleStartScan}
disabled={!canScan() || isLoading}
className="w-full"
>
{canScan() ? '开始扫码' : '请先登录'}
</Button>
</>
)}
{state === 'scanning' && (
<>
<Scan className="text-blue-500 mx-auto mb-4" size="48" />
<Text className="text-lg font-medium text-blue-600 mb-2 block">
...
</Text>
<Text className="text-gray-600 mb-6 block">
</Text>
<Button
type="default"
size="large"
onClick={reset}
className="w-full"
>
</Button>
</>
)}
{state === 'processing' && (
<>
<View className="text-orange-500 mx-auto mb-4">
<Tips size="48" />
</View>
<Text className="text-lg font-medium text-orange-600 mb-2 block">
...
</Text>
<Text className="text-gray-600 mb-6 block">
{scanType === ScanType.LOGIN ? '正在确认登录' :
scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'}
</Text>
</>
)}
{state === 'success' && result && (
<>
<Success className="text-green-500 mx-auto mb-4" size="48" />
<Text className="text-lg font-medium text-green-600 mb-2 block">
{result.message}
</Text>
{result.type === ScanType.VERIFICATION && result.data && (
<View className="bg-green-50 rounded-lg p-3 mb-4">
<Text className="text-sm text-green-800 block">
{result.data.goodsName || '未知商品'}
</Text>
<Text className="text-sm text-green-800 block">
¥{result.data.faceValue}
</Text>
</View>
)}
<View className="mt-2">
<Button
type="primary"
size="large"
onClick={handleStartScan}
className="w-full mb-2"
>
</Button>
<Button
type="default"
size="normal"
onClick={reset}
className="w-full"
>
</Button>
</View>
</>
)}
{state === 'error' && (
<>
<Failure className="text-red-500 mx-auto mb-4" size="48" />
<Text className="text-lg font-medium text-red-600 mb-2 block">
</Text>
<Text className="text-gray-600 mb-6 block">
{error || '未知错误'}
</Text>
<View className="mt-2">
<Button
type="primary"
size="large"
onClick={handleStartScan}
className="w-full mb-2"
>
</Button>
<Button
type="default"
size="normal"
onClick={reset}
className="w-full"
>
</Button>
</View>
</>
)}
</View>
</Card>
{/* 扫码历史 */}
{scanHistory.length > 0 && (
<Card className="m-4">
<View className="pb-4">
<Text className="text-lg font-medium text-gray-800 mb-3 block">
</Text>
{scanHistory.map((record, index) => (
<View
key={record.id}
className={`flex items-center justify-between p-3 bg-gray-50 rounded-lg ${index < scanHistory.length - 1 ? 'mb-2' : ''}`}
>
<View className="flex items-center flex-1">
{getStatusIcon(record.success, record.type)}
<View className="ml-3 flex-1">
<View className="flex items-center mb-1">
{record.type && getTypeTag(record.type)}
<Text className="text-sm text-gray-600 ml-2">
{record.time}
</Text>
</View>
<Text className="text-sm text-gray-800">
{record.success ? record.message : record.error}
</Text>
{record.success && record.type === ScanType.VERIFICATION && record.data && (
<Text className="text-xs text-gray-500">
{record.data.goodsName} - ¥{record.data.faceValue}
</Text>
)}
</View>
</View>
</View>
))}
</View>
</Card>
)}
{/* 功能说明 */}
<Card className="m-4 bg-blue-500 border border-blue-200">
<View className="p-4">
<View className="flex items-start">
<Tips className="text-blue-600 mr-2 mt-1" size="16" />
<View>
<Text className="text-sm font-medium text-blue-800 mb-1 block">
</Text>
<Text className="text-xs text-blue-700 block mb-1">
</Text>
<Text className="text-xs text-blue-700 block mb-1">
</Text>
<Text className="text-xs text-blue-700 block">
</Text>
</View>
</View>
</View>
</Card>
</View>
);
};
export default UnifiedQRPage;

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '详情',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
};

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { WebView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
/**
* 通用 WebView 页面
* 通过 url 参数指定要加载的网页地址
* 用于显示注册协议、隐私政策等网页内容
*/
const WebViewPage: React.FC = () => {
const router = useRouter();
const url = decodeURIComponent(router.params.url || '');
if (!url) {
Taro.navigateBack();
return null;
}
return <WebView src={url} />;
};
export default WebViewPage;