Compare commits
10 Commits
20f77ff886
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b130d4ac4c | |||
| 78b67269ba | |||
| f4935a031a | |||
| af35c9281d | |||
| f4c2a64f8d | |||
| 812df56f8c | |||
| ca4f1c2a77 | |||
| dde60c8b48 | |||
| c58060fbd7 | |||
| 2d4d73d0d1 |
74
.workbuddy/memory/2026-06-04.md
Normal file
74
.workbuddy/memory/2026-06-04.md
Normal 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/` 正常输出
|
||||||
3
src/admin/redirect/index.config.ts
Normal file
3
src/admin/redirect/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '后台管理'
|
||||||
|
})
|
||||||
84
src/admin/redirect/index.tsx
Normal file
84
src/admin/redirect/index.tsx
Normal 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">
|
||||||
|
请使用 Chrome、Edge 等浏览器打开以获得最佳体验
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminRedirect
|
||||||
@@ -98,7 +98,8 @@ export default defineAppConfig({
|
|||||||
"index",
|
"index",
|
||||||
"users/index",
|
"users/index",
|
||||||
"article/index",
|
"article/index",
|
||||||
"userVerify/index"
|
"userVerify/index",
|
||||||
|
"redirect/index"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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') {
|
const reload = (id: any) => {
|
||||||
navTo('/bszx/pay/pay?id=' + id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const reload = (id) => {
|
|
||||||
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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Loading, CellGroup, Cell, Input, Form, Calendar, Popup, SearchBar} from
|
|||||||
import {Edit, Calendar as CalendarIcon, ArrowRight, Del} from '@nutui/icons-react-taro'
|
import {Edit, Calendar as CalendarIcon, ArrowRight, Del} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {useRouter} from '@tarojs/taro'
|
import {useRouter} from '@tarojs/taro'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text, ScrollView} from '@tarojs/components'
|
||||||
import FixedButton from "@/components/FixedButton";
|
import FixedButton from "@/components/FixedButton";
|
||||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
||||||
import {
|
import {
|
||||||
@@ -359,25 +359,6 @@ const AddShopDealerApply = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证必填字段
|
|
||||||
if (!values.mobile || values.mobile.trim() === '') {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请填写联系方式',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证手机号格式
|
|
||||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
|
||||||
if (!phoneRegex.test(values.mobile)) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请填写正确的手机号',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 规范化报备人:留空=自己报备(当前登录用户)
|
// 规范化报备人:留空=自己报备(当前登录用户)
|
||||||
const rawUserId = normalizeText(values.userId);
|
const rawUserId = normalizeText(values.userId);
|
||||||
const submitUserId = rawUserId
|
const submitUserId = rawUserId
|
||||||
@@ -630,22 +611,22 @@ const AddShopDealerApply = () => {
|
|||||||
/>
|
/>
|
||||||
<View className={'bg-gray-100 h-2'}></View>
|
<View className={'bg-gray-100 h-2'}></View>
|
||||||
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
|
<Form.Item name="address" label="小区" initialValue={FormData?.address} required>
|
||||||
<Input placeholder="幸福里" disabled={isEditMode}/>
|
<Input placeholder="如:幸福里小区" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="buildingNo" label="楼栋号" required>
|
<Form.Item name="buildingNo" label="楼栋号" required>
|
||||||
<Input placeholder="3" disabled={isEditMode}/>
|
<Input placeholder="如(只填写数字):3" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="unitNo" label="单元号">
|
<Form.Item name="unitNo" label="单元号">
|
||||||
<Input placeholder="1" disabled={isEditMode}/>
|
<Input placeholder="如(只填写数字):1" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="roomNo" label="房号" required>
|
<Form.Item name="roomNo" label="房号" required>
|
||||||
<Input placeholder="1201" disabled={isEditMode}/>
|
<Input placeholder="如(只填写数字):1201" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="realName" label="姓名" initialValue={FormData?.realName} required>
|
<Form.Item name="realName" label="姓名" initialValue={FormData?.realName} required>
|
||||||
<Input placeholder="张三" disabled={isEditMode}/>
|
<Input placeholder="如:张三" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="mobile" label="手机号" initialValue={FormData?.mobile} required>
|
<Form.Item name="mobile" label="手机号/微信号" initialValue={FormData?.mobile} required>
|
||||||
<Input placeholder="手机号" disabled={isEditMode} maxLength={11}/>
|
<Input placeholder="请填写手机号/微信号" disabled={isEditMode}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="comments" label="备注" initialValue={FormData?.comments}>
|
<Form.Item name="comments" label="备注" initialValue={FormData?.comments}>
|
||||||
<Input placeholder="请输入备注信息" />
|
<Input placeholder="请输入备注信息" />
|
||||||
@@ -718,9 +699,9 @@ const AddShopDealerApply = () => {
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
round
|
round
|
||||||
onClose={() => setShowReceptionistPicker(false)}
|
onClose={() => setShowReceptionistPicker(false)}
|
||||||
style={{height: '70%'}}
|
style={{height: '70vh'}}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col h-full">
|
<View style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
<View className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
|
<View className="flex items-center justify-between px-4 py-3 border-b border-gray-100">
|
||||||
<Text className="text-base font-semibold text-gray-800">选择接待人员</Text>
|
<Text className="text-base font-semibold text-gray-800">选择接待人员</Text>
|
||||||
@@ -737,7 +718,10 @@ const AddShopDealerApply = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{/* 列表 */}
|
{/* 列表 */}
|
||||||
<View className="flex-1 overflow-y-auto">
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
style={{flex: 1, minHeight: 0, height: 'calc(70vh - 112px)'}}
|
||||||
|
>
|
||||||
{receptionistLoading ? (
|
{receptionistLoading ? (
|
||||||
<View className="flex justify-center items-center py-8">
|
<View className="flex justify-center items-center py-8">
|
||||||
<Loading>加载中</Loading>
|
<Loading>加载中</Loading>
|
||||||
@@ -761,7 +745,7 @@ const AddShopDealerApply = () => {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</View>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useState, useEffect, useCallback} from 'react'
|
import {useState, useEffect, useCallback, useMemo, useRef} from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
||||||
@@ -24,6 +24,10 @@ interface CustomerUser extends UserType {
|
|||||||
protectDays?: number; // 剩余保护天数
|
protectDays?: number; // 剩余保护天数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_LIMIT = 10;
|
||||||
|
const COUNT_LIMIT = 1;
|
||||||
|
const COUNT_REFRESH_DELAY = 300;
|
||||||
|
|
||||||
const CustomerIndex = () => {
|
const CustomerIndex = () => {
|
||||||
const [list, setList] = useState<CustomerUser[]>([])
|
const [list, setList] = useState<CustomerUser[]>([])
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
@@ -32,15 +36,59 @@ const CustomerIndex = () => {
|
|||||||
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
|
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [statusCounts, setStatusCounts] = useState({
|
||||||
|
all: 0,
|
||||||
|
pending: 0,
|
||||||
|
signed: 0,
|
||||||
|
cancelled: 0
|
||||||
|
});
|
||||||
|
const requestSeqRef = useRef(0)
|
||||||
|
const didShowReadyRef = useRef(false)
|
||||||
|
const countRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
// 权限检查:只要登录就能查看客户列表
|
// 权限检查:只要登录就能查看客户列表
|
||||||
const {user, loading: userLoading} = useUser()
|
const {user, loading: userLoading} = useUser()
|
||||||
const roleCheckFinished = !userLoading
|
const roleCheckFinished = !userLoading
|
||||||
const isLoggedIn = roleCheckFinished && user !== null
|
const isLoggedIn = roleCheckFinished && user !== null
|
||||||
|
const isSuperAdmin = user?.isSuperAdmin === true
|
||||||
|
const isAdminRole = user?.isAdmin === true || user?.roles?.some(role => role.roleCode === 'admin') === true
|
||||||
|
const canViewAllCustomers = isSuperAdmin || isAdminRole
|
||||||
|
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0
|
||||||
|
|
||||||
// Tab配置
|
// Tab配置
|
||||||
const tabList = getStatusOptions();
|
const tabList = getStatusOptions();
|
||||||
|
|
||||||
|
const buildCustomerQueryParams = useCallback((currentPage?: number, limit?: number) => {
|
||||||
|
const params: any = {
|
||||||
|
type: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentPage !== undefined) {
|
||||||
|
params.page = currentPage;
|
||||||
|
params.limit = limit || PAGE_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canViewAllCustomers) {
|
||||||
|
params.userId = currentUserId;
|
||||||
|
params.receptionistId = currentUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [canViewAllCustomers, currentUserId]);
|
||||||
|
|
||||||
|
const canHandleCustomer = useCallback((customer: ShopDealerApply) => {
|
||||||
|
if (isSuperAdmin) return true;
|
||||||
|
return Number(customer.receptionistId) === currentUserId;
|
||||||
|
}, [currentUserId, isSuperAdmin]);
|
||||||
|
|
||||||
|
const updateStatusCount = useCallback((status: CustomerStatus, count?: number) => {
|
||||||
|
if (count === undefined) return;
|
||||||
|
setStatusCounts(prev => ({
|
||||||
|
...prev,
|
||||||
|
[status]: count
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 复制手机号
|
// 复制手机号
|
||||||
const copyPhone = (phone: string) => {
|
const copyPhone = (phone: string) => {
|
||||||
Taro.setClipboardData({
|
Taro.setClipboardData({
|
||||||
@@ -129,12 +177,6 @@ const CustomerIndex = () => {
|
|||||||
// 转换为天数,向上取整
|
// 转换为天数,向上取整
|
||||||
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
|
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
console.log('=== 基于过期时间计算 ===');
|
|
||||||
console.log('过期时间:', expirationTime);
|
|
||||||
console.log('当前时间:', now.toLocaleString());
|
|
||||||
console.log('剩余天数:', remainingDays);
|
|
||||||
console.log('======================');
|
|
||||||
|
|
||||||
return Math.max(0, remainingDays);
|
return Math.max(0, remainingDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,12 +207,6 @@ const CustomerIndex = () => {
|
|||||||
// 计算剩余保护天数
|
// 计算剩余保护天数
|
||||||
const remainingDays = protectionPeriod - daysPassed;
|
const remainingDays = protectionPeriod - daysPassed;
|
||||||
|
|
||||||
console.log('=== 基于申请时间计算 ===');
|
|
||||||
console.log('申请时间:', applyTime);
|
|
||||||
console.log('已过去天数:', daysPassed);
|
|
||||||
console.log('剩余保护天数:', remainingDays);
|
|
||||||
console.log('======================');
|
|
||||||
|
|
||||||
return Math.max(0, remainingDays);
|
return Math.max(0, remainingDays);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('日期计算错误:', error);
|
console.error('日期计算错误:', error);
|
||||||
@@ -181,25 +217,24 @@ const CustomerIndex = () => {
|
|||||||
|
|
||||||
// 获取客户数据
|
// 获取客户数据
|
||||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||||||
|
const requestSeq = ++requestSeqRef.current;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
const currentPage = resetPage ? 1 : (targetPage || 1);
|
||||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
|
||||||
|
|
||||||
// 构建API参数,根据状态筛选
|
// 构建API参数,根据状态筛选
|
||||||
// 查看自己提交的(userId)或分配给自己的(receptionistId)的客户
|
// 非管理员查看自己提交的(userId)或分配给自己的(receptionistId)的客户;管理员查询全部
|
||||||
const params: any = {
|
const currentStatus = statusFilter || activeTab;
|
||||||
type: 4,
|
const params = buildCustomerQueryParams(currentPage, PAGE_LIMIT);
|
||||||
page: currentPage,
|
const applyStatus = mapCustomerStatusToApplyStatus(currentStatus);
|
||||||
userId: currentUserId,
|
|
||||||
receptionistId: currentUserId
|
|
||||||
};
|
|
||||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
|
||||||
if (applyStatus !== undefined) {
|
if (applyStatus !== undefined) {
|
||||||
params.applyStatus = applyStatus;
|
params.applyStatus = applyStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await pageShopDealerApply(params);
|
const res = await pageShopDealerApply(params);
|
||||||
|
if (requestSeq !== requestSeqRef.current) return;
|
||||||
|
|
||||||
|
updateStatusCount(currentStatus, res?.count);
|
||||||
|
|
||||||
if (res?.list && res.list.length > 0) {
|
if (res?.list && res.list.length > 0) {
|
||||||
// 正确映射状态并计算保护天数
|
// 正确映射状态并计算保护天数
|
||||||
@@ -217,7 +252,7 @@ const CustomerIndex = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 正确判断是否还有更多数据
|
// 正确判断是否还有更多数据
|
||||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
const hasMoreData = currentPage * PAGE_LIMIT < (res.count || 0);
|
||||||
setHasMore(hasMoreData);
|
setHasMore(hasMoreData);
|
||||||
} else {
|
} else {
|
||||||
if (resetPage || currentPage === 1) {
|
if (resetPage || currentPage === 1) {
|
||||||
@@ -234,9 +269,11 @@ const CustomerIndex = () => {
|
|||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
if (requestSeq === requestSeqRef.current) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeTab, page, user?.userId]);
|
}
|
||||||
|
}, [activeTab, buildCustomerQueryParams, updateStatusCount]);
|
||||||
|
|
||||||
const reloadMore = async () => {
|
const reloadMore = async () => {
|
||||||
if (loading || !hasMore) return; // 防止重复加载
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
@@ -255,60 +292,62 @@ const CustomerIndex = () => {
|
|||||||
}, [searchValue]);
|
}, [searchValue]);
|
||||||
|
|
||||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||||
const getFilteredList = () => {
|
const filteredList = useMemo(() => {
|
||||||
let filteredList = list;
|
|
||||||
|
|
||||||
// 按搜索关键词筛选
|
|
||||||
if (displaySearchValue.trim()) {
|
|
||||||
const keyword = displaySearchValue.trim().toLowerCase();
|
const keyword = displaySearchValue.trim().toLowerCase();
|
||||||
filteredList = filteredList.filter(customer =>
|
if (!keyword) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.filter(customer =>
|
||||||
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
||||||
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
|
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
|
||||||
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
|
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
|
||||||
(customer.mobile && customer.mobile.includes(keyword)) ||
|
(customer.mobile && customer.mobile.includes(keyword)) ||
|
||||||
(customer.userId && customer.userId.toString().includes(keyword))
|
(customer.userId && customer.userId.toString().includes(keyword))
|
||||||
);
|
);
|
||||||
}
|
}, [displaySearchValue, list]);
|
||||||
|
|
||||||
return filteredList;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取各状态的统计数量
|
|
||||||
const [statusCounts, setStatusCounts] = useState({
|
|
||||||
all: 0,
|
|
||||||
pending: 0,
|
|
||||||
signed: 0,
|
|
||||||
cancelled: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取所有状态的统计数量
|
// 获取所有状态的统计数量
|
||||||
const fetchStatusCounts = useCallback(async () => {
|
const fetchStatusCounts = useCallback(async (skipStatus?: CustomerStatus) => {
|
||||||
try {
|
try {
|
||||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
const baseParams = buildCustomerQueryParams();
|
||||||
const baseParams: any = {
|
const countParams = {
|
||||||
type: 4,
|
...baseParams,
|
||||||
userId: currentUserId,
|
page: 1,
|
||||||
receptionistId: currentUserId
|
limit: COUNT_LIMIT
|
||||||
};
|
};
|
||||||
|
const countRequestConfigs = [
|
||||||
|
{status: 'all' as CustomerStatus, params: {...countParams}},
|
||||||
|
{status: 'pending' as CustomerStatus, params: {...countParams, applyStatus: 10}},
|
||||||
|
{status: 'signed' as CustomerStatus, params: {...countParams, applyStatus: 20}},
|
||||||
|
{status: 'cancelled' as CustomerStatus, params: {...countParams, applyStatus: 30}}
|
||||||
|
].filter(item => item.status !== skipStatus);
|
||||||
|
|
||||||
// 并行获取各状态的数量
|
const countResponses = await Promise.all(
|
||||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
countRequestConfigs.map(item => pageShopDealerApply(item.params))
|
||||||
pageShopDealerApply({...baseParams}), // 全部
|
);
|
||||||
pageShopDealerApply({...baseParams, applyStatus: 10}), // 跟进中
|
|
||||||
pageShopDealerApply({...baseParams, applyStatus: 20}), // 已签约
|
|
||||||
pageShopDealerApply({...baseParams, applyStatus: 30}) // 已取消
|
|
||||||
]);
|
|
||||||
|
|
||||||
setStatusCounts({
|
setStatusCounts(prev => {
|
||||||
all: allRes?.count || 0,
|
const next = {...prev};
|
||||||
pending: pendingRes?.count || 0,
|
countRequestConfigs.forEach((item, index) => {
|
||||||
signed: signedRes?.count || 0,
|
next[item.status] = countResponses[index]?.count || 0;
|
||||||
cancelled: cancelledRes?.count || 0
|
});
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取状态统计失败:', error);
|
console.error('获取状态统计失败:', error);
|
||||||
}
|
}
|
||||||
}, [user?.userId]);
|
}, [buildCustomerQueryParams]);
|
||||||
|
|
||||||
|
const scheduleStatusCountsRefresh = useCallback((skipStatus?: CustomerStatus) => {
|
||||||
|
if (countRefreshTimerRef.current) {
|
||||||
|
clearTimeout(countRefreshTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
countRefreshTimerRef.current = setTimeout(() => {
|
||||||
|
fetchStatusCounts(skipStatus).then();
|
||||||
|
}, COUNT_REFRESH_DELAY);
|
||||||
|
}, [fetchStatusCounts]);
|
||||||
|
|
||||||
const getStatusCounts = () => statusCounts;
|
const getStatusCounts = () => statusCounts;
|
||||||
|
|
||||||
@@ -327,7 +366,7 @@ const CustomerIndex = () => {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
fetchCustomerData(activeTab, true).then();
|
fetchCustomerData(activeTab, true).then();
|
||||||
fetchStatusCounts().then();
|
scheduleStatusCountsRefresh(activeTab);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,15 +382,18 @@ const CustomerIndex = () => {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
fetchCustomerData(activeTab, true).then();
|
fetchCustomerData(activeTab, true).then();
|
||||||
fetchStatusCounts().then();
|
scheduleStatusCountsRefresh(activeTab);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化统计数据
|
// 清理延迟统计刷新定时器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn) return;
|
return () => {
|
||||||
fetchStatusCounts().then();
|
if (countRefreshTimerRef.current) {
|
||||||
}, [isLoggedIn]);
|
clearTimeout(countRefreshTimerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 当activeTab变化时重新获取数据
|
// 当activeTab变化时重新获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -360,21 +402,30 @@ const CustomerIndex = () => {
|
|||||||
setPage(1); // 重置页码
|
setPage(1); // 重置页码
|
||||||
setHasMore(true); // 重置加载状态
|
setHasMore(true); // 重置加载状态
|
||||||
fetchCustomerData(activeTab, true);
|
fetchCustomerData(activeTab, true);
|
||||||
}, [activeTab, isLoggedIn]);
|
scheduleStatusCountsRefresh(activeTab);
|
||||||
|
}, [activeTab, fetchCustomerData, isLoggedIn, scheduleStatusCountsRefresh]);
|
||||||
|
|
||||||
// 监听页面显示,当从其他页面返回时刷新数据
|
// 监听页面显示,当从其他页面返回时刷新数据
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
if (!isLoggedIn) return;
|
if (!isLoggedIn) return;
|
||||||
|
if (!didShowReadyRef.current) {
|
||||||
|
didShowReadyRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新当前tab的数据和统计信息
|
// 刷新当前tab的数据和统计信息
|
||||||
setList([]);
|
setList([]);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
fetchCustomerData(activeTab, true);
|
fetchCustomerData(activeTab, true);
|
||||||
fetchStatusCounts();
|
scheduleStatusCountsRefresh(activeTab);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 渲染客户项
|
// 渲染客户项
|
||||||
const renderCustomerItem = (customer: CustomerUser) => (
|
const renderCustomerItem = (customer: CustomerUser) => {
|
||||||
|
const canHandle = canHandleCustomer(customer);
|
||||||
|
|
||||||
|
return (
|
||||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||||
<View className="flex items-center mb-3">
|
<View className="flex items-center mb-3">
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
@@ -460,6 +511,7 @@ const CustomerIndex = () => {
|
|||||||
{/* 显示 comments 字段 */}
|
{/* 显示 comments 字段 */}
|
||||||
<Space className="flex items-center">
|
<Space className="flex items-center">
|
||||||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||||||
|
{canHandle && (
|
||||||
<Text
|
<Text
|
||||||
className="text-xs text-blue-500 cursor-pointer"
|
className="text-xs text-blue-500 cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -469,12 +521,13 @@ const CustomerIndex = () => {
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 跟进中状态显示操作按钮 */}
|
{/* 跟进中状态仅接待人员/超管显示操作按钮 */}
|
||||||
{(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && (
|
{(customer.applyStatus === 10 && canHandle) && (
|
||||||
<Space className="flex justify-end">
|
<Space className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -492,7 +545,7 @@ const CustomerIndex = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && (
|
{(customer.applyStatus === 30 && canHandle) && (
|
||||||
<Space className="flex justify-end">
|
<Space className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -505,10 +558,10 @@ const CustomerIndex = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染客户列表
|
// 渲染客户列表
|
||||||
const renderCustomerList = () => {
|
const renderCustomerList = () => {
|
||||||
const filteredList = getFilteredList();
|
|
||||||
const isSearching = displaySearchValue.trim().length > 0;
|
const isSearching = displaySearchValue.trim().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
|
|
||||||
const user = Taro.getStorageSync('User') || {}
|
const user = Taro.getStorageSync('User') || {}
|
||||||
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
||||||
const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
|
const title = hasInviter ? `${nickname || '我'}邀请你加入南南铝佐顿门窗伙伴计划` : '南南铝佐顿门窗伙伴计划'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@@ -245,7 +245,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
//
|
//
|
||||||
// const inviteText = `🎉 邀请您加入我的团队!
|
// const inviteText = `🎉 邀请您加入我的团队!
|
||||||
//
|
//
|
||||||
// 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
|
// 扫描小程序码或搜索"南南铝佐顿门窗"小程序,即可享受优质商品和服务!
|
||||||
//
|
//
|
||||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||||
// 🎁 新用户专享优惠等你来拿
|
// 🎁 新用户专享优惠等你来拿
|
||||||
@@ -385,7 +385,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||||
桂乐淘伙伴计划
|
南南铝佐顿门窗伙伴计划
|
||||||
</View>
|
</View>
|
||||||
<View className="text-sm text-gray-500 mb-4">
|
<View className="text-sm text-gray-500 mb-4">
|
||||||
自购省 | 分享赚 | 好友惠
|
自购省 | 分享赚 | 好友惠
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 数组的第一个角色名称
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
// 如果已登录,获取最新用户信息
|
// 如果已登录,获取最新用户信息
|
||||||
@@ -151,15 +159,14 @@ function UserCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={'header-bg pt-20'}>
|
<View className={'header-bg'}>
|
||||||
<View className={'p-4'}>
|
<View className={'user-card-wrap'}>
|
||||||
<View
|
<View
|
||||||
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
|
className={'w-full flex flex-col justify-around'}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
|
||||||
// width: '720rpx',
|
// width: '720rpx',
|
||||||
// margin: '10px auto 0px auto',
|
// margin: '10px auto 0px auto',
|
||||||
height: '120px',
|
height: '124px',
|
||||||
// borderRadius: '22px 22px 0 0',
|
// borderRadius: '22px 22px 0 0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -169,13 +176,16 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<View className={'user-info flex flex-col px-2'}>
|
<View className={'user-info flex flex-col px-3'}>
|
||||||
<View className={'py-1 text-black font-bold max-w-28'}>{getDisplayName()}</View>
|
<View className={'user-card__name py-1 font-bold'}>{getDisplayName()}</View>
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<View className={'grade text-xs py-0'}>
|
<View className={'grade text-xs py-0'}>
|
||||||
<Tag type="success">{getRoleName()}</Tag>
|
<Tag type="success">{getRoleName()}</Tag>
|
||||||
@@ -184,8 +194,8 @@ function UserCard() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={'gap-2 flex items-center'}>
|
<View className={'gap-2 flex items-center'}>
|
||||||
{isAdmin() && <Scan className={'text-gray-900'} size={24} onClick={() => navTo('/user/store/verification', true)} />}
|
{/*{isAdmin() && <Scan className={'user-card__scan'} size={24} onClick={() => navTo('/user/store/verification', true)} />}*/}
|
||||||
<View className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
|
<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>
|
</View>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import {Cell} from '@nutui/nutui-react-taro'
|
|||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {ArrowRight, LogisticsError, Tips, Ask} from '@nutui/icons-react-taro'
|
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({
|
||||||
@@ -26,13 +26,40 @@ const UserCell = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4'}>
|
<View className={'p-4'}>
|
||||||
|
|
||||||
<Cell.Group divider={true} description={
|
{isAdmin() && (
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<Cell.Group divider={true}>
|
||||||
<Text style={{marginTop: '12px'}}>我的服务</Text>
|
<Cell
|
||||||
</View>
|
className="nutui-cell-clickable"
|
||||||
}>
|
title={
|
||||||
|
<Text style={{color: '#f97316', fontWeight: 500}}>后台管理</Text>
|
||||||
|
}
|
||||||
|
align="center"
|
||||||
|
extra={<ArrowRight color="#f97316" size={18}/>}
|
||||||
|
onClick={() => navTo('/admin/redirect/index', true)}
|
||||||
|
/>
|
||||||
|
</Cell.Group>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Cell.Group divider={true}>
|
||||||
|
<Cell
|
||||||
|
className="nutui-cell-clickable"
|
||||||
|
title="个人资料"
|
||||||
|
align="center"
|
||||||
|
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||||
|
onClick={() => navTo('/user/profile/profile', true)}
|
||||||
|
/>
|
||||||
|
</Cell.Group>
|
||||||
|
<Cell.Group divider={true}>
|
||||||
|
<Cell
|
||||||
|
className="nutui-cell-clickable"
|
||||||
|
title="退出登录"
|
||||||
|
align="center"
|
||||||
|
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||||
|
onClick={onLogout}
|
||||||
|
/>
|
||||||
|
</Cell.Group>
|
||||||
<Cell
|
<Cell
|
||||||
className="nutui-cell-clickable"
|
className="nutui-cell-clickable"
|
||||||
style={{
|
style={{
|
||||||
@@ -81,62 +108,42 @@ const UserCell = () => {
|
|||||||
{/* navTo('/user/userVerify/index', true)*/}
|
{/* navTo('/user/userVerify/index', true)*/}
|
||||||
{/* }}*/}
|
{/* }}*/}
|
||||||
{/*/>*/}
|
{/*/>*/}
|
||||||
<Cell
|
{/* <Cell*/}
|
||||||
className="nutui-cell-clickable"
|
{/* className="nutui-cell-clickable"*/}
|
||||||
title={
|
{/* title={*/}
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||||
<Ask size={16}/>
|
{/* <Ask size={16}/>*/}
|
||||||
<Text className={'pl-3 text-sm'}>常见问题</Text>
|
{/* <Text className={'pl-3 text-sm'}>常见问题</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
}
|
{/* }*/}
|
||||||
align="center"
|
{/* align="center"*/}
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
navTo('/user/help/index')
|
{/* navTo('/user/help/index')*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
<Cell
|
{/* <Cell*/}
|
||||||
className="nutui-cell-clickable"
|
{/* className="nutui-cell-clickable"*/}
|
||||||
title={
|
{/* title={*/}
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||||
<Tips size={16}/>
|
{/* <Tips size={16}/>*/}
|
||||||
<Text className={'pl-3 text-sm'}>关于我们</Text>
|
{/* <Text className={'pl-3 text-sm'}>关于我们</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
}
|
{/* }*/}
|
||||||
align="center"
|
{/* align="center"*/}
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
{/* extra={<ArrowRight color="#cccccc" size={18}/>}*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
navTo('/user/about/index')
|
{/* navTo('/user/about/index')*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</Cell.Group>
|
{/*</Cell.Group>*/}
|
||||||
<Cell.Group divider={true} description={
|
{/*<Cell.Group divider={true} description={*/}
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
{/* <View style={{display: 'inline-flex', alignItems: 'center'}}>*/}
|
||||||
<Text style={{marginTop: '12px'}}>账号管理</Text>
|
{/* <Text style={{marginTop: '12px'}}>账号管理</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
}>
|
{/*}>*/}
|
||||||
<Cell
|
|
||||||
className="nutui-cell-clickable"
|
{/*</Cell.Group>*/}
|
||||||
title="个人资料"
|
|
||||||
align="center"
|
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
|
||||||
onClick={() => navTo('/user/profile/profile', true)}
|
|
||||||
/>
|
|
||||||
<Cell
|
|
||||||
className="nutui-cell-clickable"
|
|
||||||
title="返回首页"
|
|
||||||
align="center"
|
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
|
||||||
onClick={() => Taro.reLaunch({url: '/pages/index/index'})}
|
|
||||||
/>
|
|
||||||
<Cell
|
|
||||||
className="nutui-cell-clickable"
|
|
||||||
title="退出登录"
|
|
||||||
align="center"
|
|
||||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
|
||||||
onClick={onLogout}
|
|
||||||
/>
|
|
||||||
</Cell.Group>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const UserFooter = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
<div className={'text-center py-4 w-full text-gray-300 fixed bottom-2'} onClick={onLoginByPhone}>
|
||||||
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
||||||
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '我的',
|
navigationBarTitleText: '我的'
|
||||||
navigationStyle: 'custom',
|
|
||||||
navigationBarBackgroundColor: '#e9fff2'
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,74 @@
|
|||||||
.header-bg{
|
.user-page {
|
||||||
background: url('https://oss.wsdns.cn/20250621/edb5d4da976b4d97ba185cb7077d2858.jpg') no-repeat top center;
|
min-height: 100vh;
|
||||||
background-size: 100%;
|
background: linear-gradient(180deg, #f2fbf6 0%, #f8fbff 38%, #f9fafb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bg {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 12px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右上角光晕点缀 */
|
||||||
|
.header-bg::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
right: -40px;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(59, 130, 246, 0.3) 0%, rgba(96, 165, 250, 0.1) 55%, transparent 75%);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: 18px !important;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 252, 255, 0.94) 100%);
|
||||||
|
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card-header {
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card .nut-avatar {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card__name {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #ffffff;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card__profile {
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #0f3076;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card__scan {
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ecfeff;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade .nut-tag {
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect} from 'react'
|
||||||
import Taro from '@tarojs/taro';
|
// import Taro from '@tarojs/taro';
|
||||||
import UserCard from "./components/UserCard";
|
import UserCard from "./components/UserCard";
|
||||||
import UserOrder from "./components/UserOrder";
|
// import UserOrder from "./components/UserOrder";
|
||||||
import UserCell from "./components/UserCell";
|
import UserCell from "./components/UserCell";
|
||||||
import UserFooter from "./components/UserFooter";
|
import UserFooter from "./components/UserFooter";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import {NavBar} from '@nutui/nutui-react-taro';
|
// import {NavBar} from '@nutui/nutui-react-taro';
|
||||||
import {Home} from '@nutui/icons-react-taro'
|
// import {Home} from '@nutui/icons-react-taro';
|
||||||
import './user.scss'
|
import './user.scss';
|
||||||
|
|
||||||
function User() {
|
function User() {
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
// const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||||
const {
|
const {
|
||||||
isAdmin
|
isAdmin
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Taro.getSystemInfo({
|
// Taro.getSystemInfo({
|
||||||
success: (res) => {
|
// success: (res) => {
|
||||||
setStatusBarHeight(res.statusBarHeight)
|
// setStatusBarHeight(res.statusBarHeight)
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,11 +28,30 @@ function User() {
|
|||||||
*/
|
*/
|
||||||
if (isAdmin()) {
|
if (isAdmin()) {
|
||||||
return <>
|
return <>
|
||||||
<div className={'w-full'} style={{
|
<div className={'user-page w-full'}>
|
||||||
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
|
{/* 装饰性背景 - 深蓝色渐变 + 圆点装饰 */}
|
||||||
|
<div className={'h-64 w-full fixed top-0 z-0'}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #1e40af 100%)'
|
||||||
}}>
|
}}>
|
||||||
|
<div className="absolute w-32 h-32 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
top: '-16px',
|
||||||
|
right: '-16px'
|
||||||
|
}}></div>
|
||||||
|
<div className="absolute w-24 h-24 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bottom: '-12px',
|
||||||
|
left: '-12px'
|
||||||
|
}}></div>
|
||||||
|
<div className="absolute w-16 h-16 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
top: '60px',
|
||||||
|
left: '120px'
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
<UserCard/>
|
<UserCard/>
|
||||||
<UserOrder/>
|
{/*<UserOrder/>*/}
|
||||||
<UserCell/>
|
<UserCell/>
|
||||||
<UserFooter/>
|
<UserFooter/>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,31 +60,50 @@ function User() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar
|
{/*<NavBar*/}
|
||||||
fixed={true}
|
{/* fixed={true}*/}
|
||||||
style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}
|
{/* style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}*/}
|
||||||
onBackClick={() => {
|
{/* onBackClick={() => {*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
left={
|
{/* left={*/}
|
||||||
<div style={{
|
{/* <div style={{*/}
|
||||||
display: 'flex',
|
{/* display: 'flex',*/}
|
||||||
alignItems: 'center',
|
{/* alignItems: 'center',*/}
|
||||||
justifyContent: 'center',
|
{/* justifyContent: 'center',*/}
|
||||||
width: '30px',
|
{/* width: '30px',*/}
|
||||||
height: '30px',
|
{/* height: '30px',*/}
|
||||||
backgroundColor: 'white',
|
{/* backgroundColor: 'white',*/}
|
||||||
textAlign: 'center',
|
{/* textAlign: 'center',*/}
|
||||||
borderRadius: '50%',
|
{/* borderRadius: '50%',*/}
|
||||||
|
{/* }}>*/}
|
||||||
|
{/* <Home size={15} onClick={() => Taro.reLaunch({url: '/pages/index/index'})} style={{color: 'black', marginBottom: '1px'}} />*/}
|
||||||
|
{/* </div>*/}
|
||||||
|
{/* }*/}
|
||||||
|
{/*>*/}
|
||||||
|
{/* <span>我的</span>*/}
|
||||||
|
{/*</NavBar>*/}
|
||||||
|
<div className={'user-page w-full'}>
|
||||||
|
{/* 装饰性背景 - 深蓝色渐变 + 圆点装饰 */}
|
||||||
|
<div className={'h-40 w-full fixed top-0 z-0'}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #1e40af 100%)'
|
||||||
}}>
|
}}>
|
||||||
<Home size={15} onClick={() => Taro.reLaunch({url: '/pages/index/index'})} style={{color: 'black', marginBottom: '1px'}} />
|
<div className="absolute w-32 h-32 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
top: '-16px',
|
||||||
|
right: '-16px'
|
||||||
|
}}></div>
|
||||||
|
<div className="absolute w-24 h-24 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
bottom: '-12px',
|
||||||
|
left: '-12px'
|
||||||
|
}}></div>
|
||||||
|
<div className="absolute w-16 h-16 rounded-full" style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
top: '60px',
|
||||||
|
left: '120px'
|
||||||
|
}}></div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>我的</span>
|
|
||||||
</NavBar>
|
|
||||||
<div className={'w-full'} style={{
|
|
||||||
background: 'linear-gradient(to bottom, #e9fff2, #f9fafb)'
|
|
||||||
}}>
|
|
||||||
<UserCard/>
|
<UserCard/>
|
||||||
{/*<UserOrder/>*/}
|
{/*<UserOrder/>*/}
|
||||||
<UserCell/>
|
<UserCell/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '服务协议与隐私政策',
|
navigationBarTitleText: '协议详情',
|
||||||
navigationBarTextStyle: 'black'
|
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 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;"> </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>')
|
|
||||||
}
|
|
||||||
|
|
||||||
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
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-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;
|
||||||
|
}
|
||||||
@@ -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 = [
|
||||||
const handleLogin = async () => {
|
'/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) {
|
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'}}
|
<View className='login-body'>
|
||||||
/>
|
{isWeapp && (
|
||||||
</div>
|
<View className='login-methods'>
|
||||||
{/*<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
|
<Button
|
||||||
type="info"
|
className='login-btn'
|
||||||
size={'large'}
|
style={{
|
||||||
className={'w-full rounded-lg p-2'}
|
background: 'linear-gradient(135deg, #07c160, #06ad56)',
|
||||||
disabled={!isAgree}
|
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}
|
loading={loading}
|
||||||
onClick={handleLogin}
|
|
||||||
>
|
>
|
||||||
{loading ? '登录中...' : '登录'}
|
手机号快捷登录
|
||||||
</Button>
|
</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'}>
|
<View className='login-methods__tip'>
|
||||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
<Text style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '16px' }}>点击上方按钮,快速登录</Text>
|
||||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span><a
|
</View>
|
||||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
|
||||||
className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
<View className='login-methods__divider'>
|
||||||
</div>
|
<View className='divider-line' />
|
||||||
</div>
|
<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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
export default function navTo(url: string, isLogin = false) {
|
/**
|
||||||
if (isLogin && url != '/pages/user/user') {
|
* 检查是否已登录
|
||||||
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) {
|
* @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({
|
Taro.showToast({
|
||||||
title: '请先登录',
|
title: '请先登录',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 500
|
duration: 2000,
|
||||||
});
|
})
|
||||||
return false;
|
|
||||||
}
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({ url: `/passport/login${redirectParam}` })
|
||||||
|
}, 1500)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function navTo(url: string, isLogin = false) {
|
||||||
|
if (isLogin && url != '/pages/user/user') {
|
||||||
|
if (!requireLogin(url)) return false
|
||||||
}
|
}
|
||||||
Taro.navigateTo({
|
Taro.navigateTo({
|
||||||
url: url
|
url: url
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证邀请码格式
|
* 验证邀请码格式
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
BIN
小程序系统使用说明书.docx
Normal file
BIN
小程序系统使用说明书.docx
Normal file
Binary file not shown.
BIN
系统使用说明书.docx
Normal file
BIN
系统使用说明书.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user