feat(pages): 添加多个页面配置和功能模块

- 新增 .editorconfig、.eslintrc、.gitignore 配置文件
- 添加管理员文章管理页面配置和功能实现
- 添加经销商申请注册页面配置和功能实现
- 添加经销商银行卡管理页面配置和功能实现
- 添加经销商客户管理页面配置和功能实现
- 添加用户地址管理页面配置和功能实现
- 添加用户聊天消息页面配置和功能实现
- 添加用户礼品管理页面配置和功能实现
This commit is contained in:
2026-01-08 13:36:06 +08:00
commit 43106acc27
548 changed files with 75931 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '新增收货地址',
navigationBarTextStyle: 'black'
})

323
src/admin/article/add.tsx Normal file
View File

@@ -0,0 +1,323 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Button, Loading, CellGroup, Input, TextArea, Form, Switch, InputNumber, Radio, Image} from '@nutui/nutui-react-taro'
import {Edit, Upload as UploadIcon} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {ShopArticle} from "@/api/shop/shopArticle/model";
import {getShopArticle, addShopArticle, updateShopArticle} from "@/api/shop/shopArticle";
import FixedButton from "@/components/FixedButton";
const AddShopArticle = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [formData, setFormData] = useState<ShopArticle>({
type: 0, // 默认常规文章
status: 0, // 默认已发布
permission: 0, // 默认所有人可见
recommend: 0, // 默认不推荐
showType: 10, // 默认小图展示
virtualViews: 0, // 默认虚拟阅读量
actualViews: 0, // 默认实际阅读量
sortNumber: 0 // 默认排序
})
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const articleId = params.id ? Number(params.id) : undefined
// 文章类型选项
const typeOptions = [
{ text: '常规文章', value: 0 },
{ text: '视频文章', value: 1 }
]
// 状态选项
const statusOptions = [
{ text: '已发布', value: 0 },
{ text: '待审核', value: 1 },
{ text: '已驳回', value: 2 },
{ text: '违规内容', value: 3 }
]
// 可见性选项
const permissionOptions = [
{ text: '所有人可见', value: 0 },
{ text: '登录可见', value: 1 },
{ text: '密码可见', value: 2 }
]
// 显示方式选项
const showTypeOptions = [
{ text: '小图展示', value: 10 },
{ text: '大图展示', value: 20 }
]
const reload = async () => {
// 如果是编辑模式,加载文章数据
if (isEditMode && articleId) {
try {
const article = await getShopArticle(articleId)
setFormData(article)
// 更新表单值
if (formRef.current) {
formRef.current.setFieldsValue(article)
}
} catch (error) {
console.error('加载文章失败:', error)
Taro.showToast({
title: '加载文章失败',
icon: 'error'
});
}
}
}
// 图片上传处理
const handleImageUpload = async () => {
try {
const res = await Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera']
});
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
// 这里应该调用上传接口,暂时使用本地路径
const imagePath = res.tempFilePaths[0];
setFormData({
...formData,
image: imagePath
});
Taro.showToast({
title: '图片选择成功',
icon: 'success'
});
}
} catch (error) {
Taro.showToast({
title: '图片选择失败',
icon: 'error'
});
}
};
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...formData,
...values,
};
// 如果是编辑模式添加id
if (isEditMode && articleId) {
submitData.articleId = articleId;
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopArticle(submitData);
} else {
await addShopArticle(submitData);
}
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('保存失败:', error);
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
// 动态设置页面标题
Taro.setNavigationBarTitle({
title: isEditMode ? '编辑文章' : '新增文章'
});
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
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)}
>
{/* 基本信息 */}
<CellGroup title="基本信息">
<Form.Item
name="title"
label="文章标题"
required
rules={[{ required: true, message: '请输入文章标题' }]}
initialValue={formData.title}
>
<Input placeholder="请输入文章标题" maxLength={100}/>
</Form.Item>
<Form.Item name="overview" label="文章概述" initialValue={formData.overview}>
<TextArea placeholder="请输入文章概述,用于列表展示" maxLength={200} rows={3}/>
</Form.Item>
<Form.Item
name="detail"
label="文章内容"
required
rules={[{ required: true, message: '请输入文章内容' }]}
initialValue={formData.detail}
>
<TextArea placeholder="请输入文章内容" maxLength={10000} rows={8}/>
</Form.Item>
<Form.Item name="author" label="作者" initialValue={formData.author}>
<Input placeholder="请输入作者名称" maxLength={50}/>
</Form.Item>
<Form.Item name="source" label="来源" initialValue={formData.source}>
<Input placeholder="请输入文章来源" maxLength={100}/>
</Form.Item>
</CellGroup>
{/* 文章设置 */}
<CellGroup title="文章设置">
<Form.Item name="type" label="文章类型" initialValue={formData.type}>
<Radio.Group direction="horizontal" value={formData.type}>
{typeOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="status" label="发布状态" initialValue={formData.status}>
<Radio.Group direction="horizontal" value={formData.status}>
{statusOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="permission" label="可见性" initialValue={formData.permission}>
<Radio.Group direction="horizontal" value={formData.permission}>
{permissionOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
<Form.Item name="showType" label="显示方式" initialValue={formData.showType}>
<Radio.Group direction="horizontal" value={formData.showType}>
{showTypeOptions.map(option => (
<Radio key={option.value} value={option.value}>
{option.text}
</Radio>
))}
</Radio.Group>
</Form.Item>
</CellGroup>
{/* 高级设置 */}
<CellGroup title="高级设置">
<Form.Item name="recommend" label="推荐文章" initialValue={formData.recommend}>
<Switch
checked={formData.recommend === 1}
onChange={(checked) =>
setFormData({...formData, recommend: checked ? 1 : 0})
}
/>
</Form.Item>
<Form.Item name="price" label="付费金额" initialValue={formData.price}>
<Input placeholder="0.00" type="number"/>
<View className="text-xs text-gray-500 mt-1"></View>
</Form.Item>
<Form.Item name="virtualViews" label="虚拟阅读量" initialValue={formData.virtualViews}>
<InputNumber min={0} defaultValue={formData.virtualViews || 0}/>
</Form.Item>
<Form.Item name="actualViews" label="实际阅读量" initialValue={formData.actualViews}>
<InputNumber min={0} defaultValue={formData.actualViews || 0}/>
</Form.Item>
<Form.Item name="sortNumber" label="排序" initialValue={formData.sortNumber}>
<InputNumber min={0} defaultValue={formData.sortNumber || 0}/>
<View className="text-xs text-gray-500 mt-1"></View>
</Form.Item>
<Form.Item name="tags" label="标签" initialValue={formData.tags}>
<Input placeholder="请输入标签,多个标签用逗号分隔" maxLength={200}/>
</Form.Item>
<Form.Item name="topic" label="话题" initialValue={formData.topic}>
<Input placeholder="请输入话题" maxLength={100}/>
</Form.Item>
</CellGroup>
{/* 图片上传 */}
<CellGroup title="文章图片">
<Form.Item name="image" label="封面图片" initialValue={formData.image}>
<View className="flex items-center gap-3">
{formData.image && (
<Image
src={formData.image}
width="80"
height="80"
radius="8"
/>
)}
<Button
size="small"
type="primary"
fill="outline"
icon={<UploadIcon />}
onClick={handleImageUpload}
>
{formData.image ? '更换图片' : '上传图片'}
</Button>
</View>
</Form.Item>
</CellGroup>
{/* 提交按钮 */}
<FixedButton text={isEditMode ? '更新文章' : '发布文章'} onClick={() => submitSucceed} icon={<Edit />} />
</Form>
</>
);
};
export default AddShopArticle;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '商品文章管理',
navigationBarTextStyle: 'black'
})

