客户信息管理统计列表页面开发

main
jiangxucong 2 days ago
parent ef0771a4c4
commit c7c5181dec

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 177 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 179 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 176 KiB

@ -1,16 +1,25 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useMemo, useLayoutEffect } from 'react';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import styles from './CustomerInfoManagement.less'; import styles from './CustomerInfoManagement.less';
import { Select, Space, Button, Input } from 'antd';
import { PlusOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import StandardTable from '@/components/StandardTable';
import topIcon from '@/assets/business_basic/top_icon1.svg';
import topIcon2 from '@/assets/business_basic/top_icon2.svg';
import topIcon3 from '@/assets/business_basic/top_icon3.svg';
import topIcon4 from '@/assets/business_basic/top_icon4.svg';
import CustomerInfoManagementDetail from './CustomerInfoManagementDetail'; import CustomerInfoManagementDetail from './CustomerInfoManagementDetail';
const CustomerInfoManagement = () => { const CustomerInfoManagement = () => {
const [searchKeyword, setSearchKeyword] = useState(''); const [searchKeyword, setSearchKeyword] = useState('');
const [customerType, setCustomerType] = useState('全部');
const [customerLevel, setCustomerLevel] = useState('全部');
const [cooperationStatus, setCooperationStatus] = useState('全部');
const [selectedRows, setSelectedRows] = useState([]); const [selectedRows, setSelectedRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
// 列表筛选与数据(演示数据,可替换为接口)
const [filters, setFilters] = useState({
customerType: '',
customerGrade: '',
cooperationStatus: '',
});
// 新增:详情页面切换状态 // 新增:详情页面切换状态
const [showDetail, setShowDetail] = useState(false); const [showDetail, setShowDetail] = useState(false);
const [detailData, setDetailData] = useState(null); const [detailData, setDetailData] = useState(null);
@ -32,21 +41,27 @@ const CustomerInfoManagement = () => {
// 客户类型分布图表配置 // 客户类型分布图表配置
const customerTypeChartOption = { const customerTypeChartOption = {
title: { // title: {
text: '客户类型分布', // text: '客户类型分布',
left: 'left', // left: 'left',
textStyle: { // textStyle: {
fontSize: 16, // fontSize: 16,
fontWeight: 600, // fontWeight: 600,
}, // color: '#333',
}, // },
// },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { axisPointer: {
type: 'shadow', type: 'shadow',
}, },
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: '#e0e0e0',
borderWidth: 1,
textStyle: { color: '#333' },
}, },
grid: { grid: {
top: 16,
left: '3%', left: '3%',
right: '4%', right: '4%',
bottom: '3%', bottom: '3%',
@ -57,23 +72,44 @@ const CustomerInfoManagement = () => {
data: ['客户', '供应商', '第3周', '第4周'], data: ['客户', '供应商', '第3周', '第4周'],
axisLabel: { axisLabel: {
rotate: 0, rotate: 0,
color: '#4E5856',
},
axisLine: {
lineStyle: { color: '#C9E3DE' },
}, },
axisTick: { show: false },
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
max: 100, max: 100,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#4E5856' },
splitLine: {
show: true,
lineStyle: { color: '#E9F3F1', type: 'dashed' },
},
}, },
series: [ series: [
{ {
name: '数量', name: '数量',
type: 'bar', type: 'bar',
data: [56, 32, 85, 15], data: [56, 32, 85, 15],
barWidth: 26,
itemStyle: { itemStyle: {
color: '#1890ff', color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#7FC8B6' },
{ offset: 1, color: '#008F8E' },
]),
// barBorderRadius: [6, 6, 0, 0],
shadowColor: 'rgba(0,0,0,0.08)',
shadowBlur: 6,
shadowOffsetY: 2,
}, },
label: { label: {
show: true, show: true,
position: 'top', position: 'top',
color: '#4E5856',
}, },
}, },
], ],
@ -81,81 +117,113 @@ const CustomerInfoManagement = () => {
// 客户价值分布环形图配置 // 客户价值分布环形图配置
const customerValueChartOption = { const customerValueChartOption = {
title: { // title: {
text: '客户价值分布', // text: '客户价值分布',
left: 'left', // left: 'left',
textStyle: { // textStyle: {
fontSize: 16, // fontSize: 16,
fontWeight: 600, // fontWeight: 600,
}, // },
}, // },
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)', formatter: '{a} <br/>{b}: {c} ({d}%)',
}, },
legend: { legend: {
orient: 'vertical', orient: 'vertical',
left: 'right', right: '6%',
top: 'middle', top: '32%',
itemWidth: 16,
itemHeight: 4,
itemGap: 16,
textStyle: {
fontSize: 12,
color: '#666',
fontWeight: 'normal'
}
}, },
series: [ series: [
{ {
name: '客户价值', name: '客户价值',
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['40%', '65%'],
avoidLabelOverlap: false, center: ['30%', '50%'],
// avoidLabelOverlap: false,
itemStyle: { itemStyle: {
borderRadius: 10, // borderRadius: 10,
borderColor: '#fff', // borderColor: '#fff',
borderWidth: 2, // borderWidth: 2,
}, },
label: { label: {
show: false, show: false,
}, },
emphasis: { // emphasis: {
label: { // label: {
show: true, // show: true,
fontSize: 16, // fontSize: 16,
fontWeight: 'bold', // fontWeight: 'bold',
}, // },
}, // },
data: [ data: [
{ value: 180, name: '高价值客户', itemStyle: { color: '#5B9BD5' } }, { value: 180, name: '高价值客户', itemStyle: { color: '#5CB3FF' } },
{ value: 120, name: '中等客户', itemStyle: { color: '#FFC000' } }, { value: 120, name: '中等客户', itemStyle: { color: '#FFDE73' } },
{ value: 89, name: '小客户', itemStyle: { color: '#9E7CC1' } }, { value: 89, name: '小客户', itemStyle: { color: '#A990EA' } },
], ],
}, },
], ],
}; };
// 初始化图表 // 图表初始化与销毁:根据详情模式切换,返回列表后重新初始化
useEffect(() => { useEffect(() => {
// 初始化客户类型分布图表 if (!showDetail) {
if (customerTypeChartRef.current) { if (customerTypeChartRef.current) {
customerTypeChartInstance.current = echarts.init(customerTypeChartRef.current); customerTypeChartInstance.current = echarts.init(customerTypeChartRef.current);
customerTypeChartInstance.current.setOption(customerTypeChartOption); customerTypeChartInstance.current.setOption(customerTypeChartOption);
// 额外一次 resize确保初始化后尺寸正确
requestAnimationFrame(() => {
customerTypeChartInstance.current?.resize();
});
} }
// 初始化客户价值分布图表
if (customerValueChartRef.current) { if (customerValueChartRef.current) {
customerValueChartInstance.current = echarts.init(customerValueChartRef.current); customerValueChartInstance.current = echarts.init(customerValueChartRef.current);
customerValueChartInstance.current.setOption(customerValueChartOption); customerValueChartInstance.current.setOption(customerValueChartOption);
// 额外一次 resize确保初始化后尺寸正确
requestAnimationFrame(() => {
customerValueChartInstance.current?.resize();
});
} }
// 响应式调整
const handleResize = () => { const handleResize = () => {
customerTypeChartInstance.current?.resize(); customerTypeChartInstance.current?.resize();
customerValueChartInstance.current?.resize(); customerValueChartInstance.current?.resize();
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
// 清理函数
return () => { return () => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
customerTypeChartInstance.current?.dispose(); customerTypeChartInstance.current?.dispose();
customerTypeChartInstance.current = null;
customerValueChartInstance.current?.dispose(); customerValueChartInstance.current?.dispose();
customerValueChartInstance.current = null;
}; };
}, []); } else {
// 进入详情时,释放实例以避免无容器绑定
customerTypeChartInstance.current?.dispose();
customerTypeChartInstance.current = null;
customerValueChartInstance.current?.dispose();
customerValueChartInstance.current = null;
}
}, [showDetail]);
// 返回列表时恢复滚动位置,确保布局稳定后再滚动
useLayoutEffect(() => {
if (!showDetail) {
// 等待本帧布局完成
requestAnimationFrame(() => {
window.scrollTo(0, prevScrollY || 0);
});
}
}, [showDetail]);
// 最近活动数据 // 最近活动数据
const recentActivities = [ const recentActivities = [
@ -239,6 +307,48 @@ const CustomerInfoManagement = () => {
}, },
]; ];
// 列配置(用于 StandardTable
const columns = useMemo(() => {
return [
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 220 },
{ title: '联系人', dataIndex: 'contact', key: 'contact', width: 120 },
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 140 },
{
title: '分类', dataIndex: 'classification', key: 'classification', width: 110,
render: (val) => (
<span className={`${styles.tag} ${val === '高价值' ? styles.tagGold : styles.tagBlue}`}>{val}</span>
)
},
{
title: '交易额度(月)', dataIndex: 'monthlyAmount', key: 'monthlyAmount', width: 150,
render: (v) => `¥${Number(v).toLocaleString()}`
},
{
title: '合作状态', dataIndex: 'cooperationStatus', key: 'cooperationStatus', width: 110,
render: (val) => (
<span className={`${styles.tag} ${val === '合作中' ? styles.tagGreen : styles.tagOrange}`}>{val}</span>
)
},
{
title: '满意度', dataIndex: 'satisfaction', key: 'satisfaction', width: 120,
},
{
title: '操作', key: 'action', fixed: 'right', width: 200,
render: (_, row) => (
<Space size={12}>
<Button type="link" size="small" className="viewDetailBtn" onClick={() => {
setPrevScrollY(window.pageYOffset || document.documentElement.scrollTop || 0);
setDetailData(row);
setShowDetail(true);
}}>查看详情</Button>
<Button type="link" size="small" className="editBtn">修改</Button>
<Button type="link" size="small" danger className="deleteBtn">删除</Button>
</Space>
)
},
];
}, []);
// 处理选择 // 处理选择
const handleSelectRow = (id) => { const handleSelectRow = (id) => {
if (selectedRows.includes(id)) { if (selectedRows.includes(id)) {
@ -282,7 +392,7 @@ const CustomerInfoManagement = () => {
if (showDetail) { if (showDetail) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div style={{marginBottom:10}}> <div style={{ marginBottom: 10 }}>
<button <button
className={styles.backBtn} className={styles.backBtn}
onClick={() => { onClick={() => {
@ -292,7 +402,7 @@ const CustomerInfoManagement = () => {
window.scrollTo(0, prevScrollY); window.scrollTo(0, prevScrollY);
}, 0); }, 0);
}} }}
style={{padding:'6px 12px',border:'1px solid #d9d9d9',borderRadius:6,background:'#fff',fontSize:12,cursor:'pointer'}} style={{ padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: 6, background: '#fff', fontSize: 12, cursor: 'pointer' }}
> >
返回列表 返回列表
</button> </button>
@ -304,42 +414,57 @@ const CustomerInfoManagement = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.mainCard}>
{/* KPI卡片区域 */} {/* KPI卡片区域 */}
<div className={styles.kpiRow}> <div className={styles.kpiGrid}>
<div className={styles.kpiCard}> <div className={styles.kpiCard}>
<div className={styles.kpiIcon}>👥</div> <div className={styles.kpiText}>
<div className={styles.kpiValue}>{kpiData.totalCustomers}</div> <div className={styles.kpiValue}>{kpiData.totalCustomers}</div>
<div className={styles.kpiTitle}>总客户数</div> <div className={styles.kpiTitle}>总客户数</div>
</div> </div>
<img src={topIcon} alt="icon" className={styles.kpiIconImg} />
</div>
<div className={styles.kpiCard}> <div className={styles.kpiCard}>
<div className={styles.kpiIcon}>👑</div> <div className={styles.kpiText}>
<div className={styles.kpiValue}>{kpiData.highValueCustomers}</div> <div className={styles.kpiValue}>{kpiData.highValueCustomers}</div>
<div className={styles.kpiTitle}>高价值客户</div> <div className={styles.kpiTitle}>高价值客户</div>
</div> </div>
<img src={topIcon2} alt="icon" className={styles.kpiIconImg} />
</div>
<div className={styles.kpiCard}> <div className={styles.kpiCard}>
<div className={styles.kpiIcon}>🤝</div> <div className={styles.kpiText}>
<div className={styles.kpiValue}>{kpiData.inCooperation}</div> <div className={styles.kpiValue}>{kpiData.inCooperation}</div>
<div className={styles.kpiTitle}>合作中</div> <div className={styles.kpiTitle}>合作中</div>
</div> </div>
<img src={topIcon3} alt="icon" className={styles.kpiIconImg} />
</div>
<div className={styles.kpiCard}> <div className={styles.kpiCard}>
<div className={styles.kpiIcon}></div> <div className={styles.kpiText}>
<div className={styles.kpiValue}>{kpiData.newThisMonth}</div> <div className={styles.kpiValue}>{kpiData.newThisMonth}</div>
<div className={styles.kpiTitle}>本月新增</div> <div className={styles.kpiTitle}>本月新增</div>
</div> </div>
<img src={topIcon4} alt="icon" className={styles.kpiIconImg} />
</div>
</div> </div>
{/* 图表和活动区域 */} {/* 图表和活动区域 */}
<div className={styles.chartRow}> <div className={styles.chartRow}>
{/* 客户类型分布图表 */} {/* 客户类型分布图表 */}
<div className={styles.chartCard}> <div className={styles.chartCard} style={{ flex: 9 }}>
<div className={styles.chartHeader}> <div className={styles.chartHeader}>
<div className={styles.chartTitle}>客户类型分布</div> <div className={styles.chartTitle}>客户类型分布</div>
<select className={styles.timeSelect} defaultValue="全部时间"> <Select
<option value="全部时间">全部时间</option> size="small"
<option value="本月">本月</option> className={styles.timeSelect}
<option value="本季度">本季度</option> defaultValue="全部时间"
<option value="本年">本年</option> options={[
</select> { value: '全部时间', label: '全部时间' },
{ value: '本月', label: '本月' },
{ value: '本季度', label: '本季度' },
{ value: '本年', label: '本年' },
]}
onChange={(v) => {/* 可根据需要触发筛选 */ }}
/>
</div> </div>
<div <div
ref={customerTypeChartRef} ref={customerTypeChartRef}
@ -348,7 +473,7 @@ const CustomerInfoManagement = () => {
</div> </div>
{/* 客户价值分布图表 */} {/* 客户价值分布图表 */}
<div className={styles.chartCard}> <div className={styles.chartCard} style={{ flex: 6 }}>
<div className={styles.chartHeader}> <div className={styles.chartHeader}>
<div className={styles.chartTitle}>客户价值分布</div> <div className={styles.chartTitle}>客户价值分布</div>
</div> </div>
@ -359,24 +484,18 @@ const CustomerInfoManagement = () => {
</div> </div>
{/* 最近活动列表 */} {/* 最近活动列表 */}
<div className={styles.activityCard}> <div className={styles.activityCard} style={{ flex: 9 }}>
<div className={styles.chartHeader}> <div className={styles.chartHeader}>
<div className={styles.chartTitle}>最近活动</div> <div className={styles.chartTitle}>最近活动</div>
</div> </div>
<div className={styles.activityList}> <div className={styles.activityGrid}>
{recentActivities.map((activity) => ( {recentActivities.map((activity) => (
<div key={activity.id} className={styles.activityItem}> <div key={activity.id} className={styles.activityCardItem}>
<div className={styles.activityIcon}> <div className={styles.activityCardHeader}>
{activity.id === 1 && '📄'} <div className={styles.activityCardTitle}>{activity.title}</div>
{activity.id === 2 && '✅'} <div className={styles.activityCardTime}>{activity.time}</div>
{activity.id === 3 && '💬'}
{activity.id === 4 && '👤'}
</div>
<div className={styles.activityContent}>
<div className={styles.activityTitle}>{activity.title}</div>
<div className={styles.activityDesc}>{activity.description}</div>
<div className={styles.activityTime}>🕐 {activity.time}</div>
</div> </div>
<div className={styles.activityCardDesc}>{activity.description}</div>
</div> </div>
))} ))}
</div> </div>
@ -384,206 +503,109 @@ const CustomerInfoManagement = () => {
</div> </div>
{/* 客户列表区域 */} {/* 客户列表区域 */}
<div className={styles.tableCard}> <div className={styles.bottomSection}>
<div className={styles.tableHeader}> <div className={styles.sectionHeader}>
<div className={styles.tableTitle}>客户列表</div> <span className={styles.sectionBar} />
<span className={styles.sectionTitle}>客户列表</span>
</div> </div>
<div className={styles.filterContent}>
{/* 筛选栏 */} <div className={styles.filterItem}>
<div className={styles.filterBar}> <Input.Search
<div className={styles.filterGroup}>
<input
type="text"
className={styles.searchInput}
placeholder="搜索关键词" placeholder="搜索关键词"
allowClear
value={searchKeyword} value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)} onChange={(e) => setSearchKeyword(e.target.value)}
onSearch={(val) => setSearchKeyword(val)}
style={{ minWidth: 150 }}
/> />
<select
className={styles.filterSelect}
value={customerType}
onChange={(e) => setCustomerType(e.target.value)}
>
<option value="全部">客户类型: 全部</option>
<option value="客户">客户</option>
<option value="供应商">供应商</option>
</select>
<select
className={styles.filterSelect}
value={customerLevel}
onChange={(e) => setCustomerLevel(e.target.value)}
>
<option value="全部">客户等级: 全部</option>
<option value="高价值">高价值</option>
<option value="常规客户">常规客户</option>
</select>
<select
className={styles.filterSelect}
value={cooperationStatus}
onChange={(e) => setCooperationStatus(e.target.value)}
>
<option value="全部">合作状态: 全部</option>
<option value="合作中">合作中</option>
<option value="暂停合作">暂停合作</option>
</select>
<button className={styles.queryBtn} onClick={() => console.log('查询')}>
🔍 查询
</button>
<button className={styles.resetBtn} onClick={() => {
setCustomerType('全部');
setCustomerLevel('全部');
setCooperationStatus('全部');
}}>
🔄 重置
</button>
<button className={styles.addBtn}>
新增客户
</button>
<button className={styles.exportBtn}>
📥 批量导出
</button>
</div> </div>
</div> <div className={styles.filterItem}>
<span className={styles.filterLabel}>客户类型:</span>
{/* 表格 */} <Select
<div className={styles.tableWrapper}> value={filters.customerType}
<table className={styles.dataTable}> onChange={(v) => setFilters({ ...filters, customerType: v })}
<thead> placeholder="全部"
<tr> options={[
<th> { label: '全部', value: '' },
<input ]}
type="checkbox" allowClear
checked={selectedRows.length === tableData.length}
onChange={handleSelectAll}
/> />
</th>
<th>客户名称</th>
<th>联系人</th>
<th>联系电话</th>
<th>分类</th>
<th>交易额度()</th>
<th>合作状态</th>
<th>满意度</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{tableData.map((row) => (
<tr key={row.id}>
<td>
<input
type="checkbox"
checked={selectedRows.includes(row.id)}
onChange={() => handleSelectRow(row.id)}
/>
</td>
<td>{row.customerName}</td>
<td>{row.contact}</td>
<td>{row.phone}</td>
<td>
<span className={`${styles.tag} ${row.classification === '高价值' ? styles.tagGold : styles.tagBlue}`}>
{row.classification}
</span>
</td>
<td>¥{row.monthlyAmount.toLocaleString()}</td>
<td>
<span className={`${styles.tag} ${row.cooperationStatus === '合作中' ? styles.tagGreen : styles.tagOrange}`}>
{row.cooperationStatus}
</span>
</td>
<td>
<div className={styles.stars}>
{renderStars(row.satisfaction)}
</div> </div>
</td> <div className={styles.filterItem}>
<td> <span className={styles.filterLabel}>客户等级:</span>
<div className={styles.actionBtns}> <Select
<button value={filters.customerGrade}
className={styles.actionBtn} onChange={(v) => setFilters({ ...filters, customerGrade: v })}
onClick={() => { placeholder="全部"
// 记录当前滚动位置 options={[
setPrevScrollY(window.pageYOffset || document.documentElement.scrollTop || 0); { label: '全部', value: '' },
setDetailData(row); ]}
setShowDetail(true); allowClear
}} />
>
查看详情
</button>
<button className={styles.actionBtn}>修改</button>
<button className={`${styles.actionBtn} ${styles.deleteBtn}`}>删除</button>
</div> </div>
</td> <div className={styles.filterItem}>
</tr> <span className={styles.filterLabel}>合作状态:</span>
))} <Select
</tbody> value={filters.cooperationStatus}
</table> onChange={(v) => setFilters({ ...filters, cooperationStatus: v })}
placeholder="全部"
options={[
{ label: '全部', value: '' },
]}
allowClear
/>
</div> </div>
<Space className={styles.filterButtons}>
{/* 分页 */} <Button
<div className={styles.pagination}> type="primary"
<div className={styles.paginationInfo}> icon={<SearchOutlined />}
共85条 className={styles.queryBtn}
<select
className={styles.pageSizeSelect}
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
}}
> >
<option value="10">10 / page</option> 查询
<option value="20">20 / page</option> </Button>
<option value="50">50 / page</option> <Button
<option value="100">100 / page</option> icon={<ReloadOutlined />}
</select> className={styles.resetBtn}
</div>
<div className={styles.paginationControls}>
<button
className={styles.pageBtn}
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
> >
重置
</button> </Button>
{currentPage > 3 && ( </Space>
<> <Space className={styles.filterButtonsRight}>
<button className={styles.pageBtn} onClick={() => setCurrentPage(1)}>1</button> <Button
<span className={styles.pageEllipsis}>...</span> type="primary"
</> icon={<PlusOutlined />}
)} className={styles.addBtn}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
if (pageNum > totalPages) return null;
return (
<button
key={pageNum}
className={`${styles.pageBtn} ${currentPage === pageNum ? styles.pageBtnActive : ''}`}
onClick={() => setCurrentPage(pageNum)}
> >
{pageNum} 新增客户
</button> </Button>
); <Button
})} // icon={<ExportOutlined />}
{currentPage < totalPages - 2 && ( className={styles.exportBtn}
<>
<span className={styles.pageEllipsis}>...</span>
<button className={styles.pageBtn} onClick={() => setCurrentPage(totalPages)}>{totalPages}</button>
</>
)}
<button
className={styles.pageBtn}
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
> >
批量导出
</button> </Button>
</Space>
</div>
<div className={styles.tableWrapper}>
<StandardTable
rowKey="id"
columns={columns}
data={{
list: tableData,
pagination: {
total: tableData.length,
pageSize,
current: currentPage,
showTotal: (total) => `${total}`,
},
}}
selectionType="checkbox"
selectedRowKeys={selectedRows}
onSelectRow={(row) => handleSelectRow(row.id)}
onSelectAll={(checked) => checked ? setSelectedRows(tableData.map(i => i.id)) : setSelectedRows([])}
onChangePage={(page) => setCurrentPage(page)}
onChangePageSize={(size) => { setPageSize(size); setCurrentPage(1); }}
/>
</div> </div>
</div> </div>
</div> </div>

