门店信息

This commit is contained in:
liu
2026-08-10 10:09:30 +08:00
parent 0203c84357
commit b288502e0e
19 changed files with 1001 additions and 748 deletions
+2
View File
@@ -9,6 +9,8 @@ export default defineAppConfig({
'pages/statement/index',
'pages/settings/index',
'pages/login/index',
'pages/register/index',
'pages/store-info/index',
],
window: {
backgroundTextStyle: 'light',
+84 -5
View File
@@ -184,7 +184,7 @@
// ===== 下单确认弹层 =====
.order-popup {
padding: 32rpx 32rpx 24rpx;
padding: 32rpx 32rpx 130rpx;
&__title {
font-size: 34rpx;
@@ -192,8 +192,78 @@
display: block;
}
// --- 配送信息 ---
&__delivery {
margin-top: 24rpx;
display: flex;
align-items: center;
background: #f7f8fa;
border-radius: 12rpx;
padding: 20rpx 24rpx;
}
&__delivery-icon {
flex-shrink: 0;
margin-right: 16rpx;
}
&__delivery-info {
flex: 1;
min-width: 0;
}
&__delivery-head {
display: flex;
align-items: baseline;
}
&__delivery-name {
font-size: 28rpx;
color: #323233;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__delivery-contact {
margin-left: 16rpx;
font-size: 24rpx;
color: #646566;
flex-shrink: 0;
}
&__delivery-address {
margin-top: 8rpx;
font-size: 24rpx;
color: #969799;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
&__delivery-warn {
margin-top: 8rpx;
font-size: 24rpx;
color: #ee0a24;
}
&__delivery-tip {
flex: 1;
font-size: 26rpx;
color: #969799;
}
&__delivery-arrow {
margin-left: 16rpx;
font-size: 32rpx;
color: #c8c9cc;
flex-shrink: 0;
}
&__list {
max-height: 480rpx;
max-height: 400rpx;
margin-top: 20rpx;
}
@@ -208,6 +278,16 @@
flex: 1;
min-width: 0;
display: flex;
}
&-image {
width: 80rpx;
height: 80rpx;
margin-right: 10rpx;
}
&-title {
display: flex;
flex-direction: column;
}
@@ -256,7 +336,7 @@
&__remark-input {
margin-top: 12rpx;
width: 100%;
min-height: 120rpx;
height: 80rpx;
background: #f7f8fa;
border-radius: 12rpx;
padding: 16rpx;
@@ -274,13 +354,12 @@
&__total {
font-size: 28rpx;
color: #323233;
// 金额颜色由内部元素控制
}
&__submit {
width: 280rpx;
border-radius: 999rpx;
margin: 0;
}
}
}
+63 -5
View File
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Image, Textarea, ScrollView } from '@tarojs/components'
import { Button, Empty, Popup, Stepper } from '@antmjs/vantui'
import { Button, Empty, Icon, Popup, Stepper } from '@antmjs/vantui'
import useCartStore from '@/stores/cart/useCartStore'
import { createOrderApi } from '@/services/order'
import { getStoreInfoApi } from '@/services/store'
import type { CartItem } from '@/types/cart'
import type { StoreDetail } from '@/types/store'
import './index.less'
export default function CartPage() {
@@ -27,12 +29,27 @@ export default function CartPage() {
const [remark, setRemark] = useState('')
const [submitting, setSubmitting] = useState(false)
/** 配送信息(下单弹层展示,打开时拉取) */
const [storeInfo, setStoreInfo] = useState<StoreDetail | null>(null)
const [storeLoading, setStoreLoading] = useState(false)
/** 可购项(status=1 */
const purchasable = items.filter(item => item.status === 1)
const hasInvalid = items.length > 0 && purchasable.length < items.length
/** 拉取门店配送信息 */
const fetchStoreInfo = useCallback(() => {
setStoreLoading(true)
getStoreInfoApi()
.then(res => setStoreInfo(res.data))
.catch(() => setStoreInfo(null))
.finally(() => setStoreLoading(false))
}, [])
useDidShow(() => {
fetchCart().catch(() => {})
// 从门店信息编辑页返回且弹层仍打开时 → 刷新配送信息
if (showOrder) fetchStoreInfo()
})
/** 同步本地编辑数量:删除不存在的项,保留在编数量 */
@@ -106,14 +123,24 @@ export default function CartPage() {
})
}, [clearCart])
/** 打开下单弹层 */
/** 打开下单弹层(同时拉取配送信息) */
const handleOrderTap = useCallback(() => {
if (!purchasable.length) {
Taro.showToast({ title: '没有可购买的商品', icon: 'none' })
return
}
setShowOrder(true)
}, [purchasable.length])
fetchStoreInfo()
}, [purchasable.length, fetchStoreInfo])
/** 配送信息点击:拉取失败时重试,否则前往门店信息编辑页 */
const handleDeliveryTap = useCallback(() => {
if (!storeLoading && !storeInfo) {
fetchStoreInfo()
return
}
Taro.navigateTo({ url: '/pages/store-info/index' })
}, [storeLoading, storeInfo, fetchStoreInfo])
/** 提交订单(金额一律服务端重算) */
const handleSubmitOrder = useCallback(async () => {
@@ -240,12 +267,43 @@ export default function CartPage() {
>
<View className='order-popup'>
<Text className='order-popup__title'></Text>
{/* 配送信息(点击前往修改门店信息) */}
<View className='order-popup__delivery' onClick={handleDeliveryTap}>
<Icon name='location-o' size={20} color='#ee0a24' className='order-popup__delivery-icon' />
{storeLoading ? (
<Text className='order-popup__delivery-tip'>...</Text>
) : storeInfo ? (
<View className='order-popup__delivery-info'>
<View className='order-popup__delivery-head'>
<Text className='order-popup__delivery-name'>{storeInfo.name}</Text>
{(storeInfo.contact || storeInfo.phone) && (
<Text className='order-popup__delivery-contact'>
{storeInfo.contact} {storeInfo.phone}
</Text>
)}
</View>
{storeInfo.address ? (
<Text className='order-popup__delivery-address'>{storeInfo.address}</Text>
) : (
<Text className='order-popup__delivery-warn'></Text>
)}
</View>
) : (
<Text className='order-popup__delivery-tip'></Text>
)}
<Text className='order-popup__delivery-arrow'></Text>
</View>
<ScrollView scrollY className='order-popup__list'>
{purchasable.map(item => (
<View key={item.id} className='order-popup__item'>
<View className='order-popup__item-info'>
<Text className='order-popup__item-name'>{item.name}</Text>
<Text className='order-popup__item-spec'>{item.spec} / {item.unit}</Text>
<Image className='order-popup__item-image' src={item.image} mode='aspectFill' lazyLoad />
<View className='order-popup__item-title'>
<Text className='order-popup__item-name'>{item.name}</Text>
<Text className='order-popup__item-spec'>{item.spec} / {item.unit}</Text>
</View>
</View>
<View className='order-popup__item-right'>
<Text className='order-popup__item-qty'>×{displayQty(item)}</Text>
+19
View File
@@ -144,6 +144,25 @@
opacity: 0.75;
}
/* ========== 去注册入口 ========== */
.login-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: #1989fa;
margin-left: 8px;
}
}
/* ========== 协议文字 ========== */
.login-agreement {
display: flex;
+40 -72
View File
@@ -6,15 +6,10 @@ import useAuthStore from '@/stores/auth/useAuthStore'
import './index.less'
export default function LoginPage() {
const user = useAuthStore(s => s.user)
const login = useAuthStore(s => s.login)
const bindPhone = useAuthStore(s => s.bindPhone)
const isLoggedIn = useAuthStore(s => !!s.token && !!s.user)
const [submitting, setSubmitting] = useState(false)
const [binding, setBinding] = useState(false)
/** 登录成功但身份待绑定(type=0)时,引导绑定手机号 */
const [needsBind, setNeedsBind] = useState(false)
const isWeb = Taro.getEnv() === Taro.ENV_TYPE.WEB
@@ -28,17 +23,17 @@ export default function LoginPage() {
}
}, [])
/** 已登录且已绑定身份 → 返回;已登录未绑定 → 引导绑定手机号 */
useEffect(() => {
if (!isLoggedIn) return
if (user && user.type === 0) {
setNeedsBind(true)
} else {
goBack()
}
}, [isLoggedIn, user, goBack])
/** 前往注册页 */
const goRegister = useCallback(() => {
Taro.navigateTo({ url: '/pages/register/index' })
}, [])
/** 微信一键登录(wx.login code 换 openid,新用户自动注册) */
/** 已登录 → 自动返回 */
useEffect(() => {
if (isLoggedIn) goBack()
}, [isLoggedIn, goBack])
/** 微信一键登录(wx.login code 换 openid,仅已注册用户可登录) */
const handleLogin = useCallback(async () => {
if (submitting) return
// H5 环境无法获取微信登录凭证
@@ -54,45 +49,24 @@ export default function LoginPage() {
return
}
await login({ code: res.code })
// 登录结果(type=0 → needsBind,否则自动返回)由 effect 处理
} catch {
// 业务/网络错误已由 request 层提示
// 登录成功后由 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 {
setSubmitting(false)
}
}, [login, submitting, isWeb])
/** 微信手机号授权绑定(自动匹配门店/供应商) */
const handleGetPhoneNumber = useCallback(
async (e: any) => {
if (binding) 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
}
setBinding(true)
try {
await bindPhone(detail.code)
Taro.showToast({ title: '绑定成功', icon: 'success' })
// 绑定成功后 type 更新,由 effect 自动返回
} catch {
// 业务/网络错误已由 request 层提示
} finally {
setBinding(false)
}
},
[binding, bindPhone],
)
}, [login, submitting, isWeb, goRegister])
/** 查看用户协议 */
const handleShowAgreement = useCallback(() => {
@@ -107,7 +81,7 @@ export default function LoginPage() {
return (
<View className='login-page'>
{/* ========== 导航栏 ========== */}
<CustomNavBar title="登录" />
<CustomNavBar title='登录' />
{/* ========== 内容区域 ========== */}
<View className='login-content'>
@@ -127,26 +101,20 @@ export default function LoginPage() {
{/* 登录操作 */}
<View className='login-actions'>
{needsBind ? (
<Button
className={`login-btn ${binding ? 'login-btn--loading' : ''}`}
openType='getPhoneNumber'
onGetPhoneNumber={handleGetPhoneNumber}
loading={binding}
disabled={binding}
>
{binding ? '绑定中...' : '微信手机号授权绑定'}
</Button>
) : (
<Button
className={`login-btn ${submitting ? 'login-btn--loading' : ''}`}
onClick={handleLogin}
loading={submitting}
disabled={submitting}
>
{submitting ? '登录中...' : '微信一键登录'}
</Button>
)}
<Button
className={`login-btn ${submitting ? 'login-btn--loading' : ''}`}
onClick={handleLogin}
loading={submitting}
disabled={submitting}
>
{submitting ? '登录中...' : '微信一键登录'}
</Button>
{/* 未注册用户入口 */}
<View className='login-switch' onClick={goRegister}>
<Text className='switch-text'></Text>
<Text className='switch-link'></Text>
</View>
<View className='login-agreement'>
<Text className='agree-text'></Text>
+16 -2
View File
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Image } from '@tarojs/components'
import { Icon } from '@antmjs/vantui'
@@ -45,6 +45,20 @@ export default function ProfilePage() {
const loggedIn = !!token && !!user
/** 功能菜单(门店账号追加「门店信息」入口) */
const menuItems = useMemo(() => {
if (!user?.store) return MENU_ITEMS
return [
{
key: 'store-info',
label: '门店信息',
icon: 'shop-o',
onClick: () => Taro.navigateTo({ url: '/pages/store-info/index' }),
},
...MENU_ITEMS,
]
}, [user?.store])
useDidShow(() => {
if (!loggedIn) return
// 刷新用户信息(门店/客户等级可能变化)
@@ -157,7 +171,7 @@ export default function ProfilePage() {
{/* ========== 功能菜单 ========== */}
<View className='profile-section profile-section--menu'>
{MENU_ITEMS.map(item => (
{menuItems.map(item => (
<View key={item.key} className='menu-cell' onClick={item.onClick}>
<View className='menu-cell__left'>
<Icon name={item.icon} size={20} color='#ee0a24' />
+4
View File
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom',
navigationBarTitleText: '注册',
})
+196
View File
@@ -0,0 +1,196 @@
/* ========================================
注册页面
======================================== */
.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, #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;
}
}
/* ========== 注册表单 ========== */
.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: #e8f7ef;
color: #07c160;
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, #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;
}
}
.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: #1989fa;
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: #1989fa;
}
}
+222
View File
@@ -0,0 +1,222 @@
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>
)
}
+3 -87
View File
@@ -1,9 +1,6 @@
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Input } from '@tarojs/components'
import { Button, Popup } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import { updatePaymentCycleApi } from '@/services/store'
import { useCallback } from 'react'
import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import './index.less'
/**
@@ -11,38 +8,6 @@ import './index.less'
* 门店设置:回款周期;后续在此追加更多设置项
*/
export default function SettingsPage() {
const token = useAuthStore(s => s.token)
/** 回款周期弹层 */
const [showCycle, setShowCycle] = useState(false)
const [cycleInput, setCycleInput] = useState('1')
const [cycleLoading, setCycleLoading] = useState(false)
const loggedIn = !!token
useDidShow(() => {
// 预留:进入设置页时刷新门店设置
})
/** 保存回款周期 */
const handleSaveCycle = useCallback(async () => {
const days = Number(cycleInput)
if (!Number.isInteger(days) || days < 0) {
Taro.showToast({ title: '请输入不小于 0 的整数', icon: 'none' })
return
}
if (cycleLoading) return
setCycleLoading(true)
try {
await updatePaymentCycleApi(days)
Taro.showToast({ title: '回款周期已更新', icon: 'success' })
setShowCycle(false)
} catch {
// 错误已由 request 层 toast
} finally {
setCycleLoading(false)
}
}, [cycleInput, cycleLoading])
/** 占位菜单:后续单独页面开发 */
const handlePlaceholder = useCallback((label: string) => {
@@ -51,22 +16,6 @@ export default function SettingsPage() {
return (
<View className='settings-page'>
{/* ========== 门店设置 ========== */}
<View className='settings-section'>
<View className='settings-section__header'>
<Text className='settings-section__title'></Text>
</View>
<View className='setting-cell' onClick={() => setShowCycle(true)}>
<Text className='setting-cell__label'></Text>
<Text className='setting-cell__value'> </Text>
</View>
<View className='setting-cell' onClick={() => handlePlaceholder('收货信息')}>
<Text className='setting-cell__label'></Text>
<Text className='setting-cell__value'></Text>
</View>
</View>
{/* ========== 通用 ========== */}
<View className='settings-section'>
<View className='setting-cell' onClick={() => handlePlaceholder('清除缓存')}>
<Text className='setting-cell__label'></Text>
@@ -85,39 +34,6 @@ export default function SettingsPage() {
<Text className='setting-cell__value'></Text>
</View>
</View>
{/* ========== 回款周期弹层 ========== */}
<Popup
show={showCycle}
position='bottom'
round
closeable
closeOnClickOverlay
safeAreaInsetBottom
onClose={() => setShowCycle(false)}
>
<View className='cycle-popup'>
<Text className='cycle-popup__title'></Text>
<Text className='cycle-popup__desc'>0 = </Text>
<Input
className='cycle-popup__input'
type='number'
value={cycleInput}
placeholder='请输入回款周期天数'
onInput={e => setCycleInput(e.detail.value)}
/>
<Button
type='danger'
block
round
loading={cycleLoading}
className='cycle-popup__submit'
onClick={handleSaveCycle}
>
</Button>
</View>
</Popup>
</View>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '门店信息',
})
+87
View File
@@ -0,0 +1,87 @@
.store-info-page {
min-height: 100vh;
background: #f7f8fa;
padding: 20rpx 24rpx 60rpx;
box-sizing: border-box;
// ===== 区块 =====
.store-section {
background: #fff;
border-radius: 20rpx;
padding: 8rpx 28rpx;
margin-bottom: 20rpx;
&__header {
padding: 24rpx 0 8rpx;
border-bottom: 1rpx solid #f2f3f5;
}
&__title {
font-size: 30rpx;
font-weight: 600;
}
}
// ===== 表单行 =====
.store-field {
display: flex;
align-items: center;
padding: 28rpx 0;
border-bottom: 1rpx solid #f2f3f5;
&:last-child {
border-bottom: none;
}
&--textarea {
align-items: flex-start;
}
&__label {
width: 160rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #323233;
}
&__value {
flex: 1;
font-size: 28rpx;
color: #646566;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__input {
flex: 1;
font-size: 28rpx;
color: #323233;
}
&__textarea {
flex: 1;
min-height: 120rpx;
font-size: 28rpx;
color: #323233;
line-height: 1.5;
}
&__placeholder {
color: #c8c9cc;
}
}
// ===== 保存按钮 =====
.store-submit {
margin-top: 40rpx;
}
// ===== 加载 / 异常占位 =====
.store-placeholder {
padding: 160rpx 0;
text-align: center;
font-size: 28rpx;
color: #969799;
}
}
+174
View File
@@ -0,0 +1,174 @@
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, Input, Textarea } from '@tarojs/components'
import { Button } from '@antmjs/vantui'
import useAuthStore from '@/stores/auth/useAuthStore'
import { getStoreInfoApi, updateStoreInfoApi } from '@/services/store'
import type { StoreDetail } from '@/types/store'
import './index.less'
/** 字段长度限制(与后端一致) */
const LIMITS = {
contact: 50,
phone: 20,
address: 255,
} as const
/**
* 门店信息编辑页
* 名称 / 编码 / 回款周期为只读(后台维护),仅支持修改联系人 / 电话 / 地址
*/
export default function StoreInfoPage() {
const token = useAuthStore(s => s.token)
/** 门店详情(含只读字段) */
const [detail, setDetail] = useState<StoreDetail | null>(null)
const [loading, setLoading] = useState(true)
/** 可编辑字段 */
const [contact, setContact] = useState('')
const [phone, setPhone] = useState('')
const [address, setAddress] = useState('')
const [saving, setSaving] = useState(false)
const loggedIn = !!token
/** 拉取门店详情并回显 */
const fetchDetail = useCallback(() => {
setLoading(true)
getStoreInfoApi()
.then(res => {
setDetail(res.data)
setContact(res.data.contact || '')
setPhone(res.data.phone || '')
setAddress(res.data.address || '')
})
.catch(() => {
setDetail(null)
})
.finally(() => setLoading(false))
}, [])
useDidShow(() => {
if (!loggedIn) return
fetchDetail()
})
/** 保存:仅提交白名单字段(联系人 / 电话 / 地址) */
const handleSave = useCallback(async () => {
if (saving) return
if (contact.length > LIMITS.contact) {
Taro.showToast({ title: `联系人不能超过 ${LIMITS.contact} 个字符`, icon: 'none' })
return
}
if (phone.length > LIMITS.phone) {
Taro.showToast({ title: `联系电话不能超过 ${LIMITS.phone} 个字符`, icon: 'none' })
return
}
if (address.length > LIMITS.address) {
Taro.showToast({ title: `地址不能超过 ${LIMITS.address} 个字符`, icon: 'none' })
return
}
setSaving(true)
try {
await updateStoreInfoApi({
contact: contact.trim(),
phone: phone.trim(),
address: address.trim(),
})
Taro.showToast({ title: '门店信息已更新', icon: 'success' })
} catch {
// 错误已由 request 层 toast
} finally {
setSaving(false)
}
}, [contact, phone, address, saving])
return (
<View className='store-info-page'>
{!loggedIn ? (
<View className='store-placeholder'></View>
) : loading ? (
<View className='store-placeholder'>...</View>
) : !detail ? (
<View className='store-placeholder'></View>
) : (
<>
{/* ========== 基础信息(只读,后台维护) ========== */}
<View className='store-section'>
<View className='store-section__header'>
<Text className='store-section__title'></Text>
</View>
<View className='store-field'>
<Text className='store-field__label'></Text>
<Text className='store-field__value'>{detail.name || '-'}</Text>
</View>
<View className='store-field'>
<Text className='store-field__label'></Text>
<Text className='store-field__value'>{detail.code || '-'}</Text>
</View>
<View className='store-field'>
<Text className='store-field__label'></Text>
<Text className='store-field__value'>{detail.payment_cycle_days} </Text>
</View>
</View>
{/* ========== 联系信息(可编辑) ========== */}
<View className='store-section'>
<View className='store-section__header'>
<Text className='store-section__title'></Text>
</View>
<View className='store-field'>
<Text className='store-field__label'></Text>
<Input
className='store-field__input'
type='text'
value={contact}
maxlength={LIMITS.contact}
placeholder='请输入联系人'
placeholderClass='store-field__placeholder'
onInput={e => setContact(e.detail.value)}
/>
</View>
<View className='store-field'>
<Text className='store-field__label'></Text>
<Input
className='store-field__input'
type='text'
value={phone}
maxlength={LIMITS.phone}
placeholder='请输入联系电话'
placeholderClass='store-field__placeholder'
onInput={e => setPhone(e.detail.value)}
/>
</View>
<View className='store-field store-field--textarea'>
<Text className='store-field__label'></Text>
<Textarea
className='store-field__textarea'
value={address}
maxlength={LIMITS.address}
placeholder='请输入地址'
placeholderClass='store-field__placeholder'
autoHeight
onInput={e => setAddress(e.detail.value)}
/>
</View>
</View>
{/* ========== 保存 ========== */}
<Button
type='danger'
block
round
loading={saving}
className='store-submit'
onClick={handleSave}
>
</Button>
</>
)}
</View>
)
}
+11 -7
View File
@@ -7,26 +7,30 @@ export interface WxLoginParams {
code: string
}
/** 绑定手机号参数 */
export interface BindPhoneParams {
/** 微信注册参数 */
export interface RegisterParams {
/** wx.login 的临时凭证 */
code: string
/** wx.getPhoneNumber 授权得到的 code */
phoneCode: string
/** 门店编码(后台门店管理维护) */
storeCode: string
}
/** 登录 / 绑定手机号返回 */
/** 登录 / 注册返回 */
export interface AuthResult {
token: string
user: User
}
/** 微信登录(自动注册):POST /mini/auth/login */
/** 微信登录(仅已注册用户可登录):POST /mini/auth/login */
export function wxLoginApi(params: WxLoginParams) {
return post<AuthResult>('/mini/auth/login', params)
}
/** 绑定手机号(自动匹配门店/供应商):POST /mini/auth/phone */
export function bindPhoneApi(params: BindPhoneParams) {
return post<AuthResult>('/mini/auth/phone', params)
/** 微信注册(code 换 openid + phoneCode 换手机号 + storeCode 绑定门店):POST /mini/auth/register */
export function registerApi(params: RegisterParams) {
return post<AuthResult>('/mini/auth/register', params)
}
/** 当前用户信息(含门店客户等级):GET /mini/auth/info */
+12 -1
View File
@@ -1,4 +1,5 @@
import { put } from '@/utils/request'
import { get, put } from '@/utils/request'
import type { StoreDetail, UpdateStoreInfoParams } from '@/types/store'
/** 修改回款周期(≥0,无上限;0 = 当天结算):PUT /mini/store/paymentCycle */
export function updatePaymentCycleApi(payment_cycle_days: number) {
@@ -6,3 +7,13 @@ export function updatePaymentCycleApi(payment_cycle_days: number) {
payment_cycle_days,
})
}
/** 门店详情(编辑页回显):GET /mini/store/info */
export function getStoreInfoApi() {
return get<StoreDetail>('/mini/store/info')
}
/** 修改门店信息(白名单:联系人 / 电话 / 地址):PUT /mini/store/info */
export function updateStoreInfoApi(data: UpdateStoreInfoParams) {
return put<UpdateStoreInfoParams>('/mini/store/info', data)
}
+20 -20
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand'
import Taro from '@tarojs/taro'
import { bindPhoneApi, wxLoginApi } from '@/services/auth'
import type { WxLoginParams } from '@/services/auth'
import { registerApi, wxLoginApi } from '@/services/auth'
import type { RegisterParams, WxLoginParams } from '@/services/auth'
import type { User } from '@/types/user'
/** 存储 key */
@@ -26,13 +26,23 @@ function loadFromStorage(): { user: User | null; token: string | null } {
return { user: null, token: null }
}
/** 登录 / 注册成功后持久化 token 与用户信息 */
function persistAuth(token: string, user: User): void {
try {
Taro.setStorageSync(STORAGE_KEYS.TOKEN, token)
Taro.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(user))
} catch {
// storage 写入失败不阻塞登录流程
}
}
interface AuthState {
user: User | null
token: string | null
loading: boolean
login: (params: WxLoginParams) => Promise<void>
/** 微信手机号授权码绑定手机号(自动匹配门店/供应商 */
bindPhone: (phoneCode: string) => Promise<void>
/** 微信注册(手机号授权 + 门店编码绑定门店 */
register: (params: RegisterParams) => Promise<void>
logout: () => void
/** 更新用户信息(用于编辑资料后同步 store) */
setUser: (user: User) => void
@@ -47,30 +57,20 @@ const useAuthStore = create<AuthState>((set) => {
token: initial.token,
loading: !!(initial.token && initial.user), // 已恢复则立即 ready
/** 登录 */
/** 登录(仅已注册用户可登录,未注册由页面引导去注册) */
login: async (params: WxLoginParams) => {
const res = await wxLoginApi(params)
const { token, user } = res.data
set({ user, token })
try {
Taro.setStorageSync(STORAGE_KEYS.TOKEN, token)
Taro.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(user))
} catch {
// storage 写入失败不阻塞登录流程
}
persistAuth(token, user)
},
/** 绑定手机号POST /mini/auth/phone(登录后 type=0 待绑定时调用) */
bindPhone: async (phoneCode: string) => {
const res = await bindPhoneApi({ phoneCode })
/** 注册POST /mini/auth/register */
register: async (params: RegisterParams) => {
const res = await registerApi(params)
const { token, user } = res.data
set({ user, token })
try {
Taro.setStorageSync(STORAGE_KEYS.TOKEN, token)
Taro.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(user))
} catch {
// storage 写入失败不阻塞绑定流程
}
persistAuth(token, user)
},
/** 退出登录 */
+23
View File
@@ -0,0 +1,23 @@
/** 门店详情(GET /mini/store/info 返回,编辑页回显用) */
export interface StoreDetail {
id: number
/** 门店名称(只读,后台维护) */
name: string
/** 门店编码(只读) */
code: string
/** 联系人 */
contact: string
/** 联系电话 */
phone: string
/** 地址 */
address: string
/** 回款周期天数(只读,后台维护) */
payment_cycle_days: number
}
/** 修改门店信息参数(白名单:仅联系人 / 电话 / 地址) */
export interface UpdateStoreInfoParams {
contact?: string
phone?: string
address?: string
}
+22 -4
View File
@@ -27,13 +27,31 @@ export const USER_TYPE_MAP: Record<UserType, string> = {
2: '供应商',
}
/** 用户信息 */
/** 用户信息user 表实际返回字段) */
export interface User {
id: number
/** 用户名(注册时生成 wx_xxxx */
username: string
/** 昵称(注册默认「微信用户」) */
nickname: string
avatar: string
/** 手机号(未绑定为空) */
phone: string
type: UserType
store: StoreInfo | null
supplier: SupplierInfo | null
/** 绑定门店ID(0 未绑定) */
store_id: number
/** 1 正常 / 0 停用 */
status: number
/** 微信标识 */
openid: string
unionid: string
email: string
last_login_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
}