271
src/admin/article/index.tsx Normal file
View File

@@ -0,0 +1,271 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {CmsArticle} from "@/api/cms/cmsArticle/model";
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
import FixedButton from "@/components/FixedButton";
import dayjs from "dayjs";
const ArticleArticleManage = () => {
const [list, setList] = useState<CmsArticle[]>([])
const [loading, setLoading] = useState(false)
// const [refreshing, setRefreshing] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [searchValue, setSearchValue] = useState('')
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const reload = async (isRefresh = false) => {
if (isRefresh) {
setPage(1)
setList([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const res = await pageCmsArticle({
page: currentPage,
limit: 10,
keywords: searchValue
})
if (res && res.list) {
const newList = isRefresh ? res.list : [...list, ...res.list]
setList(newList)
setTotal(res.count || 0)
// 判断是否还有更多数据
setHasMore(res.list.length === 10) // 如果返回的数据等于limit说明可能还有更多
if (!isRefresh) {
setPage(currentPage + 1)
} else {
setPage(2) // 刷新后下一页是第2页
}
} else {
setHasMore(false)
setTotal(0)
}
} catch (error) {
console.error('获取文章失败:', error)
Taro.showToast({
title: '获取文章失败',
icon: 'error'
});
} finally {
setLoading(false)
}
}
// 搜索功能
const handleSearch = (value: string) => {
setSearchValue(value)
reload(true)
}
// 下拉刷新
const handleRefresh = async () => {
// setRefreshing(true)
await reload(true)
// setRefreshing(false)
}
// 删除文章
const handleDelete = async (id?: number) => {
Taro.showModal({
title: '确认删除',
content: '确定要删除这篇文章吗?',
success: async (res) => {
if (res.confirm) {
try {
await removeCmsArticle(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload(true);
} catch (error) {
Taro.showToast({
title: '删除失败',
icon: 'error'
});
}
}
}
});
}
// 编辑文章
const handleEdit = (item: CmsArticle) => {
Taro.navigateTo({
url: `/shop/shopArticle/add?id=${item.articleId}`
});
}
// 查看文章详情
const handleView = (item: CmsArticle) => {
// 这里可以跳转到文章详情页面
Taro.navigateTo({
url: `/cms/detail/index?id=${item.articleId}`
})
}
// 获取状态标签
const getStatusTag = (status?: number) => {
switch (status) {
case 0:
return <Tag type="success"></Tag>
case 1:
return <Tag type="warning"></Tag>
case 2:
return <Tag type="danger"></Tag>
case 3:
return <Tag type="danger"></Tag>
default:
return <Tag></Tag>
}
}
// 加载更多
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false) // 不刷新,追加数据
}
}
useDidShow(() => {
reload(true).then()
});
return (
<ConfigProvider>
{/* 搜索栏 */}
<View className="py-2">
<SearchBar
placeholder="搜索关键词"
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
/>
</View>
{/* 统计信息 */}
{total > 0 && (
<View className="px-4 py-2 text-sm text-gray-500">
{total}
</View>
)}
{/* 文章列表 */}
<PullToRefresh
onRefresh={handleRefresh}
headHeight={60}
>
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
{list.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
<Empty
description="暂无文章数据"
style={{backgroundColor: 'transparent'}}
/>
</View>
) : (
<InfiniteLoading
target="article-scroll"
hasMore={hasMore}
onLoadMore={loadMore}
loadingText={
<View className="flex justify-center items-center py-4">
<Loading />
<View className="ml-2">...</View>
</View>
}
loadMoreText={
<View className="text-center py-4 text-gray-500">
{list.length === 0 ? "暂无数据" : "没有更多了"}
</View>
}
>
{list.map((item, index) => (
<CellGroup key={item.articleId || index} className="mb-4">
<Cell>
<View className="flex flex-col gap-3 w-full">
{/* 文章标题和状态 */}
<View className="flex justify-between items-start">
<View className="flex-1 pr-2">
<View className="text-lg font-bold text-gray-900 line-clamp-2">
{item.title}
</View>
</View>
{getStatusTag(item.status)}
</View>
{/* 文章概述 */}
{item.overview && (
<View className="text-sm text-gray-600 line-clamp-2">
{item.overview}
</View>
)}
{/* 文章信息 */}
<View className="flex justify-between items-center text-xs text-gray-500">
<View className="flex items-center gap-4">
<View>: {item.actualViews || 0}</View>
{item.price && <View>: ¥{item.price}</View>}
<View>: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
</View>
</View>
{/* 操作按钮 */}
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
<Button
size="small"
fill="outline"
icon={<Eye/>}
onClick={() => handleView(item)}
>
</Button>
<Button
size="small"
fill="outline"
icon={<Edit/>}
onClick={() => handleEdit(item)}
>
</Button>
<Button
size="small"
type="danger"
fill="outline"
icon={<Del/>}
onClick={() => handleDelete(item.articleId)}
>
</Button>
</View>
</View>
</Cell>
</CellGroup>
))}
</InfiniteLoading>
)}
</View>
</PullToRefresh>
{/* 底部浮动按钮 */}
<FixedButton
text="发布文章"
icon={<Edit />}
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
/>
</ConfigProvider>
);
};
export default ArticleArticleManage;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '管理中心'
})

