Compare commits
4 Commits
3bd26acbb9
...
8a7bdca6b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a7bdca6b1 | |||
| 69b72a9c1c | |||
| ffe983b9da | |||
| f0d8f6bac4 |
+2
-1
@@ -7,6 +7,7 @@ export default defineAppConfig({
|
||||
'pages/message/index',
|
||||
'pages/profile/index',
|
||||
'pages/order-list/index',
|
||||
'pages/report/index',
|
||||
'pages/bill/index',
|
||||
'pages/bill-detail/index',
|
||||
'pages/payment/index',
|
||||
@@ -14,7 +15,7 @@ export default defineAppConfig({
|
||||
'pages/payment-detail/index',
|
||||
'pages/settings/index',
|
||||
'pages/login/index',
|
||||
'pages/register/index',
|
||||
'pages/change-password/index',
|
||||
'pages/store-info/index',
|
||||
],
|
||||
window: {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// ===== 零售价标注:小字置灰 =====
|
||||
.price-text__retail {
|
||||
font-size: 20rpx;
|
||||
color: #969799;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// inline 模式:与大价格同行,左间距分隔
|
||||
.price-text__retail--inline {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
// block 模式:大价格 / 零售价上下两行(窄卡片布局)
|
||||
.price-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Text, View } from '@tarojs/components'
|
||||
import { formatRetailPrice } from '@/utils/format'
|
||||
import './index.less'
|
||||
|
||||
interface PriceTextProps {
|
||||
/** 售价(展示为 ¥price) */
|
||||
price: string | number
|
||||
/** 包规(用于计算零售价;无法计算时不展示零售价) */
|
||||
spec?: string | number | null
|
||||
/** 大价格样式类(字号 / 颜色由调用方控制) */
|
||||
className?: string
|
||||
/**
|
||||
* 零售价布局:
|
||||
* - inline 跟随大价格同行(宽裕区域:商品详情、各类弹层行)
|
||||
* - block 独占一行(窄卡片:首页推荐、商品列表、购物车)
|
||||
*/
|
||||
mode?: 'inline' | 'block'
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品价格:售价 + 零售价标注(小字)
|
||||
* price=30、spec=15 → ¥30 零售价:¥2
|
||||
*/
|
||||
export default function PriceText({ price, spec, className, mode = 'inline' }: PriceTextProps) {
|
||||
const retail = formatRetailPrice(price, spec)
|
||||
|
||||
// 窄卡片:大价格 / 零售价上下两行,避免与右侧按钮(+/步进器)挤压换行
|
||||
if (mode === 'block') {
|
||||
return (
|
||||
<View className={`price-text ${className ?? ''}`}>
|
||||
<Text className='price-text__main'>¥{price}</Text>
|
||||
{retail !== null && (
|
||||
<Text className='price-text__retail'>零售价:¥{retail}</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text className={className}>
|
||||
¥{price}
|
||||
{retail !== null && (
|
||||
<Text className='price-text__retail price-text__retail--inline'>零售价:¥{retail}</Text>
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { Empty, Popup } from '@antmjs/vantui'
|
||||
import { getBillDetailApi } from '@/services/bill'
|
||||
import { getOrderDetailApi } from '@/services/order'
|
||||
import { ORDER_STATUS_TEXT } from '@/types/order'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import { formatSpec, resolveFileUrl } from '@/utils/format'
|
||||
import PriceText from '@/components/PriceText'
|
||||
import type { OrderDetail } from '@/types/order'
|
||||
import type { BillDetail } from '@/services/bill'
|
||||
import './index.less'
|
||||
@@ -161,7 +162,8 @@ export default function BillDetailPage() {
|
||||
<View className='bill-goods__main'>
|
||||
<Text className='bill-goods__name'>{item.product_name}</Text>
|
||||
<Text className='bill-goods__spec'>
|
||||
{item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit}
|
||||
{formatSpec(item.product_spec, item.unit)}{' '}
|
||||
<PriceText price={item.price} spec={item.product_spec} />
|
||||
</Text>
|
||||
</View>
|
||||
<View className='bill-goods__side'>
|
||||
@@ -214,7 +216,8 @@ export default function BillDetailPage() {
|
||||
<View className='order-popup__item-info'>
|
||||
<Text className='order-popup__item-name'>{item.product_name}</Text>
|
||||
<Text className='order-popup__item-spec'>
|
||||
{item.product_spec ? `${item.product_spec} ` : ''}¥{item.price}/{item.unit} × {item.quantity}
|
||||
{formatSpec(item.product_spec, item.unit)}{' '}
|
||||
<PriceText price={item.price} spec={item.product_spec} /> × {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='order-popup__item-amount'>¥{item.amount}</Text>
|
||||
|
||||
@@ -5,6 +5,8 @@ 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 PriceText from '@/components/PriceText'
|
||||
import { formatSpec } from '@/utils/format'
|
||||
import type { CartItem } from '@/types/cart'
|
||||
import type { StoreDetail } from '@/types/store'
|
||||
import './index.less'
|
||||
@@ -205,10 +207,10 @@ export default function CartPage() {
|
||||
<Text className='cart-item__name'>{item.name}</Text>
|
||||
{item.status === 0 && <Text className='cart-item__invalid-tag'>已失效</Text>}
|
||||
</View>
|
||||
<Text className='cart-item__spec'>{item.spec} / {item.unit}</Text>
|
||||
<Text className='cart-item__spec'>{formatSpec(item.spec, item.unit)}</Text>
|
||||
<View className='cart-item__bottom'>
|
||||
{item.price !== null ? (
|
||||
<Text className='cart-item__price'>¥{item.price}</Text>
|
||||
<PriceText className='cart-item__price' price={item.price} spec={item.spec} mode='block' />
|
||||
) : (
|
||||
<Text className='cart-item__price cart-item__price--none'>价格待定</Text>
|
||||
)}
|
||||
@@ -302,7 +304,7 @@ export default function CartPage() {
|
||||
<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>
|
||||
<Text className='order-popup__item-spec'>{formatSpec(item.spec, item.unit)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='order-popup__item-right'>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '修改密码',
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import type { HomeConfig } from '@/services/home'
|
||||
import { getProductListApi } from '@/services/product'
|
||||
import { getProductCover } from '@/types/product'
|
||||
import type { Product } from '@/types/product'
|
||||
import PriceText from '@/components/PriceText'
|
||||
import { formatSpec } from '@/utils/format'
|
||||
import './index.less'
|
||||
|
||||
/** 首页 → 商品页的本地存储传参 key(switchTab 无法带参) */
|
||||
@@ -247,10 +249,10 @@ export default function IndexPage() {
|
||||
/>
|
||||
<View className='product-card__info'>
|
||||
<Text className='product-card__name'>{product.name}</Text>
|
||||
<Text className='product-card__spec'>{product.spec} / {product.unit}</Text>
|
||||
<Text className='product-card__spec'>{formatSpec(product.spec, product.unit)}</Text>
|
||||
<View className='product-card__bottom'>
|
||||
{product.price !== null ? (
|
||||
<Text className='product-card__price'>¥{product.price}</Text>
|
||||
<PriceText className='product-card__price' price={product.price} spec={product.spec} mode='block' />
|
||||
) : (
|
||||
<Text className='product-card__price product-card__price--none'>登陆后查看价格</Text>
|
||||
)}
|
||||
|
||||
+38
-17
@@ -100,17 +100,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 功能介绍 ========== */
|
||||
.login-features {
|
||||
margin-bottom: 80px;
|
||||
/* ========== 登录表单 ========== */
|
||||
.login-form {
|
||||
width: 100%;
|
||||
background: #f7f8fa;
|
||||
border-radius: 24px;
|
||||
padding: 0 32px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 26px;
|
||||
color: #c8c9cc;
|
||||
letter-spacing: 2px;
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
width: 100%;
|
||||
@@ -144,23 +171,17 @@
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* ========== 去注册入口 ========== */
|
||||
.login-switch {
|
||||
/* ========== 客服提示 ========== */
|
||||
.login-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
|
||||
.switch-text {
|
||||
font-size: 28px;
|
||||
.tip-text {
|
||||
font-size: 26px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.switch-link {
|
||||
font-size: 28px;
|
||||
color: #ee0a24;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 协议文字 ========== */
|
||||
|
||||
+56
-48
@@ -1,18 +1,24 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
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 useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import './index.less'
|
||||
|
||||
/** 登录账号长度限制(与后端一致:4~20 位) */
|
||||
const USERNAME_MAX = 20
|
||||
/** 密码长度限制 */
|
||||
const PASSWORD_MAX = 20
|
||||
|
||||
export default function LoginPage() {
|
||||
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 isWeb = Taro.getEnv() === Taro.ENV_TYPE.WEB
|
||||
|
||||
/** 返回上一页(无页面栈时回首页) */
|
||||
const goBack = useCallback(() => {
|
||||
const pages = Taro.getCurrentPages()
|
||||
@@ -23,50 +29,29 @@ export default function LoginPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 前往注册页 */
|
||||
const goRegister = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/register/index' })
|
||||
}, [])
|
||||
|
||||
/** 已登录 → 自动返回 */
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) goBack()
|
||||
}, [isLoggedIn, goBack])
|
||||
|
||||
/** 微信一键登录(wx.login code 换 openid,仅已注册用户可登录) */
|
||||
/** 账号密码登录:POST /mini/auth/login */
|
||||
const handleLogin = useCallback(async () => {
|
||||
if (submitting) return
|
||||
// H5 环境无法获取微信登录凭证
|
||||
if (isWeb) {
|
||||
Taro.showToast({ title: '请在微信小程序中使用微信登录', icon: 'none' })
|
||||
const account = username.trim()
|
||||
if (!account) {
|
||||
Taro.showToast({ title: '请输入登录账号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!password) {
|
||||
Taro.showToast({ title: '请输入登录密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await Taro.login()
|
||||
if (!res.code) {
|
||||
Taro.showToast({ title: '获取登录凭证失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
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()
|
||||
},
|
||||
})
|
||||
}
|
||||
await login({ username: account, password })
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
goBack()
|
||||
} catch {
|
||||
// 错误提示已由 request 层 toast(账号或密码错误 / 账号已停用等)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [login, submitting, isWeb, goRegister])
|
||||
}, [login, submitting, username, password, goBack])
|
||||
|
||||
/** 查看用户协议 */
|
||||
const handleShowAgreement = useCallback(() => {
|
||||
@@ -94,9 +79,34 @@ export default function LoginPage() {
|
||||
<Text className='app-slogan'>门店订货 · 对账结算 · 一站式采购</Text>
|
||||
</View>
|
||||
|
||||
{/* 功能介绍 */}
|
||||
<View className='login-features'>
|
||||
<Text className='feature-text'>在线订货 · 价格透明 · 周期对账</Text>
|
||||
{/* 登录表单 */}
|
||||
<View className='login-form'>
|
||||
<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>
|
||||
|
||||
{/* 登录操作 */}
|
||||
@@ -107,13 +117,11 @@ export default function LoginPage() {
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '登录中...' : '微信一键登录'}
|
||||
{submitting ? '登录中...' : '登 录'}
|
||||
</Button>
|
||||
|
||||
{/* 未注册用户入口 */}
|
||||
<View className='login-switch' onClick={goRegister}>
|
||||
<Text className='switch-text'>还没有账号?</Text>
|
||||
<Text className='switch-link'>立即注册</Text>
|
||||
<View className='login-tip'>
|
||||
<Text className='tip-text'>账号密码由商家分配,如需帮助请联系客服</Text>
|
||||
</View>
|
||||
|
||||
<View className='login-agreement'>
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Empty, Popup } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { cancelOrderApi, getOrderDetailApi, getOrderListApi } from '@/services/order'
|
||||
import { ORDER_STATUS_FILTERS, ORDER_STATUS_TEXT } from '@/types/order'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import { formatSpec, resolveFileUrl } from '@/utils/format'
|
||||
import PriceText from '@/components/PriceText'
|
||||
import type { OrderDetail, OrderListItem, OrderStatus } from '@/types/order'
|
||||
import './index.less'
|
||||
|
||||
@@ -184,8 +185,8 @@ export default function OrderListPage() {
|
||||
)}
|
||||
<View className='order-item__goods-info'>
|
||||
<Text className='order-item__goods-name'>{i.product_name}</Text>
|
||||
{!!i.product_spec && (
|
||||
<Text className='order-item__goods-spec'>{i.product_spec} {i.unit}</Text>
|
||||
{!!formatSpec(i.product_spec, i.unit) && (
|
||||
<Text className='order-item__goods-spec'>{formatSpec(i.product_spec, i.unit)}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='order-item__goods-qty'>×{i.quantity} </Text>
|
||||
@@ -262,7 +263,8 @@ export default function OrderListPage() {
|
||||
<View className='detail-popup__item-info'>
|
||||
<Text className='detail-popup__item-name'>{item.product_name}</Text>
|
||||
<Text className='detail-popup__item-spec'>
|
||||
{item.product_spec ? `${item.product_spec}${item.unit} ` : ''}¥{item.price} × {item.quantity}
|
||||
{formatSpec(item.product_spec, item.unit)}{' '}
|
||||
<PriceText price={item.price} spec={item.product_spec} /> × {item.quantity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='detail-popup__item-amount'>¥{item.amount}</Text>
|
||||
|
||||
@@ -61,12 +61,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__unit {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Empty, Stepper, Swiper, SwiperItem } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import useCartStore from '@/stores/cart/useCartStore'
|
||||
import { getProductDetailApi } from '@/services/product'
|
||||
import { resolveFileUrl } from '@/utils/format'
|
||||
import { formatSpec, resolveFileUrl } from '@/utils/format'
|
||||
import PriceText from '@/components/PriceText'
|
||||
import type { Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
@@ -115,16 +116,15 @@ export default function ProductDetailPage() {
|
||||
<View className='goods-card'>
|
||||
<View className='goods-card__price-row'>
|
||||
{product.price !== null ? (
|
||||
<Text className='goods-card__price'>¥{product.price}</Text>
|
||||
<PriceText className='goods-card__price' price={product.price} spec={product.spec} />
|
||||
) : (
|
||||
<Text className='goods-card__price goods-card__price--none'>
|
||||
{loggedIn ? '价格待定' : '登录后查看价格'}
|
||||
</Text>
|
||||
)}
|
||||
<Text className='goods-card__unit'>/{product.unit}</Text>
|
||||
</View>
|
||||
<Text className='goods-card__name'>{product.name}</Text>
|
||||
<Text className='goods-card__spec'>{product.spec}</Text>
|
||||
<Text className='goods-card__spec'>{formatSpec(product.spec, product.unit)}</Text>
|
||||
<View className='goods-card__meta'>
|
||||
{!!product.shelf_life && product.shelf_life > 0 && (
|
||||
<Text className='goods-card__tag'>保质期 {product.shelf_life} 天</Text>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { getCategoriesApi, getProductListApi } from '@/services/product'
|
||||
import type { ProductListParams } from '@/services/product'
|
||||
import { getProductCover } from '@/types/product'
|
||||
import type { Category, Product } from '@/types/product'
|
||||
import PriceText from '@/components/PriceText'
|
||||
import { formatSpec } from '@/utils/format'
|
||||
import './index.less'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
@@ -288,10 +290,10 @@ export default function ProductPage() {
|
||||
/>
|
||||
<View className='product-item__info'>
|
||||
<Text className='product-item__name'>{product.name}</Text>
|
||||
<Text className='product-item__spec'>{product.spec} / {product.unit}</Text>
|
||||
<Text className='product-item__spec'>{formatSpec(product.spec, product.unit)}</Text>
|
||||
<View className='product-item__bottom'>
|
||||
{product.price !== null ? (
|
||||
<Text className='product-item__price'>¥{product.price}</Text>
|
||||
<PriceText className='product-item__price' price={product.price} spec={product.spec} mode='block' />
|
||||
) : (
|
||||
<Text className='product-item__price product-item__price--none'>价格待定</Text>
|
||||
)}
|
||||
@@ -339,9 +341,9 @@ export default function ProductPage() {
|
||||
/>
|
||||
<View className='add-popup__info'>
|
||||
<Text className='add-popup__name'>{current.name}</Text>
|
||||
<Text className='add-popup__spec'>{current.spec} / {current.unit}</Text>
|
||||
<Text className='add-popup__spec'>{formatSpec(current.spec, current.unit)}</Text>
|
||||
{current.price !== null ? (
|
||||
<Text className='add-popup__price'>¥{current.price}</Text>
|
||||
<PriceText className='add-popup__price' price={current.price} spec={current.spec} />
|
||||
) : (
|
||||
<Text className='add-popup__price add-popup__price--none'>价格待定</Text>
|
||||
)}
|
||||
|
||||
@@ -144,6 +144,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 运营报表入口 =====
|
||||
.report-entry {
|
||||
margin-top: 12rpx;
|
||||
border-top: 1rpx solid #f2f3f5;
|
||||
padding-top: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fff0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
font-size: 32rpx;
|
||||
color: #c8c9cc;
|
||||
line-height: 1;
|
||||
margin-left: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 账单入口 =====
|
||||
.bill-entry {
|
||||
display: flex;
|
||||
|
||||
+31
-28
@@ -8,7 +8,6 @@ import { getBillListApi } from '@/services/bill'
|
||||
import type { BillSummary } from '@/services/bill'
|
||||
import { ORDER_NAV_ITEMS } from '@/types/order'
|
||||
import { resolveAvatarUrl } from '@/utils/format'
|
||||
import type { UserType } from '@/types/user'
|
||||
import './index.less'
|
||||
|
||||
/** 菜单项(订单/账单入口已由上方专区承载,后续单独页面开发时在此追加) */
|
||||
@@ -38,9 +37,9 @@ export default function ProfilePage() {
|
||||
|
||||
const loggedIn = !!token && !!user
|
||||
|
||||
/** 功能菜单(门店账号追加「门店信息」「支付记录」入口) */
|
||||
/** 功能菜单(登录门店可用:门店信息 / 支付记录 / 修改密码) */
|
||||
const menuItems = useMemo(() => {
|
||||
if (!user?.store) return MENU_ITEMS
|
||||
if (!loggedIn) return MENU_ITEMS
|
||||
return [
|
||||
{
|
||||
key: 'store-info',
|
||||
@@ -54,9 +53,15 @@ export default function ProfilePage() {
|
||||
icon: 'balance-o',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/payment-records/index' }),
|
||||
},
|
||||
{
|
||||
key: 'change-password',
|
||||
label: '修改密码',
|
||||
icon: 'lock',
|
||||
onClick: () => Taro.navigateTo({ url: '/pages/change-password/index' }),
|
||||
},
|
||||
...MENU_ITEMS,
|
||||
]
|
||||
}, [user?.store])
|
||||
}, [loggedIn])
|
||||
|
||||
useDidShow(() => {
|
||||
if (!loggedIn) return
|
||||
@@ -70,18 +75,16 @@ export default function ProfilePage() {
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
/** 身份标签 */
|
||||
const getTypeLabel = useCallback((type: UserType): string => {
|
||||
if (type === 1) return '门店'
|
||||
if (type === 2) return '供应商'
|
||||
return '待绑定'
|
||||
}, [])
|
||||
|
||||
/** 订单总汇 → 订单列表页(按状态) */
|
||||
const handleOrderNav = useCallback((status?: number) => {
|
||||
Taro.navigateTo({ url: `/pages/order-list/index?status=${status ?? 'all'}` })
|
||||
}, [])
|
||||
|
||||
/** 运营报表入口 → 运营报表页 */
|
||||
const goReport = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/report/index' })
|
||||
}, [])
|
||||
|
||||
/** 账单入口 → 账单列表页 */
|
||||
const goBill = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/bill/index' })
|
||||
@@ -130,29 +133,18 @@ export default function ProfilePage() {
|
||||
/>
|
||||
) : (
|
||||
<View className='profile-card__avatar profile-card__avatar--text'>
|
||||
{user?.nickname?.[0] || '用'}
|
||||
{user?.name?.[0] || '店'}
|
||||
</View>
|
||||
)}
|
||||
<View className='profile-card__info'>
|
||||
<Text className='profile-card__name'>{user?.nickname}</Text>
|
||||
<Text className='profile-card__desc'>{user?.phone || '未绑定手机号'}</Text>
|
||||
<Text className='profile-card__name'>{user?.name}</Text>
|
||||
<Text className='profile-card__desc'>{user?.phone || '未设置联系电话'}</Text>
|
||||
</View>
|
||||
{user?.type === 0 && (
|
||||
<View className='profile-card__btn' onClick={goLogin}>绑定手机号</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='profile-card__identity'>
|
||||
{user?.store ? (
|
||||
<>
|
||||
<Text className='profile-card__tag'>门店 · {user.store.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>
|
||||
<Text className='profile-card__tag'>门店编码 · {user?.code}</Text>
|
||||
{user?.level && (
|
||||
<Text className='profile-card__tag profile-card__tag--level'>{user.level.name}</Text>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
@@ -175,6 +167,17 @@ export default function ProfilePage() {
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{/* 运营报表入口 */}
|
||||
<View className='report-entry' onClick={goReport}>
|
||||
<View className='report-entry__icon'>
|
||||
<Icon name='bar-chart-o' size={28} color='#ee0a24' />
|
||||
</View>
|
||||
<View className='report-entry__info'>
|
||||
<Text className='report-entry__title'>运营报表</Text>
|
||||
<Text className='report-entry__desc'>按周/月统计采购金额与单品占比</Text>
|
||||
</View>
|
||||
<Text className='report-entry__arrow'>›</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 我的账单 ========== */}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '注册',
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '运营报表',
|
||||
})
|
||||
@@ -0,0 +1,222 @@
|
||||
.report-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
padding: 20rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
// ===== 周期切换 =====
|
||||
.period-bar {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.period-chip {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: #fff;
|
||||
font-size: 26rpx;
|
||||
color: #323233;
|
||||
|
||||
&.active {
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 汇总卡片 =====
|
||||
.report-summary {
|
||||
margin-top: 20rpx;
|
||||
background: linear-gradient(135deg, #ee0a24, #ff4d4f);
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
margin-top: 12rpx;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__range {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 28rpx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.25);
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
&__meta-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__meta-value {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__meta-label {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 单品排行 =====
|
||||
.report-list {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx 8rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
|
||||
.report-item {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f2f3f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__rank {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f2f3f5;
|
||||
color: #969799;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
|
||||
&--top {
|
||||
background: #fff0f0;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 28rpx;
|
||||
color: #323233;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 30rpx;
|
||||
color: #ee0a24;
|
||||
font-weight: 600;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__spec {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #969799;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__percent {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #323233;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
margin-top: 14rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f2f3f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__bar-inner {
|
||||
height: 100%;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #ff8a8f, #ee0a24);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 空态 / 加载中 =====
|
||||
.report-empty {
|
||||
margin-top: 60rpx;
|
||||
|
||||
&__btn {
|
||||
margin-top: 20rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 14rpx 48rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #ee0a24;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.report-loading {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #969799;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Calendar, Empty } from '@antmjs/vantui'
|
||||
import useAuthStore from '@/stores/auth/useAuthStore'
|
||||
import { getPurchaseReportApi } from '@/services/report'
|
||||
import type { PurchaseReport, PurchaseReportParams, ReportPreset } from '@/services/report'
|
||||
import './index.less'
|
||||
|
||||
/** 周期选项 key(custom 为前端伪预设:选中自定义区间后生效) */
|
||||
type PeriodKey = ReportPreset
|
||||
|
||||
/** 周期切换 chips */
|
||||
const PERIOD_TABS: Array<{ key: PeriodKey; label: string }> = [
|
||||
{ key: 'week', label: '本周' },
|
||||
{ key: 'last_week', label: '上周' },
|
||||
{ key: 'month', label: '本月' },
|
||||
{ key: 'last_month', label: '上月' },
|
||||
{ key: 'custom', label: '自定义' },
|
||||
]
|
||||
|
||||
/** 自定义区间可选范围:2020-01-01 ~ 今天(进行中的周期由后端封顶今天) */
|
||||
const MIN_DATE = new Date(2020, 0, 1).getTime()
|
||||
const MAX_DATE = Date.now()
|
||||
|
||||
/** Date → Y-m-d */
|
||||
function formatDate(d: Date): string {
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${d.getFullYear()}-${m}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 运营报表页
|
||||
* 按周期(本周/上周/本月/上月/自定义区间)统计门店采购总金额与单品累计金额占比;
|
||||
* 数据为下单快照口径,已取消/已删除订单不计入
|
||||
*/
|
||||
export default function ReportPage() {
|
||||
const token = useAuthStore(s => s.token)
|
||||
|
||||
const [period, setPeriod] = useState<PeriodKey>('month')
|
||||
/** 自定义区间(period=custom 时使用) */
|
||||
const [range, setRange] = useState<{ start: string; end: string } | null>(null)
|
||||
const [report, setReport] = useState<PurchaseReport | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showCalendar, setShowCalendar] = useState(false)
|
||||
const loadingRef = useRef(false)
|
||||
|
||||
const loggedIn = !!token
|
||||
|
||||
/** 拉取报表 */
|
||||
const load = useCallback(
|
||||
async (params: PurchaseReportParams) => {
|
||||
if (!loggedIn || loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPurchaseReportApi(params)
|
||||
setReport(res.data)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loggedIn],
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
load(
|
||||
period === 'custom' && range
|
||||
? { start_date: range.start, end_date: range.end }
|
||||
: { preset: period === 'custom' ? 'month' : period },
|
||||
)
|
||||
})
|
||||
|
||||
/** 切换周期;自定义打开日历选择区间 */
|
||||
const handlePeriodTap = useCallback(
|
||||
(key: PeriodKey) => {
|
||||
if (key === 'custom') {
|
||||
setShowCalendar(true)
|
||||
return
|
||||
}
|
||||
if (key === period) return
|
||||
setPeriod(key)
|
||||
load({ preset: key })
|
||||
},
|
||||
[period, load],
|
||||
)
|
||||
|
||||
/** 日历确认区间 → 自定义区间查询(优先于 preset) */
|
||||
const handleCalendarConfirm = useCallback(
|
||||
(e: { detail: { value: Date | Date[] } }) => {
|
||||
const value = Array.isArray(e.detail.value) ? e.detail.value : [e.detail.value]
|
||||
const [start, end] = value
|
||||
if (!start || !end) return
|
||||
const next = { start: formatDate(start), end: formatDate(end) }
|
||||
setRange(next)
|
||||
setPeriod('custom')
|
||||
setShowCalendar(false)
|
||||
load({ start_date: next.start, end_date: next.end })
|
||||
},
|
||||
[load],
|
||||
)
|
||||
|
||||
const goLogin = useCallback(() => {
|
||||
Taro.navigateTo({ url: '/pages/login/index' })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className='report-page'>
|
||||
{/* ========== 周期切换 ========== */}
|
||||
<View className='period-bar'>
|
||||
{PERIOD_TABS.map(tab => (
|
||||
<View
|
||||
key={tab.key}
|
||||
className={`period-chip ${period === tab.key ? 'active' : ''}`}
|
||||
onClick={() => handlePeriodTap(tab.key)}
|
||||
>
|
||||
<Text>{tab.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{!loggedIn ? (
|
||||
<Empty description='登录后查看运营报表' className='report-empty'>
|
||||
<View className='report-empty__btn' onClick={goLogin}>去登录</View>
|
||||
</Empty>
|
||||
) : (
|
||||
<>
|
||||
{/* ========== 汇总卡片 ========== */}
|
||||
{report && (
|
||||
<View className='report-summary'>
|
||||
<Text className='report-summary__label'>采购总金额(元)</Text>
|
||||
<Text className='report-summary__amount'>¥{report.total_amount}</Text>
|
||||
<Text className='report-summary__range'>
|
||||
{report.start_date} ~ {report.end_date}
|
||||
</Text>
|
||||
<View className='report-summary__meta'>
|
||||
<View className='report-summary__meta-item'>
|
||||
<Text className='report-summary__meta-value'>{report.order_count}</Text>
|
||||
<Text className='report-summary__meta-label'>订货单数</Text>
|
||||
</View>
|
||||
<View className='report-summary__meta-item'>
|
||||
<Text className='report-summary__meta-value'>{report.item_count}</Text>
|
||||
<Text className='report-summary__meta-label'>单品数</Text>
|
||||
</View>
|
||||
<View className='report-summary__meta-item'>
|
||||
<Text className='report-summary__meta-value'>{report.total_quantity}</Text>
|
||||
<Text className='report-summary__meta-label'>订货总量</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 单品排行 ========== */}
|
||||
{report && report.items.length > 0 && (
|
||||
<View className='report-list'>
|
||||
<View className='report-list__header'>
|
||||
<Text className='report-list__title'>单品采购排行</Text>
|
||||
<Text className='report-list__desc'>共 {report.item_count} 种,按金额降序</Text>
|
||||
</View>
|
||||
{report.items.map((item, index) => (
|
||||
<View key={item.product_id} className='report-item'>
|
||||
<View className={`report-item__rank ${index < 3 ? 'report-item__rank--top' : ''}`}>
|
||||
{index + 1}
|
||||
</View>
|
||||
<View className='report-item__main'>
|
||||
<View className='report-item__row'>
|
||||
<Text className='report-item__name'>{item.product_name}</Text>
|
||||
<Text className='report-item__amount'>¥{item.amount}</Text>
|
||||
</View>
|
||||
<View className='report-item__row'>
|
||||
<Text className='report-item__spec'>
|
||||
{item.product_spec ? `${item.product_spec} · ` : ''}
|
||||
累计 {item.quantity}{item.unit}
|
||||
{parseFloat(item.weight) > 0 ? ` · 称重 ${item.weight}` : ''}
|
||||
</Text>
|
||||
<Text className='report-item__percent'>{item.percent}%</Text>
|
||||
</View>
|
||||
<View className='report-item__bar'>
|
||||
<View
|
||||
className='report-item__bar-inner'
|
||||
style={{ width: `${Math.min(Math.max(item.percent, 0), 100)}%` }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ========== 空态 / 加载中 ========== */}
|
||||
{(!report || report.items.length === 0) && (
|
||||
loading ? (
|
||||
<View className='report-loading'><Text>加载中...</Text></View>
|
||||
) : (
|
||||
<Empty description='该时间段暂无采购数据' className='report-empty' />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ========== 自定义区间日历 ========== */}
|
||||
<Calendar
|
||||
show={showCalendar}
|
||||
type='range'
|
||||
allowSameDay
|
||||
firstDayOfWeek={1}
|
||||
minDate={MIN_DATE}
|
||||
maxDate={MAX_DATE}
|
||||
color='#ee0a24'
|
||||
title='选择统计区间'
|
||||
defaultDate={range ? [new Date(range.start).getTime(), new Date(range.end).getTime()] : undefined}
|
||||
onClose={() => setShowCalendar(false)}
|
||||
onConfirm={handleCalendarConfirm}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
+27
-24
@@ -1,39 +1,42 @@
|
||||
import { get, post } from '@/utils/request'
|
||||
import { get, post, put } from '@/utils/request'
|
||||
import type { User } from '@/types/user'
|
||||
|
||||
/** 微信登录参数 */
|
||||
export interface WxLoginParams {
|
||||
/** wx.login 的临时凭证 */
|
||||
code: string
|
||||
/** 账号密码登录参数 */
|
||||
export interface LoginParams {
|
||||
/** 登录账号(商家后台分配,4~20 位) */
|
||||
username: string
|
||||
/** 登录密码 */
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 微信注册参数 */
|
||||
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 */
|
||||
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)
|
||||
}
|
||||
|
||||
/** 微信注册(code 换 openid + phoneCode 换手机号 + storeCode 绑定门店):POST /mini/auth/register */
|
||||
export function registerApi(params: RegisterParams) {
|
||||
return post<AuthResult>('/mini/auth/register', params)
|
||||
}
|
||||
|
||||
/** 当前用户信息(含门店客户等级):GET /mini/auth/info */
|
||||
/** 当前门店信息(含客户等级):GET /mini/auth/info */
|
||||
export function getUserInfoApi() {
|
||||
return get<User>('/mini/auth/info')
|
||||
}
|
||||
|
||||
/** 修改密码(成功后现有 token 仍有效):PUT /mini/auth/password */
|
||||
export function changePasswordApi(params: ChangePasswordParams) {
|
||||
return put<null>('/mini/auth/password', params)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 统计周期预设:
|
||||
* week 本周 / last_week 上周 / month 本月 / last_month 上月;
|
||||
* custom 仅出现在响应中(传 start_date + end_date 自定义区间时生效,优先于 preset)
|
||||
*/
|
||||
export type ReportPreset = 'week' | 'last_week' | 'month' | 'last_month' | 'custom'
|
||||
|
||||
/** 单品累计行(按金额降序) */
|
||||
export interface PurchaseReportItem {
|
||||
product_id: number
|
||||
/** 品名(下单时快照) */
|
||||
product_name: string
|
||||
/** 规格/包规(快照) */
|
||||
product_spec: string
|
||||
/** 计价单位(快照) */
|
||||
unit: string
|
||||
/** 周期内累计订货量 */
|
||||
quantity: number
|
||||
/** 周期内累计重量(3 位小数,未称重为 0.000) */
|
||||
weight: string
|
||||
/** 周期内累计采购金额(元,2 位小数字符串) */
|
||||
amount: string
|
||||
/** 金额占比(%,1 位小数;如 8.8 表示 8.8%) */
|
||||
percent: number
|
||||
}
|
||||
|
||||
/** 采购运营报表 */
|
||||
export interface PurchaseReport {
|
||||
/** 实际生效的周期预设 */
|
||||
preset: ReportPreset
|
||||
/** 实际统计开始日期(Y-m-d,进行中的周期封顶为今天) */
|
||||
start_date: string
|
||||
/** 实际统计结束日期(Y-m-d) */
|
||||
end_date: string
|
||||
/** 周期内采购总金额(元,2 位小数字符串) */
|
||||
total_amount: string
|
||||
/** 周期内订货总量(各单品数量之和) */
|
||||
total_quantity: number
|
||||
/** 周期内有效订货单数 */
|
||||
order_count: number
|
||||
/** 单品个数(= items 长度) */
|
||||
item_count: number
|
||||
/** 单品累计列表,按金额降序 */
|
||||
items: PurchaseReportItem[]
|
||||
}
|
||||
|
||||
/** 报表查询参数:自定义区间(start_date + end_date 需成对)优先于 preset,preset 缺省为 month */
|
||||
export interface PurchaseReportParams {
|
||||
preset?: Exclude<ReportPreset, 'custom'>
|
||||
/** 自定义开始日期(Y-m-d) */
|
||||
start_date?: string
|
||||
/** 自定义结束日期(Y-m-d),不得早于 start_date */
|
||||
end_date?: string
|
||||
}
|
||||
|
||||
/** 采购运营报表:GET /mini/report/purchase(仅当前门店自身数据) */
|
||||
export function getPurchaseReportApi(params: PurchaseReportParams = {}) {
|
||||
return get<PurchaseReport>('/mini/report/purchase', { data: params })
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { registerApi, wxLoginApi } from '@/services/auth'
|
||||
import type { RegisterParams, WxLoginParams } from '@/services/auth'
|
||||
import { loginApi } from '@/services/auth'
|
||||
import type { LoginParams } from '@/services/auth'
|
||||
import type { User } from '@/types/user'
|
||||
|
||||
/** 存储 key */
|
||||
@@ -26,7 +26,7 @@ function loadFromStorage(): { user: User | null; token: string | null } {
|
||||
return { user: null, token: null }
|
||||
}
|
||||
|
||||
/** 登录 / 注册成功后持久化 token 与用户信息 */
|
||||
/** 登录成功后持久化 token 与门店信息 */
|
||||
function persistAuth(token: string, user: User): void {
|
||||
try {
|
||||
Taro.setStorageSync(STORAGE_KEYS.TOKEN, token)
|
||||
@@ -40,11 +40,10 @@ interface AuthState {
|
||||
user: User | null
|
||||
token: string | null
|
||||
loading: boolean
|
||||
login: (params: WxLoginParams) => Promise<void>
|
||||
/** 微信注册(手机号授权 + 门店编码绑定门店) */
|
||||
register: (params: RegisterParams) => Promise<void>
|
||||
/** 账号密码登录(门店账号由商家后台分配) */
|
||||
login: (params: LoginParams) => Promise<void>
|
||||
logout: () => void
|
||||
/** 更新用户信息(用于编辑资料后同步 store) */
|
||||
/** 更新门店信息(用于编辑资料后同步 store) */
|
||||
setUser: (user: User) => void
|
||||
}
|
||||
|
||||
@@ -57,17 +56,9 @@ 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 })
|
||||
persistAuth(token, user)
|
||||
},
|
||||
|
||||
/** 注册:POST /mini/auth/register */
|
||||
register: async (params: RegisterParams) => {
|
||||
const res = await registerApi(params)
|
||||
/** 账号密码登录:POST /mini/auth/login */
|
||||
login: async (params: LoginParams) => {
|
||||
const res = await loginApi(params)
|
||||
const { token, user } = res.data
|
||||
set({ user, token })
|
||||
persistAuth(token, user)
|
||||
|
||||
+20
-41
@@ -4,54 +4,33 @@ export interface StoreLevel {
|
||||
name: string
|
||||
}
|
||||
|
||||
/** 门店信息 */
|
||||
export interface StoreInfo {
|
||||
id: number
|
||||
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 表实际返回字段) */
|
||||
/**
|
||||
* 登录门店信息(门店即用户)
|
||||
* 用户表与门店表已合并:登录 / auth/info 返回的 user 就是门店本身(扁平结构)
|
||||
*/
|
||||
export interface User {
|
||||
id: number
|
||||
/** 用户名(注册时生成 wx_xxxx) */
|
||||
/** 门店名称 */
|
||||
name: string
|
||||
/** 门店编码(后台分配) */
|
||||
code: string
|
||||
/** 登录账号(后台分配,4~20 位) */
|
||||
username: string
|
||||
/** 昵称(注册默认「微信用户」) */
|
||||
nickname: string
|
||||
/** 头像(可能为空) */
|
||||
avatar: string
|
||||
/** 手机号(未绑定为空) */
|
||||
level_id: number
|
||||
/** 客户等级(level_id > 0 才可展示价格) */
|
||||
level: StoreLevel | null
|
||||
/** 联系人 */
|
||||
contact: string
|
||||
/** 联系电话 */
|
||||
phone: string
|
||||
/** 绑定门店ID(0 未绑定) */
|
||||
store_id: number
|
||||
/** 地址 */
|
||||
address: string
|
||||
/** 回款周期天数 */
|
||||
payment_cycle_days: 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
|
||||
}
|
||||
|
||||
@@ -55,3 +55,26 @@ export function resolveFileUrl(url?: string): string {
|
||||
export function resolveAvatarUrl(avatar?: string): string {
|
||||
return resolveFileUrl(avatar)
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品规格展示:包规与单位直接拼接
|
||||
* spec=20、unit=斤/箱 → 20斤/箱
|
||||
*/
|
||||
export function formatSpec(spec?: string | number | null, unit?: string | null): string {
|
||||
const s = spec === null || spec === undefined ? '' : String(spec).trim()
|
||||
const u = (unit ?? '').trim()
|
||||
return `${s}${u}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 零售价 = 售价 ÷ 包规(如 30¥/箱 ÷ 20斤/箱 = 2¥/斤)
|
||||
* 保留两位小数并去掉尾零(2 → "2",2.50 → "2.5")
|
||||
* 售价为空、包规非数字或 ≤0 时返回 null(不展示零售价)
|
||||
*/
|
||||
export function formatRetailPrice(price?: string | number | null, spec?: string | number | null): string | null {
|
||||
if (price === null || price === undefined || price === '') return null
|
||||
const p = Number(price)
|
||||
const s = Number(spec)
|
||||
if (!Number.isFinite(p) || !Number.isFinite(s) || s <= 0) return null
|
||||
return String(Math.round((p / s) * 100) / 100)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ const LOGIN_PATH = '/pages/login/index'
|
||||
/** 默认请求超时(ms) */
|
||||
const DEFAULT_TIMEOUT = 15000
|
||||
/** 接口根地址(uploadFile 等原生请求同样使用) */
|
||||
export const BASE_URL = "http://localhost:8000/index.php"
|
||||
// export const BASE_URL = "http://localhost:8000/index.php"
|
||||
export const BASE_URL = "https://purchase.henanklkj.com/index.php"
|
||||
|
||||
/**
|
||||
* HTTP 状态码 → 错误提示映射
|
||||
|
||||
Reference in New Issue
Block a user