refactor(admin): 移除文章管理相关页面和配置

- 删除文章管理页面组件及配置文件
- 删除收货地址相关页面配置文件
- 更新应用配置中的导航栏颜色和图标路径
- 删除搜索页面的商品项样式和组件
- 删除分类页面的商品列表组件
- 删除多个页面的配置文件和样式文件
This commit is contained in:
2026-03-11 19:28:16 +08:00
parent e990d4e4ef
commit faed75fc70
49 changed files with 12 additions and 3674 deletions

View File

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

View File

@@ -1,323 +0,0 @@
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

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

View File

@@ -1,271 +0,0 @@
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

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

View File

@@ -1,157 +0,0 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {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)'
}}>
{dealerUser.mobile}
</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>
)}
{/* 功能导航 */}
<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('/admin/users/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>
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
)
}
export default DealerIndex

View File

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

View File

@@ -1,319 +0,0 @@
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

View File

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

View File

@@ -1,256 +0,0 @@
import {useRef, useState} from 'react'
import Taro, {useDidShow} from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {
Avatar,
Button,
Empty,
InfiniteLoading,
Loading,
PullToRefresh,
SearchBar,
Tag
} from '@nutui/nutui-react-taro'
import type {User} from '@/api/system/user/model'
import {pageUsers} from '@/api/system/user'
import {listUserRole, updateUserRole} from '@/api/system/userRole'
import {listRoles} from '@/api/system/role'
const PAGE_SIZE = 10
const AdminUsers = () => {
const [searchValue, setSearchValue] = useState('')
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const roleIdMapRef = useRef<Record<string, number>>({})
const roleMapLoadedRef = useRef(false)
const getRoleIdByCode = async (roleCode: string) => {
if (!roleMapLoadedRef.current) {
const roles = await listRoles()
const nextMap: Record<string, number> = {}
roles?.forEach(role => {
if (role.roleCode && role.roleId) nextMap[role.roleCode] = role.roleId
})
roleIdMapRef.current = nextMap
roleMapLoadedRef.current = true
}
return roleIdMapRef.current[roleCode]
}
const reload = async (isRefresh = false, overrideKeywords?: string) => {
if (loading) return
if (isRefresh) {
setPage(1)
setUsers([])
setHasMore(true)
}
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const res = await pageUsers({
page: currentPage,
limit: PAGE_SIZE,
keywords: overrideKeywords ?? searchValue
})
if (res?.list) {
const nextUsers = isRefresh ? res.list : [...users, ...res.list]
setUsers(nextUsers)
setTotal(res.count || 0)
setHasMore(res.list.length === PAGE_SIZE)
setPage(isRefresh ? 2 : currentPage + 1)
} else {
setUsers([])
setTotal(0)
setHasMore(false)
}
} catch (error) {
console.error('获取用户列表失败:', error)
Taro.showToast({title: '获取用户列表失败', icon: 'error'})
} finally {
setLoading(false)
}
}
const getUserRoleCodes = (target: User): string[] => {
const fromRoles = target.roles?.map(r => r.roleCode).filter(Boolean) as string[] | undefined
const fromSingle = target.roleCode ? [target.roleCode] : []
const merged = [...(fromRoles || []), ...fromSingle].filter(Boolean)
return Array.from(new Set(merged))
}
const getPrimaryRoleCode = (target: User): string | undefined => {
const codes = getUserRoleCodes(target)
if (codes.includes('superAdmin')) return 'superAdmin'
if (codes.includes('admin')) return 'admin'
if (codes.includes('dealer')) return 'dealer'
if (codes.includes('user')) return 'user'
return codes[0]
}
const renderRoleTag = (target: User) => {
const code = getPrimaryRoleCode(target)
if (code === 'superAdmin') return <Tag type="danger"></Tag>
if (code === 'admin') return <Tag type="danger"></Tag>
if (code === 'dealer') return <Tag type="primary"></Tag>
if (code === 'user') return <Tag type="success"></Tag>
return <Tag></Tag>
}
const toggleRole = async (target: User) => {
const current = getPrimaryRoleCode(target)
const nextRoleCode = current === 'dealer' ? 'user' : 'dealer'
const nextRoleName = nextRoleCode === 'user' ? '注册会员' : '业务员'
const confirmRes = await Taro.showModal({
title: '确认切换角色',
content: `确定将该用户切换为「${nextRoleName}」吗?`
})
if (!confirmRes.confirm) return
try {
const userId = target.userId
if (!userId) return
const nextRoleId = await getRoleIdByCode(nextRoleCode)
if (!nextRoleId) {
throw new Error(`未找到角色配置:${nextRoleCode}`)
}
const roles = await listUserRole({userId})
const candidate = roles?.find(r => r.roleCode === 'dealer' || r.roleCode === 'user')
if (candidate) {
await updateUserRole({
...candidate,
roleId: nextRoleId
})
} else {
await updateUserRole({
userId,
roleId: nextRoleId
})
}
Taro.showToast({title: '切换成功', icon: 'success'})
await reload(true)
} catch (error) {
console.error('切换角色失败:', error)
Taro.showToast({title: '切换失败', icon: 'error'})
}
}
const handleSearch = (value: string) => {
setSearchValue(value)
reload(true, value).then()
}
const loadMore = async () => {
if (!loading && hasMore) {
await reload(false)
}
}
useDidShow(() => {
const init = async () => {
try {
await reload(true)
} catch (error) {
console.error('初始化失败:', error)
Taro.showToast({title: '初始化失败', icon: 'error'})
}
}
init().then()
})
return (
<View className="bg-gray-50 min-h-screen">
<View className="py-2 px-3">
<SearchBar
placeholder="搜索昵称/手机号/UID"
value={searchValue}
onChange={setSearchValue}
onSearch={handleSearch}
/>
</View>
{total > 0 && (
<View className="px-4 py-2 text-sm text-gray-500">
{total}
</View>
)}
<PullToRefresh onRefresh={() => reload(true)} headHeight={60}>
<View className="px-4" style={{height: 'calc(100vh - 190px)', overflowY: 'auto'}} id="users-scroll">
{users.length === 0 && !loading ? (
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 260px)'}}>
<Empty description="暂无成员数据" style={{backgroundColor: 'transparent'}}/>
</View>
) : (
<InfiniteLoading
target="users-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">
{users.length === 0 ? '暂无数据' : '没有更多了'}
</View>
}
>
{users.map((item, index) => {
const displayName = item.alias || item.nickname || item.realName || item.username || `用户${item.userId || ''}`
const phone = item.phone || item.mobile || '-'
const primaryRole = getPrimaryRoleCode(item)
const toggleText = primaryRole === 'dealer' ? '设为注册会员' : '设为业务员'
return (
<View key={item.userId || index} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center">
<Avatar size="40" src={item.avatar || item.avatarUrl} className="mr-3"/>
<View className="flex-1">
<View className="flex items-center justify-between">
<Text className="font-semibold text-gray-800">{displayName}</Text>
{renderRoleTag(item)}
</View>
<View className="text-xs text-gray-500 mt-1">
UID{item.userId || '-'} · {phone}
</View>
</View>
</View>
<View className="flex justify-end gap-2 pt-3 mt-3 border-t border-gray-100">
<Button
size="small"
fill="outline"
onClick={() => toggleRole(item)}
>
{toggleText}
</Button>
</View>
</View>
)
})}
</InfiniteLoading>
)}
</View>
</PullToRefresh>
</View>
)
}
export default AdminUsers

View File

