This commit is contained in:
lik
2026-06-08 12:01:40 +08:00
parent 010cf160a0
commit 894a9881d7
51 changed files with 2667 additions and 740 deletions

View File

@@ -1,66 +1,269 @@
// pages/customer/index.js
const API = require('../../utils/api.js')
// 性别映射
const SEX_MAP = {
male: { label: '男', text: '先生' },
female: { label: '女', text: '女士' },
other: { label: '其他', text: '' },
'': { label: '未知', text: '' }
}
Page({
/**
* 页面的初始数据
*/
data: {
// 搜索相关
searchKey: '',
// 客户列表
customerList: [],
// 分页
page: 1,
pageSize: 10,
hasMore: true,
// 加载状态
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
// 统计数据
stats: {
total: 0,
male: 0,
female: 0
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadCustomerList();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时如果已有数据则刷新
if (this.data.customerList.length > 0) {
this.refreshData();
}
},
/**
* 生命周期函数--监听页面隐藏
* 格式化日期
*/
onHide() {
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
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}`;
},
/**
* 生命周期函数--监听页面卸载
* 处理客户数据
*/
onUnload() {
processCustomers(customers) {
return customers.map(customer => {
const profile = customer.profile || {};
const sexInfo = SEX_MAP[profile.sex] || SEX_MAP[''];
return {
...customer,
name: profile.name || '未知姓名',
mobile: profile.mobile || '暂无电话',
sex: profile.sex || '',
sexLabel: sexInfo.label,
sexText: sexInfo.text,
avatar: profile.avatar || '',
avatarText: profile.name ? profile.name[0].toUpperCase() : '客',
birth: profile.birth ? this.formatDate(profile.birth) : '',
createdAt: customer.meta?.createtime ? this.formatDate(customer.meta.createtime) : '',
locationText: this.getLocationText(customer)
};
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
* 获取位置文本
*/
getLocationText(customer) {
const location = customer.location || {};
if (location.province || location.city) {
return `${location.province || ''}${location.city || ''}`;
}
return '';
},
/**
* 计算统计数据
*/
calculateStats(customers) {
const stats = {
total: customers.length,
male: 0,
female: 0
};
customers.forEach(customer => {
const sex = customer.profile?.sex || '';
if (sex === 'male') stats.male++;
else if (sex === 'female') stats.female++;
});
return stats;
},
/**
* 加载客户列表
*/
loadCustomerList(isRefresh = false) {
if (this.data.isLoading || this.data.isLoadingMore) return;
const { page, pageSize, searchKey } = this.data;
this.setData({
isLoading: !isRefresh && page === 1,
isLoadingMore: isRefresh && page > 1
});
const params = {
page,
pageSize
};
if (searchKey && searchKey.trim()) {
params.keyword = searchKey.trim();
}
API.user.userList(params)
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '获取客户列表失败', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
return;
}
const data = res.data || {};
const list = data.users || [];
const total = data?.users?.length || 0;
const processedCustomers = this.processCustomers(list);
let allCustomers = isRefresh && page > 1
? [...this.data.customerList, ...processedCustomers]
: processedCustomers;
// 如果有搜索条件,在前端过滤
if (searchKey && searchKey.trim()) {
const keyword = searchKey.trim().toLowerCase();
allCustomers = allCustomers.filter(customer => {
return customer.name.toLowerCase().includes(keyword) ||
customer.mobile.includes(keyword);
});
}
const stats = this.calculateStats(allCustomers);
this.setData({
customerList: allCustomers,
stats,
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
hasMore: allCustomers.length < total
});
})
.catch(err => {
console.error('获取客户列表失败', err);
wx.showToast({ title: '网络错误,请重试', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
});
},
/**
* 刷新数据
*/
refreshData() {
this.setData({ page: 1, hasMore: true }, () => {
this.loadCustomerList();
});
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({ searchKey: e.detail.value });
},
/**
* 搜索确认
*/
onSearch() {
this.setData({ page: 1, hasMore: true, customerList: [] }, () => {
this.loadCustomerList();
});
},
/**
* 清除搜索
*/
onClearSearch() {
if (this.data.searchKey) {
this.setData({ searchKey: '', page: 1, hasMore: true, customerList: [] }, () => {
this.loadCustomerList();
});
}
},
/**
* 客户详情
*/
onCustomerDetail(e) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/customer/detail?id=${id}`
});
},
/**
* 拨打电话
*/
onCallPhone(e) {
const { phone } = e.currentTarget.dataset;
if (!phone) {
wx.showToast({ title: '暂无电话号码', icon: 'none' });
return;
}
wx.makePhoneCall({
phoneNumber: phone,
fail: () => {
wx.showToast({ title: '拨打电话失败', icon: 'none' });
}
});
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ isRefreshing: true, page: 1, hasMore: true }, () => {
this.loadCustomerList();
});
wx.stopPullDownRefresh();
},
/**
* 页面上拉触底事件的处理函数
* 上拉加载更多
*/
onReachBottom() {
if (!this.data.hasMore || this.data.isLoadingMore) return;
this.setData({ page: this.data.page + 1 }, () => {
this.loadCustomerList(true);
});
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '客户管理',
path: '/pages/customer/index'
};
}
})
});

