Compare commits
14 Commits
316aab2616
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| faed75fc70 | |||
| e990d4e4ef | |||
| 9c7f99d4fb | |||
| ba5048eaf0 | |||
| 610aded9d5 | |||
| 6e8d6b1c0d | |||
| a834f88aaa | |||
| d1fa0f3ec0 | |||
| ba694813c4 | |||
| c654f62b8c | |||
| 429c5d282b | |||
| ceea662420 | |||
| 07d35d48d7 | |||
| 217f3556fc |
BIN
assets/assets/tabbar/home-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/home.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/message-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/message.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/toolbox-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/toolbox.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/user-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
assets/assets/tabbar/user.png
Normal file
|
After Width: | Height: | Size: 67 B |
21
assets/tabbar/create-icons.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# 创建简单的占位图标(实际项目中应该使用设计好的图标)
|
||||
|
||||
# 使用 ImageMagick 创建简单的彩色圆点作为占位图标
|
||||
# 首页图标
|
||||
convert -size 48x48 xc:none -fill '#999999' -draw "circle 24,24 24,12" assets/tabbar/home.png
|
||||
convert -size 48x48 xc:none -fill '#1890FF' -draw "circle 24,24 24,12" assets/tabbar/home-active.png
|
||||
|
||||
# 消息图标
|
||||
convert -size 48x48 xc:none -fill '#999999' -draw "rectangle 12,16 36,32" assets/tabbar/message.png
|
||||
convert -size 48x48 xc:none -fill '#1890FF' -draw "rectangle 12,16 36,32" assets/tabbar/message-active.png
|
||||
|
||||
# 工具箱图标
|
||||
convert -size 48x48 xc:none -fill '#999999' -draw "rectangle 10,18 38,34" assets/tabbar/toolbox.png
|
||||
convert -size 48x48 xc:none -fill '#1890FF' -draw "rectangle 10,18 38,34" assets/tabbar/toolbox-active.png
|
||||
|
||||
# 我的图标
|
||||
convert -size 48x48 xc:none -fill '#999999' -draw "circle 24,18 24,8" assets/tabbar/user.png
|
||||
convert -size 48x48 xc:none -fill '#1890FF' -draw "circle 24,18 24,8" assets/tabbar/user-active.png
|
||||
|
||||
echo "图标创建完成"
|
||||
42
assets/tabbar/create-placeholders.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 简单的 1x1 像素 PNG(白色)
|
||||
const whitePixel = Buffer.from([
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
|
||||
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
|
||||
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
||||
0x42, 0x60, 0x82
|
||||
]);
|
||||
|
||||
// 创建目录
|
||||
const dir = path.join(__dirname, '..', 'assets', 'tabbar');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// 创建占位图标文件
|
||||
const icons = [
|
||||
'home.png',
|
||||
'home-active.png',
|
||||
'message.png',
|
||||
'message-active.png',
|
||||
'toolbox.png',
|
||||
'toolbox-active.png',
|
||||
'user.png',
|
||||
'user-active.png'
|
||||
];
|
||||
|
||||
icons.forEach(icon => {
|
||||
const filePath = path.join(dir, icon);
|
||||
fs.writeFileSync(filePath, whitePixel);
|
||||
console.log(`Created: ${icon}`);
|
||||
});
|
||||
|
||||
console.log('\n占位图标创建完成!请替换为实际图标文件。');
|
||||
console.log('图标尺寸建议:81x81 像素(微信小程序推荐)');
|
||||
@@ -1,7 +1,7 @@
|
||||
import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10582';
|
||||
export const TenantId = '10589';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
|
||||
@@ -3,18 +3,21 @@ export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '南南佐顿门窗',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
// API_BASE_URL: 'http://127.0.0.1:9200/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "template-10582",
|
||||
"name": "template-10589",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "WebSoft Inc.",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "template-10582",
|
||||
"description": "南南佐顿门窗",
|
||||
"appid": "wx644669e2da8d6519",
|
||||
"projectname": "template-10589",
|
||||
"description": "网神泛能运维",
|
||||
"appid": "wxb69547a97b5f5931",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": false,
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '新增收货地址',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品文章管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '管理中心'
|
||||
})
|
||||
@@ -1,295 +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,
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
const {
|
||||
dealerUser,
|
||||
error,
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 使用主题样式
|
||||
const themeStyles = useThemeStyles()
|
||||
|
||||
// 导航到各个功能页面
|
||||
const navigateToPage = (url: string) => {
|
||||
Taro.navigateTo({url})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 获取用户主题
|
||||
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
|
||||
|
||||
// 获取渐变背景
|
||||
const getGradientBackground = (themeColor?: string) => {
|
||||
if (themeColor) {
|
||||
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
|
||||
return gradientUtils.createGradient(themeColor, darkerColor)
|
||||
}
|
||||
return userTheme.background
|
||||
}
|
||||
|
||||
console.log(getGradientBackground(),'getGradientBackground()')
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
<View>
|
||||
{/*头部信息*/}
|
||||
{dealerUser && (
|
||||
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
|
||||
{/* 装饰性背景元素 - 小程序兼容版本 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
<View className="absolute w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
bottom: '-12px',
|
||||
left: '-12px'
|
||||
}}></View>
|
||||
<View className="absolute w-16 h-16 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
top: '60px',
|
||||
left: '120px'
|
||||
}}></View>
|
||||
<View className="flex items-center justify-between relative z-10 mb-4">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={dealerUser?.qrcode}
|
||||
icon={<User/>}
|
||||
className="mr-4"
|
||||
style={{
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)'
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 flex-col">
|
||||
<View className="text-white text-lg font-bold mb-1" style={{
|
||||
}}>
|
||||
{dealerUser?.realName || '分销商'}
|
||||
</View>
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.9)'
|
||||
}}>加入时间</Text>
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}>
|
||||
{formatTime(dealerUser.createTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 佣金统计卡片 */}
|
||||
{dealerUser && (
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.money)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.freezeMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.totalMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 团队统计 */}
|
||||
{dealerUser && (
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm flex items-center"
|
||||
onClick={() => navigateToPage('/dealer/team/index')}
|
||||
>
|
||||
<Text>查看详情</Text>
|
||||
<ArrowRight size="12"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-purple-500 mb-1">
|
||||
{dealerUser.firstNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">一级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-indigo-500 mb-1">
|
||||
{dealerUser.secondNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">二级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-pink-500 mb-1">
|
||||
{dealerUser.thirdNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">三级成员</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 功能导航 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">分销工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shopping color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Purse color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
{/*<Grid*/}
|
||||
{/* columns={4}*/}
|
||||
{/* className="no-border-grid mt-4"*/}
|
||||
{/* style={{*/}
|
||||
{/* '--nutui-grid-border-color': 'transparent',*/}
|
||||
{/* '--nutui-grid-item-border-width': '0px',*/}
|
||||
{/* border: 'none'*/}
|
||||
{/* } as React.CSSProperties}*/}
|
||||
{/*>*/}
|
||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* /!* 预留其他功能位置 *!/*/}
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
{/*</Grid>*/}
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部安全区域 */}
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerIndex
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '实名审核'
|
||||
})
|
||||
@@ -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
|
||||
@@ -10,7 +10,7 @@ import {SERVER_API_URL} from "@/utils/server";
|
||||
export async function pageRoles(params: RoleParam) {
|
||||
const res = await request.get<ApiResult<PageResult<Role>>>(
|
||||
SERVER_API_URL + '/system/role/page',
|
||||
{ params }
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
@@ -24,9 +24,7 @@ export async function pageRoles(params: RoleParam) {
|
||||
export async function listRoles(params?: RoleParam) {
|
||||
const res = await request.get<ApiResult<Role[]>>(
|
||||
SERVER_API_URL + '/system/role',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/cart/cart',
|
||||
'pages/find/find',
|
||||
'pages/user/user'
|
||||
'pages/message/message',
|
||||
'pages/toolbox/toolbox',
|
||||
'pages/user/user',
|
||||
'pages/feature/index'
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
"root": "passport",
|
||||
"pages": [
|
||||
"login",
|
||||
// "register",
|
||||
// "forget",
|
||||
// "setting",
|
||||
"agreement",
|
||||
"sms-login"
|
||||
]
|
||||
@@ -74,62 +72,46 @@ export default defineAppConfig({
|
||||
"bank/add"
|
||||
]
|
||||
},
|
||||
// {
|
||||
// "root": "shop",
|
||||
// "pages": ['category/index',
|
||||
// 'orderDetail/index',
|
||||
// 'goodsDetail/index',
|
||||
// 'orderConfirm/index',
|
||||
// 'orderConfirmCart/index',
|
||||
// 'search/index']
|
||||
// },
|
||||
{
|
||||
"root": "admin",
|
||||
"pages": [
|
||||
"index",
|
||||
"article/index",
|
||||
"userVerify/index"
|
||||
]
|
||||
}
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'dark',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black'
|
||||
navigationBarBackgroundColor: '#2c2c2c',
|
||||
navigationBarTitleText: '工具箱',
|
||||
navigationBarTextStyle: 'white'
|
||||
},
|
||||
tabBar: {
|
||||
custom: false,
|
||||
color: "#dbdbdb",
|
||||
selectedColor: "#2c2c2c",
|
||||
backgroundColor: "#F8F8F8",
|
||||
borderStyle: "black",
|
||||
list: [
|
||||
{
|
||||
pagePath: "pages/index/index",
|
||||
iconPath: "assets/icon/home.png",
|
||||
selectedIconPath: "assets/icon/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/message/message",
|
||||
iconPath: "assets/icon/message.png",
|
||||
selectedIconPath: "assets/icon/message-active.png",
|
||||
text: "消息",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/toolbox/toolbox",
|
||||
iconPath: "assets/icon/toolbox.png",
|
||||
selectedIconPath: "assets/icon/toolbox-active.png",
|
||||
text: "工具箱",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/user/user",
|
||||
iconPath: "assets/icon/user.png",
|
||||
selectedIconPath: "assets/icon/user-active.png",
|
||||
text: "我的",
|
||||
},
|
||||
],
|
||||
},
|
||||
// tabBar: {
|
||||
// custom: false,
|
||||
// color: "#8a8a8a",
|
||||
// selectedColor: "#0e932e",
|
||||
// backgroundColor: "#ffffff",
|
||||
// list: [
|
||||
// {
|
||||
// pagePath: "pages/index/index",
|
||||
// iconPath: "assets/tabbar/index.png",
|
||||
// selectedIconPath: "assets/tabbar/index-active.png",
|
||||
// text: "首页",
|
||||
// },
|
||||
// {
|
||||
// pagePath: "pages/find/find",
|
||||
// iconPath: "assets/tabbar/find.png",
|
||||
// selectedIconPath: "assets/tabbar/find-active.png",
|
||||
// text: "发现",
|
||||
// },
|
||||
// {
|
||||
// pagePath: "pages/cart/cart",
|
||||
// iconPath: "assets/tabbar/cart.png",
|
||||
// selectedIconPath: "assets/tabbar/cart-active.png",
|
||||
// text: "购物车",
|
||||
// },
|
||||
// {
|
||||
// pagePath: "pages/user/user",
|
||||
// iconPath: "assets/tabbar/user.png",
|
||||
// selectedIconPath: "assets/tabbar/user-active.png",
|
||||
// text: "我的",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
requiredPrivateInfos: [
|
||||
"getLocation",
|
||||
"chooseLocation",
|
||||
|
||||
14
src/app.ts
@@ -74,13 +74,13 @@ function App(props: { children: any; }) {
|
||||
trackInviteSource(inviteParams.source || 'unknown', parseInt(inviteParams.inviter || '0'))
|
||||
|
||||
// 显示邀请提示
|
||||
setTimeout(() => {
|
||||
Taro.showToast({
|
||||
title: `检测到邀请信息 ID:${inviteParams.inviter}`,
|
||||
icon: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}, 1000)
|
||||
// setTimeout(() => {
|
||||
// Taro.showToast({
|
||||
// title: `检测到邀请信息 ID:${inviteParams.inviter}`,
|
||||
// icon: 'success',
|
||||
// duration: 3000
|
||||
// })
|
||||
// }, 1000)
|
||||
|
||||
} else {
|
||||
console.log('❌ 未检测到邀请参数')
|
||||
|
||||
BIN
src/assets/icon/home-active.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
src/assets/icon/home.png
Normal file
|
After Width: | Height: | Size: 812 B |
BIN
src/assets/icon/message-active.png
Normal file
|
After Width: | Height: | Size: 754 B |
BIN
src/assets/icon/message.png
Normal file
|
After Width: | Height: | Size: 754 B |
BIN
src/assets/icon/toolbox-active.png
Normal file
|
After Width: | Height: | Size: 805 B |
BIN
src/assets/icon/toolbox.png
Normal file
|
After Width: | Height: | Size: 805 B |
BIN
src/assets/icon/user-active.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src/assets/icon/user.png
Normal file
|
After Width: | Height: | Size: 872 B |
BIN
src/assets/logo.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/tabbar/message-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
src/assets/tabbar/message.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
src/assets/tabbar/toolbox-active.png
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
src/assets/tabbar/toolbox.png
Normal file
|
After Width: | Height: | Size: 67 B |
@@ -411,9 +411,9 @@ const AddUserAddress = () => {
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID"/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID"/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
||||
<View className="flex items-center justify-between">
|
||||
<Input
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
extractDateForCalendar, formatDateForDisplay
|
||||
} from "@/utils/dateUtils";
|
||||
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
||||
import {getShopDealerUser, pageShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
|
||||
const AddShopDealerApply = () => {
|
||||
const {params} = useRouter();
|
||||
@@ -161,103 +161,124 @@ const AddShopDealerApply = () => {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
type DupKey = 'address' | 'buildingNo' | 'unitNo' | 'roomNo' | 'realName' | 'mobile';
|
||||
const DUP_LABELS: Record<DupKey, string> = {
|
||||
address: '小区名称',
|
||||
buildingNo: '楼栋号',
|
||||
unitNo: '单元号',
|
||||
roomNo: '房号',
|
||||
realName: '姓名',
|
||||
mobile: '电话',
|
||||
};
|
||||
|
||||
const normalizeText = (v: any) => (v ?? '').toString().trim();
|
||||
|
||||
const getApplyDupFields = (apply: ShopDealerApply): Record<DupKey, string> => {
|
||||
const toHalfWidth = (input: string) =>
|
||||
(input || '').replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0)).replace(/\u3000/g, ' ');
|
||||
|
||||
const parseChineseNumber = (s: string): number | null => {
|
||||
const str = (s || '').trim();
|
||||
if (!str) return null;
|
||||
// 仅处理纯中文数字(含大小写)+ 单位
|
||||
if (!/^[零〇一二三四五六七八九十百千万两兩俩壹贰叁肆伍陆柒捌玖拾佰仟萬]+$/.test(str)) return null;
|
||||
|
||||
const digitMap: Record<string, number> = {
|
||||
零: 0, 〇: 0,
|
||||
一: 1, 壹: 1,
|
||||
二: 2, 两: 2, 兩: 2, 俩: 2, 贰: 2,
|
||||
三: 3, 叁: 3,
|
||||
四: 4, 肆: 4,
|
||||
五: 5, 伍: 5,
|
||||
六: 6, 陆: 6,
|
||||
七: 7, 柒: 7,
|
||||
八: 8, 捌: 8,
|
||||
九: 9, 玖: 9,
|
||||
};
|
||||
const unitMap: Record<string, number> = {十: 10, 拾: 10, 百: 100, 佰: 100, 千: 1000, 仟: 1000, 万: 10000, 萬: 10000};
|
||||
|
||||
let total = 0;
|
||||
let section = 0;
|
||||
let number = 0;
|
||||
for (const ch of str) {
|
||||
if (digitMap[ch] !== undefined) {
|
||||
number = digitMap[ch];
|
||||
continue;
|
||||
}
|
||||
const unit = unitMap[ch];
|
||||
if (!unit) continue;
|
||||
if (unit === 10000) {
|
||||
section = (section + number) * unit;
|
||||
total += section;
|
||||
section = 0;
|
||||
} else {
|
||||
// “十/百/千”前省略“一”的情况:十=10、十二=12
|
||||
const n = number === 0 ? 1 : number;
|
||||
section += n * unit;
|
||||
}
|
||||
number = 0;
|
||||
}
|
||||
const result = total + section + number;
|
||||
return Number.isFinite(result) ? result : null;
|
||||
};
|
||||
|
||||
const normalizeCommunity = (community: string) => {
|
||||
const s = toHalfWidth(normalizeText(community));
|
||||
return s.replace(/\s+/g, '').toUpperCase();
|
||||
};
|
||||
|
||||
const normalizeHouseNoPart = (raw: string, kind: 'building' | 'unit' | 'room') => {
|
||||
let s = toHalfWidth(normalizeText(raw)).toUpperCase();
|
||||
s = s.replace(/\s+/g, '');
|
||||
|
||||
// 去掉常见后缀/装饰词
|
||||
if (kind === 'building') s = s.replace(/(号楼|栋|幢|楼)$/g, '');
|
||||
if (kind === 'unit') s = s.replace(/(单元)$/g, '');
|
||||
if (kind === 'room') s = s.replace(/(室|房|号)$/g, '');
|
||||
|
||||
// 只保留数字与字母,统一分隔符差异(如 12-01 / 12#01)
|
||||
s = s.replace(/[^0-9A-Z零〇一二三四五六七八九十百千万两兩俩壹贰叁肆伍陆柒捌玖拾佰仟萬]/g, '');
|
||||
|
||||
// 纯中文数字 => 阿拉伯数字(支持大小写)
|
||||
const cn = parseChineseNumber(s);
|
||||
if (cn !== null) return String(cn);
|
||||
|
||||
// 数字段去前导 0(如 03A => 3A,1201 不变)
|
||||
s = s.replace(/\d+/g, (m) => String(parseInt(m, 10)));
|
||||
return s;
|
||||
};
|
||||
|
||||
const buildHouseKeyNormalized = (community: string, buildingNo: string, unitNo: string | undefined, roomNo: string) => {
|
||||
const c = normalizeCommunity(community);
|
||||
const b = normalizeHouseNoPart(buildingNo, 'building');
|
||||
const u = normalizeHouseNoPart(unitNo || '', 'unit');
|
||||
const r = normalizeHouseNoPart(roomNo, 'room');
|
||||
return [c, b, u, r].join('|');
|
||||
};
|
||||
|
||||
const getNormalizedHouseKeyFromApply = (apply: ShopDealerApply) => {
|
||||
const parsed = parseHouseKey(apply.dealerCode);
|
||||
return {
|
||||
address: normalizeText(parsed.community || apply.address),
|
||||
buildingNo: normalizeText(parsed.buildingNo),
|
||||
unitNo: normalizeText(parsed.unitNo),
|
||||
roomNo: normalizeText(parsed.roomNo),
|
||||
realName: normalizeText(apply.realName),
|
||||
mobile: normalizeText(apply.mobile),
|
||||
};
|
||||
return buildHouseKeyNormalized(
|
||||
parsed.community || apply.address || '',
|
||||
parsed.buildingNo || '',
|
||||
parsed.unitNo || '',
|
||||
parsed.roomNo || ''
|
||||
);
|
||||
};
|
||||
|
||||
const getNewDupFields = (values: any): Record<DupKey, string> => ({
|
||||
address: normalizeText(values.address),
|
||||
buildingNo: normalizeText(values.buildingNo),
|
||||
unitNo: normalizeText(values.unitNo),
|
||||
roomNo: normalizeText(values.roomNo),
|
||||
realName: normalizeText(values.realName),
|
||||
mobile: normalizeText(values.mobile),
|
||||
});
|
||||
|
||||
const combinationsOf3 = <T,>(arr: T[]): [T, T, T][] => {
|
||||
const res: [T, T, T][] = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
for (let k = j + 1; k < arr.length; k++) {
|
||||
res.push([arr[i], arr[j], arr[k]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
const findExistingApplyByHouse = async (params: {houseKeyNormalized: string; houseKeyRaw: string; communityKeyword: string}) => {
|
||||
const tryByDealerCode = async (dealerCode: string) => {
|
||||
const res = await pageShopDealerApply({dealerCode, type: 4});
|
||||
return res?.list?.[0] as ShopDealerApply | undefined;
|
||||
};
|
||||
|
||||
const findMatchedTriad = (a: Record<DupKey, string>, b: Record<DupKey, string>) => {
|
||||
const availableKeys = (Object.keys(a) as DupKey[]).filter((k) => a[k] !== '');
|
||||
if (availableKeys.length < 3) return null;
|
||||
const triads = combinationsOf3(availableKeys);
|
||||
for (const triad of triads) {
|
||||
if (triad.every((k) => a[k] === b[k] && b[k] !== '')) return triad;
|
||||
const keys = Array.from(new Set([params.houseKeyNormalized, params.houseKeyRaw].filter(Boolean)));
|
||||
for (const k of keys) {
|
||||
const hit = await tryByDealerCode(k);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkDuplicateBeforeSubmit = async (values: any, opts?: { skipDealerCode?: string }) => {
|
||||
const inputFields = getNewDupFields(values);
|
||||
const nonEmptyCount = Object.values(inputFields).filter((v) => v !== '').length;
|
||||
if (nonEmptyCount < 3) return null;
|
||||
// 兼容历史数据:用关键词拉取附近数据,再用“规范化后的 houseKey”对比
|
||||
const keyword = normalizeText(params.communityKeyword);
|
||||
if (!keyword) return null;
|
||||
|
||||
const seen = new Set<number>();
|
||||
const scanPages = async (params: any) => {
|
||||
for (let page = 1; page <= DUP_CHECK_MAX_PAGES; page++) {
|
||||
const res = await pageShopDealerApply({...params, page, limit: DUP_CHECK_LIMIT});
|
||||
const res = await pageShopDealerApply({type: 4, keywords: keyword, page, limit: DUP_CHECK_LIMIT});
|
||||
const list = res?.list || [];
|
||||
|
||||
for (const item of list) {
|
||||
if (opts?.skipDealerCode && item.dealerCode === opts.skipDealerCode) continue;
|
||||
if (item.applyId && seen.has(item.applyId)) continue;
|
||||
if (item.applyId) seen.add(item.applyId);
|
||||
|
||||
const triad = findMatchedTriad(inputFields, getApplyDupFields(item));
|
||||
if (triad) return {item, triad};
|
||||
if (getNormalizedHouseKeyFromApply(item) === params.houseKeyNormalized) return item;
|
||||
}
|
||||
|
||||
if (list.length < DUP_CHECK_LIMIT) break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 优先按手机号(精确)查询,数据量更小
|
||||
if (inputFields.mobile) {
|
||||
const hit = await scanPages({type: 4, mobile: inputFields.mobile});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
// 再按小区关键字查询,覆盖房号相关组合
|
||||
if (inputFields.address) {
|
||||
const hit = await scanPages({type: 4, keywords: inputFields.address});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
// 最后按姓名关键字兜底(用于覆盖不包含“小区/电话”的组合)
|
||||
if (inputFields.realName) {
|
||||
const hit = await scanPages({type: 4, keywords: inputFields.realName});
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -265,6 +286,8 @@ const AddShopDealerApply = () => {
|
||||
const submitSucceed = async (values: any) => {
|
||||
|
||||
try {
|
||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || 0;
|
||||
|
||||
// 房号相关必填校验
|
||||
if (!values.address || values.address.trim() === '') {
|
||||
Taro.showToast({title: '请选择/填写小区', icon: 'error'});
|
||||
@@ -302,28 +325,59 @@ const AddShopDealerApply = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证报备人是否存在
|
||||
if (values.userId > 0) {
|
||||
const isExist = await pageShopDealerUser({userId: Number(values.userId)});
|
||||
if(isExist && isExist.count == 0){
|
||||
Taro.showToast({
|
||||
title: '报备人不存在',
|
||||
icon: 'error'
|
||||
});
|
||||
// 规范化报备人:留空=自己报备(当前登录用户)
|
||||
const rawUserId = normalizeText(values.userId);
|
||||
const submitUserId = rawUserId
|
||||
? Number(rawUserId)
|
||||
: (isEditMode ? (existingApply?.userId || currentUserId) : currentUserId);
|
||||
if (!Number.isFinite(submitUserId) || submitUserId <= 0) {
|
||||
Taro.showToast({title: '请填写正确的报备人ID', icon: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// 报备人存在性校验 + 获取该报备人的推荐人(用于后端展示链路)
|
||||
let reporterDealerUser: ShopDealerUser | undefined = undefined;
|
||||
if (submitUserId === currentUserId) {
|
||||
reporterDealerUser = referee;
|
||||
} else {
|
||||
try {
|
||||
reporterDealerUser = await getShopDealerUser(submitUserId);
|
||||
} catch {
|
||||
Taro.showToast({title: '报备人不存在', icon: 'error'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const houseKey = buildHouseKey(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
// 后端常用 0 表示“无推荐人”,避免传空值触发“推荐人不存在”
|
||||
const submitRefereeId = (reporterDealerUser?.refereeId && reporterDealerUser.refereeId > 0)
|
||||
? reporterDealerUser.refereeId
|
||||
: undefined;
|
||||
|
||||
const houseKeyRaw = buildHouseKey(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
const houseKeyNormalized = buildHouseKeyNormalized(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
const houseKey = houseKeyNormalized || houseKeyRaw;
|
||||
const houseDisplay = buildHouseDisplay(values.address, values.buildingNo, values.unitNo, values.roomNo);
|
||||
|
||||
// 检查房号是否已报备
|
||||
const res = await pageShopDealerApply({dealerCode: houseKey, type: 4});
|
||||
|
||||
if (res && res.count > 0) {
|
||||
const existingCustomer = res.list[0];
|
||||
|
||||
// 新增报备:提交前检查房号是否已报备(按 小区+楼栋+单元+房号 判断,且做规范化)
|
||||
if (!isEditMode) {
|
||||
const existingCustomer = await findExistingApplyByHouse({
|
||||
houseKeyNormalized,
|
||||
houseKeyRaw,
|
||||
communityKeyword: values.address
|
||||
});
|
||||
|
||||
if (existingCustomer) {
|
||||
// 报备人不同:直接拦截(避免跨报备人“抢单/续报”)
|
||||
const existingReporterId = Number(existingCustomer.userId);
|
||||
if (Number.isFinite(existingReporterId) && existingReporterId > 0 && existingReporterId !== submitUserId) {
|
||||
Taro.showToast({
|
||||
title: '请改房号,该房号信息已报备',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已签约/已取消:直接提示已报备
|
||||
if (existingCustomer.applyStatus && existingCustomer.applyStatus !== 10) {
|
||||
Taro.showToast({
|
||||
@@ -375,21 +429,6 @@ const AddShopDealerApply = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增报备:提交前做“三要素”重复校验(小区/楼栋/单元/房号/姓名/电话 任一三要素重复提示已报备)
|
||||
if (!isEditMode) {
|
||||
const dup = await checkDuplicateBeforeSubmit(values, {skipDealerCode: houseKey});
|
||||
if (dup) {
|
||||
const triadLabels = dup.triad.map((k: DupKey) => DUP_LABELS[k]).join('、');
|
||||
const existingDisplay = dup.item.dealerName || dup.item.address || '地址未知';
|
||||
Taro.showToast({
|
||||
title: `疑似重复报备:${triadLabels}一致(${existingDisplay}),已报备`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算过期时间
|
||||
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||
|
||||
@@ -406,7 +445,10 @@ const AddShopDealerApply = () => {
|
||||
// 客户姓名/手机号
|
||||
realName: values.realName,
|
||||
mobile: values.mobile,
|
||||
refereeId: referee?.refereeId,
|
||||
// 报备人(留空时用当前登录用户)
|
||||
// userId: submitUserId,
|
||||
// 推荐人(报备人的上级;无则传 0)
|
||||
refereeId: submitRefereeId,
|
||||
applyStatus: isEditMode ? 20 : 10,
|
||||
auditTime: undefined,
|
||||
// 设置保护期过期时间(15天后)
|
||||
@@ -486,8 +528,7 @@ const AddShopDealerApply = () => {
|
||||
unitNo: parsed.unitNo,
|
||||
roomNo: parsed.roomNo,
|
||||
realName: FormData.realName,
|
||||
mobile: FormData.mobile,
|
||||
userId: FormData.userId
|
||||
mobile: FormData.mobile
|
||||
});
|
||||
}, [FormData]);
|
||||
|
||||
@@ -561,13 +602,13 @@ const AddShopDealerApply = () => {
|
||||
{/*</Form.Item>*/}
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="userId" label="报备人(ID)" initialValue={FormData?.userId}>
|
||||
<Input
|
||||
placeholder="自己报备请留空"
|
||||
disabled={isEditMode}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="userId" label="报备人(ID)" initialValue={FormData?.userId}>*/}
|
||||
{/* <Input*/}
|
||||
{/* placeholder="自己报备请留空"*/}
|
||||
{/* disabled={isEditMode}*/}
|
||||
{/* type="number"*/}
|
||||
{/* />*/}
|
||||
{/*</Form.Item>*/}
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import {useState, useEffect, useCallback, useRef} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
||||
@@ -16,6 +16,7 @@ import FixedButton from "@/components/FixedButton";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
import {addShopDealerRecord} from "@/api/shop/shopDealerRecord";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
// 扩展User类型,添加客户状态和保护天数
|
||||
interface CustomerUser extends UserType {
|
||||
@@ -32,6 +33,25 @@ const CustomerIndex = () => {
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// 非分销商不允许查看客户列表
|
||||
const {user, hasRole, loading: userLoading} = useUser()
|
||||
// 管理员允许查看全部;普通分销商仅查看自己
|
||||
const isAdminUser = user?.isAdmin === true
|
||||
const canView = hasRole('dealer') || isAdminUser
|
||||
const roleCheckFinished = !userLoading
|
||||
const noPermissionShownRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!roleCheckFinished || canView) return
|
||||
if (noPermissionShownRef.current) return
|
||||
noPermissionShownRef.current = true
|
||||
Taro.showToast({
|
||||
title: '没有查看权限',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
}, [roleCheckFinished, canView])
|
||||
|
||||
// Tab配置
|
||||
const tabList = getStatusOptions();
|
||||
|
||||
@@ -178,12 +198,17 @@ const CustomerIndex = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 4,
|
||||
page: currentPage
|
||||
};
|
||||
// 非管理员:只看自己添加的客户
|
||||
if (!isAdminUser && currentUserId > 0) {
|
||||
params.userId = currentUserId;
|
||||
}
|
||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||
if (applyStatus !== undefined) {
|
||||
params.applyStatus = applyStatus;
|
||||
@@ -226,7 +251,7 @@ const CustomerIndex = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab, page]);
|
||||
}, [activeTab, page, isAdminUser, user?.userId]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
@@ -274,12 +299,19 @@ const CustomerIndex = () => {
|
||||
// 获取所有状态的统计数量
|
||||
const fetchStatusCounts = useCallback(async () => {
|
||||
try {
|
||||
const currentUserId = Number(Taro.getStorageSync('UserId')) || user?.userId || 0;
|
||||
const baseParams: any = {type: 4};
|
||||
// 非管理员:只统计自己添加的客户
|
||||
if (!isAdminUser && currentUserId > 0) {
|
||||
baseParams.userId = currentUserId;
|
||||
}
|
||||
|
||||
// 并行获取各状态的数量
|
||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||||
pageShopDealerApply({type: 4}), // 全部
|
||||
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
|
||||
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
|
||||
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
|
||||
pageShopDealerApply({...baseParams}), // 全部
|
||||
pageShopDealerApply({...baseParams, applyStatus: 10}), // 跟进中
|
||||
pageShopDealerApply({...baseParams, applyStatus: 20}), // 已签约
|
||||
pageShopDealerApply({...baseParams, applyStatus: 30}) // 已取消
|
||||
]);
|
||||
|
||||
setStatusCounts({
|
||||
@@ -291,7 +323,7 @@ const CustomerIndex = () => {
|
||||
} catch (error) {
|
||||
console.error('获取状态统计失败:', error);
|
||||
}
|
||||
}, []);
|
||||
}, [isAdminUser, user?.userId]);
|
||||
|
||||
const getStatusCounts = () => statusCounts;
|
||||
|
||||
@@ -330,22 +362,24 @@ const CustomerIndex = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
// 初始化统计数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
if (!roleCheckFinished || !canView) return;
|
||||
fetchStatusCounts().then();
|
||||
}, []);
|
||||
}, [roleCheckFinished, canView]);
|
||||
|
||||
// 当activeTab变化时重新获取数据
|
||||
useEffect(() => {
|
||||
if (!roleCheckFinished || !canView) return;
|
||||
setList([]); // 清空列表
|
||||
setPage(1); // 重置页码
|
||||
setHasMore(true); // 重置加载状态
|
||||
fetchCustomerData(activeTab, true);
|
||||
}, [activeTab]);
|
||||
}, [activeTab, roleCheckFinished, canView]);
|
||||
|
||||
// 监听页面显示,当从其他页面返回时刷新数据
|
||||
useDidShow(() => {
|
||||
if (!roleCheckFinished || !canView) return;
|
||||
// 刷新当前tab的数据和统计信息
|
||||
setList([]);
|
||||
setPage(1);
|
||||
@@ -543,6 +577,30 @@ const CustomerIndex = () => {
|
||||
);
|
||||
};
|
||||
|
||||
if (!roleCheckFinished) {
|
||||
return (
|
||||
<View className="bg-white flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 ml-2">加载中...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<View className="bg-white flex flex-col items-center justify-center p-4">
|
||||
<Empty description="没有查看权限"/>
|
||||
<Button
|
||||
size="small"
|
||||
style={{marginTop: '12px'}}
|
||||
onClick={() => Taro.navigateBack()}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
{/* 搜索栏 */}
|
||||
|
||||
@@ -256,23 +256,11 @@ export const useUser = () => {
|
||||
return user?.nickname || user?.realName || user?.username || '未登录';
|
||||
};
|
||||
|
||||
// 获取用户显示的角色(同步版本)
|
||||
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
||||
const getRoleName = () => {
|
||||
if(hasRole('superAdmin')){
|
||||
return '超级管理员';
|
||||
// Some APIs return `roles`, some return single `roleName`.
|
||||
return user?.roles?.[0]?.roleName || user?.roleName || '注册用户'
|
||||
}
|
||||
if(hasRole('admin')){
|
||||
return '管理员';
|
||||
}
|
||||
if(hasRole('staff')){
|
||||
return '员工';
|
||||
}
|
||||
if(hasRole('vip')){
|
||||
return 'VIP会员';
|
||||
}
|
||||
return '注册用户';
|
||||
}
|
||||
|
||||
// 检查用户是否已实名认证
|
||||
const isCertified = () => {
|
||||
return user?.certification === true;
|
||||
|
||||
6
src/pages/feature/index.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '功能',
|
||||
navigationBarBackgroundColor: '#1890FF',
|
||||
navigationBarTextStyle: 'white'
|
||||
})
|
||||
|
||||
29
src/pages/feature/index.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
page {
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.feature-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0a2e5a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
33
src/pages/feature/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './index.scss'
|
||||
|
||||
function FeaturePage() {
|
||||
const { params } = useRouter()
|
||||
|
||||
const title = useMemo(() => {
|
||||
const raw = params?.title || '功能'
|
||||
try {
|
||||
return decodeURIComponent(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}, [params?.title])
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({ title })
|
||||
}, [title])
|
||||
|
||||
return (
|
||||
<View className="feature-page">
|
||||
<View className="feature-card">
|
||||
<Text className="feature-title">{title}</Text>
|
||||
<Text className="feature-desc">这是占位页面,可在此接入实际业务功能与路由。</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturePage
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: 'shopLnk.cn - 数灵云店',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarTitleText: '工具箱',
|
||||
navigationBarTextStyle: 'white',
|
||||
navigationBarBackgroundColor: '#1890FF',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
|
||||
@@ -1,20 +1,133 @@
|
||||
page {
|
||||
//background: url('https://oss.wsdns.cn/20250621/33ca4ca532e647bc918a59d01f5d88a9.jpg?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90') no-repeat top center;
|
||||
//background-size: 100%;
|
||||
background: linear-gradient(to bottom, #00eda3, #ffffff);
|
||||
background-color: #F8F8F8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.buy-btn{
|
||||
height: 70px;
|
||||
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
||||
border-radius: 100px;
|
||||
color: #ffffff;
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.navbar {
|
||||
background-color: #1890FF;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
.cart-icon{
|
||||
background: linear-gradient(to bottom, #bbe094, #4ee265);
|
||||
border-radius: 100px 0 0 100px;
|
||||
height: 70px;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: #FFFFFF;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-family: 'PingFang SC', 'HarmonyOS Sans', 'Source Han Sans', sans-serif;
|
||||
}
|
||||
|
||||
.navbar-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px 16px 24px;
|
||||
}
|
||||
|
||||
/* 公司信息卡片 */
|
||||
.company-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.company-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #1890FF, #40A9FF);
|
||||
}
|
||||
|
||||
.company-logo-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.company-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: #0A2E5A;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-family: 'PingFang SC', 'HarmonyOS Sans', 'Source Han Sans', sans-serif;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.company-en {
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
font-family: 'Helvetica Neue', 'Roboto', sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 功能模块网格 */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
font-family: 'PingFang SC', 'HarmonyOS Sans', 'Source Han Sans', sans-serif;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.feature-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +1,127 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import {Sticky, NoticeBar} from '@nutui/nutui-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import Menu from "./Menu";
|
||||
import Banner from "./Banner";
|
||||
import './index.scss'
|
||||
import Grid from "@/pages/index/Grid";
|
||||
import PopUpAd from "@/pages/index/PopUpAd";
|
||||
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||
import type {Config} from "@/api/cms/cmsWebsiteField/model";
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { Apps, File, Location, Orderlist, People, Power, Service } from '@nutui/icons-react-taro';
|
||||
import './index.scss';
|
||||
|
||||
type FeatureKey =
|
||||
| 'opsWorkorder'
|
||||
| 'defect'
|
||||
| 'emergencyRepair'
|
||||
| 'autoDispatch'
|
||||
| 'staffTrack'
|
||||
| 'taskWorkorder';
|
||||
|
||||
type Feature = {
|
||||
key: FeatureKey;
|
||||
title: string;
|
||||
color: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
|
||||
function Home() {
|
||||
// 吸顶状态
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
const [config, setConfig] = useState<Config>()
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '南南佐顿门窗 - 网宿软件',
|
||||
path: `/pages/index/index`
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '南南佐顿门窗 - 网宿软件',
|
||||
path: `/pages/index/index`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
fail: function () {
|
||||
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 onSticky = (item: IArguments) => {
|
||||
if(item){
|
||||
setStickyStatus(!stickyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
|
||||
};
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取站点信息
|
||||
getShopInfo().then(() => {
|
||||
|
||||
})
|
||||
// 获取配置信息
|
||||
configWebsiteField({}).then(data => {
|
||||
setConfig(data)
|
||||
})
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 获取用户信息
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
console.log(avatar, 'avatarUrl')
|
||||
}
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => setStatusBarHeight(res.statusBarHeight ?? 0),
|
||||
fail: () => setStatusBarHeight(0),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const features: Feature[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'opsWorkorder',
|
||||
title: '综合运维工单',
|
||||
color: '#1890FF',
|
||||
icon: <File size={22} color="#FFFFFF" />,
|
||||
},
|
||||
{
|
||||
key: 'defect',
|
||||
title: '缺陷管理',
|
||||
color: '#FA8C16',
|
||||
icon: <Apps size={22} color="#FFFFFF" />,
|
||||
},
|
||||
{
|
||||
key: 'emergencyRepair',
|
||||
title: '应急报修',
|
||||
color: '#F59E0B',
|
||||
icon: <Power size={22} color="#FFFFFF" />,
|
||||
},
|
||||
{
|
||||
key: 'autoDispatch',
|
||||
title: '自动派单配置',
|
||||
color: '#1890FF',
|
||||
icon: <Orderlist size={22} color="#FFFFFF" />,
|
||||
},
|
||||
{
|
||||
key: 'staffTrack',
|
||||
title: '人员轨迹信息',
|
||||
color: '#1890FF',
|
||||
icon: <People size={22} color="#FFFFFF" />,
|
||||
},
|
||||
{
|
||||
key: 'taskWorkorder',
|
||||
title: '任务工单',
|
||||
color: '#1890FF',
|
||||
icon: <Location size={22} color="#FFFFFF" />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCustomerService = () => {
|
||||
// 微信小程序可替换为 openCustomerServiceChat
|
||||
Taro.showToast({ title: '联系客服', icon: 'none' });
|
||||
};
|
||||
|
||||
const handleFeatureClick = (feature: Feature) => {
|
||||
Taro.navigateTo({
|
||||
url: `/pages/feature/index?key=${feature.key}&title=${encodeURIComponent(feature.title)}`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sticky threshold={0} onChange={() => onSticky(arguments)}>
|
||||
<Header stickyStatus={stickyStatus}/>
|
||||
</Sticky>
|
||||
<View className={'flex flex-col mt-1'}>
|
||||
<Menu/>
|
||||
<Banner/>
|
||||
<NoticeBar content={config?.NoticeBar || '主营直购电售电业务,以更优惠电价、更全面的服务,致力为工商企业创造更优越经营环境,帮助企业减负排压,深度赋能'} />
|
||||
<BestSellers/>
|
||||
<Grid />
|
||||
<View className="container">
|
||||
{/* 顶部导航栏 */}
|
||||
<View className="navbar" style={{ paddingTop: `${statusBarHeight}px` }}>
|
||||
<Text className="navbar-title">工具箱</Text>
|
||||
<View className="navbar-icon" onClick={handleCustomerService}>
|
||||
<Service size={22} color="#FFFFFF" />
|
||||
</View>
|
||||
<PopUpAd />
|
||||
</>
|
||||
)
|
||||
</View>
|
||||
|
||||
<View className="content">
|
||||
{/* 公司信息卡片 */}
|
||||
<View className="company-card">
|
||||
<View className="company-logo">
|
||||
<Text className="company-logo-text">网神</Text>
|
||||
</View>
|
||||
<View className="company-text">
|
||||
<Text className="company-name">网神环保科技</Text>
|
||||
<Text className="company-en">WANG SHEN HUAN BAO TECHNOLOGY</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能模块网格 */}
|
||||
<View className="feature-grid">
|
||||
{features.map((feature) => (
|
||||
<View
|
||||
key={feature.key}
|
||||
className="feature-item"
|
||||
onClick={() => handleFeatureClick(feature)}
|
||||
>
|
||||
<View className="feature-icon" style={{ backgroundColor: feature.color }}>
|
||||
{feature.icon}
|
||||
</View>
|
||||
<Text className="feature-title">{feature.title}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home
|
||||
export default Home;
|
||||
|
||||
4
src/pages/message/message.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '消息',
|
||||
navigationBarBackgroundColor: '#1890FF',
|
||||
});
|
||||
17
src/pages/message/message.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.message-page {
|
||||
min-height: 100vh;
|
||||
background-color: #F8F8F8;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #999999;
|
||||
font-size: 16px;
|
||||
}
|
||||
15
src/pages/message/message.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import './message.scss';
|
||||
|
||||
function Message() {
|
||||
return (
|
||||
<View className="message-page">
|
||||
<View className="empty-state">
|
||||
<Text className="empty-text">消息页面</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Message;
|
||||
5
src/pages/toolbox/toolbox.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '工具箱',
|
||||
navigationBarBackgroundColor: '#1890FF',
|
||||
navigationBarTextStyle: 'white'
|
||||
});
|
||||
101
src/pages/toolbox/toolbox.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
page {
|
||||
background-color: #F8F8F8;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.toolbox-container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 品牌标识卡片 */
|
||||
.brand-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
margin: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
color: #0A2E5A;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-family: 'PingFang SC', 'HarmonyOS Sans', 'Source Han Sans', sans-serif;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.brand-en {
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
font-family: 'Helvetica Neue', 'Roboto', sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 功能入口网格 */
|
||||
.menu-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: calc(25% - 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
font-family: 'PingFang SC', 'HarmonyOS Sans', 'Source Han Sans', sans-serif;
|
||||
word-break: break-word;
|
||||
min-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
94
src/pages/toolbox/toolbox.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import './toolbox.scss';
|
||||
|
||||
// 功能入口数据 - 使用 icons8 的在线图标
|
||||
const menuItems = [
|
||||
{
|
||||
id: 1,
|
||||
name: '综合运维工单',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/document.png',
|
||||
bgColor: '#1890FF',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '缺陷管理',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/puzzle.png',
|
||||
bgColor: '#FA8C16',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '应急报修',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/lightning-bolt.png',
|
||||
bgColor: '#F59E0B',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '自动派单配置',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/settings.png',
|
||||
bgColor: '#1890FF',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '人员轨迹信息',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/map-marker.png',
|
||||
bgColor: '#1890FF',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '任务工单',
|
||||
icon: 'https://img.icons8.com/ios-filled/100/ffffff/clipboard.png',
|
||||
bgColor: '#1890FF',
|
||||
},
|
||||
];
|
||||
|
||||
function Toolbox() {
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = (item: typeof menuItems[0]) => {
|
||||
Taro.showToast({
|
||||
title: `点击了${item.name}`,
|
||||
icon: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="toolbox-container">
|
||||
{/* 品牌标识卡片 */}
|
||||
<View className="brand-card">
|
||||
<View className="brand-logo">
|
||||
<Image
|
||||
className="logo-image"
|
||||
src="https://img.icons8.com/color/96/environmental-technology.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<View className="brand-text">
|
||||
<Text className="brand-name">网神环保科技</Text>
|
||||
<Text className="brand-en">WANG SHEN HUAN BAO TECHNOLOGY</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 功能入口网格 */}
|
||||
<View className="menu-grid">
|
||||
{menuItems.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className="menu-item"
|
||||
onClick={() => handleMenuClick(item)}
|
||||
>
|
||||
<View className="menu-icon-wrapper" style={{ backgroundColor: item.bgColor }}>
|
||||
<Image
|
||||
className="menu-icon"
|
||||
src={item.icon}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<Text className="menu-name">{item.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toolbox;
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Avatar, Tag} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Avatar, Button, Tag} from '@nutui/nutui-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {Scan} from '@nutui/icons-react-taro';
|
||||
import {getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
@@ -8,7 +7,6 @@ import {useEffect} from "react";
|
||||
import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
|
||||
function UserCard() {
|
||||
const {
|
||||
@@ -20,7 +18,6 @@ function UserCard() {
|
||||
getDisplayName,
|
||||
getRoleName
|
||||
} = useUser();
|
||||
const {data} = useUserData();
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
@@ -180,12 +177,8 @@ function UserCard() {
|
||||
<View className={'user-info flex flex-col px-2'}>
|
||||
<View className={'py-1 text-black font-bold max-w-28'}>{getDisplayName()}</View>
|
||||
{isLoggedIn ? (
|
||||
<View className={'grade text-xs py-1'}>
|
||||
<Tag type="success" round>
|
||||
<Text className={'p-1'}>
|
||||
{getRoleName()}
|
||||
</Text>
|
||||
</Tag>
|
||||
<View className={'grade text-xs py-0'}>
|
||||
<Tag type="success">{getRoleName()}</Tag>
|
||||
</View>
|
||||
) : ''}
|
||||
</View>
|
||||
@@ -198,27 +191,6 @@ function UserCard() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{/*<View className={'flex justify-around mt-1'}>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/wallet/wallet', true)}>*/}
|
||||
{/* <Text className={'text-sm text-gray-500'}>余额</Text>*/}
|
||||
{/* <Text className={'text-xl'}>{data?.balance || '0.00'}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}>*/}
|
||||
{/* <Text className={'text-sm text-gray-500'}>积分</Text>*/}
|
||||
{/* <Text className={'text-xl'}>{data?.points || 0}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/coupon/index', true)}>*/}
|
||||
{/* <Text className={'text-sm text-gray-500'}>优惠券</Text>*/}
|
||||
{/* <Text className={'text-xl'}>{data?.coupons || 0}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/gift/index', true)}>*/}
|
||||
{/* <Text className={'text-sm text-gray-500'}>礼品卡</Text>*/}
|
||||
{/* <Text className={'text-xl'}>{data?.giftCards || 0}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/*</View>*/}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '忘记密码',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '注册账号',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '服务配置',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品分类',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品详情',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -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 来确保覆盖默认样式 */
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单确认',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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: "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单详情',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '商品搜索'
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@ import Taro from '@tarojs/taro';
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
// 模版套餐ID - 请根据实际情况修改
|
||||
export const TEMPLATE_ID = '10582';
|
||||
export const TEMPLATE_ID = '10589';
|
||||
// 服务接口 - 请根据实际情况修改
|
||||
export const SERVER_API_URL = 'https://server.websoft.top/api';
|
||||
// export const SERVER_API_URL = 'http://127.0.0.1:8000/api';
|
||||
|
||||