@@ -75,39 +75,39 @@ export default defineAppConfig({
],
window: {
backgroundTextStyle: 'dark',
navigationBarBackgroundColor: '#1890FF',
navigationBarBackgroundColor: '#2c2c2c',
navigationBarTitleText: '工具箱',
navigationBarTextStyle: 'white'
},
tabBar: {
custom: false,
color: "#999999",
selectedColor: "#1890FF",
color: "#dbdbdb",
selectedColor: "#2c2c2c",
backgroundColor: "#F8F8F8",
borderStyle: "black",
list: [
{
pagePath: "pages/index/index",
iconPath: "assets/tabbar/home.png",
selectedIconPath: "assets/tabbar/home-active.png",
iconPath: "assets/icon/home.png",
selectedIconPath: "assets/icon/home-active.png",
text: "首页",
},
{
pagePath: "pages/message/message",
iconPath: "assets/tabbar/message.png",
selectedIconPath: "assets/tabbar/message-active.png",
iconPath: "assets/icon/message.png",
selectedIconPath: "assets/icon/message-active.png",
text: "消息",
},
{
pagePath: "pages/toolbox/toolbox",
iconPath: "assets/tabbar/toolbox.png",
selectedIconPath: "assets/tabbar/toolbox-active.png",
iconPath: "assets/icon/toolbox.png",
selectedIconPath: "assets/icon/toolbox-active.png",
text: "工具箱",
},
{
pagePath: "pages/user/user",
iconPath: "assets/tabbar/user.png",
selectedIconPath: "assets/tabbar/user-active.png",
iconPath: "assets/icon/user.png",
selectedIconPath: "assets/icon/user-active.png",
text: "我的",
},
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

BIN
src/assets/icon/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

BIN
src/assets/icon/message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

BIN
src/assets/icon/toolbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

BIN
src/assets/icon/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

View File

@@ -1,3 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '消息',
navigationBarBackgroundColor: '#1890FF',
});

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '忘记密码',
navigationBarTextStyle: 'black'
})

View File

@@ -1,36 +0,0 @@
import {useEffect} from "react";
import Taro from '@tarojs/taro'
import {Input, Button} from '@nutui/nutui-react-taro'
import {copyText} from "@/utils/common";
const Register = () => {
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'flex flex-col justify-center px-5 pt-3'}>
<div className={'text-sm py-2'}></div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="新的密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex justify-between items-center bg-white rounded-lg my-2 pr-2'}>
<Input type="text" placeholder="短信验证码" style={{ backgroundColor: '#ffffff', borderRadius: '8px'}}/>
<Button onClick={() => copyText('https://site-10398.shoplnk.cn?v=1.33')}></Button>
</div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'}></Button>
</div>
</div>
</>
)
}
export default Register

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '注册账号',
navigationBarTextStyle: 'black'
})

View File

@@ -1,47 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
const Register = () => {
const [isAgree, setIsAgree] = useState(false)
const reload = () => {
Taro.hideTabBar()
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'flex flex-col justify-center px-5 pt-3'}>
<div className={'text-xl font-bold py-2'}>14</div>
<div className={'text-sm py-1 font-normal text-gray-500'}></div>
<div className={'text-sm pb-4 font-normal text-gray-500'}>
WebSoft为您提供独立站的解决方案
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="text" placeholder="手机号" maxLength={11} style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex flex-col justify-between items-center my-2'}>
<Input type="password" placeholder="再次输入密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
</div>
<div className={'flex justify-center my-5'}>
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}></Button>
</div>
<div className={'my-2 flex text-sm items-center px-1'}>
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}></span>
<a onClick={() => Taro.navigateTo({url: '/passport/agreement'})} className={'text-blue-600'}></a>
</div>
</div>
<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>
<a className={'text-blue-600'} onClick={() => Taro.navigateBack()}></a>
</div>
</>
)
}
export default Register

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '服务配置',
navigationBarTextStyle: 'black'
})

View File

@@ -1,82 +0,0 @@
import {useEffect, useState} from "react";
import Taro from '@tarojs/taro'
import {Input, Button,Form} from '@nutui/nutui-react-taro'
const Setting = () => {
const [FormData, setFormData] = useState<any>(
{
domain: undefined
}
)
// 提交表单
const submitSucceed = (values: any) => {
if(values.domain){
Taro.setStorageSync('ServerUrl',values.domain)
setFormData({
domain: values.domain
})
Taro.showToast({
title: '保存成功',
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack()
},500)
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
// Taro.showToast({ title: error[0].message, icon: 'error' })
}
const reload = () => {
Taro.hideTabBar()
if (Taro.getStorageSync('ServerUrl')) {
setFormData({
domain: Taro.getStorageSync('ServerUrl')
})
}
}
useEffect(() => {
reload()
}, [])
return (
<>
<Form
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block type="info" size={'large'}>
</Button>
</div>
}
>
<div className={'flex flex-col justify-center pt-3'}>
<div className={'text-sm py-1 px-4'}></div>
<Form.Item
name="domain"
initialValue={FormData.domain}
rules={[{message: '请输入服务域名'}]}
>
<Input placeholder="https://domain.com/api" type="text" style={{backgroundColor: '#f5f5f5', borderRadius: '8px', padding: '5px 10px'}}/>
</Form.Item>
</div>
</Form>
</>
)
}
export default Setting

View File

@@ -1,31 +0,0 @@
import { useEffect, useState } from 'react'
import { Swiper } from '@nutui/nutui-react-taro'
import {CmsAd} from "@/api/cms/cmsAd/model";
import {Image} from '@nutui/nutui-react-taro'
import {getCmsAd} from "@/api/cms/cmsAd";
const MyPage = () => {
const [item, setItem] = useState<CmsAd>()
const reload = () => {
getCmsAd(439).then(data => {
setItem(data)
})
}
useEffect(() => {
reload()
}, [])
return (
<>
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
{item?.imageList?.map((item) => (
<Swiper.Item key={item}>
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
</Swiper.Item>
))}
</Swiper>
</>
)
}
export default MyPage

View File

