Compare commits

..

4 Commits

Author SHA1 Message Date
b130d4ac4c refactor(customer): 优化客户数据查询和表单字段校验
- 移除新增客户页面对手机号的必填和格式校验
- 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制
- 新增判断当前用户是否为超级管理员逻辑
- 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件
- 优化客户列表数据获取逻辑,支持超级管理员查看全部客户
- 调整依赖项,更新使用了新构建的查询参数函数
- 增强状态统计接口参数构建,统一调用参数生成函数
- 优化副作用 Hook 依赖,保证数据加载时机正确
2026-06-04 17:15:48 +08:00
78b67269ba refactor(customer): 优化客户数据查询和表单字段校验
- 移除新增客户页面对手机号的必填和格式校验
- 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制
- 新增判断当前用户是否为超级管理员逻辑
- 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件
- 优化客户列表数据获取逻辑,支持超级管理员查看全部客户
- 调整依赖项,更新使用了新构建的查询参数函数
- 增强状态统计接口参数构建,统一调用参数生成函数
- 优化副作用 Hook 依赖,保证数据加载时机正确
2026-06-04 17:05:45 +08:00
f4935a031a refactor(customer): 优化客户数据查询和表单字段校验
- 移除新增客户页面对手机号的必填和格式校验
- 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制
- 新增判断当前用户是否为超级管理员逻辑
- 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件
- 优化客户列表数据获取逻辑,支持超级管理员查看全部客户
- 调整依赖项,更新使用了新构建的查询参数函数
- 增强状态统计接口参数构建,统一调用参数生成函数
- 优化副作用 Hook 依赖,保证数据加载时机正确
2026-06-04 16:49:04 +08:00
af35c9281d refactor(customer): 优化客户数据查询和表单字段校验
- 移除新增客户页面对手机号的必填和格式校验
- 修改手机号字段标签为“手机号/微信号”,取消必填和长度限制
- 新增判断当前用户是否为超级管理员逻辑
- 抽取并统一构建客户查询参数方法,根据权限动态设置筛选条件
- 优化客户列表数据获取逻辑,支持超级管理员查看全部客户
- 调整依赖项,更新使用了新构建的查询参数函数
- 增强状态统计接口参数构建,统一调用参数生成函数
- 优化副作用 Hook 依赖,保证数据加载时机正确
2026-06-04 16:12:22 +08:00
24 changed files with 1205 additions and 323 deletions

View File

@@ -0,0 +1,74 @@
# 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` 构建成功,无编译错误
## 个人资料完善流程优化 (2026-06-04 17:07)
### 1. 头像检查逻辑简化(仅检查头像)
**文件**: `src/pages/index/Header.tsx`
- `reload()` 中移除昵称检查,仅检查 `hasAvatar`
- 移除监听 `nickname === '微信用户'``useEffect` 自动跳转逻辑
- 新增 `useDidShow` 钩子:从 profile 页返回时重新检查头像状态
### 2. Profile 页面移除昵称字段
**文件**: `src/user/profile/profile.tsx`
- 删除昵称 `Form.Item``getWxNickname` 函数
- 移除 `Input` 导入和 `InputEvent` 类型定义
- 保留头像上传、性别、备注等字段
### 3. 修复头像更新后不立即刷新
**根因**: `useUser` 使用 `useState`,每个组件实例独立持有 state。profile 页更新 `user`UserCard 组件无法感知变化。
**修复**: `src/pages/user/components/UserCard.tsx` 新增 `useDidShow`,页面显示时调用 `fetchUserInfo()` 重新拉取用户数据。
### 4. 修复登出时 Avatar/Nickname 存储未清除
**文件**: `src/hooks/useUser.ts`
- `logoutUser()` 补充清除 `Taro.removeStorageSync('Avatar')``Taro.removeStorageSync('Nickname')`,防止切换账号时数据残留。
## 后台管理按钮新增 PC 端引导页 (2026-06-04 17:10)
### 背景
用户中心页 UserCell.tsx 中"后台管理"按钮(仅管理员可见)原本跳转到首页占位,现改为引导用户到 PC 端后台。
### 变更
1. **新增页面 `src/admin/redirect/index.tsx`** — PC 端引导页
- 显示"请在电脑端打开后台管理"提示
- 展示管理后台地址 `https://nnlzdmc.websoft.top`
- "复制链接并在电脑浏览器打开"按钮(`Taro.setClipboardData`
- 底部提示使用 Chrome/Edge 浏览器
2. **修改 `src/pages/user/components/UserCell.tsx`** — 第 40 行
- `onClick``Taro.reLaunch({url: '/pages/index/index'})` 改为 `navTo('/admin/redirect/index', true)`
3. **路由注册**`app.config.ts` admin 分包已包含 `redirect/index`(已存在配置)
### 构建验证
- `taro build --type weapp` 成功dist 目录下 `admin/redirect/` 正常输出

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '后台管理'
})

