From f4935a031a935d4648d4ef50f864877f50993aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 4 Jun 2026 16:49:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(customer):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=E5=92=8C?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E5=AD=97=E6=AE=B5=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除新增客户页面对手机号的必填和格式校验 - 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制 - 新增判断当前用户是否为超级管理员逻辑 - 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件 - 优化客户列表数据获取逻辑,支持超级管理员查看全部客户 - 调整依赖项,更新使用了新构建的查询参数函数 - 增强状态统计接口参数构建,统一调用参数生成函数 - 优化副作用 Hook 依赖,保证数据加载时机正确 --- .workbuddy/memory/2026-06-04.md | 32 +++ src/components/AddCartBar.tsx | 8 +- src/dealer/invite-stats/index.tsx | 8 +- src/dealer/qrcode/index.tsx | 80 +----- src/pages/index/QuickActions.tsx | 6 +- src/passport/agreement.config.ts | 2 +- src/passport/agreement.scss | 55 ++++ src/passport/agreement.tsx | 186 +++++++++++-- src/passport/login.scss | 377 +++++++++++++++++++++++++++ src/passport/login.tsx | 418 ++++++++++++++++++++++-------- src/user/theme/index.tsx | 2 +- src/utils/invite.ts | 32 +++ tailwind.config.js | 41 ++- 13 files changed, 996 insertions(+), 251 deletions(-) create mode 100644 .workbuddy/memory/2026-06-04.md create mode 100644 src/passport/agreement.scss create mode 100644 src/passport/login.scss diff --git a/.workbuddy/memory/2026-06-04.md b/.workbuddy/memory/2026-06-04.md new file mode 100644 index 0000000..a14eefe --- /dev/null +++ b/.workbuddy/memory/2026-06-04.md @@ -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` 构建成功,无编译错误 diff --git a/src/components/AddCartBar.tsx b/src/components/AddCartBar.tsx index def5f75..2b78bd4 100644 --- a/src/components/AddCartBar.tsx +++ b/src/components/AddCartBar.tsx @@ -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 => { diff --git a/src/dealer/invite-stats/index.tsx b/src/dealer/invite-stats/index.tsx index 3139ad4..daaa994 100644 --- a/src/dealer/invite-stats/index.tsx +++ b/src/dealer/invite-stats/index.tsx @@ -126,7 +126,7 @@ const InviteStatsPage: React.FC = () => { // 渲染统计概览 const renderStatsOverview = () => ( - + {/* 核心数据卡片 */} @@ -182,7 +182,7 @@ const InviteStatsPage: React.FC = () => { 邀请来源分析 - + {inviteStats.sourceStats.map((source, index) => ( @@ -208,7 +208,7 @@ const InviteStatsPage: React.FC = () => { const renderInviteRecords = () => ( {inviteRecords.length > 0 ? ( - + {inviteRecords.map((record, index) => ( @@ -253,7 +253,7 @@ const InviteStatsPage: React.FC = () => { {ranking.length > 0 ? ( - + {ranking.map((item, index) => ( diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index d9e05c8..1043e3c 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -415,7 +415,7 @@ const DealerQrcode: React.FC = () => { {/* 推广说明 */} 推广说明 - + @@ -436,84 +436,6 @@ const DealerQrcode: React.FC = () => { - - {/* 邀请统计数据 */} - {/**/} - {/* 我的邀请数据*/} - {/* {statsLoading ? (*/} - {/* */} - {/* */} - {/* 加载中...*/} - {/* */} - {/* ) : inviteStats ? (*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* {inviteStats.totalInvites || 0}*/} - {/* */} - {/* 总邀请数*/} - {/* */} - {/* */} - {/* */} - {/* {inviteStats.successfulRegistrations || 0}*/} - {/* */} - {/* 成功注册*/} - {/* */} - {/* */} - - {/* */} - {/* */} - {/* */} - {/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/} - {/* */} - {/* 转化率*/} - {/* */} - {/* */} - {/* */} - {/* {inviteStats.todayInvites || 0}*/} - {/* */} - {/* 今日邀请*/} - {/* */} - {/* */} - - {/* /!* 邀请来源统计 *!/*/} - {/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/} - {/* */} - {/* 邀请来源分布*/} - {/* */} - {/* {inviteStats.sourceStats.map((source, index) => (*/} - {/* */} - {/* */} - {/* */} - {/* {source.source}*/} - {/* */} - {/* */} - {/* {source.count}*/} - {/* */} - {/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/} - {/* */} - {/* */} - {/* */} - {/* ))}*/} - {/* */} - {/* */} - {/* )}*/} - {/* */} - {/* ) : (*/} - {/* */} - {/* 暂无邀请数据*/} - {/* */} - {/* 刷新数据*/} - {/* */} - {/* */} - {/* )}*/} - {/**/} ) diff --git a/src/pages/index/QuickActions.tsx b/src/pages/index/QuickActions.tsx index 18fa145..59a4318 100644 --- a/src/pages/index/QuickActions.tsx +++ b/src/pages/index/QuickActions.tsx @@ -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) diff --git a/src/passport/agreement.config.ts b/src/passport/agreement.config.ts index b44241e..c410cea 100644 --- a/src/passport/agreement.config.ts +++ b/src/passport/agreement.config.ts @@ -1,4 +1,4 @@ export default definePageConfig({ - navigationBarTitleText: '服务协议与隐私政策', + navigationBarTitleText: '协议详情', navigationBarTextStyle: 'black' }) diff --git a/src/passport/agreement.scss b/src/passport/agreement.scss new file mode 100644 index 0000000..efb03ac --- /dev/null +++ b/src/passport/agreement.scss @@ -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; + } +} diff --git a/src/passport/agreement.tsx b/src/passport/agreement.tsx index 5ce26d1..148e932 100644 --- a/src/passport/agreement.tsx +++ b/src/passport/agreement.tsx @@ -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 = ` +