@ -3,41 +3,81 @@
background: #f5f5f5; background: #f5f5f5;
min-height: 100vh; min-height: 100vh;
// KPI卡片区域 .mainCard {
.kpiRow { background: #fff;
display: flex; border-radius: 0px;
// box-shadow: 0 4px 16px rgba(0,0,0,0.08);
// border: 1px solid #eaeaea;
padding: 16px;
}
// KPI卡片区域改为贴近截图样式
.kpiGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px; gap: 16px;
margin-bottom: 20px; margin-bottom: 20px;
.kpiCard { .kpiCard {
flex: 1; display: flex;
background: #fff; align-items: center;
border-radius: 8px; justify-content: space-between;
padding: 24px; padding: 16px 20px;
text-align: center; border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); background: linear-gradient(180deg, #A1DED4 0%, #CBEDE74D 70%);
transition: all 0.3s; // border: 1px solid transparent;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: border-color 0.2s, box-shadow 0.2s;
&.kpiCardActive {
border-color: #1890ff;
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.2);
}
&:hover { &:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
} }
.kpiIcon { .kpiText {
font-size: 32px; display: flex;
margin-bottom: 12px; flex-direction: column;
gap: 6px;
} }
.kpiValue { .kpiValue {
font-size: 32px; font-size: 24px;
font-weight: 600; font-weight: 600;
color: #1890ff; color: #333;
margin-bottom: 8px;
} }
.kpiTitle { .kpiTitle {
font-size: 14px; font-size: 12px;
color: #666; color: #4E5856;
}
.kpiIconBox {
width: 40px;
height: 40px;
border-radius: 8px;
background: #1D6E6A;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
flex-shrink: 0;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.kpiIcon {
font-size: 20px;
line-height: 1;
}
.kpiIconImg {
width: 40px;
height: 40px;
display: block;
object-fit: contain;
} }
} }
} }
@ -51,363 +91,359 @@
.chartCard, .chartCard,
.activityCard { .activityCard {
flex: 1; flex: 1;
background: #fff; background: #F3F9F8;
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #76C2B5;
.chartHeader { .chartHeader {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: 8px;
padding-bottom: 12px; padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
.chartTitle { .chartTitle {
position: relative;
padding-left: 10px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
.chartTitle::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
background-color: #1D6E6A;
border-radius: 2px;
}
.timeSelect { .timeSelect {
padding: 4px 8px; :global(.ant-select-selector) {
border: 1px solid #d9d9d9; border-color: #2C9E9D !important;
border-radius: 4px; border-radius: 2px;
font-size: 12px; padding: 0 8px;
cursor: pointer; background: transparent !important;
font-size: 13px;
}
&:hover { :global(.ant-select-focused .ant-select-selector),
border-color: #40a9ff; :global(.ant-select-selector:hover) {
border-color: #2C9E9D !important;
box-shadow: 0 0 0 2px rgba(29, 110, 106, 0.15);
} }
} }
} }
.chartContainer { .chartContainer {
height: 300px; height: 200px;
width: 100%; width: 100%;
} }
} }
.activityCard { .activityCard {
.activityList { .activityGrid {
.activityItem { display: grid;
display: flex; grid-template-columns: repeat(2, 1fr);
gap: 12px; gap: 12px;
padding: 12px 0; }
border-bottom: 1px solid #f0f0f0;
&:last-child { .activityCardItem {
border-bottom: none; background: #fff;
border: 1px solid #D6EEE9;
// border-radius: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
padding: 12px 14px;
} }
.activityIcon { .activityCardHeader {
width: 40px;
height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
background: #f5f5f5; margin-bottom: 6px;
border-radius: 8px;
font-size: 20px;
flex-shrink: 0;
} }
.activityContent { .activityCardTitle {
flex: 1; color: #006665;
.activityTitle {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #333;
margin-bottom: 4px;
} }
.activityDesc { .activityCardTime {
font-size: 13px; color: #9AA3A1;
color: #666; font-size: 12px;
margin-bottom: 4px; flex-shrink: 0;
} }
.activityTime { .activityCardDesc {
color: #4E5856;
font-size: 12px; font-size: 12px;
color: #999; line-height: 1.6;
}
}
}
} }
} }
} }
// 表格区域 // 底部区域 - 客户列表
.tableCard { .bottomSection {
flex: 1;
display: flex;
flex-direction: column;
// border: 1px solid @section-border;
background: #fff; background: #fff;
border-radius: 8px; padding: 5px 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.tableHeader { // 区域头部
margin-bottom: 16px; .sectionHeader {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
.tableTitle { // 装饰条
.sectionBar {
width: 2px;
height: 16px;
background: rgba(0, 102, 101, 1);
border-radius: 1px;
}
// 标题
.sectionTitle {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
} }
.filterBar { // 筛选内容区域
margin-bottom: 16px; .filterContent {
padding: 16px;
background: #fafafa;
border-radius: 4px;
.filterGroup {
display: flex; display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
// justify-content: space-between;
.searchInput { // 筛选项
padding: 8px 12px; .filterItem {
border: 1px solid #d9d9d9; display: flex;
border-radius: 4px; align-items: center;
font-size: 14px; gap: 12px;
width: 200px;
&:focus {
outline: none;
border-color: #40a9ff;
}
}
.filterSelect {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
width: 150px;
cursor: pointer;
background: #fff;
&:hover {
border-color: #40a9ff;
}
}
.queryBtn, // 筛选标签
.resetBtn, .filterLabel {
.addBtn, color: #333;
.exportBtn {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px; font-size: 14px;
cursor: pointer;
background: #fff;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
color: #40a9ff;
}
} }
.queryBtn, :global {
.addBtn {
background: #1890ff;
color: #fff;
border-color: #1890ff;
&:hover { // 统一 Input.Search 边框颜色为绿主题
background: #40a9ff; .ant-input-affix-wrapper {
border-color: #40a9ff; border-radius: 0px !important;
} border-color: rgba(44, 158, 157, 1) !important;
}
}
} }
.tableWrapper { .ant-input-search-button {
overflow-x: auto; border-radius: 0px !important;
border-color: rgba(44, 158, 157, 1) !important;
.dataTable {
width: 100%;
border-collapse: collapse;
thead {
background: #fafafa;
th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #f0f0f0;
white-space: nowrap;
}
} }
tbody { .ant-select {
tr { min-width: 90px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.3s;
&:hover { // 选择框样式
background: #fafafa; .ant-select-selector {
border-color: rgba(44, 158, 157, 1) !important;
border-radius: 2px !important;
} }
td { &:hover .ant-select-selector {
padding: 12px; border-color: rgba(44, 158, 157, 1) !important;
color: #666;
white-space: nowrap;
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
} }
.tagGold { &.ant-select-focused .ant-select-selector {
background: #fffbe6; border-color: rgba(44, 158, 157, 1) !important;
color: #faad14; box-shadow: 0 0 0 2px rgba(44, 158, 157, 0.2) !important;
border: 1px solid #ffe58f;
} }
.tagBlue {
background: #e6f7ff;
color: #1890ff;
border: 1px solid #91d5ff;
} }
.tagGreen {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
} }
.tagOrange {
background: #fff7e6;
color: #fa8c16;
border: 1px solid #ffd591;
} }
.stars { // 筛选按钮组
.filterButtons {
display: flex; display: flex;
gap: 2px; gap: 12px;
.starFull {
color: #faad14;
font-size: 16px;
}
.starHalf { .queryBtn {
color: #faad14; background-image: url('@/assets/business_basic/Bt_bg1.png');
font-size: 16px; background-position: center;
opacity: 0.5; background-repeat: no-repeat;
background-size: cover;
border: none;
} }
.starEmpty { .resetBtn {
color: #d9d9d9; background-image: url('@/assets/business_basic/Bt_bg2.png');
font-size: 16px; background-position: center;
background-repeat: no-repeat;
background-size: cover;
border: none;
color: #006665;
} }
} }
.actionBtns { // 右侧按钮组
.filterButtonsRight {
display: flex; display: flex;
gap: 8px; gap: 12px;
margin-left: auto;
.actionBtn { .addBtn {
padding: 4px 8px; background-image: url('@/assets/business_basic/Bt_bg1.png');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
border: none; border: none;
background: transparent;
color: #1890ff;
cursor: pointer;
font-size: 12px;
border-radius: 4px;
transition: all 0.3s;
&:hover {
background: #e6f7ff;
} }
&.deleteBtn { .exportBtn {
color: #ff4d4f; background-image: url('@/assets/business_basic/Bt_bg2.png');
background-position: center;
&:hover { background-repeat: no-repeat;
background: #fff1f0; background-size: cover;
} border: none;
} color: #006665;
}
}
}
}
} }
} }
} }
.pagination { // 表格包装器
.tableWrapper {
flex: 1;
min-height: 0;
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
.paginationInfo { :global(.ant-table-wrapper) {
flex: 1;
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 8px;
color: #666;
font-size: 14px;
.pageSizeSelect {
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin-left: 8px;
} }
// 表头样式
:global {
.ant-table-thead>tr>th {
color: rgba(51, 51, 51, 1) !important;
font-weight: 450 !important;
background-color: rgba(240, 247, 247, 1) !important;
text-align: center !important;
} }
.paginationControls { // 表体样式
display: flex; .ant-table-tbody>tr>td {
gap: 4px; color: rgba(78, 88, 86, 1) !important;
align-items: center; font-weight: 400 !important;
text-align: center !important;
}
.pageBtn { // 操作列按钮样式
min-width: 32px; .viewDetailBtn {
height: 32px; color: rgba(0, 102, 101, 1) !important;
padding: 0 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
&:hover:not(:disabled) { &:hover {
border-color: #40a9ff; color: rgba(0, 102, 101, 1) !important;
color: #40a9ff;
} }
&:disabled {
opacity: 0.5;
cursor: not-allowed;
} }
&.pageBtnActive { .editBtn {
background: #1890ff; color: rgba(45, 158, 157, 1) !important;
color: #fff;
border-color: #1890ff; &:hover {
color: rgba(45, 158, 157, 1) !important;
} }
} }
.pageEllipsis { .deleteBtn {
padding: 0 8px; color: rgba(255, 130, 109, 1) !important;
color: #666;
} &:hover {
color: rgba(255, 130, 109, 1) !important;
}
}
// 状态列 Switch 样式
.statusSwitch {
// 启用状态背景色
&.ant-switch-checked {
background-color: rgba(20, 106, 89, 1) !important;
}
// 停用状态背景色
&:not(.ant-switch-checked) {
background-color: rgba(153, 153, 153, 1) !important;
}
}
// 复选框样式
.ant-checkbox-inner {
border-color: rgba(0, 102, 101, 1) !important;
&::after {
background-color: rgba(0, 102, 101, 1) !important;
}
}
.ant-checkbox-wrapper:hover .ant-checkbox-inner,
.ant-checkbox:hover .ant-checkbox-inner {
border-color: rgba(0, 102, 101, 1) !important;
}
.ant-checkbox-checked .ant-checkbox-inner {
background-color: rgba(0, 102, 101, 1) !important;
border-color: rgba(0, 102, 101, 1) !important;
}
// 分页样式(绿色主题)
// .ant-pagination {
// .ant-pagination-total-text {
// color: rgba(78, 88, 86, 1) !important;
// }
// .ant-pagination-item {
// border-radius: 2px;
// border-color: rgba(44, 158, 157, 1) !important;
// }
// .ant-pagination-item a {
// color: rgba(78, 88, 86, 1) !important;
// }
// .ant-pagination-item-active {
// border-color: rgba(0, 102, 101, 1) !important;
// background: rgba(240, 247, 247, 1) !important;
// }
// .ant-pagination-item-active a {
// color: rgba(0, 102, 101, 1) !important;
// }
// .ant-pagination-item:hover {
// border-color: rgba(0, 102, 101, 1) !important;
// }
// .ant-pagination-prev .ant-pagination-item-link,
// .ant-pagination-next .ant-pagination-item-link {
// border-radius: 2px;
// border-color: rgba(44, 158, 157, 1) !important;
// color: rgba(78, 88, 86, 1) !important;
// }
// .ant-select-selector {
// border-color: rgba(44, 158, 157, 1) !important;
// border-radius: 2px !important;
// }
// }
} }
} }
} }