View File

@@ -0,0 +1,84 @@
import React from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'
import { Link, ArrowRight } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
const PC_ADMIN_URL = 'https://nnlzdmc.websoft.top'
const AdminRedirect: React.FC = () => {
const handleCopyUrl = () => {
Taro.setClipboardData({
data: PC_ADMIN_URL,
success: () => {
Taro.showToast({
title: '链接已复制',
icon: 'success',
duration: 2000
})
}
})
}
return (
<View className="min-h-screen bg-gray-100 flex flex-col items-center justify-center px-8">
{/* 图标区域 */}
<View
className="w-20 h-20 rounded-full flex items-center justify-center mb-8"
style={{
background: 'linear-gradient(135deg, #fef3c7, #fde68a)'
}}
>
<Link color="#f97316" size={40} />
</View>
{/* 标题 */}
<Text className="text-xl font-bold text-gray-800 mb-3">
</Text>
{/* 说明文字 */}
<Text className="text-15 text-gray-500 text-center mb-8 leading-relaxed">
PC
</Text>
{/* URL 显示卡片 */}
<View className="w-full bg-white rounded-xl p-5 mb-6 border border-gray-100">
<View className="flex items-center mb-3">
<Link color="#3b82f6" size={16} />
<Text className="text-sm text-gray-400 ml-2"></Text>
</View>
<Text
className="text-17 font-medium text-blue-600"
style={{ wordBreak: 'break-all' }}
>
{PC_ADMIN_URL}
</Text>
</View>
{/* 复制按钮 */}
<Button
type="primary"
block
size="large"
onClick={handleCopyUrl}
style={{
background: 'linear-gradient(135deg, #f97316, #ea580c)',
border: 'none',
borderRadius: '12px',
height: '48px',
fontSize: '16px'
}}
>
</Button>
{/* 底部提示 */}
<Text className="text-sm text-gray-400 mt-6 text-center">
使 ChromeEdge
</Text>
</View>
)
}
export default AdminRedirect

View File

@@ -98,7 +98,8 @@ export default defineAppConfig({
"index", "index",
"users/index", "users/index",
"article/index", "article/index",
"userVerify/index" "userVerify/index",
"redirect/index"
] ]
} }
], ],

View File

