refactor(src): 移除客户详情相关代码
- 删除了客户详情页面的配置、样式和组件文件 - 更新了 app.config.ts,移除了与客户详情相关的页面引用 - 优化了首页 Grid 组件的导航逻辑
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
import type {ApiResult, PageResult} from '@/api/index';
|
||||
import type {ApiResult, PageResult} from '@/api';
|
||||
import type {User, UserParam} from './model';
|
||||
import {SERVER_API_URL} from "@/utils/server";
|
||||
|
||||
@@ -154,7 +154,7 @@ export async function updateUserStatus(userId?: number, status?: number) {
|
||||
/**
|
||||
* 修改推荐状态
|
||||
*/
|
||||
export async function updateUserRecommend(form) {
|
||||
export async function updateUserRecommend(form:any) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/system/user/recommend',
|
||||
form
|
||||
|
||||
@@ -3,12 +3,7 @@ export default defineAppConfig({
|
||||
'pages/index/index',
|
||||
'pages/cart/cart',
|
||||
'pages/find/find',
|
||||
'pages/user/user',
|
||||
'pages/customer/list',
|
||||
'pages/customer/sign',
|
||||
'pages/customer/detail',
|
||||
'pages/customer/trading',
|
||||
'pages/customer/invite'
|
||||
'pages/user/user'
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# 微信二维码图片说明
|
||||
|
||||
请将以下微信二维码图片放置在此目录中:
|
||||
|
||||
## 需要的图片文件
|
||||
|
||||
1. `wechat-service-qr.png` - 客服微信二维码
|
||||
2. `wechat-tech-qr.png` - 技术支持微信二维码
|
||||
|
||||
## 图片要求
|
||||
|
||||
- 格式:PNG 或 JPG
|
||||
- 尺寸:建议 200x200 像素或更高分辨率
|
||||
- 背景:建议白色背景,确保二维码清晰可见
|
||||
|
||||
## 使用说明
|
||||
|
||||
这些图片将在 `/dealer/wechat` 页面中显示,用户可以长按保存并扫码添加微信好友。
|
||||
|
||||
## 自定义
|
||||
|
||||
如需修改图片路径或添加更多二维码,请编辑 `src/dealer/wechat/index.tsx` 文件中的 `qrCodeData` 数组。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
@@ -180,7 +180,9 @@ const DealerOrders: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无收益"/>
|
||||
<Empty description="暂无收益" style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
navigationBarTitleText: '邀请推广'
|
||||
})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Space, Avatar, Loading} from '@nutui/nutui-react-taro'
|
||||
import {Space,Empty, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {User} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
|
||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import navTo from "@/utils/common";
|
||||
|
||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||
name?: string
|
||||
@@ -176,16 +178,28 @@ const DealerTeam: React.FC = () => {
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
<Space className="bg-gray-50 flex items-center justify-center">
|
||||
<Empty description="您还不是经销商" style={{
|
||||
backgroundColor: 'transparent'
|
||||
}} actions={[{ text: '去申请开通', onClick: () => navTo(`/dealer/apply/add`,true)}]}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="min-h-screen">
|
||||
{renderOverview()}
|
||||
{teamMembers.length > 0 ? (
|
||||
renderOverview()
|
||||
) : (
|
||||
<View className="flex items-center justify-center mt-20">
|
||||
<Empty description="暂无数据" style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<FixedButton text={'立即邀请'} onClick={() => navTo(`/dealer/qrcode/index`, true)}/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,6 @@ import './index.scss'
|
||||
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||
import {CmsWebsiteField} from "@/api/cms/cmsWebsiteField/model";
|
||||
|
||||
interface QrCodeData {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
qrCode: string
|
||||
wechatId: string
|
||||
}
|
||||
const WechatService = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [codes, setCodes] = useState<CmsWebsiteField[]>([])
|
||||
@@ -26,7 +19,6 @@ const WechatService = () => {
|
||||
className="qr-code-image"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<Text className="wechat-id">微信号:{data.comments}</Text>
|
||||
</View>
|
||||
|
||||
<View className="qr-tips">
|
||||
|
||||
@@ -42,7 +42,6 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerUser(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerUser(dealer)
|
||||
} else {
|
||||
@@ -66,7 +65,7 @@ export const useDealerUser = (): UseDealerUserReturn => {
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
fetchDealerData().then()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户详情',
|
||||
navigationBarTextStyle: 'white',
|
||||
navigationStyle: 'custom',
|
||||
backgroundColor: '#f5f5f5'
|
||||
})
|
||||
@@ -1,318 +0,0 @@
|
||||
.customer-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
padding-bottom: 100px;
|
||||
|
||||
.header-bg {
|
||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50vh;
|
||||
color: #999;
|
||||
|
||||
.nut-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 20px 16px;
|
||||
margin-top: 20px;
|
||||
|
||||
.customer-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.customer-header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.company-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.company-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nut-tag {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nut-icon {
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
|
||||
&.phone {
|
||||
color: #52c41a;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nut-button {
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
.nut-cell {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nut-cell__title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nut-cell__value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-history {
|
||||
padding: 0 16px 16px;
|
||||
|
||||
.history-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.history-type {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.history-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.history-operator {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
z-index: 100;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
&.call-btn {
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.renew-btn {
|
||||
background: #1890ff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配安全区域
|
||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||
.customer-detail-page .fixed-bottom {
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式优化
|
||||
.nut-tag {
|
||||
&.nut-tag--plain {
|
||||
border-width: 1px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.customer-detail-page {
|
||||
.detail-container {
|
||||
padding: 16px 12px;
|
||||
|
||||
.customer-card {
|
||||
padding: 16px;
|
||||
|
||||
.customer-header {
|
||||
.company-info {
|
||||
.company-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
.contact-item {
|
||||
.contact-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-section {
|
||||
.section-header {
|
||||
padding: 14px 12px 6px;
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
.nut-cell {
|
||||
padding: 10px 12px;
|
||||
|
||||
.nut-cell__title,
|
||||
.nut-cell__value {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-history {
|
||||
padding: 0 12px 12px;
|
||||
|
||||
.history-item {
|
||||
.history-content {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
.action-buttons {
|
||||
.action-btn {
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
// import Taro, { useRouter } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import {
|
||||
NavBar,
|
||||
Button,
|
||||
Cell,
|
||||
Tag,
|
||||
Toast
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import {
|
||||
Phone,
|
||||
Location,
|
||||
Calendar,
|
||||
User,
|
||||
Edit,
|
||||
Share
|
||||
} from '@nutui/icons-react-taro';
|
||||
import './detail.scss';
|
||||
|
||||
// 已签约客户详情数据类型
|
||||
interface CustomerDetail {
|
||||
id: string;
|
||||
companyName: string;
|
||||
contactPerson: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
signDate: string;
|
||||
contractInfo: {
|
||||
contractType: string;
|
||||
contractAmount: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
paymentMethod: string;
|
||||
paymentCycle: string;
|
||||
status: 'active' | 'expired' | 'terminated';
|
||||
};
|
||||
businessInfo: {
|
||||
industry: string;
|
||||
scale: string;
|
||||
registeredCapital: string;
|
||||
businessScope: string;
|
||||
};
|
||||
contactHistory: Array<{
|
||||
id: string;
|
||||
date: string;
|
||||
type: string;
|
||||
content: string;
|
||||
operator: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const CustomerDetail = () => {
|
||||
// const router = useRouter();
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
const [customerDetail, setCustomerDetail] = useState<CustomerDetail | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMsg, setToastMsg] = useState('');
|
||||
|
||||
// 模拟客户详情数据
|
||||
const mockCustomerDetail: CustomerDetail = {
|
||||
id: '1',
|
||||
companyName: '广州雅虎信息科技公司',
|
||||
contactPerson: '张经理',
|
||||
phone: '13882223433',
|
||||
address: '广西南宁市良庆区五象大道401号五象新城1号楼1226室',
|
||||
signDate: '2025-08-15 10:23:33',
|
||||
contractInfo: {
|
||||
contractType: '服务合同',
|
||||
contractAmount: '500,000',
|
||||
startDate: '2025-08-15',
|
||||
endDate: '2026-08-14',
|
||||
paymentMethod: '分期付款',
|
||||
paymentCycle: '按季度付款',
|
||||
status: 'active'
|
||||
},
|
||||
businessInfo: {
|
||||
industry: '信息技术服务',
|
||||
scale: '中型企业',
|
||||
registeredCapital: '1000万元',
|
||||
businessScope: '软件开发、技术咨询、系统集成'
|
||||
},
|
||||
contactHistory: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2025-08-20',
|
||||
type: '电话沟通',
|
||||
content: '讨论项目进度,客户反馈良好',
|
||||
operator: '李销售'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2025-08-18',
|
||||
type: '现场拜访',
|
||||
content: '实地考察客户需求,确认技术方案',
|
||||
operator: '王工程师'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
date: '2025-08-15',
|
||||
type: '合同签署',
|
||||
content: '正式签署服务合同,项目启动',
|
||||
operator: '张经理'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const showToastMsg = (msg: string) => {
|
||||
setToastMsg(msg);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 2000);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '#52c41a';
|
||||
case 'expired':
|
||||
return '#ff6b35';
|
||||
case 'terminated':
|
||||
return '#ff4d4f';
|
||||
default:
|
||||
return '#999';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '履行中';
|
||||
case 'expired':
|
||||
return '已到期';
|
||||
case 'terminated':
|
||||
return '已终止';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const handleCall = () => {
|
||||
if (customerDetail?.phone) {
|
||||
Taro.makePhoneCall({
|
||||
phoneNumber: customerDetail.phone
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
showToastMsg('编辑功能开发中');
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
showToastMsg('分享功能开发中');
|
||||
};
|
||||
|
||||
const handleAddContact = () => {
|
||||
showToastMsg('添加联系记录功能开发中');
|
||||
};
|
||||
|
||||
const handleViewContract = () => {
|
||||
showToastMsg('查看合同详情功能开发中');
|
||||
};
|
||||
|
||||
const handleRenewContract = () => {
|
||||
showToastMsg('续约功能开发中');
|
||||
};
|
||||
|
||||
const loadCustomerDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
setCustomerDetail(mockCustomerDetail);
|
||||
} catch (error) {
|
||||
showToastMsg('加载失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(Number(res.statusBarHeight));
|
||||
},
|
||||
});
|
||||
|
||||
loadCustomerDetail();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="customer-detail-page">
|
||||
<View className="loading-container">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!customerDetail) {
|
||||
return (
|
||||
<View className="customer-detail-page">
|
||||
<View className="error-container">
|
||||
<Text>客户信息加载失败</Text>
|
||||
<Button onClick={loadCustomerDetail}>重新加载</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="customer-detail-page">
|
||||
{/* 头部背景 */}
|
||||
<View className="header-bg" style={{ height: '180px' }} />
|
||||
|
||||
{/* 导航栏 */}
|
||||
<NavBar
|
||||
style={{
|
||||
marginTop: `${statusBarHeight}px`,
|
||||
backgroundColor: 'transparent',
|
||||
color: '#ffffff'
|
||||
}}
|
||||
onBackClick={() => Taro.navigateBack()}
|
||||
right={
|
||||
<View className="nav-actions">
|
||||
<Edit size={20} color="#ffffff" onClick={handleEdit} />
|
||||
<Share size={20} color="#ffffff" onClick={handleShare} />
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<Text style={{ color: '#ffffff', fontSize: '18px', fontWeight: 'bold' }}>
|
||||
客户详情
|
||||
</Text>
|
||||
</NavBar>
|
||||
|
||||
{/* 客户基本信息 */}
|
||||
<View className="detail-container">
|
||||
<View className="customer-card">
|
||||
<View className="customer-header">
|
||||
<View className="company-info">
|
||||
<Text className="company-name">{customerDetail.companyName}</Text>
|
||||
<Tag
|
||||
color={getStatusColor(customerDetail.contractInfo.status)}
|
||||
plain
|
||||
>
|
||||
{getStatusText(customerDetail.contractInfo.status)}
|
||||
</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="contact-info">
|
||||
<View className="contact-item">
|
||||
<User size={16} color="#666" />
|
||||
<Text className="contact-text">{customerDetail.contactPerson}</Text>
|
||||
</View>
|
||||
<View className="contact-item" onClick={handleCall}>
|
||||
<Phone size={16} color="#52c41a" />
|
||||
<Text className="contact-text phone">{customerDetail.phone}</Text>
|
||||
</View>
|
||||
<View className="contact-item">
|
||||
<Location size={16} color="#666" />
|
||||
<Text className="contact-text">{customerDetail.address}</Text>
|
||||
</View>
|
||||
<View className="contact-item">
|
||||
<Calendar size={16} color="#666" />
|
||||
<Text className="contact-text">签约时间:{customerDetail.signDate}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 合同信息 */}
|
||||
<View className="info-section">
|
||||
<View className="section-header">
|
||||
<Text className="section-title">合同信息</Text>
|
||||
<Button size="small" onClick={handleViewContract}>查看合同</Button>
|
||||
</View>
|
||||
<View className="info-content">
|
||||
<Cell title="合同类型" extra={customerDetail.contractInfo.contractType} />
|
||||
<Cell title="合同金额" extra={`¥${customerDetail.contractInfo.contractAmount}`} />
|
||||
<Cell title="合同期限" extra={`${customerDetail.contractInfo.startDate} 至 ${customerDetail.contractInfo.endDate}`} />
|
||||
<Cell title="付款方式" extra={customerDetail.contractInfo.paymentMethod} />
|
||||
<Cell title="付款周期" extra={customerDetail.contractInfo.paymentCycle} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 企业信息 */}
|
||||
<View className="info-section">
|
||||
<View className="section-header">
|
||||
<Text className="section-title">企业信息</Text>
|
||||
</View>
|
||||
<View className="info-content">
|
||||
<Cell title="所属行业" extra={customerDetail.businessInfo.industry} />
|
||||
<Cell title="企业规模" extra={customerDetail.businessInfo.scale} />
|
||||
<Cell title="注册资本" extra={customerDetail.businessInfo.registeredCapital} />
|
||||
<Cell title="经营范围" extra={customerDetail.businessInfo.businessScope} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 联系记录 */}
|
||||
<View className="info-section">
|
||||
<View className="section-header">
|
||||
<Text className="section-title">联系记录</Text>
|
||||
<Button size="small" onClick={handleAddContact}>添加记录</Button>
|
||||
</View>
|
||||
<View className="contact-history">
|
||||
{customerDetail.contactHistory.map((record) => (
|
||||
<View key={record.id} className="history-item">
|
||||
<View className="history-header">
|
||||
<Text className="history-type">{record.type}</Text>
|
||||
<Text className="history-date">{record.date}</Text>
|
||||
</View>
|
||||
<Text className="history-content">{record.content}</Text>
|
||||
<Text className="history-operator">操作人:{record.operator}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
<View className="fixed-bottom">
|
||||
<View className="action-buttons">
|
||||
<Button className="action-btn call-btn" onClick={handleCall}>
|
||||
<Phone size={16} />
|
||||
拨打电话
|
||||
</Button>
|
||||
<Button className="action-btn renew-btn" onClick={handleRenewContract}>
|
||||
续约
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Toast提示 */}
|
||||
<Toast
|
||||
visible={showToast}
|
||||
content={toastMsg}
|
||||
duration={2000}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerDetail;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请好友'
|
||||
})
|
||||
@@ -1,429 +0,0 @@
|
||||
.customer-invite-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
|
||||
.header-bg {
|
||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.nut-tabs {
|
||||
background: transparent;
|
||||
|
||||
.nut-tabs__titles {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 12px 16px;
|
||||
|
||||
&.nut-tabs__titles-item--active {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__line {
|
||||
background: #ffffff;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 20px 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// 二维码标签页样式
|
||||
.qrcode-container {
|
||||
.qr-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.qr-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.qr-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qr-subtitle {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.qr-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
margin: 0 auto;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
background: #fafafa;
|
||||
|
||||
.qr-text {
|
||||
margin-top: 8px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
border: none;
|
||||
|
||||
&:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-tips {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.tips-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.tips-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tips-content {
|
||||
.tip-item {
|
||||
display: block;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manual-invite {
|
||||
.manual-btn {
|
||||
width: 100%;
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请记录标签页样式
|
||||
.records-container {
|
||||
.record-item {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-details {
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
color: #666;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-records {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请统计标签页样式
|
||||
.stats-container {
|
||||
.stats-overview {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reward-summary {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.reward-card {
|
||||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
|
||||
.reward-title {
|
||||
opacity: 0.9;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.reward-amount {
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.withdraw-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-chart {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.chart-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请表单弹窗样式
|
||||
.invite-form {
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
|
||||
.form-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.form-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-form-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.nut-form-item__label {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nut-input {
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
|
||||
.cancel-btn,
|
||||
.submit-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式优化
|
||||
.nut-tag {
|
||||
&.nut-tag--plain {
|
||||
border-width: 1px;
|
||||
padding: 2px 6px;
|
||||
|
||||
&.nut-tag--small {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.customer-invite-page {
|
||||
.content-container {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
.qr-card {
|
||||
padding: 20px;
|
||||
|
||||
.qr-code {
|
||||
.qr-placeholder {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-actions {
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
.stats-overview {
|
||||
padding: 16px;
|
||||
|
||||
.stat-item {
|
||||
.stat-number {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reward-summary {
|
||||
.reward-card {
|
||||
padding: 16px;
|
||||
|
||||
.reward-amount {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import {
|
||||
Space,
|
||||
Tabs,
|
||||
Button,
|
||||
Tag,
|
||||
Toast,
|
||||
Popup,
|
||||
Input,
|
||||
Form
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import {
|
||||
QrCode,
|
||||
Share,
|
||||
Copy,
|
||||
Download,
|
||||
User,
|
||||
Phone,
|
||||
Calendar,
|
||||
Gift
|
||||
} from '@nutui/icons-react-taro';
|
||||
import './invite.scss';
|
||||
|
||||
// 邀请记录数据类型
|
||||
interface InviteRecord {
|
||||
id: string;
|
||||
inviteeName: string;
|
||||
inviteePhone: string;
|
||||
inviteDate: string;
|
||||
status: 'pending' | 'registered' | 'signed';
|
||||
reward: string;
|
||||
}
|
||||
|
||||
// 邀请统计数据类型
|
||||
interface InviteStats {
|
||||
totalInvites: number;
|
||||
registeredCount: number;
|
||||
signedCount: number;
|
||||
totalReward: string;
|
||||
}
|
||||
|
||||
const CustomerInvite = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
const [activeTab, setActiveTab] = useState<string>('qrcode');
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
|
||||
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([]);
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMsg, setToastMsg] = useState('');
|
||||
const [showInviteForm, setShowInviteForm] = useState(false);
|
||||
const [inviteFormData, setInviteFormData] = useState({
|
||||
name: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
// 模拟邀请记录数据
|
||||
const mockInviteRecords: InviteRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
inviteeName: '张三',
|
||||
inviteePhone: '138****1234',
|
||||
inviteDate: '2025-08-20 14:30:00',
|
||||
status: 'signed',
|
||||
reward: '500'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
inviteeName: '李四',
|
||||
inviteePhone: '139****5678',
|
||||
inviteDate: '2025-08-19 10:15:00',
|
||||
status: 'registered',
|
||||
reward: '200'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
inviteeName: '王五',
|
||||
inviteePhone: '136****9012',
|
||||
inviteDate: '2025-08-18 16:45:00',
|
||||
status: 'pending',
|
||||
reward: '0'
|
||||
}
|
||||
];
|
||||
|
||||
// 模拟邀请统计数据
|
||||
const mockInviteStats: InviteStats = {
|
||||
totalInvites: 15,
|
||||
registeredCount: 8,
|
||||
signedCount: 3,
|
||||
totalReward: '2,800'
|
||||
};
|
||||
|
||||
const tabList = [
|
||||
{ title: '邀请二维码', value: 'qrcode' },
|
||||
{ title: '邀请记录', value: 'records' },
|
||||
{ title: '邀请统计', value: 'stats' }
|
||||
];
|
||||
|
||||
const showToastMsg = (msg: string) => {
|
||||
setToastMsg(msg);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 2000);
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待注册';
|
||||
case 'registered':
|
||||
return '已注册';
|
||||
case 'signed':
|
||||
return '已签约';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '#ff6b35';
|
||||
case 'registered':
|
||||
return '#1890ff';
|
||||
case 'signed':
|
||||
return '#52c41a';
|
||||
default:
|
||||
return '#999';
|
||||
}
|
||||
};
|
||||
|
||||
// 生成二维码
|
||||
const generateQRCode = () => {
|
||||
// 模拟生成二维码URL
|
||||
const inviteCode = 'INV' + Date.now().toString().slice(-6);
|
||||
const qrUrl = `https://example.com/invite?code=${inviteCode}`;
|
||||
setQrCodeUrl(qrUrl);
|
||||
|
||||
// 实际项目中这里应该调用二维码生成库
|
||||
showToastMsg('二维码已生成');
|
||||
};
|
||||
|
||||
// 复制邀请链接
|
||||
const copyInviteLink = () => {
|
||||
const inviteLink = `https://example.com/invite?code=INV123456`;
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: inviteLink,
|
||||
success: () => {
|
||||
showToastMsg('邀请链接已复制到剪贴板');
|
||||
},
|
||||
fail: () => {
|
||||
showToastMsg('复制失败,请重试');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 分享邀请
|
||||
const shareInvite = () => {
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
showToastMsg('分享成功');
|
||||
},
|
||||
fail: () => {
|
||||
showToastMsg('分享失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 下载二维码
|
||||
const downloadQRCode = () => {
|
||||
showToastMsg('下载功能开发中');
|
||||
};
|
||||
|
||||
// 手动邀请
|
||||
const handleManualInvite = () => {
|
||||
setShowInviteForm(true);
|
||||
};
|
||||
|
||||
// 提交邀请表单
|
||||
const submitInviteForm = () => {
|
||||
if (!inviteFormData.name || !inviteFormData.phone) {
|
||||
showToastMsg('请填写完整信息');
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟发送邀请
|
||||
showToastMsg('邀请已发送');
|
||||
setShowInviteForm(false);
|
||||
setInviteFormData({ name: '', phone: '' });
|
||||
|
||||
// 刷新邀请记录
|
||||
loadInviteData().then();
|
||||
};
|
||||
|
||||
// 加载邀请数据
|
||||
const loadInviteData = async () => {
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setInviteRecords(mockInviteRecords);
|
||||
setInviteStats(mockInviteStats);
|
||||
} catch (error) {
|
||||
showToastMsg('加载失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(Number(res.statusBarHeight))
|
||||
},
|
||||
});
|
||||
|
||||
generateQRCode();
|
||||
loadInviteData().then();
|
||||
}, []);
|
||||
|
||||
const renderQRCodeTab = () => (
|
||||
<View className="qrcode-container">
|
||||
<View className="qr-card">
|
||||
<View className="qr-header">
|
||||
<View className="qr-title pt-2">我的邀请二维码</View>
|
||||
<View className="qr-subtitle py-2">扫码注册即可获得奖励</View>
|
||||
</View>
|
||||
|
||||
<View className="qr-code">
|
||||
{qrCodeUrl ? (
|
||||
<View className="qr-placeholder">
|
||||
<QrCode size={80} color="#333" />
|
||||
<Text className="qr-text">二维码</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="qr-loading">
|
||||
<Text>生成中...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Space className="qr-actions mt-4">
|
||||
<Button
|
||||
className="action-btn"
|
||||
size="small"
|
||||
onClick={copyInviteLink}
|
||||
>
|
||||
<View className={'flex items-center justify-center px-1'}>
|
||||
<Copy size={14} />
|
||||
<Text className="ml-1">复制链接</Text>
|
||||
</View>
|
||||
</Button>
|
||||
<Button
|
||||
className="action-btn"
|
||||
size="small"
|
||||
onClick={shareInvite}
|
||||
>
|
||||
<View className={'flex items-center justify-center px-1'}>
|
||||
<Share size={14} />
|
||||
<Text className="ml-1">分享邀请</Text>
|
||||
</View>
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={downloadQRCode}
|
||||
>
|
||||
<View className={'flex items-center justify-center px-1'}>
|
||||
<Download size={14} />
|
||||
<Text className="ml-1">下载二维码</Text>
|
||||
</View>
|
||||
</Button>
|
||||
</Space>
|
||||
</View>
|
||||
|
||||
<View className="invite-tips">
|
||||
<View className="tips-header">
|
||||
<Gift size={16} color="#52c41a" />
|
||||
<Text className="tips-title">邀请奖励规则</Text>
|
||||
</View>
|
||||
<View className="tips-content">
|
||||
<Text className="tip-item">• 邀请好友注册:奖励200元</Text>
|
||||
<Text className="tip-item">• 好友成功签约:额外奖励300元</Text>
|
||||
<Text className="tip-item">• 奖励将在好友完成相应操作后24小时内到账</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="manual-invite">
|
||||
<Button
|
||||
className="manual-btn"
|
||||
onClick={handleManualInvite}
|
||||
>
|
||||
手动邀请好友
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderRecordsTab = () => (
|
||||
<View className="records-container">
|
||||
{inviteRecords.length > 0 ? (
|
||||
inviteRecords.map((record) => (
|
||||
<View key={record.id} className="record-item">
|
||||
<View className="record-header">
|
||||
<View className="user-info">
|
||||
<User size={16} color="#666" />
|
||||
<Text className="user-name">{record.inviteeName}</Text>
|
||||
</View>
|
||||
<Tag
|
||||
color={getStatusColor(record.status)}
|
||||
plain
|
||||
>
|
||||
{getStatusText(record.status)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="record-details">
|
||||
<View className="detail-item">
|
||||
<Phone size={14} color="#999" />
|
||||
<Text className="detail-text">{record.inviteePhone}</Text>
|
||||
</View>
|
||||
<View className="detail-item">
|
||||
<Calendar size={14} color="#999" />
|
||||
<Text className="detail-text">{record.inviteDate}</Text>
|
||||
</View>
|
||||
<View className="detail-item">
|
||||
<Gift size={14} color="#52c41a" />
|
||||
<Text className="detail-text">
|
||||
奖励:¥{record.reward}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className="empty-records">
|
||||
<Text>暂无邀请记录</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderStatsTab = () => (
|
||||
<View className="stats-container">
|
||||
{inviteStats && (
|
||||
<>
|
||||
<View className="stats-overview">
|
||||
<View className="stat-item">
|
||||
<Text className="stat-number">{inviteStats.totalInvites}</Text>
|
||||
<Text className="stat-label">总邀请数</Text>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<Text className="stat-number">{inviteStats.registeredCount}</Text>
|
||||
<Text className="stat-label">已注册</Text>
|
||||
</View>
|
||||
<View className="stat-item">
|
||||
<Text className="stat-number">{inviteStats.signedCount}</Text>
|
||||
<Text className="stat-label">已签约</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="reward-summary">
|
||||
<Space className="reward-card flex items-center justify-center">
|
||||
<Text>累计奖励</Text>
|
||||
<Text>¥{inviteStats.totalReward}</Text>
|
||||
<Button type={'success'} size="small">
|
||||
提现
|
||||
</Button>
|
||||
</Space>
|
||||
</View>
|
||||
|
||||
<View className="stats-chart">
|
||||
<Text className="chart-title">邀请趋势</Text>
|
||||
<View className="chart-placeholder">
|
||||
<Text>图表功能开发中</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="customer-invite-page">
|
||||
{/* 头部背景 */}
|
||||
<View className="header-bg" style={{ height: '180px' }} />
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="tabs-container">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value as string)}
|
||||
>
|
||||
{tabList.map(tab => (
|
||||
<Tabs.TabPane key={tab.value} title={tab.title} value={tab.value} />
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<View className="content-container">
|
||||
{activeTab === 'qrcode' && renderQRCodeTab()}
|
||||
{activeTab === 'records' && renderRecordsTab()}
|
||||
{activeTab === 'stats' && renderStatsTab()}
|
||||
</View>
|
||||
|
||||
{/* 手动邀请弹窗 */}
|
||||
<Popup
|
||||
visible={showInviteForm}
|
||||
position="bottom"
|
||||
onClose={() => setShowInviteForm(false)}
|
||||
>
|
||||
<View className="invite-form">
|
||||
<View className="form-header">
|
||||
<Text className="form-title">邀请好友</Text>
|
||||
</View>
|
||||
<Form>
|
||||
<Form.Item label="姓名" required>
|
||||
<Input
|
||||
placeholder="请输入好友姓名"
|
||||
value={inviteFormData.name}
|
||||
onChange={(value) => setInviteFormData(prev => ({ ...prev, name: value }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" required>
|
||||
<Input
|
||||
placeholder="请输入好友手机号"
|
||||
type="tel"
|
||||
value={inviteFormData.phone}
|
||||
onChange={(value) => setInviteFormData(prev => ({ ...prev, phone: value }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<View className="form-actions">
|
||||
<Button
|
||||
className="cancel-btn"
|
||||
onClick={() => setShowInviteForm(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className="submit-btn"
|
||||
onClick={submitInviteForm}
|
||||
>
|
||||
发送邀请
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* Toast提示 */}
|
||||
<Toast
|
||||
visible={showToast}
|
||||
content={toastMsg}
|
||||
duration={2000}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerInvite;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户列表'
|
||||
})
|
||||
@@ -1,197 +0,0 @@
|
||||
.customer-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
|
||||
.header-bg {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.nut-tabs {
|
||||
background: transparent;
|
||||
|
||||
.nut-tabs__titles {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 12px 20px;
|
||||
|
||||
&.nut-tabs__titles-item--active {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__line {
|
||||
background: #ffffff;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-list {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 20px 16px 100px;
|
||||
margin-top: 20px;
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px 30px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.customer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.company-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.contact-label {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.phone {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.phone-icon {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.address-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.address {
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.time-row {
|
||||
.time {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.action-btn {
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
|
||||
&.sign-btn {
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.cancel-btn {
|
||||
background: #ff4d4f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.detail-btn {
|
||||
background: #1890ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
z-index: 100;
|
||||
|
||||
.report-btn {
|
||||
width: 100%;
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
border-radius: 24px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配安全区域
|
||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||
.customer-list-page .fixed-bottom {
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {View, Text} from '@tarojs/components';
|
||||
import {Space, Tabs, Button, Empty} from '@nutui/nutui-react-taro';
|
||||
import {Phone} from '@nutui/icons-react-taro';
|
||||
import './list.scss';
|
||||
import {pageUsers} from "@/api/system/user";
|
||||
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
||||
|
||||
|
||||
const CustomerList = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('all');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [list, setList] = useState<ShopDealerUser[]>([]);
|
||||
|
||||
const tabList = [
|
||||
{title: '全部', value: 'all'},
|
||||
{title: '跟进中', value: 'pending'},
|
||||
{title: '已签约', value: 'confirmed'},
|
||||
{title: '已取消', value: 'cancelled'}
|
||||
];
|
||||
|
||||
const reload = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await pageUsers({status: 0});
|
||||
console.log(res, '客户列表');
|
||||
if(res?.list){
|
||||
// 为每个用户添加默认状态
|
||||
const customersWithStatus: ShopDealerUser[] = res.list.map(user => ({
|
||||
...user,
|
||||
status: 'pending' // 默认状态为跟进中
|
||||
}));
|
||||
setList(customersWithStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error);
|
||||
Taro.showToast({
|
||||
title: '获取客户列表失败',
|
||||
icon: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '跟进中';
|
||||
case 'confirmed':
|
||||
return '已签约';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '#ff6b35';
|
||||
case 'confirmed':
|
||||
return '#52c41a';
|
||||
case 'cancelled':
|
||||
return '#999';
|
||||
default:
|
||||
return '#999';
|
||||
}
|
||||
};
|
||||
|
||||
const handleCall = (phone: string) => {
|
||||
Taro.makePhoneCall({
|
||||
phoneNumber: phone
|
||||
});
|
||||
};
|
||||
|
||||
const handleAction = (customer: ShopDealerUser, action: 'sign' | 'cancel' | 'detail') => {
|
||||
switch (action) {
|
||||
case 'sign':
|
||||
// 跳转到签约页面
|
||||
Taro.navigateTo({
|
||||
url: `/pages/customer/sign?customerId=${customer.userId}`
|
||||
});
|
||||
break;
|
||||
case 'cancel':
|
||||
Taro.showModal({
|
||||
title: '确认取消',
|
||||
content: '确定要取消该客户吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 这里应该调用取消客户的API
|
||||
Taro.showToast({
|
||||
title: '已取消',
|
||||
icon: 'success'
|
||||
});
|
||||
// 刷新列表
|
||||
reload().then();
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'detail':
|
||||
// 跳转到客户详情页面
|
||||
Taro.navigateTo({
|
||||
url: `/pages/customer/detail?customerId=${customer.userId}`
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReport = () => {
|
||||
// 跳转到邀请页面
|
||||
Taro.navigateTo({
|
||||
url: '/pages/customer/invite'
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="customer-list-page">
|
||||
{/* 头部背景 */}
|
||||
<View className="header-bg" style={{
|
||||
height: '180px'
|
||||
}} />
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="tabs-container">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value as string)}
|
||||
>
|
||||
{tabList.map(tab => (
|
||||
<Tabs.TabPane key={tab.value} title={tab.title} value={tab.value} />
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 客户列表 */}
|
||||
<View className="customer-list">
|
||||
{loading ? (
|
||||
<View className="loading-container">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : list.length > 0 ? (
|
||||
list.map((record) => (
|
||||
<View key={record.userId} className="customer-item">
|
||||
<View className="customer-header">
|
||||
<Text className="company-name">{record.realName || '未知客户'}</Text>
|
||||
<Text
|
||||
className="status-tag"
|
||||
style={{color: getStatusColor('pending')}}
|
||||
>
|
||||
{getStatusText('pending')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="customer-info">
|
||||
<View className="info-row">
|
||||
<Text className="label">联系人:</Text>
|
||||
<Text className="value">{record.realName || '未知'}</Text>
|
||||
<Text className="label contact-label">联系电话:</Text>
|
||||
<Text className="value">{record.mobile || '未提供'}</Text>
|
||||
<Phone
|
||||
size={14}
|
||||
className={'text-green-500'}
|
||||
onClick={() => handleCall(`${record?.mobile}`)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="address-row">
|
||||
<Text className="label">地址:</Text>
|
||||
<Text className="address">{'地址未提供'}</Text>
|
||||
</View>
|
||||
|
||||
<View className="time-row">
|
||||
<Text className="time">添加时间:{record.createTime || '未知'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="action-buttons">
|
||||
{record.payPassword === 'pending' && (
|
||||
<Space>
|
||||
<Button
|
||||
className="action-btn sign-btn"
|
||||
size="small"
|
||||
onClick={() => handleAction(record, 'sign')}
|
||||
>
|
||||
签约
|
||||
</Button>
|
||||
<Button
|
||||
className="action-btn cancel-btn"
|
||||
size="small"
|
||||
onClick={() => handleAction(record, 'cancel')}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{record.payPassword === 'confirmed' && (
|
||||
<Button
|
||||
className="action-btn detail-btn"
|
||||
size="small"
|
||||
onClick={() => handleAction(record, 'detail')}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无客户数据" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部邀请好友按钮 */}
|
||||
<View className="fixed-bottom">
|
||||
<Button
|
||||
className="report-btn"
|
||||
onClick={handleReport}
|
||||
>
|
||||
客户报备
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerList;
|
||||
@@ -1,6 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户签约',
|
||||
navigationBarTextStyle: 'white',
|
||||
navigationStyle: 'custom',
|
||||
backgroundColor: '#f5f5f5'
|
||||
})
|
||||
@@ -1,212 +0,0 @@
|
||||
.customer-sign-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
padding-bottom: 100px;
|
||||
|
||||
.header-bg {
|
||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 20px 16px;
|
||||
margin-top: 20px;
|
||||
|
||||
.form-section {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.section-title {
|
||||
padding: 16px 16px 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nut-form-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f8f8f8;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nut-form-item__label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.nut-form-item__body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nut-input {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-textarea {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-cell {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
.nut-cell__title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nut-cell__value {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-switch {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
z-index: 100;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 24px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
|
||||
|
||||
&.nut-button--loading {
|
||||
background: #52c41a;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配安全区域
|
||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||
.customer-sign-page .fixed-bottom {
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证错误样式
|
||||
.nut-form-item--error {
|
||||
.nut-form-item__label {
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
|
||||
.nut-input {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择器样式优化
|
||||
.nut-picker {
|
||||
.nut-picker__toolbar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.nut-picker__confirm {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.nut-picker__cancel {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期选择器样式
|
||||
.nut-date-picker {
|
||||
.nut-picker__toolbar {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.nut-picker__confirm {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.nut-picker__cancel {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// Toast样式
|
||||
.nut-toast {
|
||||
.nut-toast__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 开关组件样式
|
||||
.nut-switch {
|
||||
&.nut-switch--active {
|
||||
background: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.customer-sign-page {
|
||||
.form-container {
|
||||
padding: 16px 12px;
|
||||
|
||||
.form-section {
|
||||
.nut-form-item {
|
||||
padding: 10px 12px;
|
||||
|
||||
.nut-form-item__label {
|
||||
min-width: 70px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.nut-input,
|
||||
.nut-textarea {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import {
|
||||
NavBar,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
DatePicker,
|
||||
Picker,
|
||||
TextArea,
|
||||
Cell,
|
||||
Switch,
|
||||
Toast
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import { ArrowDown } from '@nutui/icons-react-taro';
|
||||
import './sign.scss';
|
||||
|
||||
// 签约表单数据类型
|
||||
interface SignFormData {
|
||||
customerId: string;
|
||||
contractType: string;
|
||||
contractAmount: string;
|
||||
contractDate: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
paymentMethod: string;
|
||||
paymentCycle: string[];
|
||||
specialTerms: string;
|
||||
isUrgent: boolean;
|
||||
contactPerson: string;
|
||||
contactPhone: string;
|
||||
signLocation: string;
|
||||
remarks: string;
|
||||
}
|
||||
|
||||
const CustomerSign = () => {
|
||||
const router = useRouter();
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
const [formData, setFormData] = useState<SignFormData>({
|
||||
customerId: '',
|
||||
contractType: '',
|
||||
contractAmount: '',
|
||||
contractDate: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
paymentMethod: '',
|
||||
paymentCycle: [],
|
||||
specialTerms: '',
|
||||
isUrgent: false,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
signLocation: '',
|
||||
remarks: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMsg, setToastMsg] = useState('');
|
||||
|
||||
// 合同类型选项
|
||||
const contractTypes = [
|
||||
{ text: '服务合同', value: 'service' },
|
||||
{ text: '销售合同', value: 'sales' },
|
||||
{ text: '代理合同', value: 'agency' },
|
||||
{ text: '合作协议', value: 'cooperation' }
|
||||
];
|
||||
|
||||
// 付款方式选项
|
||||
const paymentMethods = [
|
||||
{ text: '一次性付款', value: 'onetime' },
|
||||
{ text: '分期付款', value: 'installment' },
|
||||
{ text: '月付', value: 'monthly' },
|
||||
{ text: '季付', value: 'quarterly' },
|
||||
{ text: '年付', value: 'yearly' }
|
||||
];
|
||||
|
||||
// 付款周期选项
|
||||
const paymentCycles = [
|
||||
{ text: '签约后立即付款', value: 'immediate' },
|
||||
{ text: '签约后7天内', value: '7days' },
|
||||
{ text: '签约后15天内', value: '15days' },
|
||||
{ text: '签约后30天内', value: '30days' },
|
||||
{ text: '按月付款', value: 'monthly' }
|
||||
];
|
||||
|
||||
const showToastMsg = (msg: string) => {
|
||||
setToastMsg(msg);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 2000);
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof SignFormData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
if (!formData.contractType) {
|
||||
showToastMsg('请选择合同类型');
|
||||
return false;
|
||||
}
|
||||
if (!formData.contractAmount) {
|
||||
showToastMsg('请输入合同金额');
|
||||
return false;
|
||||
}
|
||||
if (!formData.contractDate) {
|
||||
showToastMsg('请选择签约日期');
|
||||
return false;
|
||||
}
|
||||
if (!formData.startDate) {
|
||||
showToastMsg('请选择合同开始日期');
|
||||
return false;
|
||||
}
|
||||
if (!formData.endDate) {
|
||||
showToastMsg('请选择合同结束日期');
|
||||
return false;
|
||||
}
|
||||
if (!formData.paymentMethod) {
|
||||
showToastMsg('请选择付款方式');
|
||||
return false;
|
||||
}
|
||||
if (!formData.contactPerson) {
|
||||
showToastMsg('请输入联系人姓名');
|
||||
return false;
|
||||
}
|
||||
if (!formData.contactPhone) {
|
||||
showToastMsg('请输入联系电话');
|
||||
return false;
|
||||
}
|
||||
if (!formData.signLocation) {
|
||||
showToastMsg('请输入签约地点');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
showToastMsg('签约成功!');
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
showToastMsg('签约失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(Number(res.statusBarHeight));
|
||||
},
|
||||
});
|
||||
|
||||
// 获取客户ID
|
||||
const { customerId } = router.params;
|
||||
if (customerId) {
|
||||
setFormData(prev => ({ ...prev, customerId }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="customer-sign-page">
|
||||
{/* 头部背景 */}
|
||||
<View className="header-bg" style={{ height: '120px' }} />
|
||||
|
||||
{/* 导航栏 */}
|
||||
<NavBar
|
||||
style={{
|
||||
marginTop: `${statusBarHeight}px`,
|
||||
backgroundColor: 'transparent',
|
||||
color: '#ffffff'
|
||||
}}
|
||||
onBackClick={() => Taro.navigateBack()}
|
||||
>
|
||||
<Text style={{ color: '#ffffff', fontSize: '18px', fontWeight: 'bold' }}>
|
||||
客户签约
|
||||
</Text>
|
||||
</NavBar>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<View className="form-container">
|
||||
<Form>
|
||||
{/* 基本信息 */}
|
||||
<View className="form-section">
|
||||
<View className="section-title">基本信息</View>
|
||||
|
||||
<Form.Item label="合同类型" required>
|
||||
<Picker
|
||||
options={contractTypes}
|
||||
value={formData.contractType}
|
||||
onConfirm={(options) => handleInputChange('contractType', options[0]?.value)}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={contractTypes.find(item => item.value === formData.contractType)?.text || '请选择合同类型'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</Picker>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="合同金额" required>
|
||||
<Input
|
||||
placeholder="请输入合同金额"
|
||||
type="digit"
|
||||
value={formData.contractAmount}
|
||||
onChange={(value) => handleInputChange('contractAmount', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="签约日期" required>
|
||||
<DatePicker
|
||||
value={formData.contractDate ? new Date(formData.contractDate) : new Date()}
|
||||
onConfirm={(options, value) => handleInputChange('contractDate', formatDate(value))}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={formData.contractDate || '请选择签约日期'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</DatePicker>
|
||||
</Form.Item>
|
||||
</View>
|
||||
|
||||
{/* 合同期限 */}
|
||||
<View className="form-section">
|
||||
<View className="section-title">合同期限</View>
|
||||
|
||||
<Form.Item label="开始日期" required>
|
||||
<DatePicker
|
||||
value={formData.startDate ? new Date(formData.startDate) : new Date()}
|
||||
onConfirm={(options, value) => handleInputChange('startDate', formatDate(value))}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={formData.startDate || '请选择开始日期'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</DatePicker>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="结束日期" required>
|
||||
<DatePicker
|
||||
value={formData.endDate ? new Date(formData.endDate) : new Date()}
|
||||
onConfirm={(options, value) => handleInputChange('endDate', formatDate(value))}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={formData.endDate || '请选择结束日期'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</DatePicker>
|
||||
</Form.Item>
|
||||
</View>
|
||||
|
||||
{/* 付款信息 */}
|
||||
<View className="form-section">
|
||||
<View className="section-title">付款信息</View>
|
||||
|
||||
<Form.Item label="付款方式" required>
|
||||
<Picker
|
||||
options={paymentMethods}
|
||||
value={formData.paymentMethod}
|
||||
onConfirm={(options) => handleInputChange('paymentMethod', options[0]?.value)}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={paymentMethods.find(item => item.value === formData.paymentMethod)?.text || '请选择付款方式'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</Picker>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="付款周期">
|
||||
<Picker
|
||||
options={paymentCycles}
|
||||
value={formData.paymentCycle}
|
||||
onConfirm={(options) => handleInputChange('paymentCycle', options[0]?.value)}
|
||||
>
|
||||
<Cell
|
||||
className="picker-cell"
|
||||
title={paymentCycles.find(item => item.value === formData.paymentCycle)?.text || '请选择付款周期'}
|
||||
extra={<ArrowDown />}
|
||||
/>
|
||||
</Picker>
|
||||
</Form.Item>
|
||||
</View>
|
||||
|
||||
{/* 联系信息 */}
|
||||
<View className="form-section">
|
||||
<View className="section-title">联系信息</View>
|
||||
|
||||
<Form.Item label="联系人" required>
|
||||
<Input
|
||||
placeholder="请输入联系人姓名"
|
||||
value={formData.contactPerson}
|
||||
onChange={(value) => handleInputChange('contactPerson', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="联系电话" required>
|
||||
<Input
|
||||
placeholder="请输入联系电话"
|
||||
type="tel"
|
||||
value={formData.contactPhone}
|
||||
onChange={(value) => handleInputChange('contactPhone', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="签约地点" required>
|
||||
<Input
|
||||
placeholder="请输入签约地点"
|
||||
value={formData.signLocation}
|
||||
onChange={(value) => handleInputChange('signLocation', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</View>
|
||||
|
||||
{/* 其他信息 */}
|
||||
<View className="form-section">
|
||||
<View className="section-title">其他信息</View>
|
||||
|
||||
<Form.Item label="特殊条款">
|
||||
<TextArea
|
||||
placeholder="请输入特殊条款(可选)"
|
||||
value={formData.specialTerms}
|
||||
onChange={(value) => handleInputChange('specialTerms', value)}
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="紧急处理">
|
||||
<Switch
|
||||
checked={formData.isUrgent}
|
||||
onChange={(value) => handleInputChange('isUrgent', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="备注">
|
||||
<TextArea
|
||||
placeholder="请输入备注信息(可选)"
|
||||
value={formData.remarks}
|
||||
onChange={(value) => handleInputChange('remarks', value)}
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
</View>
|
||||
</Form>
|
||||
</View>
|
||||
|
||||
{/* 底部提交按钮 */}
|
||||
<View className="fixed-bottom">
|
||||
<Button
|
||||
className="submit-btn"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading ? '提交中...' : '确认签约'}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* Toast提示 */}
|
||||
<Toast
|
||||
visible={showToast}
|
||||
content={toastMsg}
|
||||
duration={2000}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerSign;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '入市交易'
|
||||
})
|
||||
@@ -1,251 +0,0 @@
|
||||
.customer-trading-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
position: relative;
|
||||
padding-bottom: 100px;
|
||||
|
||||
.header-bg {
|
||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 0 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
.nut-searchbar {
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 0 16px;
|
||||
margin-top: 16px;
|
||||
|
||||
.nut-tabs {
|
||||
background: transparent;
|
||||
|
||||
.nut-tabs__titles {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 12px 20px;
|
||||
|
||||
&.nut-tabs__titles-item--active {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__line {
|
||||
background: #ffffff;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trading-list {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 20px 16px;
|
||||
margin-top: 20px;
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.trading-item {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.trading-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.customer-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.nut-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-info {
|
||||
.nut-tag {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trading-details {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.label {
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
|
||||
&.amount {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.profit {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trading-footer {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f8f8f8;
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.nut-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
z-index: 100;
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配安全区域
|
||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||
.customer-trading-page .fixed-bottom {
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式优化
|
||||
.nut-tag {
|
||||
&.nut-tag--plain {
|
||||
border-width: 1px;
|
||||
padding: 2px 6px;
|
||||
|
||||
&.nut-tag--small {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新样式
|
||||
.nut-pulltorefresh {
|
||||
.nut-pulltorefresh__track {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 无限加载样式
|
||||
.nut-infiniteloading {
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 空状态样式
|
||||
.nut-empty {
|
||||
padding: 60px 20px;
|
||||
|
||||
.nut-empty__description {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.customer-trading-page {
|
||||
.search-container,
|
||||
.tabs-container {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.trading-list {
|
||||
padding: 16px 12px;
|
||||
|
||||
.trading-item {
|
||||
padding: 12px;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import {
|
||||
NavBar,
|
||||
SearchBar,
|
||||
Tabs,
|
||||
Button,
|
||||
Tag,
|
||||
Empty,
|
||||
PullToRefresh,
|
||||
InfiniteLoading
|
||||
} from '@nutui/nutui-react-taro';
|
||||
import {
|
||||
Filter,
|
||||
Calendar
|
||||
} from '@nutui/icons-react-taro';
|
||||
import './trading.scss';
|
||||
|
||||
// 交易记录数据类型
|
||||
interface TradingRecord {
|
||||
id: string;
|
||||
customerName: string;
|
||||
customerId: string;
|
||||
tradingType: 'buy' | 'sell';
|
||||
amount: string;
|
||||
price: string;
|
||||
quantity: string;
|
||||
tradingDate: string;
|
||||
status: 'pending' | 'completed' | 'cancelled';
|
||||
profit: string;
|
||||
profitRate: string;
|
||||
}
|
||||
|
||||
const CustomerTrading = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
const [activeTab, setActiveTab] = useState<string>('all');
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [tradingRecords, setTradingRecords] = useState<TradingRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
|
||||
// 模拟交易数据
|
||||
const mockTradingRecords: TradingRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
customerName: '广州雅虎信息科技公司',
|
||||
customerId: '1',
|
||||
tradingType: 'buy',
|
||||
amount: '100,000',
|
||||
price: '15.50',
|
||||
quantity: '6,451',
|
||||
tradingDate: '2025-08-21 09:30:15',
|
||||
status: 'completed',
|
||||
profit: '+8,500',
|
||||
profitRate: '+8.5%'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
customerName: '深圳腾讯科技有限公司',
|
||||
customerId: '2',
|
||||
tradingType: 'sell',
|
||||
amount: '250,000',
|
||||
price: '28.80',
|
||||
quantity: '8,680',
|
||||
tradingDate: '2025-08-21 10:15:30',
|
||||
status: 'completed',
|
||||
profit: '+15,200',
|
||||
profitRate: '+6.1%'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
customerName: '阿里巴巴网络技术有限公司',
|
||||
customerId: '3',
|
||||
tradingType: 'buy',
|
||||
amount: '500,000',
|
||||
price: '42.30',
|
||||
quantity: '11,820',
|
||||
tradingDate: '2025-08-21 11:45:20',
|
||||
status: 'pending',
|
||||
profit: '0',
|
||||
profitRate: '0%'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
customerName: '百度在线网络技术公司',
|
||||
customerId: '4',
|
||||
tradingType: 'sell',
|
||||
amount: '180,000',
|
||||
price: '22.10',
|
||||
quantity: '8,144',
|
||||
tradingDate: '2025-08-21 14:20:45',
|
||||
status: 'cancelled',
|
||||
profit: '-2,800',
|
||||
profitRate: '-1.6%'
|
||||
}
|
||||
];
|
||||
|
||||
const tabList = [
|
||||
{ title: '全部', value: 'all' },
|
||||
{ title: '买入', value: 'buy' },
|
||||
{ title: '卖出', value: 'sell' },
|
||||
{ title: '待处理', value: 'pending' }
|
||||
];
|
||||
|
||||
const getFilteredRecords = () => {
|
||||
let filtered = tradingRecords;
|
||||
|
||||
// 按标签页筛选
|
||||
if (activeTab === 'buy') {
|
||||
filtered = filtered.filter(record => record.tradingType === 'buy');
|
||||
} else if (activeTab === 'sell') {
|
||||
filtered = filtered.filter(record => record.tradingType === 'sell');
|
||||
} else if (activeTab === 'pending') {
|
||||
filtered = filtered.filter(record => record.status === 'pending');
|
||||
}
|
||||
|
||||
// 按搜索关键词筛选
|
||||
if (searchValue) {
|
||||
filtered = filtered.filter(record =>
|
||||
record.customerName.toLowerCase().includes(searchValue.toLowerCase()) ||
|
||||
record.id.includes(searchValue)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待处理';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '#ff6b35';
|
||||
case 'completed':
|
||||
return '#52c41a';
|
||||
case 'cancelled':
|
||||
return '#ff4d4f';
|
||||
default:
|
||||
return '#999';
|
||||
}
|
||||
};
|
||||
|
||||
const getTradingTypeText = (type: string) => {
|
||||
return type === 'buy' ? '买入' : '卖出';
|
||||
};
|
||||
|
||||
const getTradingTypeColor = (type: string) => {
|
||||
return type === 'buy' ? '#52c41a' : '#ff4d4f';
|
||||
};
|
||||
|
||||
const getProfitColor = (profit: string) => {
|
||||
if (profit.startsWith('+')) return '#52c41a';
|
||||
if (profit.startsWith('-')) return '#ff4d4f';
|
||||
return '#999';
|
||||
};
|
||||
|
||||
const loadTradingRecords = async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
setPage(1);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
if (isRefresh) {
|
||||
setTradingRecords(mockTradingRecords);
|
||||
setHasMore(true);
|
||||
} else {
|
||||
// 模拟分页加载
|
||||
if (page === 1) {
|
||||
setTradingRecords(mockTradingRecords);
|
||||
} else {
|
||||
setHasMore(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMore = async () => {
|
||||
if (!hasMore || loading) return;
|
||||
|
||||
setPage(prev => prev + 1);
|
||||
await loadTradingRecords();
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchValue(value);
|
||||
};
|
||||
|
||||
const handleFilter = () => {
|
||||
Taro.showToast({
|
||||
title: '筛选功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecordClick = (_: TradingRecord) => {
|
||||
Taro.showToast({
|
||||
title: '查看交易详情功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddTrading = () => {
|
||||
Taro.showToast({
|
||||
title: '新增交易功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(Number(res.statusBarHeight));
|
||||
},
|
||||
});
|
||||
loadTradingRecords();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="customer-trading-page">
|
||||
{/* 头部背景 */}
|
||||
<View className="header-bg" style={{ height: '200px' }} />
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<View className="search-container">
|
||||
<SearchBar
|
||||
placeholder="搜索客户名称或交易编号"
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="tabs-container">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value as string)}
|
||||
activeColor="#ffffff"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: '#ffffff',
|
||||
}}
|
||||
>
|
||||
{tabList.map(tab => (
|
||||
<Tabs.TabPane key={tab.value} title={tab.title} value={tab.value} />
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 交易列表 */}
|
||||
<View className="trading-list">
|
||||
<PullToRefresh
|
||||
onRefresh={() => loadTradingRecords(true)}
|
||||
disabled={refreshing}
|
||||
>
|
||||
{loading && page === 1 ? (
|
||||
<View className="loading-container">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : getFilteredRecords().length > 0 ? (
|
||||
<>
|
||||
{getFilteredRecords().map((record) => (
|
||||
<View
|
||||
key={record.id}
|
||||
className="trading-item"
|
||||
onClick={() => handleRecordClick(record)}
|
||||
>
|
||||
<View className="trading-header">
|
||||
<View className="customer-info">
|
||||
<Text className="customer-name">{record.customerName}</Text>
|
||||
<Tag
|
||||
color={getTradingTypeColor(record.tradingType)}
|
||||
plain
|
||||
>
|
||||
{getTradingTypeText(record.tradingType)}
|
||||
</Tag>
|
||||
</View>
|
||||
<View className="status-info">
|
||||
<Tag
|
||||
color={getStatusColor(record.status)}
|
||||
plain
|
||||
>
|
||||
{getStatusText(record.status)}
|
||||
</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="trading-details">
|
||||
<View className="detail-row">
|
||||
<View className="detail-item">
|
||||
<Text className="label">交易金额</Text>
|
||||
<Text className="value amount">¥{record.amount}</Text>
|
||||
</View>
|
||||
<View className="detail-item">
|
||||
<Text className="label">成交价格</Text>
|
||||
<Text className="value">¥{record.price}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="detail-row">
|
||||
<View className="detail-item">
|
||||
<Text className="label">交易数量</Text>
|
||||
<Text className="value">{record.quantity}</Text>
|
||||
</View>
|
||||
<View className="detail-item">
|
||||
<Text className="label">盈亏</Text>
|
||||
<Text
|
||||
className="value profit"
|
||||
style={{ color: getProfitColor(record.profit) }}
|
||||
>
|
||||
{record.profit} ({record.profitRate})
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="trading-footer">
|
||||
<View className="time-info">
|
||||
<Calendar size={12} color="#999" />
|
||||
<Text className="time-text">{record.tradingDate}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<InfiniteLoading
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={loading && page > 1}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无交易记录" />
|
||||
)}
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
|
||||
{/* 底部新增交易按钮 */}
|
||||
<View className="fixed-bottom">
|
||||
<Button
|
||||
className="add-btn"
|
||||
onClick={handleAddTrading}
|
||||
>
|
||||
新增交易
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerTrading;
|
||||
@@ -37,7 +37,7 @@ const MyGrid = () => {
|
||||
}}>
|
||||
{
|
||||
list.map((item) => (
|
||||
<Grid.Item key={item.navigationId} onClick={() => navTo(`${item.path}`)}>
|
||||
<Grid.Item key={item.navigationId} onClick={() => navTo(`${item.path}`,true)}>
|
||||
<Avatar src={item.icon} className={'mb-2'} shape="square" style={{
|
||||
backgroundColor: 'transparent',
|
||||
}}/>
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {Sticky} from '@nutui/nutui-react-taro'
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import { useUser } from '@/hooks/useUser';
|
||||
import Menu from "./Menu";
|
||||
import Banner from "./Banner";
|
||||
import './index.scss'
|
||||
|
||||
const Home = () => {
|
||||
const [stickyStatus, setStickyStatus] = useState(false);
|
||||
|
||||
// 使用新的hooks
|
||||
const {
|
||||
shopInfo,
|
||||
loading: shopLoading,
|
||||
error: shopError,
|
||||
getWebsiteName,
|
||||
getWebsiteLogo,
|
||||
refreshShopInfo
|
||||
} = useShopInfo();
|
||||
|
||||
const {
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading: userLoading
|
||||
} = useUser();
|
||||
|
||||
const onSticky = (args: any) => {
|
||||
setStickyStatus(args[0].isFixed);
|
||||
};
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
content: '需要获取您的用户信息',
|
||||
confirmText: '去授权',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户点击确认,打开授权设置页面
|
||||
Taro.openSetting({
|
||||
success: (settingRes) => {
|
||||
if (settingRes.authSetting['scope.userInfo']) {
|
||||
console.log('用户已授权');
|
||||
} else {
|
||||
console.log('用户拒绝授权');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 分享给好友
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${getWebsiteName()} - 精选商城`,
|
||||
path: '/pages/index/index',
|
||||
imageUrl: getWebsiteLogo(),
|
||||
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: `${getWebsiteName()} - 精选商城`,
|
||||
imageUrl: getWebsiteLogo(),
|
||||
success: function (res: any) {
|
||||
console.log('分享到朋友圈成功', res);
|
||||
},
|
||||
fail: function (res: any) {
|
||||
console.log('分享到朋友圈失败', res);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 设置页面标题
|
||||
if (shopInfo?.appName) {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: shopInfo.appName
|
||||
});
|
||||
}
|
||||
}, [shopInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
// 检查用户授权状态
|
||||
Taro.getSetting({
|
||||
success: (res) => {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
console.log('用户已经授权过,可以直接获取用户信息');
|
||||
} else {
|
||||
console.log('用户未授权,需要弹出授权窗口');
|
||||
showAuthModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 获取用户基本信息(头像、昵称等)
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
const avatar = res.userInfo.avatarUrl;
|
||||
console.log('用户头像:', avatar);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('获取用户信息失败:', err);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 处理错误状态
|
||||
if (shopError) {
|
||||
return (
|
||||
<div style={{padding: '20px', textAlign: 'center'}}>
|
||||
<div>加载商店信息失败: {shopError}</div>
|
||||
<button
|
||||
onClick={refreshShopInfo}
|
||||
style={{marginTop: '10px', padding: '10px 20px'}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
if (shopLoading) {
|
||||
return (
|
||||
<div style={{padding: '20px', textAlign: 'center'}}>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sticky threshold={0} onChange={(args) => onSticky(args)}>
|
||||
<Header stickyStatus={stickyStatus}/>
|
||||
</Sticky>
|
||||
<div className={'flex flex-col mt-12'}>
|
||||
<Menu/>
|
||||
<Banner/>
|
||||
<BestSellers/>
|
||||
|
||||
{/* 调试信息面板 - 仅在开发环境显示 */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: '10px',
|
||||
right: '10px',
|
||||
background: 'rgba(0,0,0,0.8)',
|
||||
color: 'white',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '12px',
|
||||
maxWidth: '200px'
|
||||
}}>
|
||||
<div>商店: {getWebsiteName()}</div>
|
||||
<div>用户: {isLoggedIn ? (user?.nickname || '已登录') : '未登录'}</div>
|
||||
<div>加载: {userLoading ? '用户加载中' : '已完成'}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -4,15 +4,13 @@ import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import {Sticky, Button} from '@nutui/nutui-react-taro'
|
||||
import {Sticky} 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 GoodsList from "./GoodsList";
|
||||
|
||||
function Home() {
|
||||
// 吸顶状态
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
@@ -37,10 +35,6 @@ function Home() {
|
||||
};
|
||||
});
|
||||
|
||||
// const reloadMore = async () => {
|
||||
// setPage(page + 1)
|
||||
// }
|
||||
|
||||
const showAuthModal = () => {
|
||||
Taro.showModal({
|
||||
title: '授权提示',
|
||||
@@ -117,13 +111,12 @@ function Home() {
|
||||
<Sticky threshold={0} onChange={() => onSticky(arguments)}>
|
||||
<Header stickyStatus={stickyStatus}/>
|
||||
</Sticky>
|
||||
<div className={'flex flex-col mt-1'}>
|
||||
<View className={'flex flex-col mt-1'}>
|
||||
<Menu/>
|
||||
<Banner/>
|
||||
<BestSellers/>
|
||||
<Grid />
|
||||
{/*<GoodsList/>*/}
|
||||
</div>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user