用户服务协议

+

最后更新日期:2026年5月16日

+ +

一、协议的范围与接受

+

欢迎使用南南铝佐顿门窗(以下简称"本小程序")。本小程序由南宁市网宿信息科技有限公司(以下简称"我们")开发并运营。请您在使用本小程序服务之前,仔细阅读并充分理解本协议的全部内容。您点击"同意"或实际使用本小程序服务,即视为您已阅读、理解并同意接受本协议的约束。

+ +

二、服务内容

+

本小程序为用户提供以下服务:

+
    +
  • 体育用品在线浏览与购买
  • +
  • 商品搜索、收藏与分享
  • +
  • 订单管理与物流查询
  • +
  • 会员积分与优惠活动
  • +
  • 在线客服咨询
  • +
+ +

三、账号注册与安全

+

1. 您需要使用微信账号授权登录本小程序。您应确保提供的个人信息真实、准确、完整。

+

2. 您应妥善保管自己的账号信息,对使用您账号进行的所有行为承担法律责任。

+

3. 如发现账号异常,请立即联系我们。

+ +

四、用户行为规范

+

您在使用本小程序时,应当遵守法律法规,不得从事以下行为:

+
    +
  • 发布违法违规信息
  • +
  • 侵犯他人知识产权或其他合法权益
  • +
  • 恶意刷单、虚假交易
  • +
  • 利用漏洞或技术手段干扰本小程序正常运行
  • +
+ +

五、订单与支付

+

1. 您下单后,我们将根据您提供的收货信息安排发货。

+

2. 商品价格以您下单时的页面显示为准。

+

3. 您可以选择微信支付等方式完成付款。

+ +

六、售后服务

+

我们提供7天无理由退换货服务(特殊商品除外)。如有售后问题,请联系在线客服。

+ +

七、免责声明

+

1. 因不可抗力导致的服务中断,我们不承担责任。

+

2. 因您自身原因导致的损失,我们不承担责任。

+ +

八、协议的变更

+

我们可能会根据业务需要修改本协议。修改后的协议将在本小程序内公布,您继续使用服务视为接受修改后的协议。

+ +

九、联系我们

+

如您对本协议有任何疑问,请联系:

+

客服电话:0771-5386339

+

客服邮箱:support@paopao.com

+` + +/** 隐私政策内容 */ +const PRIVACY_CONTENT = ` +

隐私政策

+

最后更新日期:2026年5月16日

+ +

广州网软信息技术有限公司(以下简称"我们")非常重视您的个人信息保护。本隐私政策说明我们如何收集、使用、存储和保护您的个人信息。

+ +

一、我们收集的信息

+

为了向您提供服务,我们可能需要收集以下信息:

+ +

1. 您主动提供的信息

+
    +
  • 账号信息:微信昵称、头像、OpenID(用于登录识别)
  • +
  • 收货信息:收货人姓名、手机号码、详细地址(用于商品配送)
  • +
  • 联系信息:您在与客服沟通时提供的信息
  • +
+ +

2. 我们自动收集的信息

+
    +
  • 设备信息:设备型号、操作系统版本
  • +
  • 日志信息:访问时间、浏览记录、操作记录
  • +
  • 位置信息:经您授权后获取的地理位置(用于推荐附近门店)
  • +
+ +

二、我们如何使用您的信息

+

我们收集您的信息用于以下目的:

+
    +
  • 用户识别与登录:使用微信 OpenID 识别您的身份,实现免密登录
  • +
  • 商品配送:使用收货地址信息完成订单配送
  • +
  • 订单管理:处理您的订单、退换货申请
  • +
  • 客户服务:响应您的咨询、投诉和建议
  • +
  • 安全风控:防范欺诈、保障交易安全
  • +
  • 服务优化:分析用户行为,改进产品体验
  • +