@@ -1,5 +1,4 @@
import {Headphones, Share} from '@nutui/icons-react-taro' import {Headphones, Share} from '@nutui/icons-react-taro'
import navTo from "@/utils/common";
import Taro, { getCurrentInstance } from '@tarojs/taro'; import Taro, { getCurrentInstance } from '@tarojs/taro';
import {getUserInfo} from "@/api/layout"; import {getUserInfo} from "@/api/layout";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
@@ -8,7 +7,6 @@ import {CmsArticle} from "@/api/cms/cmsArticle/model";
function AddCartBar() { function AddCartBar() {
const { router } = getCurrentInstance(); const { router } = getCurrentInstance();
const [id, setId] = useState<number>()
const [article, setArticle] = useState<CmsArticle>() const [article, setArticle] = useState<CmsArticle>()
const [IsLogin, setIsLogin] = useState<boolean>(false) const [IsLogin, setIsLogin] = useState<boolean>(false)
const onPay = () => { const onPay = () => {
@@ -17,20 +15,14 @@ function AddCartBar() {
setTimeout(() => { setTimeout(() => {
Taro.switchTab( Taro.switchTab(
{ {
url: '/pages/user/user', url: '/passport/login',
}, },
) )
}, 1000) }, 1000)
return false; 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) => { const reload = (id: any) => {
getCmsArticle(id).then(data => { getCmsArticle(id).then(data => {
setArticle(data) setArticle(data)
}) })
@@ -46,7 +38,6 @@ function AddCartBar() {
useEffect(() => { useEffect(() => {
const id = router?.params.id as number | undefined; const id = router?.params.id as number | undefined;
setId(id)
reload(id); reload(id);
}, []); }, []);

View File

@@ -126,7 +126,7 @@ const InviteStatsPage: React.FC = () => {
// 渲染统计概览 // 渲染统计概览
const renderStatsOverview = () => ( const renderStatsOverview = () => (
<View className="px-4 space-y-4"> <View className="px-4">
{/* 核心数据卡片 */} {/* 核心数据卡片 */}
<Card className="bg-white rounded-2xl shadow-sm"> <Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4"> <View className="p-4">
@@ -182,7 +182,7 @@ const InviteStatsPage: React.FC = () => {
<Card className="bg-white rounded-2xl shadow-sm"> <Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4"> <View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text> <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) => ( {inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> <View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<View className="flex items-center"> <View className="flex items-center">
@@ -208,7 +208,7 @@ const InviteStatsPage: React.FC = () => {
const renderInviteRecords = () => ( const renderInviteRecords = () => (
<View className="px-4"> <View className="px-4">
{inviteRecords.length > 0 ? ( {inviteRecords.length > 0 ? (
<View className="space-y-3"> <View className="p-3">
{inviteRecords.map((record, index) => ( {inviteRecords.map((record, index) => (
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm"> <Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
<View className="p-4"> <View className="p-4">
@@ -253,7 +253,7 @@ const InviteStatsPage: React.FC = () => {
</View> </View>
{ranking.length > 0 ? ( {ranking.length > 0 ? (
<View className="space-y-3"> <View className="p-3">
{ranking.map((item, index) => ( {ranking.map((item, index) => (
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm"> <Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
<View className="p-4 flex items-center"> <View className="p-4 flex items-center">

View File

@@ -415,7 +415,7 @@ const DealerQrcode: React.FC = () => {
{/* 推广说明 */} {/* 推广说明 */}
<View className="bg-white rounded-2xl p-4 mt-6 hidden"> <View className="bg-white rounded-2xl p-4 mt-6 hidden">
<Text className="font-semibold text-gray-800 mb-3">广</Text> <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="flex items-start">
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View> <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"> <Text className="text-sm text-gray-600">
@@ -436,84 +436,6 @@ const DealerQrcode: React.FC = () => {
</View> </View>
</View> </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>
</View> </View>
) )

View File

@@ -146,6 +146,8 @@ export const useUser = () => {
Taro.removeStorageSync('UserId'); Taro.removeStorageSync('UserId');
Taro.removeStorageSync('TenantId'); Taro.removeStorageSync('TenantId');
Taro.removeStorageSync('Phone'); Taro.removeStorageSync('Phone');
Taro.removeStorageSync('Avatar');
Taro.removeStorageSync('Nickname');
Taro.removeStorageSync('userInfo'); Taro.removeStorageSync('userInfo');
} catch (error) { } catch (error) {
console.error('清除用户数据失败:', error); console.error('清除用户数据失败:', error);
@@ -253,7 +255,7 @@ export const useUser = () => {
// 获取用户显示名称 // 获取用户显示名称
const getDisplayName = () => { const getDisplayName = () => {
return user?.nickname || user?.realName || user?.username || '登录'; return user?.nickname || user?.realName || user?.username || '点击登录';
}; };
// 角色名称:优先取用户 roles 数组的第一个角色名称 // 角色名称:优先取用户 roles 数组的第一个角色名称

View File

@@ -1,5 +1,5 @@
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'; import Taro, { useDidShow } from '@tarojs/taro';
import {Space} from '@nutui/nutui-react-taro' import {Space} from '@nutui/nutui-react-taro'
import {NavBar} from '@nutui/nutui-react-taro' import {NavBar} from '@nutui/nutui-react-taro'
import {getWxOpenId} from "@/api/layout"; import {getWxOpenId} from "@/api/layout";
@@ -38,14 +38,13 @@ const Header = (props: any) => {
}, },
}) })
// 检查用户是否已登录并且有头像和昵称 // 检查用户是否已登录并且有头像
if (isLoggedIn) { if (isLoggedIn) {
const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar'); const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar');
const hasNickname = user?.nickname || Taro.getStorageSync('Nickname');
if (!hasAvatar || !hasNickname) { if (!hasAvatar) {
Taro.showToast({ Taro.showToast({
title: '您还没有上传头像和昵称', title: '请先上传头像',
icon: 'none' icon: 'none'
}) })
setTimeout(() => { setTimeout(() => {
@@ -198,24 +197,23 @@ const Header = (props: any) => {
reload().then() reload().then()
}, []) }, [])
// 监听用户信息变化,当用户信息更新后重新检查 // 页面显示时重新检查头像(从 profile 页返回时兜底)
useEffect(() => { useDidShow(() => {
if (isLoggedIn && user) { if (isLoggedIn) {
console.log('用户信息已更新:', user); const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar');
// 检查是否设置头像和昵称 if (!hasAvatar) {
if (user.nickname === '微信用户') {
Taro.showToast({ Taro.showToast({
title: '请设置头像和昵称', title: '请先上传头像',
icon: 'none' icon: 'none'
}) })
setTimeout(() => { setTimeout(() => {
Taro.navigateTo({ Taro.navigateTo({
url: '/user/profile/profile' url: '/user/profile/profile'
}); })
}, 2000) }, 3000)
} }
} }
}, [user, isLoggedIn]) })
return ( return (
<> <>

View File

@@ -197,7 +197,7 @@ const Header = (props: any) => {
<div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div> <div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div>
<h3></h3> <h3></h3>
<div>: {isLoggedIn ? '已登录' : '登录'}</div> <div>: {isLoggedIn ? '已登录' : '点击登录'}</div>
{user && ( {user && (
<> <>
<div>ID: {user.userId}</div> <div>ID: {user.userId}</div>

View File

@@ -9,6 +9,7 @@ import {
Wallet Wallet
} from '@nutui/icons-react-taro' } from '@nutui/icons-react-taro'
import navTo from '@/utils/common' import navTo from '@/utils/common'
import { requireLogin } from '@/utils/common'
import './QuickActions.scss' import './QuickActions.scss'
const QuickActions: React.FC = () => { const QuickActions: React.FC = () => {
@@ -42,14 +43,7 @@ const QuickActions: React.FC = () => {
] ]
const handleClick = (action: { path: string }) => { const handleClick = (action: { path: string }) => {
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) { if (!requireLogin(action.path)) return
Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 1500
})
return
}
navTo(action.path) navTo(action.path)
} }

View File

@@ -1,8 +1,8 @@
import {Avatar, Button, Tag} from '@nutui/nutui-react-taro' import {Avatar, Button, Tag} from '@nutui/nutui-react-taro'
import {View} from '@tarojs/components' import {View} from '@tarojs/components'
import {Scan} from '@nutui/icons-react-taro'; // import {Scan} from '@nutui/icons-react-taro';
import {getWxOpenId} from '@/api/layout'; import {getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro'; import Taro, { useDidShow } from '@tarojs/taro';
import {useEffect} from "react"; import {useEffect} from "react";
import navTo from "@/utils/common"; import navTo from "@/utils/common";
import {TenantId} from "@/config/app"; import {TenantId} from "@/config/app";
@@ -11,7 +11,6 @@ import {useUser} from "@/hooks/useUser";
function UserCard() { function UserCard() {
const { const {
user, user,
isAdmin,
isLoggedIn, isLoggedIn,
loginUser, loginUser,
fetchUserInfo, fetchUserInfo,
@@ -36,6 +35,15 @@ function UserCard() {
}); });
}, []); }, []);
// 页面显示时重新拉取用户信息(确保从 profile 页更新头像后立即刷新)
useDidShow(() => {
if (isLoggedIn) {
fetchUserInfo().catch(err => {
console.error('refresh user info on show failed:', err);
});
}
});
const reload = async () => { const reload = async () => {
// 如果已登录,获取最新用户信息 // 如果已登录,获取最新用户信息
@@ -168,7 +176,10 @@ function UserCard() {
isLoggedIn ? ( isLoggedIn ? (
<Avatar size="large" src={user?.avatar} shape="round"/> <Avatar size="large" src={user?.avatar} shape="round"/>
) : ( ) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}> <Button className={'text-black'} style={{
maxWidth: '100px',
height: '100%',
}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={user?.avatar} shape="round"/> <Avatar size="large" src={user?.avatar} shape="round"/>
</Button> </Button>
) )
@@ -183,7 +194,7 @@ function UserCard() {
</View> </View>
</View> </View>
<View className={'gap-2 flex items-center'}> <View className={'gap-2 flex items-center'}>
{isAdmin() && <Scan className={'user-card__scan'} size={24} onClick={() => navTo('/user/store/verification', true)} />} {/*{isAdmin() && <Scan className={'user-card__scan'} size={24} onClick={() => navTo('/user/store/verification', true)} />}*/}
<View className={'user-card__profile mr-4 text-sm px-3 py-1'} <View className={'user-card__profile mr-4 text-sm px-3 py-1'}
onClick={() => navTo('/user/profile/profile', true)}> onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'} {'个人资料'}

View File

@@ -6,7 +6,7 @@ import {ArrowRight, LogisticsError} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser' import {useUser} from '@/hooks/useUser'
const UserCell = () => { const UserCell = () => {
const {logoutUser} = useUser(); const {logoutUser, isAdmin} = useUser();
const onLogout = () => { const onLogout = () => {
Taro.showModal({ Taro.showModal({
@@ -28,15 +28,19 @@ const UserCell = () => {
<> <>
<View className={'p-4'}> <View className={'p-4'}>
{isAdmin() && (
<Cell.Group divider={true}> <Cell.Group divider={true}>
<Cell <Cell
className="nutui-cell-clickable" className="nutui-cell-clickable"
title="后台管理" title={
<Text style={{color: '#f97316', fontWeight: 500}}></Text>
}
align="center" align="center"
extra={<ArrowRight color="#cccccc" size={18}/>} extra={<ArrowRight color="#f97316" size={18}/>}
onClick={() => Taro.reLaunch({url: '/pages/index/index'})} onClick={() => navTo('/admin/redirect/index', true)}
/> />
</Cell.Group> </Cell.Group>
)}
<Cell.Group divider={true}> <Cell.Group divider={true}>
<Cell <Cell

View File

@@ -58,7 +58,7 @@
border: 1px solid rgba(15, 118, 110, 0.22); border: 1px solid rgba(15, 118, 110, 0.22);
border-radius: 999px; border-radius: 999px;
background: #ecfdf5; background: #ecfdf5;
color: #0f766e; color: #0f3076;
white-space: nowrap; white-space: nowrap;
} }

View File

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

View 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;
}
}

View File

@@ -1,30 +1,178 @@
import {useEffect, useState} from "react"; import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro' 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 Agreement = () => {
const [loading, setLoading] = useState(true)
const [content, setContent] = useState<any>('') const [content, setContent] = useState<string>('')
const reload = () => { const [title, setTitle] = useState<string>('')
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(() => { 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 ( return (
<> <View className='agreement-page'>
<View className={'content text-gray-700 text-sm p-4'}> <RichText nodes={content} />
<RichText nodes={content}/> </View>
</View>
</>
) )
} }
export default Agreement export default Agreement

377
src/passport/login.scss Normal file
View 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-color: #ffffff;
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;
}

View File

@@ -1,147 +1,337 @@
import {useEffect, useState} from "react"; import { useEffect, useState } from 'react'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro' import { View, Text, Button, Image } from '@tarojs/components'
import {loginBySms} from '@/api/passport/login' 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 Login = () => {
const [isAgree, setIsAgree] = useState(false) const [isAgree, setIsAgree] = useState(false)
const [phone, setPhone] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showContent, setShowContent] = useState(false)
const reload = () => { const router = Taro.getCurrentInstance().router
const isWeapp = IS_WEAPP
/** 页面加载动画 */
useEffect(() => {
Taro.hideTabBar() 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 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) { if (!isAgree) {
Taro.showToast({ Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
title: '请先同意服务协议',
icon: 'none'
})
return return
} }
if (loading) return
if (!phone || phone.trim() === '') { const { code: phoneCode, errMsg } = detail || {}
Taro.showToast({ if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
title: '请输入手机号', Taro.showToast({ title: '未授权手机号', icon: 'none' })
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'
})
return return
} }
try { try {
setLoading(true) setLoading(true)
await loginBySms({
phone: phone, const inviteParams = parseInviteParams({ query: router?.params })
code: password 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({ if (res.data?.code === 0 && res.data?.data?.access_token) {
title: '登录成功', const token = res.data.data.access_token
icon: 'success' let user = res.data.data.user
})
// 延迟跳转到首页 // 获取最新的用户信息
setTimeout(() => { const freshUserInfo = await fetchUserInfo(token)
Taro.reLaunch({ if (freshUserInfo) {
url: '/pages/index/index' user = freshUserInfo
}) }
}, 1500)
} catch (error: any) { saveStorageByLoginUser(token, user)
console.error('登录失败:', error)
Taro.showToast({ // 绑定 openid + 处理邀请关系
title: error.message || '登录失败,请重试', await ensureWxOpenIdSaved()
icon: 'none' 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 { } finally {
setLoading(false) setLoading(false)
} }
} }
useEffect(() => {
reload()
}, [])
return ( return (
<> <View className={`page-login ${showContent ? 'page-login--show' : ''}`}>
<div className={'flex flex-col justify-center px-5'}> {/* 渐变背景 */}
<div className={'text-3xl text-center py-5 font-normal my-10'}></div> <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'}> <View className='login-content'>
<Input {/* Logo 和标题 */}
type="text" <View className='login-header'>
placeholder="手机号" <View className='login-logo'>
maxLength={11} <Image
value={phone} className='login-logo__image'
onChange={(val) => setPhone(val)} src={'https://oss.wsdns.cn/20260121/6fcfb2c8308c40d4933c85c88805ddd0.jpg'}
style={{backgroundColor: '#ffffff', borderRadius: '8px'}} mode='aspectFit'
/> />
</div> {/*<Text className='login-logo__text'></Text>*/}
<div className={'flex flex-col justify-between items-center my-2'}> </View>
<Input <Text className='login-title'></Text>
type="password" <Text className='login-subtitle'></Text>
placeholder="密码" </View>
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'}>
<Button
type="info"
size={'large'}
className={'w-full rounded-lg p-2'}
disabled={!isAgree}
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> <View className='login-body'>
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}></span><a {isWeapp && (
onClick={() => Taro.navigateTo({url: '/passport/agreement'})} <View className='login-methods'>
className={'text-blue-600'}></a>
</div> <Button
</div> 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}
>
</Button>
<View className='login-methods__tip'>
<Text style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '16px' }}></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 export default Login

View File

@@ -13,7 +13,6 @@ const {router} = getCurrentInstance()
import { import {
Form, Form,
Button, Button,
Input,
Radio, Radio,
} from '@nutui/nutui-react-taro' } from '@nutui/nutui-react-taro'
import {DictData} from "@/api/system/dict-data/model"; import {DictData} from "@/api/system/dict-data/model";
@@ -28,11 +27,6 @@ interface ChooseAvatarEvent {
}; };
} }
interface InputEvent {
detail: {
value: string;
};
}
function Profile() { function Profile() {
const formId = Number(router?.params.id) const formId = Number(router?.params.id)
const {user, updateUser, loading} = useUser() const {user, updateUser, loading} = useUser()
@@ -143,15 +137,6 @@ function Profile() {
}) })
} }
// 获取微信昵称
const getWxNickname = (nickname: string) => {
// 更新表单数据
setFormData({
...FormData,
nickname: nickname
});
}
// 等待 useUser 初始化完成后再加载数据 // 等待 useUser 初始化完成后再加载数据
useEffect(() => { useEffect(() => {
if (!loading) { if (!loading) {
@@ -211,20 +196,6 @@ function Profile() {
</div> </div>
} }
> >
<Form.Item
label={'昵称'}
name="nickname"
initialValue={FormData.nickname}
rules={[{message: '请获取微信昵称'}]}
>
<Input
type="nickname"
className="info-content__input"
placeholder="请输入昵称"
value={FormData?.nickname}
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
/>
</Form.Item>
<Form.Item <Form.Item
label="性别" label="性别"
name="sex" name="sex"

View File

@@ -83,7 +83,7 @@ const ThemeSelector: React.FC = () => {
> >
<Text className="text-lg font-bold mb-2"></Text> <Text className="text-lg font-bold mb-2"></Text>
<Text className="text-sm opacity-90 px-2">{currentTheme.description}</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 <View
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
style={{ backgroundColor: currentTheme.primary }} style={{ backgroundColor: currentTheme.primary }}

View File

@@ -1,15 +1,41 @@
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
/**
* 检查是否已登录
* @returns 已登录返回 true未登录返回 false
*/
export function isLoggedIn(): boolean {
return !!(Taro.getStorageSync('access_token') && Taro.getStorageSync('UserId'))
}
/**
* 要求登录未登录时弹出提示3秒后自动跳转到登录页
* @param redirect 登录成功后跳转的目标页面(可选)
* @returns 已登录返回 true未登录时执行跳转并返回 false
*/
export function requireLogin(redirect?: string): boolean {
if (isLoggedIn()) return true
const redirectParam = redirect
? `?redirect=${encodeURIComponent(redirect)}`
: ''
Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000,
})
setTimeout(() => {
Taro.navigateTo({ url: `/passport/login${redirectParam}` })
}, 1500)
return false
}
export default function navTo(url: string, isLogin = false) { export default function navTo(url: string, isLogin = false) {
if (isLogin && url != '/pages/user/user') { if (isLogin && url != '/pages/user/user') {
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) { if (!requireLogin(url)) return false
Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 500
});
return false;
}
} }
Taro.navigateTo({ Taro.navigateTo({
url: url url: url

View File

@@ -239,6 +239,38 @@ export function getSourceDisplayName(source: string): string {
return sourceMap[source] || source 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
}
}
/** /**
* 验证邀请码格式 * 验证邀请码格式
*/ */

View File

@@ -1,28 +1,27 @@
// tailwind.config.js /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'], content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: 'media', // or 'media' or 'class'
theme: { theme: {
extend: { extend: {
fontSize: { colors: {
'15': '15px', primary: {
'17': '17px', 50: '#f0f9ff',
'28': '28px', 100: '#e0f2fe',
}, 200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
}
}
}, },
}, },
variants: {
extend: {},
},
plugins: [], plugins: [],
corePlugins: { corePlugins: {
// 禁用微信小程序不支持的功能 // Taro 小程序端不需要 preflight浏览器重置样式
preflight: false, // 禁用默认样式重置 preflight: false
// 禁用包含复杂选择器的插件 }
space: false, // 禁用 space-x, space-y 等(包含 :not([hidden]) 选择器) }
divideWidth: false, // 禁用 divide-x, divide-y 等
divideColor: false,
divideStyle: false,
divideOpacity: false,
},
};