@ -175,8 +175,7 @@
:global(.ant-btn:not(.ant-btn-primary)) { :global(.ant-btn:not(.ant-btn-primary)) {
background-color: rgba(183, 229, 213, 0.2) !important; background-color: rgba(183, 229, 213, 0.2) !important;
border: 1px solid transparent !important; border: 1px solid transparent !important;
border-image-source: conic-gradient( border-image-source: conic-gradient(from 102.75deg at 50% 52.91%,
from 102.75deg at 50% 52.91%,
rgba(249, 249, 249, 0.5) -32.95deg, rgba(249, 249, 249, 0.5) -32.95deg,
rgba(140, 160, 156, 0.5) 10.52deg, rgba(140, 160, 156, 0.5) 10.52deg,
rgba(140, 160, 156, 0.35) 32.12deg, rgba(140, 160, 156, 0.35) 32.12deg,
@ -186,8 +185,7 @@
#F9F9F9 207.58deg, #F9F9F9 207.58deg,
rgba(255, 255, 255, 0.5) 287.31deg, rgba(255, 255, 255, 0.5) 287.31deg,
rgba(249, 249, 249, 0.5) 327.05deg, rgba(249, 249, 249, 0.5) 327.05deg,
rgba(140, 160, 156, 0.5) 370.52deg rgba(140, 160, 156, 0.5) 370.52deg) !important;
) !important;
border-image-slice: 1 !important; border-image-slice: 1 !important;
&:hover { &:hover {
@ -308,7 +306,7 @@
// 表头样式 // 表头样式
:global { :global {
.ant-table-thead > tr > th { .ant-table-thead>tr>th {
color: rgba(51, 51, 51, 1) !important; color: rgba(51, 51, 51, 1) !important;
font-weight: 450 !important; font-weight: 450 !important;
background-color: rgba(240, 247, 247, 1) !important; background-color: rgba(240, 247, 247, 1) !important;
@ -316,7 +314,7 @@
} }
// 表体样式 // 表体样式
.ant-table-tbody > tr > td { .ant-table-tbody>tr>td {
color: rgba(78, 88, 86, 1) !important; color: rgba(78, 88, 86, 1) !important;
font-weight: 400 !important; font-weight: 400 !important;
text-align: center !important; text-align: center !important;
@ -349,6 +347,7 @@
// 状态列 Switch 样式 // 状态列 Switch 样式
.statusSwitch { .statusSwitch {
// 启用状态背景色 // 启用状态背景色
&.ant-switch-checked { &.ant-switch-checked {
background-color: rgba(20, 106, 89, 1) !important; background-color: rgba(20, 106, 89, 1) !important;
@ -363,6 +362,10 @@
// 复选框样式 // 复选框样式
.ant-checkbox-inner { .ant-checkbox-inner {
border-color: rgba(0, 102, 101, 1) !important; border-color: rgba(0, 102, 101, 1) !important;
&::after {
background-color: rgba(0, 102, 101, 1) !important;
}
} }
.ant-checkbox-wrapper:hover .ant-checkbox-inner, .ant-checkbox-wrapper:hover .ant-checkbox-inner,

Loading…
Cancel
Save