@@ -1,51 +0,0 @@
import {Image} from '@nutui/nutui-react-taro'
import {Share} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import './GoodsList.scss'
const GoodsList = (props: any) => {
return (
<>
<div className={'py-3'}>
<div className={'flex flex-col justify-between items-center rounded-lg px-2'}>
{props.data?.map((item, index) => {
return (
<div key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
radius="10px 10px 0 0" height="180"
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
<div className={'flex flex-col p-2 rounded-lg'}>
<div>
<div className={'car-no text-sm'}>{item.name}</div>
<div className={'flex justify-between text-xs py-1'}>
<span className={'text-orange-500'}>{item.comments}</span>
<span className={'text-gray-400'}> {item.sales}</span>
</div>
<div className={'flex justify-between items-center py-2'}>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{item.price}</span>
</div>
<div className={'buy-btn'}>
<div className={'cart-icon'}>
<Share size={20} className={'mx-4 mt-2'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
</div>
<div className={'text-white pl-4 pr-5'}
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>
</div>
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</>
)
}
export default GoodsList

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品分类',
navigationBarTextStyle: 'black'
})

View File

@@ -1,78 +0,0 @@
import Taro from '@tarojs/taro'
import GoodsList from './components/GoodsList'
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
import {Loading} from '@nutui/nutui-react-taro'
import {useEffect, useState} from "react"
import {useRouter} from '@tarojs/taro'
import './index.scss'
import {pageShopGoods} from "@/api/shop/shopGoods"
import {ShopGoods} from "@/api/shop/shopGoods/model"
import {getCmsNavigation} from "@/api/cms/cmsNavigation";
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
function Category() {
const {params} = useRouter();
const [categoryId, setCategoryId] = useState<number>(0)
const [loading, setLoading] = useState<boolean>(true)
const [nav, setNav] = useState<CmsNavigation>()
const [list, setList] = useState<ShopGoods[]>([])
const reload = async () => {
// 1.加载远程数据
const id = Number(params.id)
const nav = await getCmsNavigation(id)
const shopGoods = await pageShopGoods({categoryId: id})
// 2.处理业务逻辑
setCategoryId(id)
setNav(nav)
setList(shopGoods?.list || [])
// 3.设置标题
Taro.setNavigationBarTitle({
title: `${nav?.categoryName}`
})
};
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []);
useShareTimeline(() => {
return {
title: `${nav?.categoryName}_南南佐顿门窗`,
path: `/shop/category/index?id=${categoryId}`
};
});
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_南南佐顿门窗`,
path: `/shop/category/index?id=${categoryId}`,
success: function (res) {
console.log('分享成功', res);
},
fail: function (res) {
console.log('分享失败', res);
}
};
});
if (loading) {
return (
<Loading className={'px-2 text-center'}></Loading>
)
}
return (
<>
<div className={'flex flex-col'}>
<GoodsList data={list} nav={nav}/>
</div>
</>
)
}
export default Category

View File

@@ -1,5 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品详情',
navigationBarTextStyle: 'black',
navigationStyle: 'custom'
})

View File

@@ -1,17 +0,0 @@
.cart-icon{
background: linear-gradient(to bottom, #bbe094, #4ee265);
border-radius: 100px 0 0 100px;
height: 70px;
}
/* 去掉 RichText 中图片的间距 */
rich-text img {
margin: 0 !important;
padding: 0 !important;
display: block;
}
/* 在全局样式或组件样式文件中 */
.no-margin {
margin: 0 !important; /* 使用 !important 来确保覆盖默认样式 */
}

View File

@@ -1,334 +0,0 @@
import {useEffect, useState} from "react";
import {Image, Divider, Badge} from "@nutui/nutui-react-taro";
import {ArrowLeft, Headphones, Share, Cart} from "@nutui/icons-react-taro";
import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro";
import {RichText, View} from '@tarojs/components'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {listShopGoodsSpec} from "@/api/shop/shopGoodsSpec";
import {ShopGoodsSpec} from "@/api/shop/shopGoodsSpec/model";
import {listShopGoodsSku} from "@/api/shop/shopGoodsSku";
import {ShopGoodsSku} from "@/api/shop/shopGoodsSku/model";
import {Swiper} from '@nutui/nutui-react-taro'
import navTo, {wxParse} from "@/utils/common";
import SpecSelector from "@/components/SpecSelector";
import "./index.scss";
import {useCart} from "@/hooks/useCart";
const GoodsDetail = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [files, setFiles] = useState<any[]>([]);
const [specs, setSpecs] = useState<ShopGoodsSpec[]>([]);
const [skus, setSkus] = useState<ShopGoodsSku[]>([]);
const [showSpecSelector, setShowSpecSelector] = useState(false);
const [specAction, setSpecAction] = useState<'cart' | 'buy'>('cart');
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
const [loading, setLoading] = useState(false);
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.id;
// 使用购物车Hook
const {cartCount, addToCart} = useCart();
// 处理加入购物车
const handleAddToCart = () => {
if (!goods) return;
if (!Taro.getStorageSync('UserId')) {
return Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
}
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('cart');
setShowSpecSelector(true);
return;
}
// 没有规格,直接加入购物车
addToCart({
goodsId: goods.goodsId!,
name: goods.name || '',
price: goods.price || '0',
image: goods.image || ''
});
};
// 处理立即购买
const handleBuyNow = () => {
if (!goods) return;
if (!Taro.getStorageSync('UserId')) {
return Taro.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
}
// 如果有规格,显示规格选择器
if (specs.length > 0) {
setSpecAction('buy');
setShowSpecSelector(true);
return;
}
// 没有规格,直接购买
navTo(`/shop/orderConfirm/index?goodsId=${goods?.goodsId}`, true);
};
// 规格选择确认回调
const handleSpecConfirm = (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
// setSelectedSku(sku);
setShowSpecSelector(false);
if (action === 'cart') {
// 加入购物车
addToCart({
goodsId: goods!.goodsId!,
skuId: sku.id,
name: goods!.name || '',
price: sku.price || goods!.price || '0',
image: goods!.image || '',
specInfo: sku.sku, // sku字段包含规格信息
}, quantity);
} else if (action === 'buy') {
// 立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
} else {
// 默认情况如果action未定义默认为立即购买
const orderData = {
goodsId: goods!.goodsId!,
skuId: sku.id,
quantity,
price: sku.price || goods!.price || '0'
};
navTo(`/shop/orderConfirm/index?orderData=${encodeURIComponent(JSON.stringify(orderData))}`, true);
}
};
useEffect(() => {
if (goodsId) {
setLoading(true);
// 加载商品详情
getShopGoods(Number(goodsId))
.then((res) => {
// 处理富文本内容,去掉图片间距
if (res.content) {
res.content = wxParse(res.content);
}
setGoods(res);
if (res.files) {
const arr = JSON.parse(res.files);
arr.length > 0 && setFiles(arr);
}
})
.catch((error) => {
console.error("Failed to fetch goods detail:", error);
})
.finally(() => {
setLoading(false);
});
// 加载商品规格
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
.then((data) => {
setSpecs(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods specs:", error);
});
// 加载商品SKU
listShopGoodsSku({goodsId: Number(goodsId)} as any)
.then((data) => {
setSkus(data || []);
})
.catch((error) => {
console.error("Failed to fetch goods skus:", error);
});
}
}, [goodsId]);
// 分享给好友
useShareAppMessage(() => {
return {
title: goods?.name || '精选商品',
path: `/shop/goodsDetail/index?id=${goodsId}`,
imageUrl: goods?.image, // 分享图片
success: function (res: any) {
console.log('分享成功', res);
Taro.showToast({
title: '分享成功',
icon: 'success',
duration: 2000
});
},
fail: function (res: any) {
console.log('分享失败', res);
Taro.showToast({
title: '分享失败',
icon: 'none',
duration: 2000
});
}
};
});
// 分享到朋友圈
useShareTimeline(() => {
return {
title: `${goods?.name || '精选商品'} - 南南佐顿门窗`,
path: `/shop/goodsDetail/index?id=${goodsId}`,
imageUrl: goods?.image
};
});
if (!goods || loading) {
return <div>...</div>;
}
return (
<div className={"py-0"}>
<div
className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-70"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: "50px",
left: "10px",
}}
onClick={() => Taro.navigateBack()}
>
<ArrowLeft size={14}/>
</div>
<div className={
"fixed z-10 bg-white flex justify-center items-center font-bold shadow-sm opacity-90"
}
style={{
borderRadius: "100%",
width: "32px",
height: "32px",
top: "50px",
right: "110px",
}}
onClick={() => Taro.switchTab({url: `/pages/cart/cart`})}>
<Badge value={cartCount} top="-2" right="2">
<div style={{display: 'flex', alignItems: 'center'}}>
<Cart size={16}/>
</div>
</Badge>
</div>
{
files.length > 0 && (
<Swiper defaultValue={0} indicator height={'350px'}>
{files.map((item) => (
<Swiper.Item key={item}>
<Image width="100%" height={'100%'} src={item.url} mode={'scaleToFill'} lazyLoad={false}/>
</Swiper.Item>
))}
</Swiper>
)
}
{
files.length == 0 && (
<Image
src={goods.image}
mode={"scaleToFill"}
radius="10px 10px 0 0"
height="300"
/>
)
}
<div
className={"flex flex-col justify-between items-center rounded-lg px-2"}
>
<div
className={
"flex flex-col rounded-lg bg-white shadow-sm w-full mt-2"
}
>
<div className={"flex flex-col p-2 rounded-lg"}>
<>
<div className={'flex justify-between'}>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{goods.price}</span>
</div>
<span className={"text-gray-400 text-xs"}> {goods.sales}</span>
</div>
<div className={'flex justify-between items-center'}>
<div className={'goods-info'}>
<div className={"car-no text-lg"}>
{goods.name}
</div>
<div className={"flex justify-between text-xs py-1"}>
<span className={"text-orange-500"}>
{goods.comments}
</span>
</div>
</div>
<button
className={'flex flex-col justify-center items-center text-gray-500 px-1 gap-1 text-nowrap whitespace-nowrap'}
open-type="share"><Share
size={20}/>
<span className={'text-xs'}></span>
</button>
</div>
</>
</div>
</div>
<div className={"mt-2 py-2"}>
<Divider></Divider>
<RichText nodes={goods.content || '内容详情'}/>
</div>
</div>
{/*底部购买按钮*/}
<div className={'fixed bg-white w-full bottom-0 left-0 pt-4 pb-10'}>
<View className={'btn-bar flex justify-between items-center'}>
<div className={'flex justify-center items-center mx-4'}>
<button open-type="contact" className={'flex items-center'}>
<Headphones size={18} style={{marginRight: '4px'}}/>
</button>
</div>
<div className={'buy-btn mx-4'}>
<div className={'cart-add px-4 text-sm'}
onClick={() => handleAddToCart()}>
</div>
<div className={'cart-buy pl-4 pr-5 text-sm'}
onClick={() => handleBuyNow()}>
</div>
</div>
</View>
</div>
{/* 规格选择器 */}
{showSpecSelector && (
<SpecSelector
goods={goods!}
specs={specs}
skus={skus}
action={specAction}
onConfirm={handleSpecConfirm}
onClose={() => setShowSpecSelector(false)}
/>
)}
</div>
);
};
export default GoodsDetail;

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarTextStyle: 'black'
})

View File

@@ -1,116 +0,0 @@
.order-confirm-page {
padding-bottom: 100px; // 留出底部固定按钮的空间
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.error-text {
color: #999;
margin-bottom: 20px;
font-size: 14px;
}
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.total-price {
display: flex;
align-items: center;
}
.submit-btn {
width: 150px;
}
}
}
.address-bottom-line{
width: 100%;
border-radius: 12rpx 12rpx 0 0;
background: #fff;
padding: 26rpx 49rpx 0 34rpx;
position: relative;
&:before {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5px;
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
#1989fa 45%, transparent 0, transparent 50%);
background-size: 120px;
content: "";
}
}
// 优惠券弹窗样式
.coupon-popup {
height: 100%;
display: flex;
flex-direction: column;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #333;
}
&__content {
flex: 1;
overflow-y: auto;
}
&__loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #999;
font-size: 14px;
}
&__current {
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #f0f0f0;
&-title {
font-size: 28rpx;
color: #666;
margin-bottom: 8px;
}
&-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8rpx 12rpx;
background: #fff;
border-radius: 6rpx;
font-size: 28rpx;
}
}
}

View File

@@ -1,616 +0,0 @@
import {useEffect, useState} from "react";
import {
Image,
Button,
Cell,
CellGroup,
Input,
Space,
ActionSheet,
Popup,
InputNumber,
ConfigProvider
} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {View, Text} from '@tarojs/components';
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import './index.scss'
import Gap from "@/components/Gap";
import {selectPayment} from "@/api/system/payment";
import {Payment} from "@/api/system/payment/model";
import {PaymentHandler, PaymentType, buildSingleGoodsOrder} from "@/utils/payment";
import OrderConfirmSkeleton from "@/components/OrderConfirmSkeleton";
import CouponList from "@/components/CouponList";
import {CouponCardProps} from "@/components/CouponCard";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {
transformCouponData,
calculateCouponDiscount,
isCouponUsable,
getCouponUnusableReason,
sortCoupons,
filterUsableCoupons,
filterUnusableCoupons
} from "@/utils/couponUtils";
import navTo from "@/utils/common";
const OrderConfirm = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [address, setAddress] = useState<ShopUserAddress>()
const [payments, setPayments] = useState<any[]>([])
const [payment, setPayment] = useState<Payment>()
const [isVisible, setIsVisible] = useState<boolean>(false)
const [quantity, setQuantity] = useState<number>(1)
const [orderRemark, setOrderRemark] = useState<string>('')
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string>('')
const [payLoading, setPayLoading] = useState<boolean>(false)
// InputNumber 主题配置
const customTheme = {
nutuiInputnumberButtonWidth: '28px',
nutuiInputnumberButtonHeight: '28px',
nutuiInputnumberInputWidth: '40px',
nutuiInputnumberInputHeight: '28px',
nutuiInputnumberInputBorderRadius: '4px',
nutuiInputnumberButtonBorderRadius: '4px',
}
// 优惠券相关状态
const [selectedCoupon, setSelectedCoupon] = useState<CouponCardProps | null>(null)
const [couponVisible, setCouponVisible] = useState<boolean>(false)
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
const [couponLoading, setCouponLoading] = useState<boolean>(false)
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
// 计算商品总价
const getGoodsTotal = () => {
if (!goods) return 0
return parseFloat(goods.price || '0') * quantity
}
// 计算优惠券折扣
const getCouponDiscount = () => {
if (!selectedCoupon || !goods) return 0
const total = getGoodsTotal()
return calculateCouponDiscount(selectedCoupon, total)
}
// 计算实付金额
const getFinalPrice = () => {
const total = getGoodsTotal()
const discount = getCouponDiscount()
return Math.max(0, total - discount)
}
const handleSelect = (item: any) => {
setPayment(payments.find(payment => payment.name === item.name))
setIsVisible(false)
}
// 处理数量变化
const handleQuantityChange = (value: string | number) => {
const newQuantity = typeof value === 'string' ? parseInt(value) || 1 : value
const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
setQuantity(finalQuantity)
// 数量变化时,重新排序优惠券并检查当前选中的优惠券是否还可用
if (availableCoupons.length > 0) {
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
setAvailableCoupons(sortedCoupons)
// 检查当前选中的优惠券是否还可用
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
setSelectedCoupon(null)
Taro.showToast({
title: '当前优惠券不满足使用条件,已自动取消',
icon: 'none'
})
}
}
}
// 处理优惠券选择
const handleCouponSelect = (coupon: CouponCardProps) => {
const total = getGoodsTotal()
// 检查是否可用
if (!isCouponUsable(coupon, total)) {
const reason = getCouponUnusableReason(coupon, total)
Taro.showToast({
title: reason || '优惠券不可用',
icon: 'none'
})
return
}
setSelectedCoupon(coupon)
setCouponVisible(false)
Taro.showToast({
title: '优惠券选择成功',
icon: 'success'
})
}
// 取消选择优惠券
const handleCouponCancel = () => {
setSelectedCoupon(null)
Taro.showToast({
title: '已取消使用优惠券',
icon: 'success'
})
}
// 加载用户优惠券
const loadUserCoupons = async () => {
try {
setCouponLoading(true)
// 使用新的API获取可用优惠券
const res = await getMyAvailableCoupons()
if (res && res.length > 0) {
// 转换数据格式
const transformedCoupons = res.map(transformCouponData)
// 按优惠金额排序
const total = getGoodsTotal()
const sortedCoupons = sortCoupons(transformedCoupons, total)
setAvailableCoupons(sortedCoupons)
console.log('加载优惠券成功:', {
originalData: res,
transformedData: transformedCoupons,
sortedData: sortedCoupons
})
} else {
setAvailableCoupons([])
console.log('暂无可用优惠券')
}
} catch (error) {
console.error('加载优惠券失败:', error)
setAvailableCoupons([])
Taro.showToast({
title: '加载优惠券失败',
icon: 'none'
})
} finally {
setCouponLoading(false)
}
}
/**
* 统一支付入口
*/
const onPay = async (goods: ShopGoods) => {
try {
setPayLoading(true)
// 基础校验
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
})
return;
}
if (!payment) {
Taro.showToast({
title: '请选择支付方式',
icon: 'error'
})
return;
}
// 库存校验
if (goods.stock !== undefined && quantity > goods.stock) {
Taro.showToast({
title: '商品库存不足',
icon: 'error'
})
return;
}
// 优惠券校验
if (selectedCoupon) {
const total = getGoodsTotal()
if (!isCouponUsable(selectedCoupon, total)) {
const reason = getCouponUnusableReason(selectedCoupon, total)
Taro.showToast({
title: reason || '优惠券不可用',
icon: 'error'
})
return;
}
}
// 构建订单数据
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
// 确保couponId是数字类型
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
}
);
// 根据支付方式选择支付类型
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
console.log('开始支付:', {
orderData,
paymentType,
selectedCoupon: selectedCoupon ? {
id: selectedCoupon.id,
title: selectedCoupon.title,
discount: getCouponDiscount()
} : null,
finalPrice: getFinalPrice()
});
// 执行支付 - 移除这里的成功提示让PaymentHandler统一处理
await PaymentHandler.pay(orderData, paymentType);
// ✅ 移除双重成功提示 - PaymentHandler会处理成功提示
// Taro.showToast({
// title: '支付成功',
// icon: 'success'
// })
} catch (error: any) {
return navTo('/user/order/order?statusFilter=0', true)
// console.error('支付失败:', error)
// 只处理PaymentHandler未处理的错误
// if (!error.handled) {
// let errorMessage = '支付失败,请重试';
//
// // 根据错误类型提供具体提示
// if (error.message?.includes('余额不足')) {
// errorMessage = '账户余额不足,请充值后重试';
// } else if (error.message?.includes('优惠券')) {
// errorMessage = '优惠券使用失败,请重新选择';
// } else if (error.message?.includes('库存')) {
// errorMessage = '商品库存不足,请减少购买数量';
// } else if (error.message?.includes('地址')) {
// errorMessage = '收货地址信息有误,请重新选择';
// } else if (error.message) {
// errorMessage = error.message;
// }
// Taro.showToast({
// title: errorMessage,
// icon: 'error'
// })
// console.log('跳去未付款的订单列表页面')
// }
} finally {
setPayLoading(false)
}
};
// 统一的数据加载函数
const loadAllData = async () => {
try {
setLoading(true)
setError('')
// 分别加载数据,避免类型推断问题
let goodsRes: ShopGoods | null = null
if (goodsId) {
goodsRes = await getShopGoods(Number(goodsId))
}
const [addressRes, paymentRes] = await Promise.all([
listShopUserAddress({isDefault: true}),
selectPayment({})
])
// 设置商品信息
if (goodsRes) {
setGoods(goodsRes)
}
// 设置默认收货地址
if (addressRes && addressRes.length > 0) {
setAddress(addressRes[0])
}
// 设置支付方式
if (paymentRes && paymentRes.length > 0) {
setPayments(paymentRes.map((d) => ({
type: d.type,
name: d.name
})))
setPayment(paymentRes[0])
}
// 加载优惠券(在商品信息加载完成后)
if (goodsRes) {
await loadUserCoupons()
}
} catch (err) {
console.error('加载数据失败:', err)
setError('加载数据失败,请重试')
} finally {
setLoading(false)
}
}
useDidShow(() => {
loadAllData()
})
useEffect(() => {
loadAllData()
}, [goodsId]);
// 重新加载数据
const handleRetry = () => {
loadAllData()
}
// 错误状态
if (error) {
return (
<View className="order-confirm-page">
<View className="error-state">
<Text className="error-text">{error}</Text>
<Button onClick={handleRetry}></Button>
</View>
</View>
)
}
// 加载状态
if (loading || !goods) {
return <OrderConfirmSkeleton/>
}
return (
<div className={'order-confirm-page'}>
<CellGroup>
{
address && (
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location className={'text-gray-500'}/>
<View className={'flex flex-col w-full justify-between items-start'}>
<Space className={'flex flex-row w-full'}>
<View className={'flex-wrap text-nowrap whitespace-nowrap text-gray-500'}></View>
<View className={'font-medium text-sm flex items-center w-full'}>
<View
style={{width: '64%'}}>{address.province} {address.city} {address.region} {address.address}</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
</Space>
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
</View>
</Space>
</Cell>
)
}
{!address && (
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
</Space>
</Cell>
)}
</CellGroup>
<CellGroup>
<Cell key={goods.goodsId}>
<View className={'flex w-full justify-between gap-3'}>
<View>
<Image src={goods.image} mode={'aspectFill'} style={{
width: '80px',
height: '80px',
}} lazyLoad={false}/>
</View>
<View className={'flex flex-col w-full'} style={{width: '100%'}}>
<Text className={'font-medium w-full'}>{goods.name}</Text>
<Text className={'number text-gray-400 text-sm py-2'}>80g/</Text>
<View className={'flex justify-between items-center'}>
<Text className={'text-red-500'}>{goods.price}</Text>
<View className={'flex flex-col items-end gap-1'}>
<ConfigProvider theme={customTheme}>
<InputNumber
value={quantity}
min={1}
max={goods.stock || 999}
disabled={goods.canBuyNumber != 0}
onChange={handleQuantityChange}
/>
</ConfigProvider>
{goods.stock !== undefined && (
<Text className={'text-xs text-gray-400'}>
{goods.stock}
</Text>
)}
</View>
</View>
</View>
</View>
</Cell>
</CellGroup>
<CellGroup>
<Cell
title={'支付方式'}
extra={(
<View className={'flex items-center gap-2'}>
<View className={'text-gray-900'}>{payment?.name}</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}
onClick={() => setIsVisible(true)}
/>
</CellGroup>
<CellGroup>
<Cell
title={`商品总价(共${quantity}件)`}
extra={<View className={'font-medium'}>{getGoodsTotal().toFixed(2)}</View>}
/>
<Cell
title={'优惠券'}
extra={(
<View className={'flex justify-between items-center'}>
<View className={'text-red-500 text-sm mr-1'}>
{selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'}
</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}
onClick={() => setCouponVisible(true)}
/>
<Cell title={'配送费'} extra={'¥0.00'}/>
<Cell extra={(
<View className={'flex items-end gap-2'}>
<Text></Text>
<Text className={'text-red-500 text-sm'}>{getCouponDiscount().toFixed(2)}</Text>
<Text className={'ml-2'}></Text>
<Text className={'text-gray-700 font-bold'} style={{fontSize: '18px'}}>{getFinalPrice().toFixed(2)}</Text>
</View>
)}/>
</CellGroup>
<CellGroup>
<Cell title={'订单备注'} extra={(
<Input
placeholder={'选填,请先和商家协商一致'}
style={{padding: '0'}}
value={orderRemark}
onChange={(value) => setOrderRemark(value)}
maxLength={100}
/>
)}/>
</CellGroup>
{/* 支付方式选择 */}
<ActionSheet
visible={isVisible}
options={payments}
onSelect={handleSelect}
onCancel={() => setIsVisible(false)}
/>
{/* 优惠券选择弹窗 */}
<Popup
visible={couponVisible}
position="bottom"
onClose={() => setCouponVisible(false)}
style={{height: '60vh'}}
>
<View className="coupon-popup">
<View className="coupon-popup__header">
<Text className="text-sm"></Text>
<Button
size="small"
fill="none"
onClick={() => setCouponVisible(false)}
>
</Button>
</View>
<View className="coupon-popup__content">
{couponLoading ? (
<View className="coupon-popup__loading">
<Text>...</Text>
</View>
) : (
<>
{selectedCoupon && (
<View className="coupon-popup__current">
<Text className="coupon-popup__current-title font-medium">使</Text>
<View className="coupon-popup__current-item">
<Text>{selectedCoupon.title} -{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)}</Text>
<Button size="small" onClick={handleCouponCancel}>使</Button>
</View>
</View>
)}
{(() => {
const total = getGoodsTotal()
const usableCoupons = filterUsableCoupons(availableCoupons, total)
const unusableCoupons = filterUnusableCoupons(availableCoupons, total)
return (
<>
<CouponList
title={`可用优惠券 (${usableCoupons.length})`}
coupons={usableCoupons}
layout="vertical"
onCouponClick={handleCouponSelect}
showEmpty={usableCoupons.length === 0}
emptyText="暂无可用优惠券"
/>
{unusableCoupons.length > 0 && (
<CouponList
title={`不可用优惠券 (${unusableCoupons.length})`}
coupons={unusableCoupons.map(coupon => ({
...coupon,
status: 2 as const
}))}
layout="vertical"
showEmpty={false}
/>
)}
</>
)
})()}
</>
)}
</View>
</View>
</Popup>
<Gap height={50}/>
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
<View className={'btn-bar flex justify-between items-center'}>
<div className={'flex flex-col justify-center items-start mx-4'}>
<View className={'flex items-center gap-2'}>
<span className={'total-price text-sm text-gray-500'}></span>
<span className={'text-red-500 text-xl font-bold'}>{getFinalPrice().toFixed(2)}</span>
</View>
{selectedCoupon && (
<View className={'text-xs text-gray-400'}>
{getCouponDiscount().toFixed(2)}
</View>
)}
</div>
<div className={'buy-btn mx-4'}>
<Button
type="success"
size="large"
loading={payLoading}
onClick={() => onPay(goods)}
>
{payLoading ? '支付中...' : '立即付款'}
</Button>
</div>
</View>
</div>
</div>
);
};
export default OrderConfirm;

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单确认',
navigationBarTextStyle: 'black'
})

View File

@@ -1,44 +0,0 @@
.order-confirm-page {
padding-bottom: 100px; // 留出底部固定按钮的空间
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.total-price {
display: flex;
align-items: center;
}
.submit-btn {
width: 150px;
}
}
}
.address-bottom-line{
width: 100%;
border-radius: 24rpx 24rpx 0 0;
background: #fff;
padding: 26rpx 49rpx 0 34rpx;
position: relative;
&:before {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5px;
background: repeating-linear-gradient(-45deg, #ff6c6c, #ff6c6c 20%, transparent 0, transparent 25%, #1989fa 0,
#1989fa 45%, transparent 0, transparent 50%);
background-size: 120px;
content: "";
}
}

View File

@@ -1,213 +0,0 @@
import {useEffect, useState} from "react";
import {Image, Button, Cell, CellGroup, Input, Space} from '@nutui/nutui-react-taro'
import {Location, ArrowRight} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {ShopGoods} from "@/api/shop/shopGoods/model";
import {getShopGoods} from "@/api/shop/shopGoods";
import {View} from '@tarojs/components';
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
import './index.scss'
import {useCart, CartItem} from "@/hooks/useCart";
import Gap from "@/components/Gap";
import {Payment} from "@/api/system/payment/model";
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
const OrderConfirm = () => {
const [goods, setGoods] = useState<ShopGoods | null>(null);
const [address, setAddress] = useState<ShopUserAddress>()
const [payment, setPayment] = useState<Payment>()
const [checkoutItems, setCheckoutItems] = useState<CartItem[]>([]);
const router = Taro.getCurrentInstance().router;
const goodsId = router?.params?.goodsId;
const {
cartItems,
removeFromCart
} = useCart();
console.log(goods, 'goods>>>>')
console.log(setPayment,'setPayment>>>')
const reload = async () => {
const address = await listShopUserAddress({isDefault: true});
if (address.length > 0) {
console.log(address, '111')
setAddress(address[0])
}
}
// 加载结算商品数据
const loadCheckoutItems = () => {
try {
const checkoutData = Taro.getStorageSync('checkout_items');
if (checkoutData) {
const items = JSON.parse(checkoutData) as CartItem[];
setCheckoutItems(items);
// 清除临时存储的数据
Taro.removeStorageSync('checkout_items');
} else {
// 如果没有选中商品数据,使用全部购物车商品
setCheckoutItems(cartItems);
}
} catch (error) {
console.error('加载结算商品失败:', error);
setCheckoutItems(cartItems);
}
};
/**
* 统一支付入口
*/
const onPay = async () => {
// 基础校验
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
});
return;
}
if (!checkoutItems || checkoutItems.length === 0) {
Taro.showToast({
title: '没有要结算的商品',
icon: 'error'
});
return;
}
// 构建订单数据
const orderData = buildCartOrder(
checkoutItems.map(item => ({
goodsId: item.goodsId!,
quantity: item.quantity || 1
})),
address.id,
{
comments: '购物车下单',
deliveryType: 0
}
);
// 根据支付方式选择支付类型,默认微信支付
const paymentType = payment?.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
// 执行支付
await PaymentHandler.pay(orderData, paymentType, {
onSuccess: () => {
// 支付成功后,从购物车中移除已下单的商品
checkoutItems.forEach(item => {
removeFromCart(item.goodsId);
});
}
});
};
useEffect(() => {
if (goodsId) {
getShopGoods(Number(goodsId)).then(res => {
setGoods(res);
}).catch(error => {
console.error("Failed to fetch goods detail:", error);
});
}
reload().then();
loadCheckoutItems();
}, [goodsId, cartItems]);
// 计算总价
const getTotalPrice = () => {
return checkoutItems.reduce((total, item) => {
return total + (parseFloat(item.price) * item.quantity);
}, 0).toFixed(2);
};
// 计算商品总数量
const getTotalQuantity = () => {
return checkoutItems.reduce((total, item) => total + item.quantity, 0);
};
return (
<div className={'order-confirm-page'}>
<CellGroup>
{
address && (
<Cell className={'address-bottom-line'} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
<View className={'flex flex-col w-full justify-between items-start'}>
<Space className={'flex flex-row w-full font-medium'}>
<View className={'flex-wrap text-nowrap whitespace-nowrap'}></View>
<View style={{width: '64%'}}
className={'line-clamp-1 relative'}>{address.province} {address.city} {address.region} {address.address}
</View>
</Space>
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
</View>
</Space>
</Cell>
)
}
{!address && (
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
<Space>
<Location/>
</Space>
</Cell>
)}
</CellGroup>
<CellGroup>
{checkoutItems.map((goods, _) => (
<Cell key={goods.goodsId}>
<Space>
<Image src={goods.image} mode={'aspectFill'} style={{
width: '80px',
height: '80px',
}} lazyLoad={false}/>
<View className={'flex flex-col'}>
<View className={'font-medium w-full'}>{goods.name}</View>
<View className={'number text-gray-400 text-sm py-2'}>80g/</View>
<Space className={'flex justify-start items-center'}>
<View className={'text-red-500'}>{goods.price}</View>
<View className={'text-gray-500 text-sm'}>x {goods.quantity}</View>
</Space>
</View>
</Space>
</Cell>
))}
</CellGroup>
<CellGroup>
<Cell title={`商品总价(共${getTotalQuantity()}件)`} extra={<View className={'font-medium'}>{'¥' + getTotalPrice()}</View>}/>
<Cell title={'优惠券'} extra={(
<View className={'flex justify-between items-center'}>
<View className={'text-red-500 text-sm mr-1'}>-0.00</View>
<ArrowRight className={'text-gray-400'} size={14}/>
</View>
)}/>
{/*<Cell title={'配送费'} extra={'¥' + 10}/>*/}
<Cell title={'订单备注'} extra={(
<Input placeholder={'选填,请先和商家协商一致'} style={{ padding: '0'}}/>
)}/>
</CellGroup>
<Gap height={50} />
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
<View className={'btn-bar flex justify-between items-center'}>
<div className={'flex justify-center items-center mx-4'}>
<span className={'total-price text-sm text-gray-500'}></span>
<span className={'text-red-500 text-xl font-bold'}>{getTotalPrice()}</span>
</div>
<div className={'buy-btn mx-4'}>
<Button type="success" size="large" onClick={onPay}></Button>
</div>
</View>
</div>
</div>
);
};
export default OrderConfirm;

View File

@@ -1,4 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '订单详情',
navigationBarTextStyle: 'black'
})

View File

@@ -1,27 +0,0 @@
.order-detail-page {
padding-bottom: 80px; // 留出底部固定按钮的空间
.nut-cell-group__title {
padding: 10px 16px;
font-size: 14px;
color: #999;
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 10px 20px;
background-color: #fff;
border-top: 1px solid #eee;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.nut-button {
margin-left: 10px;
}
}
}

View File

@@ -1,161 +0,0 @@
import {useEffect, useState} from "react";
import {Cell, CellGroup, Image, Space, Button} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {ShopOrder} from "@/api/shop/shopOrder/model";
import {getShopOrder, updateShopOrder} from "@/api/shop/shopOrder";
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
import dayjs from "dayjs";
import PaymentCountdown from "@/components/PaymentCountdown";
import './index.scss'
const OrderDetail = () => {
const [order, setOrder] = useState<ShopOrder | null>(null);
const [orderGoodsList, setOrderGoodsList] = useState<ShopOrderGoods[]>([]);
const router = Taro.getCurrentInstance().router;
const orderId = router?.params?.orderId;
// 处理支付超时
const handlePaymentExpired = async () => {
if (!order) return;
try {
// 自动取消过期订单
await updateShopOrder({
...order,
orderStatus: 2 // 已取消
});
// 更新本地状态
setOrder(prev => prev ? { ...prev, orderStatus: 2 } : null);
Taro.showToast({
title: '订单已自动取消',
icon: 'none',
duration: 2000
});
} catch (error) {
console.error('自动取消订单失败:', error);
}
};
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 3) return '取消中';
if (order.orderStatus === 4) return '退款申请中';
if (order.orderStatus === 5) return '退款被拒绝';
if (order.orderStatus === 6) return '退款成功';
if (order.orderStatus === 7) return '客户端申请退款';
// 检查支付状态 (payStatus为boolean类型)
if (!order.payStatus) return '待付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已收货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
if (order.orderStatus === 0) return '未使用';
return '未知状态';
};
const getPayTypeText = (payType?: number) => {
switch (payType) {
case 0: return '余额支付';
case 1: return '微信支付';
case 102: return '微信Native';
case 2: return '会员卡支付';
case 3: return '支付宝';
case 4: return '现金';
case 5: return 'POS机';
default: return '未知支付方式';
}
};
useEffect(() => {
if (orderId) {
console.log('shop-goods',orderId)
getShopOrder(Number(orderId)).then(async (res) => {
setOrder(res);
// 获取订单商品列表
const goodsRes = await listShopOrderGoods({ orderId: Number(orderId) });
if (goodsRes && goodsRes.length > 0) {
setOrderGoodsList(goodsRes);
}
}).catch(error => {
console.error("Failed to fetch order detail:", error);
});
}
}, [orderId]);
if (!order) {
return <div>...</div>;
}
return (
<div className={'order-detail-page'}>
{/* 支付倒计时显示 - 详情页实时更新 */}
{!order.payStatus && order.orderStatus !== 2 && (
<div className="order-detail-countdown p-4 bg-red-50 border-b border-red-100">
<PaymentCountdown
createTime={order.createTime}
payStatus={order.payStatus}
realTime={true}
showSeconds={true}
mode="badge"
onExpired={handlePaymentExpired}
/>
</div>
)}
<CellGroup title="订单信息">
<Cell title="订单编号" description={order.orderNo} />
<Cell title="下单时间" description={dayjs(order.createTime).format('YYYY-MM-DD HH:mm:ss')} />
<Cell title="订单状态" description={getOrderStatusText(order)} />
</CellGroup>
<CellGroup title="商品信息">
{orderGoodsList.map((item, index) => (
<Cell key={index}>
<div className={'flex items-center'}>
<Image src={item.image || '/default-goods.png'} width="80" height="80" lazyLoad={false} />
<div className={'ml-2'}>
<div className={'text-sm font-bold'}>{item.goodsName}</div>
{item.spec && <div className={'text-gray-500 text-xs'}>{item.spec}</div>}
<div className={'text-gray-500 text-xs'}>{item.totalNum}</div>
<div className={'text-red-500 text-lg'}>{item.price}</div>
</div>
</div>
</Cell>
))}
</CellGroup>
<CellGroup title="收货信息">
<Cell title="收货人" description={order.realName} />
<Cell title="手机号" description={order.phone} />
<Cell title="收货地址" description={order.address} />
</CellGroup>
<CellGroup title="支付信息">
<Cell title="支付方式" description={getPayTypeText(order.payType)} />
<Cell title="实付金额" description={`${order.payPrice}`} />
</CellGroup>
<div className={'fixed-bottom'}>
<Space>
{!order.payStatus && <Button size="small" onClick={() => console.log('取消订单')}></Button>}
{!order.payStatus && <Button size="small" type="primary" onClick={() => console.log('立即支付')}></Button>}
{order.orderStatus === 1 && <Button size="small" onClick={() => console.log('申请退款')}>退</Button>}
{order.deliveryStatus === 20 && <Button size="small" type="primary" onClick={() => console.log('确认收货')}></Button>}
</Space>
</div>
</div>
);
};
export default OrderDetail;

View File

@@ -1,33 +0,0 @@
// 使用与首页相同的样式主要依赖Tailwind CSS类名
.buy-btn {
background: linear-gradient(to right, #1cd98a, #24ca94);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
.cart-icon {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.1);
border-radius: 20px 0 0 20px;
}
}
.car-no {
font-weight: 500;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}

View File

@@ -1,58 +0,0 @@
import { View } from '@tarojs/components'
import { Image } from '@nutui/nutui-react-taro'
import { Share } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { ShopGoods } from '@/api/shop/shopGoods/model'
import './GoodsItem.scss'
interface GoodsItemProps {
goods: ShopGoods
}
const GoodsItem = ({ goods }: GoodsItemProps) => {
// 跳转到商品详情
const goToDetail = () => {
Taro.navigateTo({
url: `/shop/goodsDetail/index?id=${goods.goodsId}`
})
}
return (
<div className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
<Image
src={goods.image || ''}
mode={'aspectFit'}
lazyLoad={false}
radius="10px 10px 0 0"
height="180"
onClick={goToDetail}
/>
<div className={'flex flex-col p-2 rounded-lg'}>
<div>
<div className={'car-no text-sm'}>{goods.name || goods.goodsName}</div>
<div className={'flex justify-between text-xs py-1'}>
<span className={'text-orange-500'}>{goods.comments || ''}</span>
<span className={'text-gray-400'}> {goods.sales || 0}</span>
</div>
<div className={'flex justify-between items-center py-2'}>
<div className={'flex text-red-500 text-xl items-baseline'}>
<span className={'text-xs'}></span>
<span className={'font-bold text-2xl'}>{goods.price || '0.00'}</span>
</div>
<div className={'buy-btn'}>
<div className={'cart-icon'}>
<Share size={20} className={'mx-4 mt-2'}
onClick={goToDetail}/>
</div>
<div className={'text-white pl-4 pr-5'}
onClick={goToDetail}>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default GoodsItem

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '商品搜索'
})

View File

@@ -1,103 +0,0 @@
.search-page {
min-height: 100vh;
background: #f5f5f5;
// 搜索输入框样式
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 20px;
padding: 0 12px;
.search-icon {
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
input {
background: transparent !important;
}
}
}
.search-btn {
padding: 0 16px;
height: 36px;
border-radius: 18px;
font-size: 14px;
}
.search-content {
padding-top: calc(32px + env(safe-area-inset-top));
.search-history {
background: #fff;
margin-bottom: 8px;
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
.history-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.clear-btn {
font-size: 14px;
color: #999;
cursor: pointer;
}
}
.history-list {
padding: 16px;
display: flex;
flex-wrap: wrap;
gap: 12px;
.history-item {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 16px;
color: #666;
cursor: pointer;
&:active {
background: #e5e5e5;
}
}
}
}
.search-results {
.result-header {
padding: 16px;
color: #666;
background: #fff;
border-bottom: 1px solid #f5f5f5;
margin-bottom: 8px;
}
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
background: #fff;
}
}
}
}

View File

@@ -1,237 +0,0 @@
import {SetStateAction, useEffect, useState} from 'react'
import {useRouter} from '@tarojs/taro'
import Taro from '@tarojs/taro'
import {View} from '@tarojs/components'
import {Loading, Empty, InfiniteLoading, Input, Button} from '@nutui/nutui-react-taro'
import {Search} from '@nutui/icons-react-taro';
import {ShopGoods} from '@/api/shop/shopGoods/model'
import {pageShopGoods} from '@/api/shop/shopGoods'
import GoodsItem from './components/GoodsItem'
import './index.scss'
const SearchPage = () => {
const router = useRouter()
const [keywords, setKeywords] = useState<string>('')
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [total, setTotal] = useState(0)
const [searchHistory, setSearchHistory] = useState<string[]>([])
// 从路由参数获取搜索关键词
useEffect(() => {
const {keywords: routeKeywords} = router.params || {}
if (routeKeywords) {
setKeywords(decodeURIComponent(routeKeywords))
handleSearch(decodeURIComponent(routeKeywords), 1).then()
}
// 加载搜索历史
loadSearchHistory()
}, [])
// 加载搜索历史
const loadSearchHistory = () => {
try {
const history = Taro.getStorageSync('search_history') || []
setSearchHistory(history)
} catch (error) {
console.error('加载搜索历史失败:', error)
}
}
// 保存搜索历史
const saveSearchHistory = (keyword: string) => {
try {
let history = Taro.getStorageSync('search_history') || []
// 去重并添加到开头
history = history.filter((item: string) => item !== keyword)
history.unshift(keyword)
// 只保留最近10条
history = history.slice(0, 10)
Taro.setStorageSync('search_history', history)
setSearchHistory(history)
} catch (error) {
console.error('保存搜索历史失败:', error)
}
}
const handleKeywords = (keywords: SetStateAction<string>) => {
setKeywords(keywords)
handleSearch(typeof keywords === "string" ? keywords : '').then()
}
// 搜索商品
const handleSearch = async (searchKeywords: string, pageNum: number = 1) => {
if (!searchKeywords.trim()) {
Taro.showToast({
title: '请输入搜索关键词',
icon: 'none'
})
return
}
setLoading(true)
try {
const params = {
keywords: searchKeywords.trim(),
page: pageNum,
size: 10,
isShow: 1 // 只搜索上架商品
}
const result = await pageShopGoods(params)
if (pageNum === 1) {
setGoodsList(result?.list || [])
setTotal(result?.count || 0)
// 保存搜索历史
saveSearchHistory(searchKeywords.trim())
} else {
setGoodsList(prev => [...prev, ...(result?.list || [])])
}
setHasMore((result?.list?.length || 0) >= 10)
setPage(pageNum)
} catch (error) {
console.error('搜索失败:', error)
Taro.showToast({
title: '搜索失败,请重试',
icon: 'none'
})
} finally {
setLoading(false)
}
}
// 加载更多
const loadMore = () => {
if (!loading && hasMore && keywords.trim()) {
handleSearch(keywords, page + 1).then()
}
}
// 点击历史搜索
const onHistoryClick = (keyword: string) => {
setKeywords(keyword)
setPage(1)
handleSearch(keyword, 1)
}
// 清空搜索历史
const clearHistory = () => {
Taro.showModal({
title: '提示',
content: '确定要清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
try {
Taro.removeStorageSync('search_history')
setSearchHistory([])
} catch (error) {
console.error('清空搜索历史失败:', error)
}
}
}
})
}
return (
<View className="search-page pt-3">
<div className={'px-2'}>
<div
style={{
display: 'flex',
alignItems: 'center',
background: '#ffffff',
padding: '0 5px',
borderRadius: '20px',
marginTop: '5px',
}}
>
<Search size={18} className={'ml-2 text-gray-400'}/>
<Input
placeholder="搜索商品"
value={keywords}
onChange={handleKeywords}
onConfirm={() => handleSearch(keywords)}
style={{padding: '9px 8px'}}
/>
<div
className={'flex items-center'}
>
<Button type="success" style={{background: 'linear-gradient(to bottom, #1cd98a, #24ca94)'}}
onClick={() => handleSearch(keywords)}>
</Button>
</div>
</div>
</div>
{/*<SearchBar style={{height: `${statusBarHeight}px`}} shape="round" placeholder="搜索商品" onChange={setKeywords} onSearch={handleSearch}/>*/}
{/* 搜索内容 */}
<View className="search-content">
{/* 搜索历史 */}
{!keywords && searchHistory.length > 0 && (
<View className="search-history">
<View className="history-header">
<View className="text-sm"></View>
<View className={'text-gray-400'} onClick={clearHistory}></View>
</View>
<View className="history-list">
{searchHistory.map((item, index) => (
<View
key={index}
className="history-item"
onClick={() => onHistoryClick(item)}
>
{item}
</View>
))}
</View>
</View>
)}
{/* 搜索结果 */}
{keywords && (
<View className="search-results">
{/* 结果统计 */}
<View className="result-header">
{total}
</View>
{/* 商品列表 */}
{loading && page === 1 ? (
<View className="loading-wrapper">
<Loading>...</Loading>
</View>
) : goodsList.length > 0 ? (
<div className={'py-3'}>
<div className={'flex flex-col justify-between items-center rounded-lg px-2'}>
<InfiniteLoading
hasMore={hasMore}
// @ts-ignore
onLoadMore={loadMore}
loadingText="加载中..."
loadMoreText="没有更多了"
>
{goodsList.map((item) => (
<GoodsItem key={item.goodsId} goods={item}/>
))}
</InfiniteLoading>
</div>
</div>
) : (
<Empty description="暂无相关商品"/>
)}
</View>
)}
</View>
</View>
)
}
export default SearchPage