Browse Source

feat(dealer): 添加经销商申请功能并优化用户相关页面

- 新增经销商申请页面和相关组件- 更新用户中心页面,增加经销商状态显示和申请入口
- 修改登录逻辑,支持获取用户手机号- 优化用户钩子,增加获取用户ID和判断超级管理员权限的功能
dev
科技小王子 1 week ago
parent
commit
13b3d6407b
  1. 2
      src/api/shop/shopDealerApply/model/index.ts
  2. 2
      src/api/shop/shopDealerUser/model/index.ts
  3. 1
      src/app.config.ts
  4. 14
      src/components/FixedButton.tsx
  5. 4
      src/dealer/apply/add.config.ts
  6. 187
      src/dealer/apply/add.tsx
  7. 12
      src/hooks/useUser.ts
  8. 37
      src/pages/index/Login.tsx
  9. 121
      src/pages/user/components/IsDealer.tsx
  10. 39
      src/pages/user/components/UserCell.tsx
  11. 3
      src/pages/user/user.tsx

2
src/api/shop/shopDealerApply/model/index.ts

@ -37,5 +37,7 @@ export interface ShopDealerApply {
*/
export interface ShopDealerApplyParam extends PageParam {
applyId?: number;
mobile?: string;
userId?: number;
keywords?: string;
}

2
src/api/shop/shopDealerUser/model/index.ts

@ -45,5 +45,7 @@ export interface ShopDealerUser {
*/
export interface ShopDealerUserParam extends PageParam {
id?: number;
phone?: string;
userId?: number;
keywords?: string;
}

1
src/app.config.ts

@ -50,6 +50,7 @@ export default defineAppConfig({
"root": "dealer",
"pages": [
"index",
"apply/add",
"withdraw/index",
"orders/index",
"team/index",

14
src/components/FixedButton.tsx

@ -1,11 +1,18 @@
import React from 'react';
import {View} from '@tarojs/components';
import {Button} from '@nutui/nutui-react-taro'
function FixedButton({text, onClick, icon}) {
interface FixedButtonProps {
text?: string;
onClick?: () => void;
icon?: React.ReactNode;
disabled?: boolean;
}
function FixedButton({text, onClick, icon, disabled}: FixedButtonProps) {
return (
<>
{/* 底部安全区域占位 */
}
{/* 底部安全区域占位 */}
<View className="h-20 w-full"></View>
<View
className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 safe-area-bottom">
@ -14,6 +21,7 @@ function FixedButton({text, onClick, icon}) {
size="large"
block
icon={icon}
disabled={disabled}
className="px-6"
onClick={onClick}>
{text || '新增'}

4
src/dealer/apply/add.config.ts

@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '注册成为经销商',
navigationBarTextStyle: 'black'
})

187
src/dealer/apply/add.tsx

@ -0,0 +1,187 @@
import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Cell, Input, Form} from '@nutui/nutui-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import FixedButton from "@/components/FixedButton";
import {useUser} from "@/hooks/useUser";
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
import {
addShopDealerApply,
pageShopDealerApply,
updateShopDealerApply
} from "@/api/shop/shopDealerApply";
const AddUserAddress = () => {
const {user} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<ShopDealerApply>({})
const formRef = useRef<any>(null)
const [isEditMode, setIsEditMode] = useState<boolean>(false)
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
// 获取审核状态文字
const getApplyStatusText = (status?: number) => {
switch (status) {
case 10:
return '待审核'
case 20:
return '审核通过'
case 30:
return '驳回'
default:
return '未知状态'
}
}
const reload = async () => {
// 判断用户是否登录
if(!user?.userId){
return false;
}
// 查询当前用户ID是否已有申请记录
try {
const res = await pageShopDealerApply({userId: user?.userId});
if (res && res.count > 0) {
setIsEditMode(true);
setExistingApply(res.list[0]);
// 如果有记录,填充表单数据
setFormData(res.list[0]);
} else {
setFormData({})
setIsEditMode(false);
setExistingApply(null);
}
} catch (error) {
console.error('查询申请记录失败:', error);
setIsEditMode(false);
setExistingApply(null);
setFormData({})
}
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...values,
realName: values.realName || user?.nickname,
mobile: user?.phone,
refereeId: values.refereeId,
applyStatus: 10,
auditTime: undefined
};
// 如果是编辑模式,添加现有申请的id
if (isEditMode && existingApply?.applyId) {
submitData.applyId = existingApply.applyId;
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopDealerApply(submitData);
} else {
await addShopDealerApply(submitData);
}
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('保存失败:', error);
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}失败`,
icon: 'error'
});
}
}
// 处理固定按钮点击事件
const handleFixedButtonClick = () => {
// 触发表单提交
formRef.current?.submit();
};
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [user?.userId]); // 依赖用户ID,当用户变化时重新加载
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Form
ref={formRef}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
<View className={'bg-gray-100 h-3'}></View>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="realName" label="名称" initialValue={user?.nickname} required>
<Input placeholder="经销商名称" maxLength={10}/>
</Form.Item>
<Form.Item name="mobile" label="手机号" initialValue={user?.mobile} required>
<Input placeholder="请输入手机号" disabled={true} maxLength={11}/>
</Form.Item>
<Form.Item name="refereeId" label="推荐人ID" initialValue={FormData?.refereeId} required>
<Input placeholder="推荐人ID"/>
</Form.Item>
</CellGroup>
</Form>
{/* 审核状态显示(仅在编辑模式下显示) */}
{isEditMode && (
<CellGroup>
<Cell
title={'审核状态'}
extra={
<span style={{
color: FormData.applyStatus === 20 ? '#52c41a' :
FormData.applyStatus === 30 ? '#ff4d4f' : '#faad14'
}}>
{getApplyStatusText(FormData.applyStatus)}
</span>
}
/>
{FormData.applyStatus === 20 && (
<Cell title={'审核时间'} extra={FormData.auditTime || '无'} />
)}
{FormData.applyStatus === 30 && (
<Cell title={'驳回原因'} extra={FormData.rejectReason || '无'} />
)}
</CellGroup>
)}
{/* 底部浮动按钮 */}
{(!isEditMode || FormData.applyStatus === 10 || FormData.applyStatus === 30) && (
<FixedButton
icon={<Edit />}
text={isEditMode ? '保存修改' : '提交申请'}
disabled={FormData.applyStatus === 10}
onClick={handleFixedButtonClick}
/>
)}
</>
);
};
export default AddUserAddress;

12
src/hooks/useUser.ts

@ -164,6 +164,10 @@ export const useUser = () => {
return user?.avatar || user?.avatarUrl || '';
};
const getUserId = () => {
return user?.userId;
};
// 获取用户显示名称
const getDisplayName = () => {
return user?.nickname || user?.realName || user?.username || '未登录';
@ -196,6 +200,10 @@ export const useUser = () => {
return user?.isAdmin === true;
};
const isSuperAdmin = () => {
return user?.isSuperAdmin === true;
};
// 获取用户余额
const getBalance = () => {
return user?.balance || 0;
@ -233,6 +241,8 @@ export const useUser = () => {
isCertified,
isAdmin,
getBalance,
getPoints
getPoints,
getUserId,
isSuperAdmin
};
};

37
src/pages/index/Login.tsx

@ -5,12 +5,39 @@ import {TenantId} from "@/config/app";
import './login.scss';
import {saveStorageByLoginUser} from "@/utils/server";
const Login = (props:any) => {
// 微信获取手机号回调参数类型
interface GetPhoneNumberDetail {
code?: string;
encryptedData?: string;
iv?: string;
errMsg: string;
}
interface GetPhoneNumberEvent {
detail: GetPhoneNumberDetail;
}
interface LoginProps {
done?: (user: any) => void;
[key: string]: any;
}
// 登录接口返回数据类型
interface LoginResponse {
data: {
data: {
access_token: string;
user: any;
};
};
}
const Login = (props: LoginProps) => {
const [isAgree, setIsAgree] = useState(false)
const [env, setEnv] = useState<string>()
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}) => {
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {
@ -31,9 +58,9 @@ const Login = (props:any) => {
'content-type': 'application/json',
TenantId
},
success: function (res) {
saveStorageByLoginUser(res.data.data.access_token,res.data.data.user)
props.done(res.data.data.user);
success: function (res: LoginResponse) {
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
props.done?.(res.data.data.user);
}
})
} else {

121
src/pages/user/components/IsDealer.tsx

@ -0,0 +1,121 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
import {useEffect, useState} from "react";
import {pageShopDealerUser} from "@/api/shop/shopDealerUser";
const UserCell = () => {
const {isSuperAdmin} = useUser();
const [isDealer, setIsDealer] = useState<boolean>(false)
const reload = async () => {
const userId = Taro.getStorageSync('UserId')
console.log('当前用户ID:', userId)
const params = {
userId: Number(userId),
page: 1,
limit: 1
}
console.log('查询分销商参数:', params)
try {
const res = await pageShopDealerUser(params)
console.log('分销商查询结果:', res)
if (res?.count && res?.count > 0) {
setIsDealer(true)
} else {
setIsDealer(false)
}
} catch (error) {
console.error('查询分销商信息失败:', error)
setIsDealer(false)
}
}
useEffect(() => {
reload().then()
}, [])
/**
*
*/
if (isSuperAdmin()) {
return (
<>
<View className={'px-4'}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #e53e3e, #c53030)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/index', true)}>
<Setting className={'text-white '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-white font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#ffffff" size={18}/>}
/>
</View>
</>
)
}
/**
*
*/
if (isDealer) {
return (
<>
<View className={'px-4'}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
</View>
</>
)
}
/**
*
*/
return (
<>
<View className={'px-4'}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/dealer/apply/add', true)}
/>
</View>
</>
)
}
export default UserCell

39
src/pages/user/components/UserCell.tsx

@ -2,11 +2,11 @@ import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting} from '@nutui/icons-react-taro'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Tips, Ask} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const {logoutUser, isCertified} = useUser();
const onLogout = () => {
Taro.showModal({
@ -28,41 +28,6 @@ const UserCell = () => {
<>
<View className={'px-4'}>
{/*是否分销商*/}
{!hasRole('dealer') && !isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
{/*是否管理员*/}
{isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #ff8e0c, #ed680d)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/article/index', true)}>
<Setting className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>

3
src/pages/user/user.tsx

@ -5,6 +5,7 @@ import UserCell from "./components/UserCell";
import UserFooter from "./components/UserFooter";
import {useUser} from "@/hooks/useUser";
import './user.scss'
import IsDealer from "./components/IsDealer";
function User() {
const {
@ -24,6 +25,7 @@ function User() {
}}>
<UserCard/>
<UserOrder/>
<IsDealer/>
<UserCell/>
<UserFooter/>
</div>
@ -37,6 +39,7 @@ function User() {
}}>
<UserCard/>
<UserOrder/>
<IsDealer/>
<UserCell/>
<UserFooter/>
</div>

Loading…
Cancel
Save