登录注册
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '登录',
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
/* ========================================
|
||||
登录页面
|
||||
======================================== */
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏 ========== */
|
||||
.login-navbar {
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
|
||||
.navbar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-back {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.back-arrow {
|
||||
font-size: 48px;
|
||||
color: #323233;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: #323233;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 内容区域 ========== */
|
||||
.login-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80px 60px 0;
|
||||
}
|
||||
|
||||
/* ========== 品牌区域 ========== */
|
||||
.login-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.logo-wrapper {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 8px 32px rgba(25, 137, 250, 0.3);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 80px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 44px;
|
||||
font-weight: 600;
|
||||
color: #323233;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.app-slogan {
|
||||
font-size: 28px;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 功能介绍 ========== */
|
||||
.login-features {
|
||||
margin-bottom: 80px;
|
||||
|
||||
.feature-text {
|
||||
font-size: 26px;
|
||||
color: #c8c9cc;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 登录操作区 ========== */
|
||||
.login-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
line-height: 96px;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 48px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
box-shadow: 0 6px 24px rgba(25, 137, 250, 0.35);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
/* 重置微信 Button 默认样式 */
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn--loading {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ========== 协议文字 ========== */
|
||||
.login-agreement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 32px;
|
||||
line-height: 1.6;
|
||||
|
||||
.agree-text {
|
||||
font-size: 24px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
.agree-link {
|
||||
font-size: 24px;
|
||||
color: #1989fa;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import './index.less'
|
||||
|
||||
/** 状态栏高度 */
|
||||
function getStatusBarHeight(): number {
|
||||
try {
|
||||
return Taro.getSystemInfoSync().statusBarHeight || 20
|
||||
} catch {
|
||||
return 20
|
||||
}
|
||||
}
|
||||
|
||||
/** 导航栏高度 */
|
||||
const NAV_BAR_HEIGHT = 44
|
||||
|
||||
export default function LoginPage() {
|
||||
const statusBarH = useMemo(getStatusBarHeight, [])
|
||||
const login = useAuthStore(s => s.login)
|
||||
const isLoggedIn = useAuthStore(s => !!s.token && !!s.user)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 已登录则自动返回
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
}, [isLoggedIn])
|
||||
|
||||
/** 返回上一页 */
|
||||
const handleBack = useCallback(() => {
|
||||
Taro.navigateBack()
|
||||
}, [])
|
||||
|
||||
/** 手机号授权登录 */
|
||||
const handleGetPhoneNumber = useCallback(
|
||||
async (e: any) => {
|
||||
if (submitting) return
|
||||
|
||||
const detail = e.detail || {}
|
||||
|
||||
// 用户拒绝授权
|
||||
if (detail.errMsg && !detail.errMsg.includes(':ok')) {
|
||||
Taro.showToast({ title: '需要授权手机号才能登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
// 1. 获取微信登录 code(用于换取 openid / session_key)
|
||||
const loginRes = await Taro.login()
|
||||
if (!loginRes.code) {
|
||||
Taro.showToast({ title: '获取登录凭证失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 调用后端登录接口(仅传 code + phoneCode)
|
||||
await login({
|
||||
code: loginRes.code,
|
||||
// 新版微信 API:动态令牌,后端直接调用微信接口换手机号
|
||||
phoneCode: detail.code,
|
||||
// 旧版微信 API:加密数据,后端用 session_key 解密
|
||||
encryptedData: detail.encryptedData,
|
||||
iv: detail.iv,
|
||||
})
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1200)
|
||||
} catch {
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
},
|
||||
[login, submitting],
|
||||
)
|
||||
|
||||
/** 查看用户协议 */
|
||||
const handleShowAgreement = useCallback(() => {
|
||||
Taro.showToast({ title: '用户协议即将上线', icon: 'none' })
|
||||
}, [])
|
||||
|
||||
/** 查看隐私政策 */
|
||||
const handleShowPrivacy = useCallback(() => {
|
||||
Taro.showToast({ title: '隐私政策即将上线', icon: 'none' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='login-page'>
|
||||
{/* ========== 自定义导航栏 ========== */}
|
||||
<View className='login-navbar' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='navbar-inner' style={{ height: `${NAV_BAR_HEIGHT}px` }}>
|
||||
<View className='navbar-back' onClick={handleBack}>
|
||||
<Text className='back-arrow'>‹</Text>
|
||||
</View>
|
||||
<Text className='navbar-title'>登录</Text>
|
||||
<View className='navbar-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 内容区域 ========== */}
|
||||
<View className='login-content'>
|
||||
{/* 品牌区域 */}
|
||||
<View className='login-brand'>
|
||||
<View className='logo-wrapper'>
|
||||
<Text className='logo-text'>校</Text>
|
||||
</View>
|
||||
<Text className='app-name'>校园通</Text>
|
||||
<Text className='app-slogan'>校园生活,一触即达</Text>
|
||||
</View>
|
||||
|
||||
{/* 功能介绍 */}
|
||||
<View className='login-features'>
|
||||
<Text className='feature-text'>发布任务 · 二手交易 · 校园资讯</Text>
|
||||
</View>
|
||||
|
||||
{/* 登录操作 */}
|
||||
<View className='login-actions'>
|
||||
<Button
|
||||
className={`login-btn ${submitting ? 'login-btn--loading' : ''}`}
|
||||
openType='getPhoneNumber'
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '登录中...' : '微信手机号授权登录'}
|
||||
</Button>
|
||||
|
||||
<View className='login-agreement'>
|
||||
<Text className='agree-text'>登录即代表同意</Text>
|
||||
<Text className='agree-link' onClick={handleShowAgreement}>
|
||||
《用户协议》
|
||||
</Text>
|
||||
<Text className='agree-text'>和</Text>
|
||||
<Text className='agree-link' onClick={handleShowPrivacy}>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '编辑资料',
|
||||
})
|
||||
@@ -0,0 +1,218 @@
|
||||
/* ========================================
|
||||
编辑资料页面
|
||||
======================================== */
|
||||
|
||||
.profile-edit-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏 ========== */
|
||||
.profile-edit-navbar {
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
|
||||
.navbar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-back {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.back-arrow {
|
||||
font-size: 48px;
|
||||
color: #323233;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: #323233;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 加载状态 ========== */
|
||||
.loading-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
|
||||
.loading-text {
|
||||
font-size: 28px;
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 表单区域 ========== */
|
||||
.profile-edit-form {
|
||||
margin-top: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28px 32px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.form-label {
|
||||
font-size: 30px;
|
||||
color: #323233;
|
||||
flex-shrink: 0;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.form-input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
text-align: right;
|
||||
font-size: 30px;
|
||||
color: #323233;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
}
|
||||
|
||||
.form-value-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
font-size: 30px;
|
||||
color: #323233;
|
||||
|
||||
&--readonly {
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 头像行 */
|
||||
.form-item--avatar {
|
||||
.avatar-picker-btn {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-arrow {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 36px;
|
||||
color: #c8c9cc;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 性别行 */
|
||||
.form-item--gender {
|
||||
.gender-options {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gender-tag {
|
||||
padding: 8px 28px;
|
||||
border-radius: 32px;
|
||||
border: 2px solid #ebedf0;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
.gender-tag-text {
|
||||
font-size: 26px;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: #1989fa;
|
||||
background: #eaf3ff;
|
||||
|
||||
.gender-tag-text {
|
||||
color: #1989fa;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 保存按钮 ========== */
|
||||
.profile-edit-footer {
|
||||
padding: 48px 32px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
line-height: 96px;
|
||||
background: linear-gradient(160deg, #1989fa 0%, #07c160 100%);
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 48px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
box-shadow: 0 6px 24px rgba(25, 137, 250, 0.35);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Button, Input, Image } from '@tarojs/components'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getUserInfoApi, updateUserProfileApi } from '@/services/user'
|
||||
import type { Gender } from '@/types/user'
|
||||
import './index.less'
|
||||
|
||||
/** 性别选项 */
|
||||
const GENDER_OPTIONS: { value: Gender; label: string }[] = [
|
||||
{ value: 0, label: '未知' },
|
||||
{ value: 1, label: '男' },
|
||||
{ value: 2, label: '女' },
|
||||
]
|
||||
|
||||
/** 默认头像占位图(灰色圆) */
|
||||
const DEFAULT_AVATAR =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNTAiIGZpbGw9IiNFNUU1RTUiLz48L3N2Zz4='
|
||||
|
||||
/** 状态栏高度 */
|
||||
function getStatusBarHeight(): number {
|
||||
try {
|
||||
return Taro.getSystemInfoSync().statusBarHeight || 20
|
||||
} catch {
|
||||
return 20
|
||||
}
|
||||
}
|
||||
|
||||
/** 导航栏高度 */
|
||||
const NAV_BAR_HEIGHT = 44
|
||||
|
||||
export default function ProfileEditPage() {
|
||||
const statusBarH = useMemo(getStatusBarHeight, [])
|
||||
const storeUser = useAuthStore(s => s.user)
|
||||
const setUser = useAuthStore(s => s.setUser)
|
||||
|
||||
// 本地编辑状态
|
||||
const [avatar, setAvatar] = useState('')
|
||||
const [nickname, setNickname] = useState('')
|
||||
const [gender, setGender] = useState<Gender>(0)
|
||||
const [mobile, setMobile] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 记录初始值用于判断是否有修改
|
||||
const [initialAvatar, setInitialAvatar] = useState('')
|
||||
const [initialNickname, setInitialNickname] = useState('')
|
||||
const [initialGender, setInitialGender] = useState<Gender>(0)
|
||||
|
||||
/** 加载用户信息 */
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// 优先从 store 快速展示,再从 API 获取脱敏后的手机号
|
||||
if (storeUser) {
|
||||
setAvatar(storeUser.avatar || '')
|
||||
setNickname(storeUser.nickname || '')
|
||||
setGender(storeUser.gender)
|
||||
setMobile(storeUser.mobile || '')
|
||||
setInitialAvatar(storeUser.avatar || '')
|
||||
setInitialNickname(storeUser.nickname || '')
|
||||
setInitialGender(storeUser.gender)
|
||||
}
|
||||
|
||||
// 从 API 获取最新数据(含脱敏手机号)
|
||||
const res = await getUserInfoApi()
|
||||
const user = res.data
|
||||
setAvatar(user.avatar || '')
|
||||
setNickname(user.nickname || '')
|
||||
setGender(user.gender)
|
||||
setMobile(user.mobile || '')
|
||||
setInitialAvatar(user.avatar || '')
|
||||
setInitialNickname(user.nickname || '')
|
||||
setInitialGender(user.gender)
|
||||
} catch {
|
||||
// API 失败则使用 store 数据(已展示)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
})()
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
/** 是否已修改 */
|
||||
const isModified = useMemo(
|
||||
() =>
|
||||
avatar !== initialAvatar ||
|
||||
nickname !== initialNickname ||
|
||||
gender !== initialGender,
|
||||
[avatar, nickname, gender, initialAvatar, initialNickname, initialGender],
|
||||
)
|
||||
|
||||
/** 返回上一页 */
|
||||
const handleBack = useCallback(() => {
|
||||
Taro.navigateBack()
|
||||
}, [])
|
||||
|
||||
/** 选择头像 */
|
||||
const handleChooseAvatar = useCallback((e: any) => {
|
||||
const url = e.detail?.avatarUrl
|
||||
if (url) {
|
||||
setAvatar(url)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 昵称变化 */
|
||||
const handleNicknameChange = useCallback((e: any) => {
|
||||
setNickname(e.detail?.value || '')
|
||||
}, [])
|
||||
|
||||
/** 选择性别 */
|
||||
const handleGenderChange = useCallback((value: Gender) => {
|
||||
setGender(value)
|
||||
}, [])
|
||||
|
||||
/** 保存 */
|
||||
const handleSave = useCallback(async () => {
|
||||
if (submitting) return
|
||||
|
||||
// 校验
|
||||
if (nickname && nickname.length > 32) {
|
||||
Taro.showToast({ title: '昵称不能超过 32 个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (avatar && avatar.length > 500) {
|
||||
Taro.showToast({ title: '头像地址过长', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const params: { nickname?: string; avatar?: string; gender?: Gender } = {
|
||||
nickname, avatar, gender
|
||||
}
|
||||
|
||||
const res = await updateUserProfileApi(params)
|
||||
// 同步更新 store
|
||||
setUser(res.data)
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 1200)
|
||||
} catch {
|
||||
Taro.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [
|
||||
submitting,
|
||||
avatar,
|
||||
nickname,
|
||||
gender,
|
||||
initialAvatar,
|
||||
initialNickname,
|
||||
initialGender,
|
||||
setUser,
|
||||
])
|
||||
|
||||
/** 手机号展示(已脱敏或以 **** 显示) */
|
||||
const mobileDisplay = useMemo(() => {
|
||||
if (!mobile) return '未绑定'
|
||||
return mobile
|
||||
}, [mobile])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='profile-edit-page'>
|
||||
<View className='profile-edit-navbar' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='navbar-inner' style={{ height: `${NAV_BAR_HEIGHT}px` }}>
|
||||
<View className='navbar-back' onClick={handleBack}>
|
||||
<Text className='back-arrow'>‹</Text>
|
||||
</View>
|
||||
<Text className='navbar-title'>编辑资料</Text>
|
||||
<View className='navbar-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
<View className='loading-wrapper'>
|
||||
<Text className='loading-text'>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='profile-edit-page'>
|
||||
{/* ========== 自定义导航栏 ========== */}
|
||||
<View className='profile-edit-navbar' style={{ paddingTop: `${statusBarH}px` }}>
|
||||
<View className='navbar-inner' style={{ height: `${NAV_BAR_HEIGHT}px` }}>
|
||||
<View className='navbar-back' onClick={handleBack}>
|
||||
<Text className='back-arrow'>‹</Text>
|
||||
</View>
|
||||
<Text className='navbar-title'>编辑资料</Text>
|
||||
<View className='navbar-placeholder' />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 表单区 ========== */}
|
||||
<View className='profile-edit-form'>
|
||||
{/* 头像 */}
|
||||
<View className='form-item form-item--avatar'>
|
||||
<Text className='form-label'>头像</Text>
|
||||
<Button
|
||||
className='avatar-picker-btn'
|
||||
openType='chooseAvatar'
|
||||
onChooseAvatar={handleChooseAvatar}
|
||||
>
|
||||
<Image
|
||||
className='avatar-img'
|
||||
src={avatar || DEFAULT_AVATAR}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='avatar-arrow'>
|
||||
<Text className='arrow-icon'>›</Text>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 昵称 */}
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>昵称</Text>
|
||||
<View className='form-input-wrapper'>
|
||||
<Input
|
||||
className='form-input'
|
||||
type='nickname'
|
||||
placeholder='请输入昵称'
|
||||
value={nickname}
|
||||
onInput={handleNicknameChange}
|
||||
onBlur={handleNicknameChange}
|
||||
maxlength={32}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 性别 */}
|
||||
<View className='form-item form-item--gender'>
|
||||
<Text className='form-label'>性别</Text>
|
||||
<View className='gender-options'>
|
||||
{GENDER_OPTIONS.map(opt => (
|
||||
<View
|
||||
key={opt.value}
|
||||
className={`gender-tag ${gender === opt.value ? 'gender-tag--active' : ''}`}
|
||||
onClick={() => handleGenderChange(opt.value)}
|
||||
>
|
||||
<Text className='gender-tag-text'>{opt.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 手机号(只读) */}
|
||||
<View className='form-item'>
|
||||
<Text className='form-label'>手机号</Text>
|
||||
<View className='form-value-wrapper'>
|
||||
<Text className='form-value form-value--readonly'>{mobileDisplay}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<View className='profile-edit-footer'>
|
||||
<Button
|
||||
className={`save-btn ${!isModified || submitting ? 'save-btn--disabled' : ''}`}
|
||||
onClick={handleSave}
|
||||
disabled={!isModified || submitting}
|
||||
loading={submitting}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -72,6 +72,18 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
@@ -88,6 +100,11 @@
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gender-tag {
|
||||
font-size: 22px;
|
||||
width: 36px;
|
||||
|
||||
+107
-21
@@ -1,7 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { Cell, CellGroup, Icon } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { GENDER_MAP } from '@/types/user'
|
||||
import type { Gender } from '@/types/user'
|
||||
import './index.less'
|
||||
|
||||
/** 状态栏高度 */
|
||||
@@ -13,13 +16,13 @@ function getStatusBarHeight(): number {
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户信息(后续接入 API) */
|
||||
const USER_INFO = {
|
||||
/** 默认用户信息(未登录时展示占位) */
|
||||
const DEFAULT_USER = {
|
||||
avatar: 'https://picsum.photos/seed/avatar/200/200',
|
||||
name: '张三',
|
||||
className: '计算机科学与技术 2022级',
|
||||
name: '未登录',
|
||||
className: '点击登录体验更多功能',
|
||||
gender: '男' as const,
|
||||
stats: { likes: 128, comments: 46, favorites: 89 },
|
||||
stats: { likes: '-', comments: '-', favorites: '-' },
|
||||
}
|
||||
|
||||
/** 订单入口 */
|
||||
@@ -37,32 +40,80 @@ const FEATURE_LIST = [
|
||||
{ key: 'address', label: '地址管理', icon: 'location-o' },
|
||||
{ key: 'about', label: '关于我们', icon: 'info-o' },
|
||||
{ key: 'feedback', label: '意见反馈', icon: 'chat-o' },
|
||||
{ key: 'settings', label: '系统设置', icon: 'setting-o' },
|
||||
] as const
|
||||
|
||||
export default function Profile() {
|
||||
const statusBarH = useMemo(getStatusBarHeight, [])
|
||||
const user = useAuthStore(s => s.user)
|
||||
const logout = useAuthStore(s => s.logout)
|
||||
const isLoggedIn = useAuthStore(s => !!s.token && !!s.user)
|
||||
|
||||
/** 跳转登录页 */
|
||||
const handleGoToLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
const handleSettings = () => {
|
||||
Taro.showToast({ title: '设置页面即将上线', icon: 'none' })
|
||||
}
|
||||
|
||||
const handleEditProfile = () => {
|
||||
Taro.showToast({ title: '编辑资料即将上线', icon: 'none' })
|
||||
if (isLoggedIn) {
|
||||
Taro.navigateTo({ url: '/pages/profile-edit/index' })
|
||||
} else {
|
||||
handleGoToLogin()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOrderTab = (key: string) => {
|
||||
if (!isLoggedIn) {
|
||||
handleGoToLogin()
|
||||
return
|
||||
}
|
||||
Taro.showToast({ title: `${ORDER_TABS.find(t => t.key === key)?.label || ''}订单即将上线`, icon: 'none' })
|
||||
}
|
||||
|
||||
const handleFeature = (item: typeof FEATURE_LIST[number]) => {
|
||||
if (!isLoggedIn) {
|
||||
handleGoToLogin()
|
||||
return
|
||||
}
|
||||
Taro.showToast({ title: `${item.label}即将上线`, icon: 'none' })
|
||||
}
|
||||
|
||||
const handleAllOrders = () => {
|
||||
if (!isLoggedIn) {
|
||||
handleGoToLogin()
|
||||
return
|
||||
}
|
||||
Taro.showToast({ title: '全部订单即将上线', icon: 'none' })
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
const handleLogout = useCallback(() => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
logout()
|
||||
Taro.showToast({ title: '已退出登录', icon: 'success' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [logout])
|
||||
|
||||
// 根据登录状态决定展示的用户信息
|
||||
const displayUser = isLoggedIn && user
|
||||
? {
|
||||
avatar: user.avatar,
|
||||
name: user.nickname,
|
||||
className: '在校学生', // 后续可扩展班级字段
|
||||
gender: GENDER_MAP[user.gender] || '未知',
|
||||
stats: { likes: '-', comments: '-', favorites: '-' }, // 暂用占位,后续从 API 获取
|
||||
}
|
||||
: DEFAULT_USER
|
||||
|
||||
return (
|
||||
<View className='profile-page'>
|
||||
{/* ========== 背景图区域 ========== */}
|
||||
@@ -78,30 +129,50 @@ export default function Profile() {
|
||||
</View>
|
||||
|
||||
{/* 头像 + 用户信息 */}
|
||||
<View className='user-card' onClick={handleEditProfile}>
|
||||
<Image className='avatar' src={USER_INFO.avatar} mode='aspectFill' />
|
||||
<View className='user-info'>
|
||||
<View className='name-row'>
|
||||
<Text className='name'>{USER_INFO.name}</Text>
|
||||
<Text className='gender-tag'>{USER_INFO.gender === '男' ? '♂' : '♀'}</Text>
|
||||
</View>
|
||||
<Text className='class-name'>{USER_INFO.className}</Text>
|
||||
</View>
|
||||
<Icon name='arrow' size={18} color='rgba(255,255,255,0.7)' />
|
||||
<View
|
||||
className={`user-card ${!isLoggedIn ? 'user-card--login' : ''}`}
|
||||
onClick={isLoggedIn ? handleEditProfile : handleGoToLogin}
|
||||
>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Image className='avatar' src={displayUser.avatar} mode='aspectFill' />
|
||||
<View className='user-info'>
|
||||
<View className='name-row'>
|
||||
<Text className='name'>{displayUser.name}</Text>
|
||||
<Text className='gender-tag'>{displayUser.gender === '男' ? '♂' : displayUser.gender === '女' ? '♀' : ''}</Text>
|
||||
</View>
|
||||
<Text className='class-name'>{displayUser.className}</Text>
|
||||
</View>
|
||||
<Icon name='arrow' size={18} color='rgba(255,255,255,0.7)' />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View className='avatar-placeholder'>
|
||||
<Icon name='user-circle-o' size={48} color='rgba(255,255,255,0.7)' />
|
||||
</View>
|
||||
<View className='user-info'>
|
||||
<View className='name-row'>
|
||||
<Text className='name login-text'>点击登录账号</Text>
|
||||
</View>
|
||||
<Text className='class-name'>登录后体验更多功能</Text>
|
||||
</View>
|
||||
<Icon name='arrow' size={18} color='rgba(255,255,255,0.7)' />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 点赞/评论/收藏 */}
|
||||
<View className='stats-row'>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{USER_INFO.stats.likes}</Text>
|
||||
<Text className='stat-num'>{displayUser.stats.likes}</Text>
|
||||
<Text className='stat-label'>点赞</Text>
|
||||
</View>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{USER_INFO.stats.comments}</Text>
|
||||
<Text className='stat-num'>{displayUser.stats.comments}</Text>
|
||||
<Text className='stat-label'>评论</Text>
|
||||
</View>
|
||||
<View className='stat-item'>
|
||||
<Text className='stat-num'>{USER_INFO.stats.favorites}</Text>
|
||||
<Text className='stat-num'>{displayUser.stats.favorites}</Text>
|
||||
<Text className='stat-label'>收藏</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -148,6 +219,21 @@ export default function Profile() {
|
||||
</CellGroup>
|
||||
</View>
|
||||
|
||||
{/* ========== 退出登录(仅登录后显示) ========== */}
|
||||
{isLoggedIn && (
|
||||
<View className='section-card feature-list'>
|
||||
<CellGroup inset>
|
||||
<Cell
|
||||
title='退出登录'
|
||||
icon='cross'
|
||||
isLink
|
||||
border={false}
|
||||
onClick={handleLogout}
|
||||
/>
|
||||
</CellGroup>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 底部信息 ========== */}
|
||||
<View className='footer-info'>
|
||||
<Text className='footer-icp'>湘ICP备2024000000号-1</Text>
|
||||
|
||||
Reference in New Issue
Block a user