View File

@@ -1,3 +1,11 @@
{
"usingComponents": {}
}
"navigationBarTitleText": "客户管理",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -1 +1,328 @@
/* pages/customer/index.wxss */
/* pages/customer/index.less */
// 颜色变量
@bg-primary: #f5f6fa;
@bg-secondary: #ffffff;
@bg-card: #ffffff;
@accent-primary: #4c6ef5;
@accent-secondary: #6b7aff;
@accent-gradient-start: #4c6ef5;
@accent-gradient-end: #748ffc;
@text-primary: #1a1a2e;
@text-secondary: #6b7280;
@text-muted: #9ca3af;
@border-color: #e5e7eb;
@male-color: #4c6ef5;
@female-color: #ff6b6b;
@divider-color: #f3f4f6;
page {
background-color: @bg-primary;
color: @text-primary;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.customer-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: @bg-primary;
}
// ========== 搜索区域 ==========
.search-section {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background-color: @bg-secondary;
border-bottom: 1rpx solid @border-color;
}
.search-bar {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx 24rpx;
background-color: @bg-primary;
border-radius: 32rpx;
border: 1rpx solid @border-color;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: @text-primary;
height: 40rpx;
line-height: 40rpx;
&::placeholder {
color: @text-muted;
}
}
.search-clear {
display: flex;
align-items: center;
justify-content: center;
padding: 4rpx;
}
.search-btn {
padding: 16rpx 28rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: #ffffff;
font-size: 26rpx;
font-weight: 500;
border-radius: 32rpx;
white-space: nowrap;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25);
&:active {
opacity: 0.9;
transform: scale(0.98);
}
}
// ========== 统计区域 ==========
.stats-section {
padding: 20rpx 24rpx 0;
}
.stats-card {
display: flex;
align-items: center;
justify-content: space-around;
padding: 28rpx 24rpx;
background-color: @bg-card;
border-radius: 24rpx;
border: 1rpx solid @border-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stats-value {
font-size: 40rpx;
font-weight: 700;
color: @text-primary;
&.male {
color: @male-color;
}
&.female {
color: @female-color;
}
}
.stats-label {
font-size: 24rpx;
color: @text-secondary;
}
.stats-divider {
width: 1rpx;
height: 60rpx;
background-color: @divider-color;
}
// ========== 客户列表区域 ==========
.customer-list {
flex: 1;
overflow: hidden;
padding: 20rpx 24rpx 0;
}
.customer-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 40rpx;
}
.loading-text {
color: @text-secondary !important;
font-size: 26rpx !important;
}
.empty-text {
color: @text-secondary !important;
font-size: 28rpx !important;
}
.empty-action {
margin-top: 30rpx;
padding: 16rpx 48rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: #ffffff;
font-size: 28rpx;
border-radius: 32rpx;
display: inline-block;
}
// ========== 客户卡片 ==========
.customer-cards {
padding-bottom: 40rpx;
}
.customer-card {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 20rpx;
padding: 28rpx;
background-color: @bg-card;
border-radius: 24rpx;
border: 1rpx solid @border-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
}
// 头像
.customer-avatar {
flex-shrink: 0;
}
.avatar-img {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
&.male {
background: linear-gradient(135deg, #4c6ef5, #748ffc);
}
&.female {
background: linear-gradient(135deg, #ff6b6b, #ff8787);
}
}
.avatar-text {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
// 客户信息
.customer-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
min-width: 0;
}
.info-row {
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.customer-name {
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
}
.gender-tag {
display: inline-flex;
align-items: center;
padding: 4rpx 14rpx;
border-radius: 10rpx;
font-size: 22rpx;
font-weight: 500;
&.male {
background-color: rgba(76, 110, 245, 0.1);
color: @male-color;
}
&.female {
background-color: rgba(255, 107, 107, 0.1);
color: @female-color;
}
}
.customer-date {
font-size: 22rpx;
color: @text-muted;
margin-left: auto;
}
.customer-phone {
font-size: 26rpx;
color: @text-secondary;
}
.customer-location {
font-size: 24rpx;
color: @text-muted;
}
// 操作按钮
.customer-action {
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: rgba(76, 110, 245, 0.08);
transition: all 0.2s ease;
&:active {
background-color: rgba(76, 110, 245, 0.15);
transform: scale(0.95);
}
&.call {
background-color: rgba(76, 110, 245, 0.08);
}
}
// ========== 加载更多 ==========
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
}
.no-more {
font-size: 24rpx;
color: @text-muted;
}

View File

@@ -1,2 +1,116 @@
<!--pages/customer/index.wxml-->
<text>pages/customer/index.wxml</text>
<view class="customer-page">
<!-- 搜索区域 -->
<view class="search-section">
<view class="search-bar">
<t-icon name="search" size="32rpx" color="#9ca3af" />
<input
class="search-input"
type="text"
placeholder="搜索客户姓名或手机号"
value="{{searchKey}}"
bindinput="onSearchInput"
confirm-type="search"
bindconfirm="onSearch"
/>
<view class="search-clear" wx:if="{{searchKey}}" bindtap="onClearSearch">
<t-icon name="close-circle" size="32rpx" color="#9ca3af" />
</view>
</view>
<view class="search-btn" bindtap="onSearch">搜索</view>
</view>
<!-- 统计卡片 -->
<view class="stats-section" wx:if="{{!isLoading && customerList.length > 0}}">
<view class="stats-card">
<view class="stats-item">
<text class="stats-value">{{stats.total}}</text>
<text class="stats-label">客户总数</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-value male">{{stats.male}}</text>
<text class="stats-label">男</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-value female">{{stats.female}}</text>
<text class="stats-label">女</text>
</view>
</view>
</view>
<!-- 客户列表 -->
<view class="customer-list">
<scroll-view
scroll-y
class="customer-scroll"
refresher-enabled
refresher-triggered="{{isRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
bindscrolltolower="onReachBottom"
>
<!-- 加载中 -->
<view class="loading-container" wx:if="{{isLoading && customerList.length === 0}}">
<t-loading theme="spinner" size="40rpx" text="加载中..." t-class-text="loading-text" />
</view>
<!-- 空状态 -->
<view class="empty-container" wx:elif="{{!isLoading && customerList.length === 0}}">
<t-empty icon="user" description="暂无客户数据" t-class-description="empty-text">
<view slot="action">
<view class="empty-action" bindtap="refreshData">刷新试试</view>
</view>
</t-empty>
</view>
<!-- 客户卡片列表 -->
<view class="customer-cards" wx:else>
<view
class="customer-card"
wx:for="{{customerList}}"
wx:key="_id"
data-id="{{item._id}}"
bindtap="onCustomerDetail"
>
<!-- 头像 -->
<view class="customer-avatar">
<image wx:if="{{item.avatar}}" class="avatar-img" src="{{item.avatar}}" mode="aspectFill" />
<view wx:else class="avatar-placeholder {{item.sex}}">
<text class="avatar-text">{{item.avatarText}}</text>
</view>
</view>
<!-- 客户信息 -->
<view class="customer-info">
<view class="info-row">
<text class="customer-name">{{item.name}}</text>
<view class="gender-tag {{item.sex}}" wx:if="{{item.sex}}">
<text>{{item.sexLabel}}</text>
</view>
<text class="customer-date" wx:if="{{item.createdAt}}">{{item.createdAt}}</text>
</view>
<view class="info-row">
<text class="customer-phone">{{item.mobile}}</text>
<text class="customer-location" wx:if="{{item.locationText}}">{{item.locationText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="customer-action">
<view class="action-btn call" data-phone="{{item.mobile}}" catchtap="onCallPhone">
<t-icon name="call" size="32rpx" color="#4c6ef5" />
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more">
<t-loading wx:if="{{isLoadingMore}}" theme="spinner" size="32rpx" text="加载中..." t-class-text="loading-text" />
<text class="no-more" wx:elif="{{!hasMore}}">没有更多了</text>
</view>
</view>
</scroll-view>
</view>
</view>