feat(src): 新增文章、经销商申请、用户地址和礼物添加功能

- 新增文章添加页面,支持文章基本信息、设置、高级设置和图片上传
- 新增经销商申请页面,支持申请信息填写和审核状态显示
- 新增用户地址添加页面,支持地址信息填写和地址识别功能
- 新增礼物添加页面,功能与文章添加类似
- 统一使用 .tsx 文件格式
- 添加 .editorconfig、.eslintrc 和 .gitignore 文件,规范代码风格和项目结构
This commit is contained in:
2025-08-20 14:56:38 +08:00
commit 217bfacadd
507 changed files with 70034 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,249 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
function UserCard() {
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [couponCount, setCouponCount] = useState(0)
// const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
useEffect(() => {
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
showAuthModal();
}
}
});
}, []);
const loadUserStats = (userId: number) => {
// 加载优惠券数量
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
})
// 加载积分数量
console.log(userId)
// getUserPointsStats(userId)
// .then((res: any) => {
// setPointsCount(res.currentPoints || 0)
// })
// .catch((error: any) => {
// console.error('Points stats error:', error)
// })
// 加载礼品劵数量
setGiftCount(0)
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
// setGiftCount(res.count || 0)
// })
}
const reload = () => {
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
setUserInfo({
avatar,
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
getUserInfo().then((data) => {
if (data) {
setUserInfo(data)
setIsLogin(true);
Taro.setStorageSync('UserId', data.userId)
// 加载用户统计数据
if (data.userId) {
loadUserStats(data.userId)
}
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
}
}).catch(() => {
console.log('未登录')
});
}
});
};
const showAuthModal = () => {
Taro.showModal({
title: '授权提示',
content: '需要获取您的用户信息',
confirmText: '去授权',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击确认,打开授权设置页面
openSetting();
}
}
});
};
const openSetting = () => {
// Taro.openSetting调起客户端小程序设置界面返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户授权成功,可以获取用户信息
reload();
} else {
// 用户拒绝授权,提示授权失败
Taro.showToast({
title: '授权失败',
icon: 'none'
});
}
}
});
};
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token)
Taro.setStorageSync('UserId', res.data.data.user.userId)
setUserInfo(res.data.data.user)
setIsLogin(true)
}
})
} else {
console.log('登录失败!')
}
}
})
}
return (
<div className={'header-bg pt-20'}>
<div className={'p-4'}>
<div
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// width: '720rpx',
// margin: '10px auto 0px auto',
height: '170px',
// borderRadius: '22px 22px 0 0',
}}
>
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
<div className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
</Button>
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>
{getRoleName()}
</div>
</Tag>
</div>
) : ''}
</div>
</div>
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
</div>
</div>
<div className={'flex justify-around mt-1'}>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{couponCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{giftCount}</span>
</div>
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
{/*</div>*/}
</div>
</div>
</div>
</div>
)
}
export default UserCard;

View File

@@ -0,0 +1,186 @@
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, Scan} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
return (
<>
<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>
</View>
}>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Scan size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
style={{
display: 'none'
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<LogisticsError size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/address/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}></Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}></Text>
)}
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Ask size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/help/index')
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Tips size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/about/index')
}}
/>
</Cell.Group>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title="账号安全"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={onLogout}
/>
</Cell.Group>
</View>
</>
)
}
export default UserCell

View File

@@ -0,0 +1,102 @@
import {loginBySms} from "@/api/passport/login";
import {useState} from "react";
import Taro from '@tarojs/taro'
import {Popup} from '@nutui/nutui-react-taro'
import {UserParam} from "@/api/system/user/model";
import {Button} from '@nutui/nutui-react-taro'
import {Form, Input} from '@nutui/nutui-react-taro'
import {Copyright, Version} from "@/config/app";
const UserFooter = () => {
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
const [clickNum, setClickNum] = useState<number>(0)
const [FormData, setFormData] = useState<UserParam>(
{
phone: undefined,
password: undefined
}
)
const onLoginByPhone = () => {
setFormData({})
setClickNum(clickNum + 1);
if (clickNum > 10) {
setOpenLoginByPhone(true);
}
}
const closeLoginByPhone = () => {
setClickNum(0)
setOpenLoginByPhone(false)
}
// 提交表单
const submitByPhone = (values: any) => {
loginBySms({
phone: values.phone,
code: values.code
}).then(() => {
setOpenLoginByPhone(false);
setTimeout(() => {
Taro.reLaunch({
url: '/pages/index/index'
})
},1000)
})
}
return (
<>
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
<div className={'text-xs text-gray-400 py-1'}>{Version}</div>
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
</div>
<Popup
style={{width: '350px', padding: '10px'}}
visible={openLoginByPhone}
closeOnOverlayClick={false}
closeable={true}
onClose={closeLoginByPhone}
>
<Form
style={{width: '350px',padding: '10px'}}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitByPhone(values)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
</Button>
</div>
}
>
<Form.Item
label={'手机号码'}
name="phone"
required
rules={[{message: '手机号码'}]}
>
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
</Form.Item>
<Form.Item
label={'短信验证码'}
name="code"
required
rules={[{message: '短信验证码'}]}
>
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
</Form.Item>
</Form>
</Popup>
</>
)
}
export default UserFooter

View File

@@ -0,0 +1,69 @@
import {useEffect} from "react";
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components';
import {ArrowRight, Wallet, Comment, Transit, Refund, Package} from '@nutui/icons-react-taro';
function UserOrder() {
const reload = () => {
};
useEffect(() => {
reload()
}, []);
return (
<>
<View className={'px-4 pb-2'}>
<View
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// margin: '10px auto 0px auto',
height: '120px',
// borderRadius: '22px 22px 0 0',
}}
>
<View className={'title-bar flex justify-between pt-2'}>
<Text className={'title font-medium px-4'}></Text>
<View className={'more flex items-center px-2'} onClick={() => navTo('/user/order/order', true)}>
<Text className={'text-xs text-gray-500'}></Text>
<ArrowRight color="#cccccc" size={12}/>
</View>
</View>
<View className={'flex justify-around pb-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=0', true)}>
<Wallet size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=1', true)}>
<Package size={26} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=3', true)}>
<Transit size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=5', true)}>
<Comment size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=6', true)}>
<Refund size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}>退/</Text>
</View>
</View>
</View>
</View>
</>
)
}
export default UserOrder;

View File

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

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

@@ -0,0 +1,25 @@
import {useEffect} from 'react'
import {useUser} from "@/hooks/useUser";
import {Text} from '@tarojs/components';
function Admin() {
const {
isAdmin
} = useUser();
useEffect(() => {
}, []);
if (!isAdmin()) {
return (
<Text></Text>
);
}
return (
<>
<Text>...</Text>
</>
)
}
export default Admin