+ +

三、信息的存储与保护

+

1. 我们采用加密技术保护您的个人信息安全。

+

2. 您的个人信息存储在中国大陆境内的服务器上。

+

3. 我们仅在实现服务目的所必需的期限内保留您的信息。

+ +

四、信息的共享与披露

+

我们不会向第三方出售您的个人信息。仅在以下情况下可能共享:

+
    +
  • 经您明确同意
  • +
  • 为完成商品配送,向物流公司提供必要的收货信息
  • +
  • 根据法律法规要求或政府机关的合法要求
  • +
+ +

五、您的权利

+

您享有以下权利:

+
    +
  • 查询、更正您的个人信息
  • +
  • 删除您的个人信息
  • +
  • 撤回授权同意
  • +
  • 注销账号
  • +
+

如需行使上述权利,请联系我们的客服。

+ +

六、未成年人保护

+

我们非常重视未成年人的个人信息保护。如果您是未成年人,请在监护人指导下使用本小程序。

+ +

七、政策更新

+

我们可能会适时更新本隐私政策。更新后的政策将在本小程序内公布。

+ +

八、联系我们

+

如您对本隐私政策有任何疑问,请联系:

+

客服电话:0771-5386339

+

客服邮箱:privacy@paopao.com

+` const Agreement = () => { - - const [content, setContent] = useState('') - const reload = () => { - Taro.hideTabBar() - setContent('

' + - '欢迎使用' + - ' ' + - '【WebSoft】' + - '服务协议 ' + - '

') - } + const [loading, setLoading] = useState(true) + const [content, setContent] = useState('') + const [title, setTitle] = useState('') 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('

协议内容加载失败

') + } finally { + setLoading(false) + } + } + init() }, []) + // 动态设置导航栏标题 + useEffect(() => { + if (title) { + Taro.setNavigationBarTitle({ title }) + } + }, [title]) + + if (loading) { + return 加载中 + } + return ( - <> - - - - + + + ) } export default Agreement diff --git a/src/passport/login.scss b/src/passport/login.scss new file mode 100644 index 0000000..bde80cb --- /dev/null +++ b/src/passport/login.scss @@ -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; +} diff --git a/src/passport/login.tsx b/src/passport/login.tsx index 76103cd..7236fa0 100644 --- a/src/passport/login.tsx +++ b/src/passport/login.tsx @@ -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 { + 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 | 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 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 handleLogin = async () => { + /** 手机号快捷登录 */ + 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 ( - <> -
-
账号登录
+ + {/* 渐变背景 */} + + + + + + - <> -
- setPhone(val)} - style={{backgroundColor: '#ffffff', borderRadius: '8px'}} + {/* 主要内容区域 */} + + {/* Logo 和标题 */} + + + -
-
- setPassword(val)} - style={{backgroundColor: '#ffffff', borderRadius: '8px'}} - /> -
- {/**/} -
- -
- {/*
*/} - {/* */} - {/*
*/} - {/**/} - + {/**/} +
+ 欢迎来到南南佐顿门窗 + 登录后享受更多精彩功能 +
-
- setIsAgree(!isAgree)}> - setIsAgree(!isAgree)}>勾选表示您已阅读并同意 Taro.navigateTo({url: '/passport/agreement'})} - className={'text-blue-600'}>《服务协议及隐私政策》 -
-
- + {/* 登录按钮区域 */} + + {isWeapp && ( + + + + + + 点击上方按钮,快速登录 + + + + + 安全快捷 + + + + + + 🔒 + 安全可靠 + + + + 一键登录 + + + 🎁 + 专属福利 + + + + )} + + {/* 非微信小程序环境提示 */} + {!isWeapp && ( + + 💻 + 请在微信小程序中打开 + 当前环境不支持手机号快捷登录,请在微信小程序中使用完整功能 + + )} + + + {/* 协议勾选 - 自定义实现 */} + + setIsAgree(!isAgree)}> + + {isAgree && } + + + {'我已阅读并同意'} + { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=terms' }) }}> + {'《用户协议》'} + + {'和'} + { e.stopPropagation(); Taro.navigateTo({ url: '/passport/agreement?type=privacy' }) }}> + {'《隐私政策》'} + + + + + + ) } + export default Login diff --git a/src/user/theme/index.tsx b/src/user/theme/index.tsx index ec41396..58a63f4 100644 --- a/src/user/theme/index.tsx +++ b/src/user/theme/index.tsx @@ -83,7 +83,7 @@ const ThemeSelector: React.FC = () => { > 当前主题预览 {currentTheme.description} - + { + 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 + } +} + /** * 验证邀请码格式 */ diff --git a/tailwind.config.js b/tailwind.config.js index dadde1c..45d1a1d 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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 + } +}