账户密码登录

This commit is contained in:
liu
2026-08-21 11:01:37 +08:00
parent 3bd26acbb9
commit f0d8f6bac4
13 changed files with 328 additions and 599 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ export default defineAppConfig({
'pages/payment-detail/index', 'pages/payment-detail/index',
'pages/settings/index', 'pages/settings/index',
'pages/login/index', 'pages/login/index',
'pages/register/index', 'pages/change-password/index',
'pages/store-info/index', 'pages/store-info/index',
], ],
window: { window: {
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '修改密码',
})
+48
View File
@@ -0,0 +1,48 @@
.change-password-page {
min-height: 100vh;
background: #f7f8fa;
padding: 20rpx 24rpx 60rpx;
box-sizing: border-box;
// ===== 表单区块 =====
.pwd-section {
background: #fff;
border-radius: 20rpx;
padding: 8rpx 28rpx;
margin-bottom: 20rpx;
}
// ===== 表单行 =====
.pwd-field {
display: flex;
align-items: center;
padding: 28rpx 0;
border-bottom: 1rpx solid #f2f3f5;
&:last-child {
border-bottom: none;
}
&__label {
width: 160rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #323233;
}
&__input {
flex: 1;
font-size: 28rpx;
color: #323233;
}
&__placeholder {
color: #c8c9cc;
}
}
// ===== 提交按钮 =====
.pwd-submit {
margin-top: 40rpx;
}
}
+111
View File
@@ -0,0 +1,111 @@
import { useCallback, useState } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, Input } from '@tarojs/components'
import { Button } from '@antmjs/vantui'
import { changePasswordApi } from '@/services/auth'
import './index.less'
/** 新密码长度限制(与后端一致:6~20 位) */
const LIMITS = {
passwordMin: 6,
passwordMax: 20,
} as const
/**
* 修改密码页
* 修改成功后现有 token 仍然有效,无需重新登录
*/
export default function ChangePasswordPage() {
/** 原密码 */
const [oldPassword, setOldPassword] = useState('')
/** 新密码 */
const [newPassword, setNewPassword] = useState('')
/** 确认新密码 */
const [rePassword, setRePassword] = useState('')
const [saving, setSaving] = useState(false)
/** 提交:PUT /mini/auth/password */
const handleSubmit = useCallback(async () => {
if (saving) return
if (!oldPassword) {
Taro.showToast({ title: '请输入原密码', icon: 'none' })
return
}
if (newPassword.length < LIMITS.passwordMin) {
Taro.showToast({ title: `新密码至少 ${LIMITS.passwordMin}`, icon: 'none' })
return
}
if (newPassword !== rePassword) {
Taro.showToast({ title: '两次输入的密码不一致', icon: 'none' })
return
}
setSaving(true)
try {
await changePasswordApi({ oldPassword, newPassword, rePassword })
Taro.showToast({ title: '密码修改成功', icon: 'success' })
setTimeout(() => Taro.navigateBack(), 800)
} catch {
// 错误提示已由 request 层 toast(原密码不正确等)
} finally {
setSaving(false)
}
}, [saving, oldPassword, newPassword, rePassword])
return (
<View className='change-password-page'>
{/* ========== 密码表单 ========== */}
<View className='pwd-section'>
<View className='pwd-field'>
<Text className='pwd-field__label'></Text>
<Input
className='pwd-field__input'
password
value={oldPassword}
maxlength={LIMITS.passwordMax}
placeholder='请输入原密码'
placeholderClass='pwd-field__placeholder'
onInput={e => setOldPassword(e.detail.value)}
/>
</View>
<View className='pwd-field'>
<Text className='pwd-field__label'></Text>
<Input
className='pwd-field__input'
password
value={newPassword}
maxlength={LIMITS.passwordMax}
placeholder={`请输入新密码(${LIMITS.passwordMin}~${LIMITS.passwordMax} 位)`}
placeholderClass='pwd-field__placeholder'
onInput={e => setNewPassword(e.detail.value)}
/>
</View>
<View className='pwd-field'>
<Text className='pwd-field__label'></Text>
<Input
className='pwd-field__input'
password
value={rePassword}
maxlength={LIMITS.passwordMax}
placeholder='请再次输入新密码'
placeholderClass='pwd-field__placeholder'
confirmType='done'
onInput={e => setRePassword(e.detail.value)}
onConfirm={handleSubmit}
/>
</View>
</View>
{/* ========== 提交 ========== */}
<Button
type='danger'
block
round
loading={saving}
className='pwd-submit'
onClick={handleSubmit}
>
</Button>
</View>
)
}
+38 -17
View File
@@ -100,17 +100,44 @@
} }
} }
/* ========== 功能介绍 ========== */ /* ========== 登录表单 ========== */
.login-features { .login-form {
margin-bottom: 80px; width: 100%;
background: #f7f8fa;
border-radius: 24px;
padding: 0 32px;
margin-bottom: 60px;
}
.feature-text { .form-item {
font-size: 26px; display: flex;
color: #c8c9cc; align-items: center;
letter-spacing: 2px; height: 112px;
border-bottom: 1px solid #ebedf0;
&:last-child {
border-bottom: none;
} }
} }
.form-label {
width: 120px;
font-size: 30px;
color: #323233;
flex-shrink: 0;
}
.form-input {
flex: 1;
height: 100%;
font-size: 30px;
color: #323233;
}
.form-input-placeholder {
color: #c8c9cc;
}
/* ========== 登录操作区 ========== */ /* ========== 登录操作区 ========== */
.login-actions { .login-actions {
width: 100%; width: 100%;
@@ -144,23 +171,17 @@
opacity: 0.75; opacity: 0.75;
} }
/* ========== 去注册入口 ========== */ /* ========== 客服提示 ========== */
.login-switch { .login-tip {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 32px; margin-top: 32px;
.switch-text { .tip-text {
font-size: 28px; font-size: 26px;
color: #969799; color: #969799;
} }
.switch-link {
font-size: 28px;
color: #ee0a24;
margin-left: 8px;
}
} }
/* ========== 协议文字 ========== */ /* ========== 协议文字 ========== */
+56 -48
View File
@@ -1,18 +1,24 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useState } from 'react'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { View, Text, Button } from '@tarojs/components' import { View, Text, Button, Input } from '@tarojs/components'
import CustomNavBar from '@/components/NavBar' import CustomNavBar from '@/components/NavBar'
import useAuthStore from '@/stores/auth/useAuthStore' import useAuthStore from '@/stores/auth/useAuthStore'
import './index.less' import './index.less'
/** 登录账号长度限制(与后端一致:4~20 位) */
const USERNAME_MAX = 20
/** 密码长度限制 */
const PASSWORD_MAX = 20
export default function LoginPage() { export default function LoginPage() {
const login = useAuthStore(s => s.login) const login = useAuthStore(s => s.login)
const isLoggedIn = useAuthStore(s => !!s.token && !!s.user)
/** 登录账号(商家后台分配) */
const [username, setUsername] = useState('')
/** 登录密码 */
const [password, setPassword] = useState('')
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const isWeb = Taro.getEnv() === Taro.ENV_TYPE.WEB
/** 返回上一页(无页面栈时回首页) */ /** 返回上一页(无页面栈时回首页) */
const goBack = useCallback(() => { const goBack = useCallback(() => {
const pages = Taro.getCurrentPages() const pages = Taro.getCurrentPages()
@@ -23,50 +29,29 @@ export default function LoginPage() {
} }
}, []) }, [])
/** 前往注册页 */ /** 账号密码登录:POST /mini/auth/login */
const goRegister = useCallback(() => {
Taro.navigateTo({ url: '/pages/register/index' })
}, [])
/** 已登录 → 自动返回 */
useEffect(() => {
if (isLoggedIn) goBack()
}, [isLoggedIn, goBack])
/** 微信一键登录(wx.login code 换 openid,仅已注册用户可登录) */
const handleLogin = useCallback(async () => { const handleLogin = useCallback(async () => {
if (submitting) return if (submitting) return
// H5 环境无法获取微信登录凭证 const account = username.trim()
if (isWeb) { if (!account) {
Taro.showToast({ title: '请在微信小程序中使用微信登录', icon: 'none' }) Taro.showToast({ title: '请输入登录账号', icon: 'none' })
return
}
if (!password) {
Taro.showToast({ title: '请输入登录密码', icon: 'none' })
return return
} }
setSubmitting(true) setSubmitting(true)
try { try {
const res = await Taro.login() await login({ username: account, password })
if (!res.code) { Taro.showToast({ title: '登录成功', icon: 'success' })
Taro.showToast({ title: '获取登录凭证失败', icon: 'none' }) goBack()
return } catch {
} // 错误提示已由 request 层 toast(账号或密码错误 / 账号已停用等)
await login({ code: res.code })
// 登录成功后由 effect 自动返回
} catch (e: any) {
// 未注册用户:引导前往注册页(其余错误已由 request 层提示)
if (typeof e?.msg === 'string' && e.msg.includes('用户不存在')) {
Taro.showModal({
title: '未注册',
content: '该微信账号尚未注册,需授权手机号并填写门店编码完成注册',
confirmText: '去注册',
cancelText: '取消',
success: res => {
if (res.confirm) goRegister()
},
})
}
} finally { } finally {
setSubmitting(false) setSubmitting(false)
} }
}, [login, submitting, isWeb, goRegister]) }, [login, submitting, username, password, goBack])
/** 查看用户协议 */ /** 查看用户协议 */
const handleShowAgreement = useCallback(() => { const handleShowAgreement = useCallback(() => {
@@ -94,9 +79,34 @@ export default function LoginPage() {
<Text className='app-slogan'> · · </Text> <Text className='app-slogan'> · · </Text>
</View> </View>
{/* 功能介绍 */} {/* 登录表单 */}
<View className='login-features'> <View className='login-form'>
<Text className='feature-text'>线 · · </Text> <View className='form-item'>
<Text className='form-label'></Text>
<Input
className='form-input'
type='text'
value={username}
maxlength={USERNAME_MAX}
placeholder='请输入登录账号'
placeholderClass='form-input-placeholder'
onInput={e => setUsername(e.detail.value)}
/>
</View>
<View className='form-item'>
<Text className='form-label'></Text>
<Input
className='form-input'
password
value={password}
maxlength={PASSWORD_MAX}
placeholder='请输入登录密码'
placeholderClass='form-input-placeholder'
confirmType='done'
onInput={e => setPassword(e.detail.value)}
onConfirm={handleLogin}
/>
</View>
</View> </View>
{/* 登录操作 */} {/* 登录操作 */}
@@ -107,13 +117,11 @@ export default function LoginPage() {
loading={submitting} loading={submitting}
disabled={submitting} disabled={submitting}
> >
{submitting ? '登录中...' : '微信一键登录'} {submitting ? '登录中...' : '登 录'}
</Button> </Button>
{/* 未注册用户入口 */} <View className='login-tip'>
<View className='login-switch' onClick={goRegister}> <Text className='tip-text'></Text>
<Text className='switch-text'></Text>
<Text className='switch-link'></Text>
</View> </View>
<View className='login-agreement'> <View className='login-agreement'>
+15 -28
View File
@@ -8,7 +8,6 @@ import { getBillListApi } from '@/services/bill'
import type { BillSummary } from '@/services/bill' import type { BillSummary } from '@/services/bill'
import { ORDER_NAV_ITEMS } from '@/types/order' import { ORDER_NAV_ITEMS } from '@/types/order'
import { resolveAvatarUrl } from '@/utils/format' import { resolveAvatarUrl } from '@/utils/format'
import type { UserType } from '@/types/user'
import './index.less' import './index.less'
/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */ /** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */
@@ -38,9 +37,9 @@ export default function ProfilePage() {
const loggedIn = !!token && !!user const loggedIn = !!token && !!user
/** 功能菜单(门店账号追加「门店信息」「支付记录」入口 */ /** 功能菜单(登录门店可用:门店信息 / 支付记录 / 修改密码 */
const menuItems = useMemo(() => { const menuItems = useMemo(() => {
if (!user?.store) return MENU_ITEMS if (!loggedIn) return MENU_ITEMS
return [ return [
{ {
key: 'store-info', key: 'store-info',
@@ -54,9 +53,15 @@ export default function ProfilePage() {
icon: 'balance-o', icon: 'balance-o',
onClick: () => Taro.navigateTo({ url: '/pages/payment-records/index' }), onClick: () => Taro.navigateTo({ url: '/pages/payment-records/index' }),
}, },
{
key: 'change-password',
label: '修改密码',
icon: 'lock',
onClick: () => Taro.navigateTo({ url: '/pages/change-password/index' }),
},
...MENU_ITEMS, ...MENU_ITEMS,
] ]
}, [user?.store]) }, [loggedIn])
useDidShow(() => { useDidShow(() => {
if (!loggedIn) return if (!loggedIn) return
@@ -70,13 +75,6 @@ export default function ProfilePage() {
.catch(() => {}) .catch(() => {})
}) })
/** 身份标签 */
const getTypeLabel = useCallback((type: UserType): string => {
if (type === 1) return '门店'
if (type === 2) return '供应商'
return '待绑定'
}, [])
/** 订单总汇 → 订单列表页(按状态) */ /** 订单总汇 → 订单列表页(按状态) */
const handleOrderNav = useCallback((status?: number) => { const handleOrderNav = useCallback((status?: number) => {
Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` }) Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` })
@@ -130,29 +128,18 @@ export default function ProfilePage() {
/> />
) : ( ) : (
<View className='profile-card__avatar profile-card__avatar--text'> <View className='profile-card__avatar profile-card__avatar--text'>
{user?.nickname?.[0] || ''} {user?.name?.[0] || ''}
</View> </View>
)} )}
<View className='profile-card__info'> <View className='profile-card__info'>
<Text className='profile-card__name'>{user?.nickname}</Text> <Text className='profile-card__name'>{user?.name}</Text>
<Text className='profile-card__desc'>{user?.phone || '未绑定手机号'}</Text> <Text className='profile-card__desc'>{user?.phone || '未设置联系电话'}</Text>
</View> </View>
{user?.type === 0 && (
<View className='profile-card__btn' onClick={goLogin}></View>
)}
</View> </View>
<View className='profile-card__identity'> <View className='profile-card__identity'>
{user?.store ? ( <Text className='profile-card__tag'> · {user?.code}</Text>
<> {user?.level && (
<Text className='profile-card__tag'> · {user.store.name}</Text> <Text className='profile-card__tag profile-card__tag--level'>{user.level.name}</Text>
{user.store.level && (
<Text className='profile-card__tag profile-card__tag--level'>{user.store.level.name}</Text>
)}
</>
) : user?.supplier ? (
<Text className='profile-card__tag'> · {user.supplier.name}</Text>
) : (
<Text className='profile-card__tag'>{getTypeLabel(user?.type ?? 0)}</Text>
)} )}
</View> </View>
</> </>
-4
View File
@@ -1,4 +0,0 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '注册',
})
-196
View File
@@ -1,196 +0,0 @@
/* ========================================
注册页面
======================================== */
.register-page {
min-height: 100vh;
background: #fff;
}
/* ========== 内容区域 ========== */
.register-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 60px 0;
}
/* ========== 品牌区域 ========== */
.register-brand {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60px;
.logo-wrapper {
width: 160px;
height: 160px;
border-radius: 50%;
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(238, 10, 36, 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;
}
}
/* ========== 注册表单 ========== */
.register-form {
width: 100%;
background: #f7f8fa;
border-radius: 24px;
padding: 0 32px;
margin-bottom: 80px;
}
.form-item {
display: flex;
align-items: center;
height: 112px;
border-bottom: 1px solid #ebedf0;
&:last-child {
border-bottom: none;
}
}
.form-label {
width: 160px;
font-size: 30px;
color: #323233;
flex-shrink: 0;
}
.form-input {
flex: 1;
height: 100%;
font-size: 30px;
color: #323233;
}
.form-input-placeholder {
color: #c8c9cc;
}
/* 手机号已授权状态 */
.form-phone-ok {
flex: 1;
display: flex;
align-items: center;
.form-phone-ok__text {
font-size: 30px;
color: #07c160;
}
}
/* 手机号授权按钮(微信原生 Button 需重置样式) */
.phone-auth-btn {
flex: 1;
height: 64px;
line-height: 64px;
background: #fff0f0;
color: #ee0a24;
font-size: 28px;
font-weight: 500;
border: none;
border-radius: 32px;
text-align: center;
padding: 0 32px;
/* 重置微信 Button 默认样式 */
&::after {
border: none;
}
}
/* ========== 注册操作区 ========== */
.register-actions {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.register-btn {
width: 100%;
height: 96px;
line-height: 96px;
background: linear-gradient(160deg, #ee0a24 0%, #ff4d4f 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(238, 10, 36, 0.35);
transition: opacity 0.2s;
/* 重置微信 Button 默认样式 */
&::after {
border: none;
}
}
.register-btn--loading {
opacity: 0.75;
}
/* ========== 去登录入口 ========== */
.register-switch {
display: flex;
align-items: center;
justify-content: center;
margin-top: 32px;
.switch-text {
font-size: 28px;
color: #969799;
}
.switch-link {
font-size: 28px;
color: #ee0a24;
margin-left: 8px;
}
}
/* ========== 协议文字 ========== */
.register-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: #ee0a24;
}
}
-222
View File
@@ -1,222 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import Taro from '@tarojs/taro'
import { View, Text, Button, Input } from '@tarojs/components'
import CustomNavBar from '@/components/NavBar'
import useAuthStore from '@/stores/auth/useAuthStore'
import './index.less'
export default function RegisterPage() {
const register = useAuthStore(s => s.register)
const isLoggedIn = useAuthStore(s => !!s.token && !!s.user)
/** 门店编码(后台门店管理维护) */
const [storeCode, setStoreCode] = useState('')
/** 微信手机号授权得到的 code */
const [phoneCode, setPhoneCode] = useState('')
const [submitting, setSubmitting] = useState(false)
const isWeb = Taro.getEnv() === Taro.ENV_TYPE.WEB
/** 返回上一页(无页面栈时回首页) */
const goBack = useCallback(() => {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
Taro.switchTab({ url: '/pages/index/index' })
}
}, [])
/** 前往登录页 */
const goLogin = useCallback(() => {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
Taro.navigateTo({ url: '/pages/login/index' })
}
}, [])
/** 已注册成功(登录态就绪)→ 自动返回 */
useEffect(() => {
if (isLoggedIn) goBack()
}, [isLoggedIn, goBack])
/** 发起注册:wx.login 换 code → POST /mini/auth/register */
const doRegister = useCallback(
async (phoneCodeValue: string) => {
if (submitting) return
if (isWeb) {
Taro.showToast({ title: '请在微信小程序中注册', icon: 'none' })
return
}
const code = storeCode.trim()
if (!code) {
Taro.showToast({ title: '请填写门店编码', icon: 'none' })
return
}
setSubmitting(true)
try {
const res = await Taro.login()
if (!res.code) {
Taro.showToast({ title: '获取登录凭证失败', icon: 'none' })
return
}
await register({ code: res.code, phoneCode: phoneCodeValue, storeCode: code })
// 注册成功后由 effect 自动返回
} catch (e: any) {
// 该微信已注册:引导前往登录(其余错误已由 request 层提示)
if (typeof e?.msg === 'string' && e.msg.includes('已经注册')) {
Taro.showModal({
title: '提示',
content: '该微信已经注册,请直接登录',
confirmText: '去登录',
cancelText: '取消',
success: res => {
if (res.confirm) goLogin()
},
})
}
} finally {
setSubmitting(false)
}
},
[register, submitting, isWeb, storeCode, goLogin],
)
/** 微信手机号授权(openType getPhoneNumber */
const handleGetPhoneNumber = useCallback(
(e: any) => {
if (isWeb) {
Taro.showToast({ title: '请在微信小程序中授权手机号', icon: 'none' })
return
}
const detail = e.detail || {}
// 用户拒绝授权
if (detail.errMsg && !detail.errMsg.includes(':ok')) {
Taro.showToast({ title: '需要授权手机号才能注册', icon: 'none' })
return
}
if (!detail.code) {
Taro.showToast({ title: '未获取到手机号授权凭证', icon: 'none' })
return
}
setPhoneCode(detail.code)
if (storeCode.trim()) {
// 门店编码已填 → 直接发起注册
doRegister(detail.code)
} else {
Taro.showToast({ title: '手机号已授权,请填写门店编码', icon: 'none' })
}
},
[isWeb, storeCode, doRegister],
)
/** 点击注册按钮(门店编码已填 + 手机号已授权) */
const handleSubmit = useCallback(() => {
if (!phoneCode) {
Taro.showToast({ title: '请先授权手机号', icon: 'none' })
return
}
if (!storeCode.trim()) {
Taro.showToast({ title: '请填写门店编码', icon: 'none' })
return
}
doRegister(phoneCode)
}, [phoneCode, storeCode, doRegister])
/** 查看用户协议 */
const handleShowAgreement = useCallback(() => {
Taro.showToast({ title: '用户协议即将上线', icon: 'none' })
}, [])
/** 查看隐私政策 */
const handleShowPrivacy = useCallback(() => {
Taro.showToast({ title: '隐私政策即将上线', icon: 'none' })
}, [])
return (
<View className='register-page'>
{/* ========== 导航栏 ========== */}
<CustomNavBar title='注册' />
{/* ========== 内容区域 ========== */}
<View className='register-content'>
{/* 品牌区域 */}
<View className='register-brand'>
<View className='logo-wrapper'>
<Text className='logo-text'></Text>
</View>
<Text className='app-name'></Text>
<Text className='app-slogan'></Text>
</View>
{/* 注册表单 */}
<View className='register-form'>
{/* 门店编码 */}
<View className='form-item'>
<Text className='form-label'></Text>
<Input
className='form-input'
type='text'
value={storeCode}
placeholder='请输入门店编码(门店管理员提供)'
placeholderClass='form-input-placeholder'
onInput={e => setStoreCode(e.detail.value)}
/>
</View>
{/* 手机号授权 */}
<View className='form-item'>
<Text className='form-label'></Text>
{phoneCode ? (
<View className='form-phone-ok'>
<Text className='form-phone-ok__text'></Text>
</View>
) : (
<Button
className='phone-auth-btn'
openType='getPhoneNumber'
onGetPhoneNumber={handleGetPhoneNumber}
>
</Button>
)}
</View>
</View>
{/* 注册操作 */}
<View className='register-actions'>
<Button
className={`register-btn ${submitting ? 'register-btn--loading' : ''}`}
onClick={handleSubmit}
loading={submitting}
disabled={submitting}
>
{submitting ? '注册中...' : '注 册'}
</Button>
{/* 已注册用户入口 */}
<View className='register-switch' onClick={goLogin}>
<Text className='switch-text'></Text>
<Text className='switch-link'></Text>
</View>
<View className='register-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>
)
}
+27 -24
View File
@@ -1,39 +1,42 @@
import { get, post } from '@/utils/request' import { get, post, put } from '@/utils/request'
import type { User } from '@/types/user' import type { User } from '@/types/user'
/** 微信登录参数 */ /** 账号密码登录参数 */
export interface WxLoginParams { export interface LoginParams {
/** wx.login 的临时凭证 */ /** 登录账号(商家后台分配,4~20 位) */
code: string username: string
/** 登录密码 */
password: string
} }
/** 微信注册参数 */ /** 登录返回 */
export interface RegisterParams {
/** wx.login 的临时凭证 */
code: string
/** wx.getPhoneNumber 授权得到的 code */
phoneCode: string
/** 门店编码(后台门店管理维护) */
storeCode: string
}
/** 登录 / 注册返回 */
export interface AuthResult { export interface AuthResult {
token: string token: string
/** 门店即用户(扁平结构) */
user: User user: User
} }
/** 微信登录(仅已注册用户可登录):POST /mini/auth/login */ /** 修改密码参数 */
export function wxLoginApi(params: WxLoginParams) { export interface ChangePasswordParams {
/** 原密码 */
oldPassword: string
/** 新密码(6~20 位) */
newPassword: string
/** 确认新密码(须与 newPassword 一致) */
rePassword: string
}
/** 账号密码登录:POST /mini/auth/login */
export function loginApi(params: LoginParams) {
return post<AuthResult>('/mini/auth/login', params) return post<AuthResult>('/mini/auth/login', params)
} }
/** 微信注册(code 换 openid + phoneCode 换手机号 + storeCode 绑定门店):POST /mini/auth/register */ /** 当前门店信息(含客户等级):GET /mini/auth/info */
export function registerApi(params: RegisterParams) {
return post<AuthResult>('/mini/auth/register', params)
}
/** 当前用户信息(含门店客户等级):GET /mini/auth/info */
export function getUserInfoApi() { export function getUserInfoApi() {
return get<User>('/mini/auth/info') return get<User>('/mini/auth/info')
} }
/** 修改密码(成功后现有 token 仍有效):PUT /mini/auth/password */
export function changePasswordApi(params: ChangePasswordParams) {
return put<null>('/mini/auth/password', params)
}
+9 -18
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { registerApi, wxLoginApi } from '@/services/auth' import { loginApi } from '@/services/auth'
import type { RegisterParams, WxLoginParams } from '@/services/auth' import type { LoginParams } from '@/services/auth'
import type { User } from '@/types/user' import type { User } from '@/types/user'
/** 存储 key */ /** 存储 key */
@@ -26,7 +26,7 @@ function loadFromStorage(): { user: User | null; token: string | null } {
return { user: null, token: null } return { user: null, token: null }
} }
/** 登录 / 注册成功后持久化 token 与用户信息 */ /** 登录成功后持久化 token 与门店信息 */
function persistAuth(token: string, user: User): void { function persistAuth(token: string, user: User): void {
try { try {
Taro.setStorageSync(STORAGE_KEYS.TOKEN, token) Taro.setStorageSync(STORAGE_KEYS.TOKEN, token)
@@ -40,11 +40,10 @@ interface AuthState {
user: User | null user: User | null
token: string | null token: string | null
loading: boolean loading: boolean
login: (params: WxLoginParams) => Promise<void> /** 账号密码登录(门店账号由商家后台分配) */
/** 微信注册(手机号授权 + 门店编码绑定门店) */ login: (params: LoginParams) => Promise<void>
register: (params: RegisterParams) => Promise<void>
logout: () => void logout: () => void
/** 更新用户信息(用于编辑资料后同步 store) */ /** 更新门店信息(用于编辑资料后同步 store) */
setUser: (user: User) => void setUser: (user: User) => void
} }
@@ -57,17 +56,9 @@ const useAuthStore = create<AuthState>((set) => {
token: initial.token, token: initial.token,
loading: !!(initial.token && initial.user), // 已恢复则立即 ready loading: !!(initial.token && initial.user), // 已恢复则立即 ready
/** 登录(仅已注册用户可登录,未注册由页面引导去注册) */ /** 账号密码登录:POST /mini/auth/login */
login: async (params: WxLoginParams) => { login: async (params: LoginParams) => {
const res = await wxLoginApi(params) const res = await loginApi(params)
const { token, user } = res.data
set({ user, token })
persistAuth(token, user)
},
/** 注册:POST /mini/auth/register */
register: async (params: RegisterParams) => {
const res = await registerApi(params)
const { token, user } = res.data const { token, user } = res.data
set({ user, token }) set({ user, token })
persistAuth(token, user) persistAuth(token, user)
+20 -41
View File
@@ -4,54 +4,33 @@ export interface StoreLevel {
name: string name: string
} }
/** 门店信息 */ /**
export interface StoreInfo { * 登录门店信息(门店即用户)
id: number * 用户表与门店表已合并:登录 / auth/info 返回的 user 就是门店本身(扁平结构)
name: string */
/** 客户等级(level_id > 0 才可展示价格) */
level: StoreLevel | null
}
/** 供应商信息 */
export interface SupplierInfo {
id: number
name: string
}
/** 用户类型:0 待绑定 / 1 门店 / 2 供应商 */
export type UserType = 0 | 1 | 2
export const USER_TYPE_MAP: Record<UserType, string> = {
0: '待绑定',
1: '门店',
2: '供应商',
}
/** 用户信息(user 表实际返回字段) */
export interface User { export interface User {
id: number id: number
/** 用户名(注册时生成 wx_xxxx */ /** 门店名称 */
name: string
/** 门店编码(后台分配) */
code: string
/** 登录账号(后台分配,4~20 位) */
username: string username: string
/** 昵称(注册默认「微信用户」 */ /** 头像(可能为空 */
nickname: string
avatar: string avatar: string
/** 手机号(未绑定为空) */ level_id: number
/** 客户等级(level_id > 0 才可展示价格) */
level: StoreLevel | null
/** 联系人 */
contact: string
/** 联系电话 */
phone: string phone: string
/** 绑定门店ID0 未绑定) */ /** 地址 */
store_id: number address: string
/** 回款周期天数 */
payment_cycle_days: number
/** 1 正常 / 0 停用 */ /** 1 正常 / 0 停用 */
status: number status: number
/** 微信标识 */
openid: string
unionid: string
email: string
last_login_at: string | null last_login_at: string | null
created_at: string | null created_at: string | null
updated_at: string | null
/** 兼容旧 /mini/auth/info 返回(身份类型) */
type?: UserType
/** 兼容旧 /mini/auth/info 返回(门店信息) */
store?: StoreInfo | null
/** 兼容旧 /mini/auth/info 返回(供应商信息) */
supplier?: SupplierInfo | null
} }