refactor(customer): 优化客户数据查询和表单字段校验
- 移除新增客户页面对手机号的必填和格式校验 - 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制 - 新增判断当前用户是否为超级管理员逻辑 - 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件 - 优化客户列表数据获取逻辑,支持超级管理员查看全部客户 - 调整依赖项,更新使用了新构建的查询参数函数 - 增强状态统计接口参数构建,统一调用参数生成函数 - 优化副作用 Hook 依赖,保证数据加载时机正确
This commit is contained in:
32
.workbuddy/memory/2026-06-04.md
Normal file
32
.workbuddy/memory/2026-06-04.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 2026-06-04 工作日志
|
||||
|
||||
## 登录页迁移 (paopao-taro → template-10582)
|
||||
|
||||
从 `/Users/gxwebsoft/VUE/paopao-taro/src/passport/login.tsx` 迁移微信手机号快捷登录功能到当前项目。
|
||||
|
||||
### 变更文件
|
||||
1. **`src/passport/login.tsx`** — 完全重写,从手机号+密码表单登录改为微信手机号快捷登录
|
||||
- 使用 `openType='getPhoneNumber'` 微信授权登录
|
||||
- 调用 `/wx-login/loginByMpWxPhone` 接口
|
||||
- 支持邀请参数解析与推荐关系绑定
|
||||
- 登录后自动绑定 openid、处理邀请关系
|
||||
- 品牌「南南佐顿门窗」,TenantId = 10582
|
||||
- 无 logo.png 资源,改用品牌名首字「南」文字 logo
|
||||
|
||||
2. **`src/passport/login.scss`** — 新建,从 paopao-taro 迁移的渐变背景登录页样式
|
||||
- 紫蓝渐变背景 + 浮动圆圈动画
|
||||
- 绿色微信登录按钮
|
||||
- 自定义协议勾选框
|
||||
|
||||
3. **`src/utils/invite.ts`** — 新增 `checkAndHandleInviteRelation` 函数
|
||||
- 登录成功后自动检查并处理待处理的邀请关系
|
||||
- 复用已有的 `handleInviteRelation` 函数
|
||||
|
||||
### 依赖确认
|
||||
- `@/api/layout`: `getWxOpenId`, `getUserInfo` ✅ 已有
|
||||
- `@/utils/server`: `saveStorageByLoginUser`, `SERVER_API_URL` ✅ 已有
|
||||
- `@/utils/invite`: `parseInviteParams`, `saveInviteParams`, `trackInviteSource`, `hasPendingInvite` ✅ 已有
|
||||
- `@/config/app`: `TenantId` ✅ 已有 (config/app.ts, TenantId='10582')
|
||||
|
||||
### 构建验证
|
||||
- `taro build --type weapp` 构建成功,无编译错误
|
||||
@@ -17,18 +17,12 @@ function AddCartBar() {
|
||||
setTimeout(() => {
|
||||
Taro.switchTab(
|
||||
{
|
||||
url: '/pages/user/user',
|
||||
url: '/passport/login',
|
||||
},
|
||||
)
|
||||
}, 1000)
|
||||
return false;
|
||||
}
|
||||
if (article?.model == 'bm') {
|
||||
navTo('/bszx/bm/bm?id=' + id)
|
||||
}
|
||||
if (article?.model == 'pay') {
|
||||
navTo('/bszx/pay/pay?id=' + id)
|
||||
}
|
||||
}
|
||||
const reload = (id) => {
|
||||
getCmsArticle(id).then(data => {
|
||||
|
||||
@@ -126,7 +126,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
|
||||
// 渲染统计概览
|
||||
const renderStatsOverview = () => (
|
||||
<View className="px-4 space-y-4">
|
||||
<View className="px-4">
|
||||
{/* 核心数据卡片 */}
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
@@ -182,7 +182,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
||||
<View className="space-y-3">
|
||||
<View className="p-3">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
@@ -208,7 +208,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
const renderInviteRecords = () => (
|
||||
<View className="px-4">
|
||||
{inviteRecords.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
<View className="p-3">
|
||||
{inviteRecords.map((record, index) => (
|
||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4">
|
||||
@@ -253,7 +253,7 @@ const InviteStatsPage: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{ranking.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
<View className="p-3">
|
||||
{ranking.map((item, index) => (
|
||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4 flex items-center">
|
||||
|
||||
@@ -415,7 +415,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* 推广说明 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||||
<View className="space-y-2">
|
||||
<View className="p-2">
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
@@ -436,84 +436,6 @@ const DealerQrcode: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 邀请统计数据 */}
|
||||
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
||||
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
||||
{/* {statsLoading ? (*/}
|
||||
{/* <View className="flex items-center justify-center py-8">*/}
|
||||
{/* <Loading/>*/}
|
||||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : inviteStats ? (*/}
|
||||
{/* <View className="space-y-4">*/}
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||||
{/* {inviteStats.totalInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
||||
{/* {inviteStats.successfulRegistrations || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
||||
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
||||
{/* {inviteStats.todayInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
{/* /!* 邀请来源统计 *!/*/}
|
||||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||||
{/* <View className="mt-4">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||||
{/* <View className="space-y-2">*/}
|
||||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
||||
{/* <View className="flex items-center">*/}
|
||||
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
||||
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-right">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
||||
{/* <Text className="text-xs text-gray-500">*/}
|
||||
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* ))}*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <View className="text-center py-8">*/}
|
||||
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
||||
{/* <Button*/}
|
||||
{/* size="small"*/}
|
||||
{/* type="primary"*/}
|
||||
{/* className="mt-2"*/}
|
||||
{/* onClick={fetchInviteStats}*/}
|
||||
{/* >*/}
|
||||
{/* 刷新数据*/}
|
||||
{/* </Button>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/*</View>*/}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -43,11 +43,7 @@ const QuickActions: React.FC = () => {
|
||||
|
||||
const handleClick = (action: { path: string }) => {
|
||||
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
Taro.navigateTo({url: '/passport/login'})
|
||||
return
|
||||
}
|
||||
navTo(action.path)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '服务协议与隐私政策',
|
||||
navigationBarTitleText: '协议详情',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
55
src/passport/agreement.scss
Normal file
55
src/passport/agreement.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.agreement-page {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #444;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 8px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 4px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,178 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import { RichText, View } from '@tarojs/components'
|
||||
import { wxParse } from '@/utils/common'
|
||||
import './agreement.scss'
|
||||
|
||||
/** 用户服务协议内容 */
|
||||
const TERMS_CONTENT = `
|
||||
<h2>用户服务协议</h2>
|
||||
<p><strong>最后更新日期:2026年5月16日</strong></p>
|
||||
|
||||
<h3>一、协议的范围与接受</h3>
|
||||
<p>欢迎使用南南铝佐顿门窗(以下简称"本小程序")。本小程序由南宁市网宿信息科技有限公司(以下简称"我们")开发并运营。请您在使用本小程序服务之前,仔细阅读并充分理解本协议的全部内容。您点击"同意"或实际使用本小程序服务,即视为您已阅读、理解并同意接受本协议的约束。</p>
|
||||
|
||||
<h3>二、服务内容</h3>
|
||||
<p>本小程序为用户提供以下服务:</p>
|
||||
<ul>
|
||||
<li>体育用品在线浏览与购买</li>
|
||||
<li>商品搜索、收藏与分享</li>
|
||||
<li>订单管理与物流查询</li>
|
||||
<li>会员积分与优惠活动</li>
|
||||
<li>在线客服咨询</li>
|
||||
</ul>
|
||||
|
||||
<h3>三、账号注册与安全</h3>
|
||||
<p>1. 您需要使用微信账号授权登录本小程序。您应确保提供的个人信息真实、准确、完整。</p>
|
||||
<p>2. 您应妥善保管自己的账号信息,对使用您账号进行的所有行为承担法律责任。</p>
|
||||
<p>3. 如发现账号异常,请立即联系我们。</p>
|
||||
|
||||
<h3>四、用户行为规范</h3>
|
||||
<p>您在使用本小程序时,应当遵守法律法规,不得从事以下行为:</p>
|
||||
<ul>
|
||||
<li>发布违法违规信息</li>
|
||||
<li>侵犯他人知识产权或其他合法权益</li>
|
||||
<li>恶意刷单、虚假交易</li>
|
||||
<li>利用漏洞或技术手段干扰本小程序正常运行</li>
|
||||
</ul>
|
||||
|
||||
<h3>五、订单与支付</h3>
|
||||
<p>1. 您下单后,我们将根据您提供的收货信息安排发货。</p>
|
||||
<p>2. 商品价格以您下单时的页面显示为准。</p>
|
||||
<p>3. 您可以选择微信支付等方式完成付款。</p>
|
||||
|
||||
<h3>六、售后服务</h3>
|
||||
<p>我们提供7天无理由退换货服务(特殊商品除外)。如有售后问题,请联系在线客服。</p>
|
||||
|
||||
<h3>七、免责声明</h3>
|
||||
<p>1. 因不可抗力导致的服务中断,我们不承担责任。</p>
|
||||
<p>2. 因您自身原因导致的损失,我们不承担责任。</p>
|
||||
|
||||
<h3>八、协议的变更</h3>
|
||||
<p>我们可能会根据业务需要修改本协议。修改后的协议将在本小程序内公布,您继续使用服务视为接受修改后的协议。</p>
|
||||
|
||||
<h3>九、联系我们</h3>
|
||||
<p>如您对本协议有任何疑问,请联系:</p>
|
||||
<p>客服电话:0771-5386339</p>
|
||||
<p>客服邮箱:support@paopao.com</p>
|
||||
`
|
||||
|
||||
/** 隐私政策内容 */
|
||||
const PRIVACY_CONTENT = `
|
||||
<h2>隐私政策</h2>
|
||||
<p><strong>最后更新日期:2026年5月16日</strong></p>
|
||||
|
||||
<p>广州网软信息技术有限公司(以下简称"我们")非常重视您的个人信息保护。本隐私政策说明我们如何收集、使用、存储和保护您的个人信息。</p>
|
||||
|
||||
<h3>一、我们收集的信息</h3>
|
||||
<p>为了向您提供服务,我们可能需要收集以下信息:</p>
|
||||
|
||||
<h4>1. 您主动提供的信息</h4>
|
||||
<ul>
|
||||
<li><strong>账号信息</strong>:微信昵称、头像、OpenID(用于登录识别)</li>
|
||||
<li><strong>收货信息</strong>:收货人姓名、手机号码、详细地址(用于商品配送)</li>
|
||||
<li><strong>联系信息</strong>:您在与客服沟通时提供的信息</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 我们自动收集的信息</h4>
|
||||
<ul>
|
||||
<li><strong>设备信息</strong>:设备型号、操作系统版本</li>
|
||||
<li><strong>日志信息</strong>:访问时间、浏览记录、操作记录</li>
|
||||
<li><strong>位置信息</strong>:经您授权后获取的地理位置(用于推荐附近门店)</li>
|
||||
</ul>
|
||||
|
||||
<h3>二、我们如何使用您的信息</h3>
|
||||
<p>我们收集您的信息用于以下目的:</p>
|
||||
<ul>
|
||||
<li><strong>用户识别与登录</strong>:使用微信 OpenID 识别您的身份,实现免密登录</li>
|
||||
<li><strong>商品配送</strong>:使用收货地址信息完成订单配送</li>
|
||||
<li><strong>订单管理</strong>:处理您的订单、退换货申请</li>
|
||||
<li><strong>客户服务</strong>:响应您的咨询、投诉和建议</li>
|
||||
<li><strong>安全风控</strong>:防范欺诈、保障交易安全</li>
|
||||
<li><strong>服务优化</strong>:分析用户行为,改进产品体验</li>
|
||||
</ul>
|
||||
|
||||
<h3>三、信息的存储与保护</h3>
|
||||
<p>1. 我们采用加密技术保护您的个人信息安全。</p>
|
||||
<p>2. 您的个人信息存储在中国大陆境内的服务器上。</p>
|
||||
<p>3. 我们仅在实现服务目的所必需的期限内保留您的信息。</p>
|
||||
|
||||
<h3>四、信息的共享与披露</h3>
|
||||
<p>我们不会向第三方出售您的个人信息。仅在以下情况下可能共享:</p>
|
||||
<ul>
|
||||
<li>经您明确同意</li>
|
||||
<li>为完成商品配送,向物流公司提供必要的收货信息</li>
|
||||
<li>根据法律法规要求或政府机关的合法要求</li>
|
||||
</ul>
|
||||
|
||||
<h3>五、您的权利</h3>
|
||||
<p>您享有以下权利:</p>
|
||||
<ul>
|
||||
<li>查询、更正您的个人信息</li>
|
||||
<li>删除您的个人信息</li>
|
||||
<li>撤回授权同意</li>
|
||||
<li>注销账号</li>
|
||||
</ul>
|
||||
<p>如需行使上述权利,请联系我们的客服。</p>
|
||||
|
||||
<h3>六、未成年人保护</h3>
|
||||
<p>我们非常重视未成年人的个人信息保护。如果您是未成年人,请在监护人指导下使用本小程序。</p>
|
||||
|
||||
<h3>七、政策更新</h3>
|
||||
<p>我们可能会适时更新本隐私政策。更新后的政策将在本小程序内公布。</p>
|
||||
|
||||
<h3>八、联系我们</h3>
|
||||
<p>如您对本隐私政策有任何疑问,请联系:</p>
|
||||
<p>客服电话:0771-5386339</p>
|
||||
<p>客服邮箱:privacy@paopao.com</p>
|
||||
`
|
||||
|
||||
const Agreement = () => {
|
||||
|
||||
const [content, setContent] = useState<any>('')
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
setContent('<p>' +
|
||||
'<span style="font-size: 14px;">欢迎使用</span>' +
|
||||
'<span style="font-size: 14px;"> </span>' +
|
||||
'<span style="font-size: 14px;"><strong><span style="color: rgb(255, 0, 0);">【WebSoft】</span></strong></span>' +
|
||||
'<span style="font-size: 14px;">服务协议 </span>' +
|
||||
'</p>')
|
||||
}
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [content, setContent] = useState<string>('')
|
||||
const [title, setTitle] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
const init = async () => {
|
||||
try {
|
||||
// 获取页面参数 type=terms 或 type=privacy
|
||||
const pages = Taro.getCurrentPages()
|
||||
const current = pages[pages.length - 1]
|
||||
const type = (current as any)?.options?.type || 'terms'
|
||||
|
||||
if (type === 'privacy') {
|
||||
setTitle('隐私政策')
|
||||
setContent(wxParse(PRIVACY_CONTENT))
|
||||
} else {
|
||||
setTitle('用户服务协议')
|
||||
setContent(wxParse(TERMS_CONTENT))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('load agreement failed', e)
|
||||
setContent('<p>协议内容加载失败</p>')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [])
|
||||
|
||||
// 动态设置导航栏标题
|
||||
useEffect(() => {
|
||||
if (title) {
|
||||
Taro.setNavigationBarTitle({ title })
|
||||
}
|
||||
}, [title])
|
||||
|
||||
if (loading) {
|
||||
return <Loading className='px-2'>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'content text-gray-700 text-sm p-4'}>
|
||||
<RichText nodes={content}/>
|
||||
<View className='agreement-page'>
|
||||
<RichText nodes={content} />
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Agreement
|
||||
|
||||
377
src/passport/login.scss
Normal file
377
src/passport/login.scss
Normal file
@@ -0,0 +1,377 @@
|
||||
// 页面容器
|
||||
.page-login {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s ease-in-out;
|
||||
|
||||
&--show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景装饰
|
||||
.login-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-bg__gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
right: -50%;
|
||||
bottom: -50%;
|
||||
background: radial-gradient(circle at 30% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 50%);
|
||||
animation: gradientMove 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientMove {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -30px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-bg__circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
background: #ffffff;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.login-bg__circle--1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: -150px;
|
||||
right: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.login-bg__circle--2 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
bottom: 10%;
|
||||
left: -100px;
|
||||
animation-delay: -7s;
|
||||
}
|
||||
|
||||
.login-bg__circle--3 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 40%;
|
||||
right: -50px;
|
||||
animation-delay: -14s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -30px) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.login-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 50px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// 头部 Logo 和标题
|
||||
.login-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 120px;
|
||||
margin-bottom: 60px;
|
||||
animation: slideDown 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 40px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: logoFloat 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.login-logo__image {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
animation: fadeIn 1s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 28px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
animation: fadeIn 1s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
// 登录主体区域
|
||||
.login-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
animation: slideUp 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-methods {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.login-methods__tip {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
// 登录按钮
|
||||
.login-btn {
|
||||
height: 96px;
|
||||
width: 100%;
|
||||
border-radius: 48px;
|
||||
border: none;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login-btn__icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.login-btn__text {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
// 分割线
|
||||
.login-methods__divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
// 特性列表
|
||||
.login-features {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 40px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 22px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
// 非微信小程序环境提示
|
||||
.login-non-weapp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.non-weapp-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.non-weapp-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.non-weapp-desc {
|
||||
font-size: 26px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// 底部协议区域
|
||||
.login-footer {
|
||||
padding: 30px 0 50px;
|
||||
}
|
||||
|
||||
.login-agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 自定义勾选框
|
||||
.login-agreement__checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.5);
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&--checked {
|
||||
background: #07c160;
|
||||
border-color: #07c160;
|
||||
}
|
||||
}
|
||||
|
||||
.login-agreement__check {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.login-agreement__text {
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,147 +1,337 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms} from '@/api/passport/login'
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import { TenantId } from '@/config/app'
|
||||
import { getWxOpenId, getUserInfo } from '@/api/layout'
|
||||
import { saveStorageByLoginUser, SERVER_API_URL } from '@/utils/server'
|
||||
import {
|
||||
checkAndHandleInviteRelation,
|
||||
hasPendingInvite,
|
||||
parseInviteParams,
|
||||
saveInviteParams,
|
||||
trackInviteSource,
|
||||
} from '@/utils/invite'
|
||||
import './login.scss'
|
||||
|
||||
interface GetPhoneNumberDetail {
|
||||
code?: string
|
||||
encryptedData?: string
|
||||
iv?: string
|
||||
errMsg?: string
|
||||
}
|
||||
|
||||
interface GetPhoneNumberEvent {
|
||||
detail: GetPhoneNumberDetail
|
||||
}
|
||||
|
||||
/** 同步判断当前是否是微信小程序环境 */
|
||||
function detectIsWeapp(): boolean {
|
||||
try {
|
||||
return Taro.getEnv() === Taro.ENV_TYPE.WEAPP
|
||||
} catch {
|
||||
return process.env.TARO_ENV === 'weapp'
|
||||
}
|
||||
}
|
||||
|
||||
const IS_WEAPP = detectIsWeapp()
|
||||
|
||||
/** 小程序登录获取 code */
|
||||
async function getWeappLoginCode(): Promise<string | undefined> {
|
||||
try {
|
||||
const res = await new Promise<{ code?: string }>((resolve, reject) => {
|
||||
Taro.login({ success: (r) => resolve(r), fail: reject })
|
||||
})
|
||||
return res?.code
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 确保微信 openid 已保存到服务端 */
|
||||
async function ensureWxOpenIdSaved() {
|
||||
try {
|
||||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return
|
||||
} catch {
|
||||
if (process.env.TARO_ENV !== 'weapp') return
|
||||
}
|
||||
|
||||
const code = await getWeappLoginCode()
|
||||
if (!code) return
|
||||
|
||||
try {
|
||||
await getWxOpenId({ code })
|
||||
const freshUser = await getUserInfo()
|
||||
if (freshUser) {
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
saveStorageByLoginUser(token, freshUser)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('登录后绑定 openid 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取用户信息 */
|
||||
async function fetchUserInfo(token: string) {
|
||||
try {
|
||||
const res: any = await Taro.request({
|
||||
url: `${SERVER_API_URL}/auth/user`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'content-type': 'application/json',
|
||||
'TenantId': String(TenantId),
|
||||
},
|
||||
})
|
||||
|
||||
if (res.data?.code === 0 && res.data?.data) {
|
||||
return res.data.data
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const Login = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const [phone, setPhone] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showContent, setShowContent] = useState(false)
|
||||
|
||||
const reload = () => {
|
||||
const router = Taro.getCurrentInstance().router
|
||||
const isWeapp = IS_WEAPP
|
||||
|
||||
/** 页面加载动画 */
|
||||
useEffect(() => {
|
||||
Taro.hideTabBar()
|
||||
setTimeout(() => setShowContent(true), 100)
|
||||
}, [])
|
||||
|
||||
/** 解析 redirect 参数 */
|
||||
const redirectUrl = (() => {
|
||||
const raw = (router?.params as Record<string, string> | undefined)?.redirect
|
||||
if (!raw) return ''
|
||||
try {
|
||||
const decoded = decodeURIComponent(raw)
|
||||
return decoded.startsWith('/') ? decoded : `/${decoded}`
|
||||
} catch {
|
||||
return raw.startsWith('/') ? raw : `/${raw}`
|
||||
}
|
||||
})()
|
||||
|
||||
/** 处理邀请参数 */
|
||||
useEffect(() => {
|
||||
try {
|
||||
const inviteParams = parseInviteParams({ query: router?.params })
|
||||
if (inviteParams?.inviter) {
|
||||
saveInviteParams(inviteParams)
|
||||
trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter, 10))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('登录页处理邀请参数失败:', e)
|
||||
}
|
||||
}, [router?.params])
|
||||
|
||||
/** 登录成功后跳转 */
|
||||
const navigateAfterLogin = async () => {
|
||||
if (!redirectUrl) {
|
||||
await Taro.reLaunch({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
const tabBarUrls = [
|
||||
'/pages/index/index',
|
||||
'/pages/shop/index',
|
||||
'/pages/points/index',
|
||||
'/pages/user/user',
|
||||
]
|
||||
const pure = redirectUrl.split('?')[0]
|
||||
|
||||
if (tabBarUrls.includes(pure)) {
|
||||
await Taro.switchTab({ url: pure })
|
||||
return
|
||||
}
|
||||
|
||||
await Taro.redirectTo({ url: redirectUrl })
|
||||
}
|
||||
|
||||
/** 手机号快捷登录 */
|
||||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||||
if (!isAgree) {
|
||||
Taro.showToast({
|
||||
title: '请先同意服务协议',
|
||||
icon: 'none'
|
||||
})
|
||||
Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (loading) return
|
||||
|
||||
if (!phone || phone.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请输入手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
if (!phoneRegex.test(phone)) {
|
||||
Taro.showToast({
|
||||
title: '请输入正确的手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
const { code: phoneCode, errMsg } = detail || {}
|
||||
if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
|
||||
Taro.showToast({ title: '未授权手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await loginBySms({
|
||||
phone: phone,
|
||||
code: password
|
||||
|
||||
const inviteParams = parseInviteParams({ query: router?.params })
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0
|
||||
|
||||
const res: any = await Taro.request({
|
||||
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: phoneCode,
|
||||
notVerifyPhone: true,
|
||||
refereeId,
|
||||
sceneType: 'save_referee',
|
||||
tenantId: Number(TenantId),
|
||||
},
|
||||
header: { 'content-type': 'application/json', TenantId: String(TenantId) },
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
if (res.data?.code === 0 && res.data?.data?.access_token) {
|
||||
const token = res.data.data.access_token
|
||||
let user = res.data.data.user
|
||||
|
||||
// 延迟跳转到首页
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '登录失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
// 获取最新的用户信息
|
||||
const freshUserInfo = await fetchUserInfo(token)
|
||||
if (freshUserInfo) {
|
||||
user = freshUserInfo
|
||||
}
|
||||
|
||||
saveStorageByLoginUser(token, user)
|
||||
|
||||
// 绑定 openid + 处理邀请关系
|
||||
await ensureWxOpenIdSaved()
|
||||
if (hasPendingInvite()) {
|
||||
try { await checkAndHandleInviteRelation() } catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => navigateAfterLogin(), 800)
|
||||
} else {
|
||||
Taro.showToast({ title: res.data?.message || '登录失败', icon: 'none' })
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('微信登录失败:', e)
|
||||
Taro.showToast({ title: e?.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<View className={`page-login ${showContent ? 'page-login--show' : ''}`}>
|
||||
{/* 渐变背景 */}
|
||||
<View className='login-bg'>
|
||||
<View className='login-bg__gradient' />
|
||||
<View className='login-bg__circle login-bg__circle--1' />
|
||||
<View className='login-bg__circle login-bg__circle--2' />
|
||||
<View className='login-bg__circle login-bg__circle--3' />
|
||||
</View>
|
||||
|
||||
<>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="手机号"
|
||||
maxLength={11}
|
||||
value={phone}
|
||||
onChange={(val) => setPhone(val)}
|
||||
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}
|
||||
{/* 主要内容区域 */}
|
||||
<View className='login-content'>
|
||||
{/* Logo 和标题 */}
|
||||
<View className='login-header'>
|
||||
<View className='login-logo'>
|
||||
<Image
|
||||
className='login-logo__image'
|
||||
src={'https://oss.wsdns.cn/20260121/6fcfb2c8308c40d4933c85c88805ddd0.jpg'}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(val) => setPassword(val)}
|
||||
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'}>
|
||||
{/*<Text className='login-logo__text'></Text>*/}
|
||||
</View>
|
||||
<Text className='login-title'>欢迎来到南南佐顿门窗</Text>
|
||||
<Text className='login-subtitle'>登录后享受更多精彩功能</Text>
|
||||
</View>
|
||||
|
||||
{/* 登录按钮区域 */}
|
||||
<View className='login-body'>
|
||||
{isWeapp && (
|
||||
<View className='login-methods'>
|
||||
|
||||
<Button
|
||||
type="info"
|
||||
size={'large'}
|
||||
className={'w-full rounded-lg p-2'}
|
||||
disabled={!isAgree}
|
||||
className='login-btn'
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #07c160, #06ad56)',
|
||||
border: 'none',
|
||||
borderRadius: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
opacity: (!isAgree || loading) ? 0.5 : 1,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
openType='getPhoneNumber'
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
disabled={!isAgree || loading}
|
||||
loading={loading}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
手机号快捷登录
|
||||
</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>
|
||||
</>
|
||||
<View className='login-methods__tip'>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '24px' }}>点击上方按钮,快速登录</Text>
|
||||
</View>
|
||||
|
||||
<View className='login-methods__divider'>
|
||||
<View className='divider-line' />
|
||||
<Text className='divider-text'>安全快捷</Text>
|
||||
<View className='divider-line' />
|
||||
</View>
|
||||
|
||||
<View className='login-features'>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>🔒</Text>
|
||||
<Text className='feature-text'>安全可靠</Text>
|
||||
</View>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>⚡</Text>
|
||||
<Text className='feature-text'>一键登录</Text>
|
||||
</View>
|
||||
<View className='feature-item'>
|
||||
<Text className='feature-icon'>🎁</Text>
|
||||
<Text className='feature-text'>专属福利</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 非微信小程序环境提示 */}
|
||||
{!isWeapp && (
|
||||
<View className='login-non-weapp'>
|
||||
<View className='non-weapp-icon'>💻</View>
|
||||
<Text className='non-weapp-title'>请在微信小程序中打开</Text>
|
||||
<Text className='non-weapp-desc'>当前环境不支持手机号快捷登录,请在微信小程序中使用完整功能</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 协议勾选 - 自定义实现 */}
|
||||
<View className='login-footer'>
|
||||
<View className='login-agreement' onClick={() => setIsAgree(!isAgree)}>
|
||||
<View className={`login-agreement__checkbox ${isAgree ? 'login-agreement__checkbox--checked' : ''}`}>
|
||||
{isAgree && <Text className='login-agreement__check'>✓</Text>}
|
||||
</View>
|
||||
<Text className='login-agreement__text'>
|
||||
{'我已阅读并同意'}
|
||||
<Text className='link' onClick={(e) => { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=terms' }) }}>
|
||||
{'《用户协议》'}
|
||||
</Text>
|
||||
{'和'}
|
||||
<Text className='link' onClick={(e) => { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=privacy' }) }}>
|
||||
{'《隐私政策》'}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
||||
@@ -83,7 +83,7 @@ const ThemeSelector: React.FC = () => {
|
||||
>
|
||||
<Text className="text-lg font-bold mb-2">当前主题预览</Text>
|
||||
<Text className="text-sm opacity-90 px-2">{currentTheme.description}</Text>
|
||||
<View className="mt-4 flex justify-center space-x-4">
|
||||
<View className="mt-4 flex justify-center">
|
||||
<View
|
||||
className="w-8 h-8 rounded-full"
|
||||
style={{ backgroundColor: currentTheme.primary }}
|
||||
|
||||
@@ -239,6 +239,38 @@ export function getSourceDisplayName(source: string): string {
|
||||
return sourceMap[source] || source
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并处理待处理的邀请关系
|
||||
* 登录成功后调用,自动绑定推荐人关系
|
||||
*/
|
||||
export async function checkAndHandleInviteRelation(): Promise<boolean> {
|
||||
try {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (!inviteParams?.inviter) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inviterId = parseInt(inviteParams.inviter)
|
||||
if (isNaN(inviterId)) {
|
||||
clearInviteParams()
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取当前登录用户ID
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
if (!userId) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 复用已有的 handleInviteRelation
|
||||
const result = await handleInviteRelation(Number(userId))
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码格式
|
||||
*/
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
// tailwind.config.js
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
darkMode: 'media', // or 'media' or 'class'
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontSize: {
|
||||
'15': '15px',
|
||||
'17': '17px',
|
||||
'28': '28px',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
// 禁用微信小程序不支持的功能
|
||||
preflight: false, // 禁用默认样式重置
|
||||
// 禁用包含复杂选择器的插件
|
||||
space: false, // 禁用 space-x, space-y 等(包含 :not([hidden]) 选择器)
|
||||
divideWidth: false, // 禁用 divide-x, divide-y 等
|
||||
divideColor: false,
|
||||
divideStyle: false,
|
||||
divideOpacity: false,
|
||||
},
|
||||
};
|
||||
// Taro 小程序端不需要 preflight(浏览器重置样式)
|
||||
preflight: false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user