门店信息
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '注册',
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '门店信息',
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
/** 退出登录 */
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
-545
@@ -1,545 +0,0 @@
|
||||
# 订货采购系统 · 小程序端 API 文档
|
||||
|
||||
> 版本:V1.0 更新日期:2026-08-06
|
||||
> 适用:微信小程序门店端 / 供应商端;接口由后端 `app/Http/Controllers/Mini/` 提供(Laravel 12 + Sanctum)。
|
||||
|
||||
## 1. 通用说明
|
||||
|
||||
### 1.1 基础信息
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| Base URL | `http://localhost:8000`(生产域名待定,通常为 HTTPS) |
|
||||
| 数据格式 | JSON(请求/响应均 `Content-Type: application/json`) |
|
||||
| 金额字段 | 后端统一 `decimal` 字符串返回(如 `"13.00"`),下单/购物车金额**一律服务端重算**,前端传的金额字段会被忽略 |
|
||||
|
||||
### 1.2 认证方式
|
||||
|
||||
除「登录」接口外,全部接口需携带 `Authorization: Bearer <token>`(登录接口返回的 token,Sanctum plainTextToken)。
|
||||
|
||||
```http
|
||||
Authorization: Bearer 1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
token 附带 `abilities: ["mini"]` 仅作来源标记;后端按 `users` guard 解析用户。
|
||||
|
||||
### 1.3 统一响应格式
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{ "success": true, "data": { ... } }
|
||||
```
|
||||
|
||||
成功带提示:
|
||||
|
||||
```json
|
||||
{ "success": true, "data": { ... }, "msg": "下单成功" }
|
||||
```
|
||||
|
||||
失败(业务错误/验证错误均返回 HTTP 200,`success=false`):
|
||||
|
||||
```json
|
||||
{ "success": false, "msg": "尚未绑定门店,请联系客服处理", "showType": 1 }
|
||||
```
|
||||
|
||||
分页数据统一结构(`data` 字段内):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"data": [ ... ],
|
||||
"total": 35,
|
||||
"pageSize": 10,
|
||||
"current": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 角色前置校验
|
||||
|
||||
| 接口域 | 前置要求 | 未满足时提示 |
|
||||
|--------|----------|--------------|
|
||||
| 商品/购物车/订单/对账单/门店设置 | 用户 `type=门店(1)` 且已绑定正常门店 | 「尚未绑定门店,请联系客服处理」 |
|
||||
| 供应商采购单 | 用户 `type=供应商(2)` 且已绑定正常供应商 | 「尚未绑定供应商,请联系客服处理」 |
|
||||
| 商品价格展示 | 门店已设置客户等级(`store.level_id > 0`) | 「门店未设置客户等级,无法展示价格,请联系客服」 |
|
||||
|
||||
> 登录后未绑定身份的用户 `type=0`(待绑定):可通过绑定手机号自动匹配门店/供应商,或由后台人工绑定。
|
||||
|
||||
### 1.5 价格体系
|
||||
|
||||
- 商品价格按「门店客户等级」展示,同一商品不同等级价格不同
|
||||
- 等级价格支持两种计价类型:
|
||||
- **固定价**(`price_type=0`):直接存储实际单价
|
||||
- **成本百分比**(`price_type=1`):实际价 = 成本价 × (100 + 上浮百分点) / 100
|
||||
- 小程序端接口返回的 `price` 均为**换算后的实际价**;成本价为商业敏感数据,**不会**下发到小程序端
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证
|
||||
|
||||
### 2.1 微信登录(自动注册)
|
||||
|
||||
`POST /mini/auth/login`
|
||||
|
||||
用 `wx.login()` 获取的 code 换 openid,已注册用户直接登录,新用户自动注册并返回 token。
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| code | string | 是 | `wx.login` 的临时凭证 |
|
||||
|
||||
响应(`data`):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| token | string | Bearer 令牌(后续请求头携带) |
|
||||
| user.id | int | 用户ID |
|
||||
| user.nickname | string | 昵称 |
|
||||
| user.avatar | string | 头像 |
|
||||
| user.phone | string | 手机号(未绑定为空) |
|
||||
| user.type | int | 0 待绑定 / 1 门店 / 2 供应商 |
|
||||
| user.store | object\|null | 绑定门店信息(含 `level`:客户等级 `{id,name}`) |
|
||||
| user.supplier | object\|null | 绑定供应商信息 |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "1|abc...",
|
||||
"user": {
|
||||
"id": 5, "nickname": "微信用户1", "avatar": "", "phone": "",
|
||||
"type": 1,
|
||||
"store": { "id": 2, "name": "菜市场A店", "level": { "id": 1, "name": "一级客户" } },
|
||||
"supplier": null
|
||||
}
|
||||
},
|
||||
"msg": "登录成功"
|
||||
}
|
||||
```
|
||||
|
||||
错误:`code` 缺失 → 「缺少登录凭证 code」;账号被停用 → 「账号已被停用,请联系客服」。
|
||||
|
||||
### 2.2 绑定手机号
|
||||
|
||||
`POST /mini/auth/phone`(需登录)
|
||||
|
||||
用微信手机号授权码换手机号,并按手机号自动匹配门店/供应商(均未命中则保持待绑定,由后台处理)。
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| phoneCode | string | 是 | `wx.getPhoneNumber` 授权得到的 code |
|
||||
|
||||
响应(`data`):`user` 结构同 2.1。
|
||||
|
||||
### 2.3 当前用户信息
|
||||
|
||||
`GET /mini/auth/info`(需登录)
|
||||
|
||||
响应(`data`):`user` 结构同 2.1(含门店客户等级——小程序全局价格体系的依据)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 商品
|
||||
|
||||
### 3.1 商品分类树
|
||||
|
||||
`GET /mini/product/categories`(需登录 + 门店)
|
||||
|
||||
返回分类树,**仅包含有上架商品的分类及其全部祖先**(保证树结构完整)。
|
||||
|
||||
响应(`data`):分类树数组,节点字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 分类ID |
|
||||
| parent_id | int | 父级分类ID(0 为顶级) |
|
||||
| name | string | 分类名称 |
|
||||
| children | array | 子分类(递归) |
|
||||
|
||||
### 3.2 商品列表
|
||||
|
||||
`GET /mini/product/list?category_id=&keyword=&page=&pageSize=`(需登录 + 门店 + 客户等级)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| category_id | int | 否 | - | 分类ID过滤 |
|
||||
| keyword | string | 否 | - | 搜索品名/规格(模糊) |
|
||||
| page | int | 否 | 1 | 页码 |
|
||||
| pageSize | int | 否 | 10 | 每页条数 |
|
||||
|
||||
响应(`data` 为分页结构),每项字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 商品ID |
|
||||
| category_id | int | 分类ID |
|
||||
| supplier_id | int | 默认供应商ID |
|
||||
| name | string | 品名 |
|
||||
| spec | string | 规格/包规 |
|
||||
| unit | string | 计价单位 |
|
||||
| content | string | 商品图文详情(HTML) |
|
||||
| **price** | string\|null | **当前门店等级的实际销售价**(未设等级价为 null) |
|
||||
| images_arr | array | 商品图片数组(`{id, file_url, ...}`) |
|
||||
| sort / shelf_life / stock / status | - | 排序 / 保质期 / 库存 / 状态(仅返回上架商品) |
|
||||
|
||||
> 注意:只返回上架商品;`cost_price`、计价类型、上浮百分点等成本信息不会下发。
|
||||
|
||||
---
|
||||
|
||||
## 4. 购物车
|
||||
|
||||
> 购物车为下单前的编辑容器,同商品重复加购自动合并数量;提交订单复用「5.1 下单」接口。
|
||||
|
||||
### 4.1 加购
|
||||
|
||||
`POST /mini/cart`(需登录 + 门店 + 客户等级)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| product_id | int | 是 | 商品ID(须上架且已设本等级价格) |
|
||||
| quantity | number | 是 | 数量(>0,最多 99999999.99) |
|
||||
|
||||
响应(`data`):
|
||||
|
||||
```json
|
||||
{ "id": 12, "quantity": "2.50" }
|
||||
```
|
||||
|
||||
提示:`已加入购物车`。错误:商品未设本等级价格 → 「商品「xx」未设置您所在等级的价格,无法加购」;超上限 → 「该商品在购物车中的数量已达上限」。
|
||||
|
||||
### 4.2 购物车列表
|
||||
|
||||
`GET /mini/cart`(需登录 + 门店)
|
||||
|
||||
响应(`data`):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| items | array | 购物车项(倒序) |
|
||||
| total_count | int | 总项数 |
|
||||
| total_quantity | string | 可购项总数量 |
|
||||
| total_amount | string | 可购项总金额 |
|
||||
|
||||
items 每项:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 购物车项ID |
|
||||
| product_id | int | 商品ID |
|
||||
| name / spec / unit | string | 商品快照 |
|
||||
| image | string | 商品首图 URL |
|
||||
| **price** | string\|null | **当前等级实际价**(商品下架或未设等级价为 null) |
|
||||
| quantity | string | 数量 |
|
||||
| amount | string\|null | 金额 = price × quantity(不可购为 null) |
|
||||
| status | int | 1 可购 / 0 商品下架、缺失或未设等级价 |
|
||||
|
||||
### 4.3 修改数量
|
||||
|
||||
`PUT /mini/cart/{id}`(需登录)
|
||||
|
||||
请求参数:`quantity`(number,必填,>0)。
|
||||
|
||||
响应:`{ id, quantity }`,提示「已修改数量」。
|
||||
|
||||
### 4.4 删除单项
|
||||
|
||||
`DELETE /mini/cart/{id}`(需登录)
|
||||
|
||||
响应:`success=true`,提示「已删除」;不存在 → 「购物车项不存在」。
|
||||
|
||||
### 4.5 清空购物车
|
||||
|
||||
`DELETE /mini/cart`(需登录)
|
||||
|
||||
仅清空当前用户;响应:`success=true`,提示「购物车已清空」。
|
||||
|
||||
---
|
||||
|
||||
## 5. 门店订单
|
||||
|
||||
### 5.1 下单
|
||||
|
||||
`POST /mini/order`(需登录 + 门店 + 客户等级)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| items | array | 是 | 订单明细(至少 1 行) |
|
||||
| items[].product_id | int | 是 | 商品ID(须上架) |
|
||||
| items[].quantity | number | 是 | 数量(>0) |
|
||||
| remark | string | 否 | 订单备注(≤255 字符) |
|
||||
|
||||
> 金额不接受前端传入:服务端按商品当前等级**实际价**逐行快照并重算 `amount` 与 `total_amount`。
|
||||
|
||||
响应(`data`):
|
||||
|
||||
```json
|
||||
{ "id": 23, "order_no": "SO202608060001", "total_amount": "39.00" }
|
||||
```
|
||||
|
||||
提示:「下单成功」。错误示例:存在已下架商品 → 「存在已下架或不存在的商品,请刷新后重试」;未设等级价 → 「商品「xx」未设置您所在等级的价格,无法下单」。
|
||||
|
||||
### 5.2 历史订单
|
||||
|
||||
`GET /mini/order?status=&page=&pageSize=`(需登录 + 门店,强制本店隔离)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| status | int | 否 | - | 0 待汇总 / 1 已汇总 / 2 配送中 / 3 已完成 / 9 已取消 |
|
||||
| page / pageSize | - | 否 | 1 / 10 | 分页 |
|
||||
|
||||
响应(`data` 分页结构),订单字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 订单ID |
|
||||
| order_no | string | 单号(SO + 日期 + 序列) |
|
||||
| order_date | string | 订货日期(Y-m-d) |
|
||||
| total_quantity / total_amount | string | 总数量 / 总金额 |
|
||||
| status | int | 状态(见上) |
|
||||
| remark | string | 备注 |
|
||||
|
||||
### 5.3 周期汇总
|
||||
|
||||
`GET /mini/order/summary?period=day|week|month`(需登录 + 门店)
|
||||
|
||||
请求参数:`period`(day/week/month,默认 month)。
|
||||
|
||||
响应(`data`):
|
||||
|
||||
```json
|
||||
{
|
||||
"period": "month",
|
||||
"groups": [
|
||||
{ "period_label": "2026-07", "total_amount": "1280.50", "total_quantity": "86.00", "order_count": 12 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> `period_label` 格式:day=`Y-m-d`、week=`Y-W+周数`、month=`Y-m`;不含已取消订单;最多返回 50 组。
|
||||
|
||||
### 5.4 订单详情
|
||||
|
||||
`GET /mini/order/{id}`(需登录 + 门店,校验本店归属)
|
||||
|
||||
响应(`data`):订单对象 + `items` 数组(明细字段见下表)。
|
||||
|
||||
明细字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id / order_id | int | 明细ID / 订单ID |
|
||||
| product_id / product_name / product_spec | - | 商品快照 |
|
||||
| price | string | 下单时等级实际价快照 |
|
||||
| quantity / weight | string | 数量 / 称重(默认 0) |
|
||||
| amount | string | 金额 = price × quantity |
|
||||
| remark | string | 行备注 |
|
||||
|
||||
### 5.5 取消订单
|
||||
|
||||
`PUT /mini/order/{id}/cancel`(需登录 + 门店)
|
||||
|
||||
仅「待汇总(0)」可取消;响应提示「订单已取消」;非待汇总 → 「仅待汇总的订单可以取消」。
|
||||
|
||||
---
|
||||
|
||||
## 6. 对账单(门店自助)
|
||||
|
||||
### 6.1 对账单列表
|
||||
|
||||
`GET /mini/statement?page=&pageSize=`(需登录 + 门店,仅本店)
|
||||
|
||||
响应(`data` 分页结构),对账单字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 对账单ID |
|
||||
| statement_no | string | 单号(ST + 日期 + 序列) |
|
||||
| period_start / period_end | string | 对账周期 |
|
||||
| total_amount | string | 总金额 |
|
||||
| payment_cycle_days | int | 生成时快照的回款周期 |
|
||||
| settlement_date | string\|null | 应结算日期 = 周期结束 + 回款周期天 |
|
||||
| status | int | 0 待对账 / 1 已对账 / 2 已结算 |
|
||||
| reconciled_at / settled_at | string\|null | 对账 / 结算时间 |
|
||||
| remark | string | 备注 |
|
||||
|
||||
### 6.2 生成对账单
|
||||
|
||||
`POST /mini/statement/generate`(需登录 + 门店)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| period_start | string | 是 | 周期开始(Y-m-d) |
|
||||
| period_end | string | 是 | 周期结束(Y-m-d,不早于开始) |
|
||||
|
||||
响应(`data`):`{ id, statement_no, total_amount, settlement_date }`,提示「对账单已生成」。
|
||||
|
||||
> 快照当前回款周期计算结算日期。业务约束:周期内本店无订单 → 「周期内本店无订单数据,无法生成对账单」;周期内订单均已生成过对账单 → 「周期内的订单明细均已生成过对账单」。
|
||||
|
||||
### 6.3 对账单详情
|
||||
|
||||
`GET /mini/statement/{id}`(需登录 + 门店,校验归属)
|
||||
|
||||
响应(`data`):对账单对象 + `items` 数组,明细字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| order_id / order_item_id | int | 源订单 / 源明细ID |
|
||||
| product_id / product_name | - | 商品快照 |
|
||||
| price | string | 单价 |
|
||||
| quantity / weight / amount | string | 数量 / 称重 / 金额 |
|
||||
| is_reconciled | int | 0 未对账 / 1 已对账 |
|
||||
| store_remark | string | 门店备注 |
|
||||
|
||||
### 6.4 导出对账单
|
||||
|
||||
`GET /mini/statement/{id}/export?format=xlsx|pdf`(需登录 + 门店,校验归属)
|
||||
|
||||
- `format` 默认 `xlsx`(支持 `xlsx` / `pdf`)
|
||||
- 返回文件流(附件下载,含中文文件名),非 JSON
|
||||
|
||||
---
|
||||
|
||||
## 7. 门店设置
|
||||
|
||||
### 7.1 修改回款周期
|
||||
|
||||
`PUT /mini/store/paymentCycle`(需登录 + 门店)
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| payment_cycle_days | int | 是 | 回款周期天数(≥0,无上限;0 = 当天结算) |
|
||||
|
||||
响应(`data`):`{ "payment_cycle_days": 1 }`,提示「回款周期已更新」。
|
||||
|
||||
> 该值影响后续生成对账单的 `settlement_date`(周期结束 + 回款周期天)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 通知
|
||||
|
||||
### 8.1 通知列表
|
||||
|
||||
`GET /mini/notice?page=&pageSize=`(需登录)
|
||||
|
||||
返回本人通知 + 全员广播(本人已读的广播自动隐藏)。响应(`data` 分页结构 + 附加字段):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| unread_count | int | 未读总数 |
|
||||
| 分页内字段 | - | 标准分页结构 |
|
||||
|
||||
通知字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 通知ID |
|
||||
| type | string | `order` 订单 / `price` 价格变更 / `system` 系统 |
|
||||
| title / content | string | 标题 / 内容 |
|
||||
| data | object | 附加数据(价格变更通知含 `product_ids`、`level_ids`) |
|
||||
| is_read | int | 0 未读 / 1 已读 |
|
||||
| read_at | string\|null | 已读时间 |
|
||||
|
||||
### 8.2 标记已读
|
||||
|
||||
`PUT /mini/notice/{id}/read`(需登录)
|
||||
|
||||
- 个人通知:直接标记已读
|
||||
- 全员广播:复制一条本人专属已读记录(原广播对他人仍为未读)
|
||||
|
||||
响应:`success=true`;通知不存在 → 「通知不存在」。
|
||||
|
||||
---
|
||||
|
||||
## 9. 供应商端
|
||||
|
||||
### 9.1 收到的采购单
|
||||
|
||||
`GET /mini/supplier/purchases?page=&pageSize=`(需登录 + 供应商)
|
||||
|
||||
返回**含本供应商已发送明细**(`is_sent=1`)的采购单(去重,按日期倒序)。
|
||||
|
||||
响应(`data` 分页结构),采购单字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 采购单ID |
|
||||
| purchase_no | string | 单号(PO + 日期 + 序列) |
|
||||
| purchase_date | string | 采购日期 |
|
||||
| status | int | 0 待发送 / 1 部分发送 / 2 全部发送 / 3 已完成 |
|
||||
| total_quantity / estimate_amount / actual_amount | string | 总数量 / 估算金额 / 实际金额 |
|
||||
| remark | string | 备注 |
|
||||
|
||||
### 9.2 采购单明细
|
||||
|
||||
`GET /mini/supplier/purchases/{id}`(需登录 + 供应商)
|
||||
|
||||
仅返回**本供应商且已发送**的明细行。响应(`data`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3, "purchase_no": "PO202608060001", "purchase_date": "2026-08-06", "remark": "",
|
||||
"items": [
|
||||
{ "id": 11, "product_id": 2, "product_name": "大白菜", "product_spec": "10斤/箱",
|
||||
"price": "4.00", "quantity": "10.00", "weight": "0.000", "amount": "40.00",
|
||||
"sort": 1, "is_sent": 1, "sent_at": "...", "supplier_confirmed_at": null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
无本供应商明细 → 「该采购单无贵司的采购明细」。
|
||||
|
||||
### 9.3 确认接单
|
||||
|
||||
`PUT /mini/supplier/purchases/{id}/confirm`(需登录 + 供应商)
|
||||
|
||||
批量记录本供应商全部已发送明细的 `supplier_confirmed_at`(幂等,已确认的跳过)。
|
||||
|
||||
响应(`data`):`{ "confirmed": 2 }`(本次新确认条数),提示「已确认接单」。
|
||||
|
||||
---
|
||||
|
||||
## 10. 状态字典汇总
|
||||
|
||||
| 枚举 | 值 | 含义 |
|
||||
|------|-----|------|
|
||||
| 用户类型 user.type | 0 / 1 / 2 | 待绑定 / 门店 / 供应商 |
|
||||
| 门店订单 status | 0 / 1 / 2 / 3 / 9 | 待汇总 / 已汇总 / 配送中 / 已完成 / 已取消 |
|
||||
| 采购单 status | 0 / 1 / 2 / 3 | 待发送 / 部分发送 / 全部发送 / 已完成 |
|
||||
| 采购明细 is_sent | 0 / 1 | 未发送 / 已发送 |
|
||||
| 对账单 status | 0 / 1 / 2 | 待对账 / 已对账 / 已结算 |
|
||||
| 对账明细 is_reconciled | 0 / 1 | 未对账 / 已对账 |
|
||||
| 通知 type | order / price / system | 订单 / 价格变更 / 系统 |
|
||||
| 通知 is_read | 0 / 1 | 未读 / 已读 |
|
||||
| 商品 status | 0 / 1 | 下架 / 上架 |
|
||||
| 门店/供应商 status | 0 / 1 | 停用 / 正常 |
|
||||
|
||||
## 11. 常见错误提示
|
||||
|
||||
| 提示语 | 触发场景 |
|
||||
|--------|----------|
|
||||
| 尚未绑定门店,请联系客服处理 | 门店端接口但用户未绑定门店 |
|
||||
| 尚未绑定供应商,请联系客服处理 | 供应商端接口但用户未绑定供应商 |
|
||||
| 门店未设置客户等级,无法展示价格,请联系客服 | 门店 `level_id=0`(商品/购物车/下单) |
|
||||
| 商品「xx」未设置您所在等级的价格,无法下单 | 下单商品缺本等级价格 |
|
||||
| 存在已下架或不存在的商品,请刷新后重试 | 下单商品已下架 |
|
||||
| 账号不存在或已被停用 | token 用户被停用 |
|
||||
| 账号已被停用,请联系客服 | 登录时账号被停用 |
|
||||
Reference in New Issue
Block a user