295
src/admin/index.tsx Normal file
View File

@@ -0,0 +1,295 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
const {
dealerUser,
error,
refresh,
} = useDealerUser()
// 使用主题样式
const themeStyles = useThemeStyles()
// 导航到各个功能页面
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId} | : {dealerUser.refereeId || '无'}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
)
}
export default DealerIndex

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '实名审核'
})

View File

@@ -0,0 +1,319 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Space,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh,
Button,
Dialog,
Image,
ImagePreview,
TextArea
} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageUserVerify, updateUserVerify} from '@/api/system/userVerify'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {UserVerify} from "@/api/system/userVerify/model";
const UserVeirfyAdmin: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>(0)
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [list, setList] = useState<UserVerify[]>([])
const [rejectDialogVisible, setRejectDialogVisible] = useState<boolean>(false)
const [rejectReason, setRejectReason] = useState<string>('')
const [currentRecord, setCurrentRecord] = useState<ShopDealerWithdraw | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [showPreview2, setShowPreview2] = useState(false)
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// activeTab变化会自动触发useEffect重新获取数据无需手动调用
}
// 获取审核记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const currentStatus = Number(activeTab)
const result = await pageUserVerify({
page: 1,
limit: 100,
status: currentStatus // 后端筛选,提高性能
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record
}))
setList(processedRecords)
}
} catch (error) {
console.error('获取审核记录失败:', error)
Taro.showToast({
title: '获取审核记录失败',
icon: 'none'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, activeTab])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchWithdrawRecords()])
setRefreshing(false)
}
// 审核通过
const handleApprove = async (record: ShopDealerWithdraw) => {
try {
await updateUserVerify({
...record,
status: 1, // 审核通过
})
Taro.showToast({
title: '审核通过',
icon: 'success'
})
await fetchWithdrawRecords()
} catch (error: any) {
if (error !== 'cancel') {
console.error('审核通过失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
}
// 驳回申请
const handleReject = (record: ShopDealerWithdraw) => {
setCurrentRecord(record)
setRejectReason('')
setRejectDialogVisible(true)
}
// 确认驳回
const confirmReject = async () => {
if (!rejectReason.trim()) {
Taro.showToast({
title: '请输入驳回原因',
icon: 'none'
})
return
}
try {
await updateUserVerify({
...currentRecord!,
status: 2, // 驳回
comments: rejectReason.trim()
})
Taro.showToast({
title: '已驳回',
icon: 'success'
})
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
await fetchWithdrawRecords()
} catch (error: any) {
console.error('驳回失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchWithdrawRecords().then()
}
}, [fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 0:
return '待审核'
case 1:
return '审核通过'
case 2:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 0:
return 'warning'
case 1:
return 'success'
case 2:
return 'danger'
default:
return 'default'
}
}
const renderWithdrawRecords = () => {
console.log('渲染审核记录:', {loading, recordsCount: list.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{loading ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : list.length > 0 ? (
list.map(record => (
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<Space direction={'vertical'}>
<Text className="font-semibold text-gray-800">
{record.realName}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.phone}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.idCard}
</Text>
</Space>
<Tag type={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
</View>
<View className="flex gap-2 mb-2">
<Image src={record.sfz1} height={100} onClick={() => setShowPreview(true)}/>
<Image src={record.sfz2} height={100} onClick={() => setShowPreview2(true)}/>
</View>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview}
onClose={() => setShowPreview(false)}
/>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview2}
onClose={() => setShowPreview2(false)}
/>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.status == 1 && (
<Text className="block mt-1">
{record.updateTime}
</Text>
)}
{record.status == 2 && (
<Text className="block mt-1 text-red-500">
{record.comments}
</Text>
)}
</View>
{/* 操作按钮 */}
{record.status === 0 && (
<View className="flex gap-2 mt-3">
<Button
type="success"
size="small"
className="flex-1"
onClick={() => handleApprove(record)}
>
</Button>
<Button
type="danger"
size="small"
className="flex-1"
onClick={() => handleReject(record)}
>
</Button>
</View>
)}
</View>
))
) : (
<Empty description="暂无申请记录"/>
)}
</View>
</PullToRefresh>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="待审核" value="0">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已通过" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已驳回" value="2">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
{/* 驳回原因对话框 */}
<Dialog
visible={rejectDialogVisible}
title="驳回原因"
onCancel={() => {
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
}}
onConfirm={confirmReject}
>
<View className="p-4">
<TextArea
placeholder="请输入驳回原因"
value={rejectReason}
onChange={(value) => setRejectReason(value)}
maxLength={200}
rows={4}
/>
</View>
</Dialog>
</View>
)
}
export default UserVeirfyAdmin