From 6362673ccb5486f381ae4ae56144d2c740f7c8f6 Mon Sep 17 00:00:00 2001 From: RUI <298977887@qq.com> Date: Sat, 7 Jun 2025 00:20:00 +0800 Subject: [PATCH] 0607.1 --- package.json | 1 + pnpm-lock.yaml | 3 + src/components/layout/Layout.tsx | 183 +++-- src/components/layout/ThemeSwitcher.tsx | 199 ++--- src/models/index.ts | 14 +- src/models/types.ts | 1 + src/pages/api/admin/cleanup-indexes.ts | 42 + src/pages/api/backstage/brands/[id].ts | 50 ++ src/pages/api/backstage/brands/index.ts | 29 + src/pages/api/backstage/categories/[id].ts | 50 ++ src/pages/api/backstage/categories/index.ts | 31 + src/pages/api/backstage/customers/[id].ts | 68 ++ src/pages/api/backstage/customers/index.ts | 53 ++ .../api/backstage/customers/sales/[id].ts | 59 ++ .../api/backstage/payment-platforms/[id].ts | 51 ++ .../api/backstage/payment-platforms/index.ts | 30 + src/pages/api/backstage/products/[id].ts | 85 ++ src/pages/api/backstage/products/index.ts | 56 ++ .../api/backstage/sales/Records/index.ts | 12 +- src/pages/api/backstage/sales/updateStatus.ts | 98 +++ src/pages/api/backstage/suppliers/[id].ts | 50 ++ src/pages/api/backstage/suppliers/index.ts | 33 + src/pages/api/backstage/users/[id].ts | 56 ++ src/pages/api/backstage/users/index.ts | 41 + src/pages/api/tools/logistics/index.ts | 7 +- src/pages/api/user.ts | 5 +- src/pages/team/SaleRecord/AfterSalesModal.tsx | 39 +- src/pages/team/SaleRecord/index.tsx | 298 ++++++- src/pages/team/SaleRecord/sales-modal.tsx | 73 +- src/pages/team/SaleRecord/ship-modal.tsx | 52 +- .../SaleRecord/{test.tsx => test.tsx.bak} | 0 .../sale/components/AddCustomerComponent.tsx | 388 +++++++++ .../sale/components/AddProductComponent.tsx | 766 ++++++++++++++++++ .../sale/components/CustomerInfoComponent.tsx | 329 ++++++++ .../team/sale/components/EditCustomerInfo.tsx | 119 +++ .../sale/components/ProductInfoComponent.tsx | 193 +++++ src/pages/team/sale/components/Recharge.tsx | 152 ++++ .../team/sale/components/SelectCustomer.tsx | 45 + .../team/sale/components/SelectProduct.tsx | 51 ++ .../sale/components/address-smart-parse.d.ts | 21 + .../team/sale/components/coupon-modal.tsx | 159 ++++ .../team/sale/components/sales-record.tsx | 142 ++++ src/pages/team/sale/index copy 2.tsx | 228 ++++++ src/pages/team/sale/index copy.tsx | 137 ++++ src/pages/team/sale/index.tsx | 546 +++++++++++++ src/pages/team/sale/sales-record-modal.tsx | 216 +++++ src/styles/antd.css | 17 + src/utils/ConnectDB.ts | 265 +----- src/utils/cleanupIndexes.ts | 120 +++ src/utils/connectDB.ts.bak | 240 ++++++ src/utils/theme.ts | 154 ++-- 51 files changed, 5542 insertions(+), 515 deletions(-) create mode 100644 src/pages/api/admin/cleanup-indexes.ts create mode 100644 src/pages/api/backstage/brands/[id].ts create mode 100644 src/pages/api/backstage/brands/index.ts create mode 100644 src/pages/api/backstage/categories/[id].ts create mode 100644 src/pages/api/backstage/categories/index.ts create mode 100644 src/pages/api/backstage/customers/[id].ts create mode 100644 src/pages/api/backstage/customers/index.ts create mode 100644 src/pages/api/backstage/customers/sales/[id].ts create mode 100644 src/pages/api/backstage/payment-platforms/[id].ts create mode 100644 src/pages/api/backstage/payment-platforms/index.ts create mode 100644 src/pages/api/backstage/products/[id].ts create mode 100644 src/pages/api/backstage/products/index.ts create mode 100644 src/pages/api/backstage/sales/updateStatus.ts create mode 100644 src/pages/api/backstage/suppliers/[id].ts create mode 100644 src/pages/api/backstage/suppliers/index.ts create mode 100644 src/pages/api/backstage/users/[id].ts create mode 100644 src/pages/api/backstage/users/index.ts rename src/pages/team/SaleRecord/{test.tsx => test.tsx.bak} (100%) create mode 100644 src/pages/team/sale/components/AddCustomerComponent.tsx create mode 100644 src/pages/team/sale/components/AddProductComponent.tsx create mode 100644 src/pages/team/sale/components/CustomerInfoComponent.tsx create mode 100644 src/pages/team/sale/components/EditCustomerInfo.tsx create mode 100644 src/pages/team/sale/components/ProductInfoComponent.tsx create mode 100644 src/pages/team/sale/components/Recharge.tsx create mode 100644 src/pages/team/sale/components/SelectCustomer.tsx create mode 100644 src/pages/team/sale/components/SelectProduct.tsx create mode 100644 src/pages/team/sale/components/address-smart-parse.d.ts create mode 100644 src/pages/team/sale/components/coupon-modal.tsx create mode 100644 src/pages/team/sale/components/sales-record.tsx create mode 100644 src/pages/team/sale/index copy 2.tsx create mode 100644 src/pages/team/sale/index copy.tsx create mode 100644 src/pages/team/sale/index.tsx create mode 100644 src/pages/team/sale/sales-record-modal.tsx create mode 100644 src/utils/cleanupIndexes.ts create mode 100644 src/utils/connectDB.ts.bak diff --git a/package.json b/package.json index d0578db..e5eed95 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/lodash": "^4.17.17", "antd": "^5.25.4", "bcryptjs": "^3.0.2", + "dayjs": "^1.11.13", "geist": "^1.4.2", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21b34fc..4bac4d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: bcryptjs: specifier: ^3.0.2 version: 3.0.2 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 geist: specifier: ^1.4.2 version: 1.4.2(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 03390cb..4eb9ef3 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -2,7 +2,7 @@ * Layout主布局组件 * 作者: 阿瑞 * 功能: 应用主布局,包含导航菜单、用户信息、主题切换等功能 - * 版本: v2.0 - 性能优化版本 + * 版本: v3.0 - 性能优化版本,移除realDark,添加React.memo优化 */ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { useRouter } from 'next/router'; @@ -22,21 +22,44 @@ interface LayoutProps { children: React.ReactNode; } -// 生成动态路由的函数 - 优化版本 +// 生成动态路由的函数 - 优化版本,添加缓存机制 +const routeCache = new Map(); + const generateDynamicRoutes = (permissions: IPermission[]): MenuDataItem[] => { if (!permissions?.length) return []; + // 关键代码行注释:生成缓存键 + const cacheKey = permissions.map(p => `${p.路径 || ''}-${p.排序 ?? 0}`).join('|'); + + // 关键代码行注释:尝试从缓存获取 + if (routeCache.has(cacheKey)) { + return routeCache.get(cacheKey)!; + } + // 对权限进行排序 const sortedPermissions = permissions.sort((a, b) => (a.排序 ?? 0) - (b.排序 ?? 0)); // 映射权限数据到菜单项 - return sortedPermissions.map(permission => ({ - path: permission.路径, - name: permission.名称, - icon: , + const routes = sortedPermissions.map(permission => ({ + path: permission.路径 || '/', + name: permission.名称 || '', + icon: , component: './DynamicComponent', routes: permission.子级 ? generateDynamicRoutes(permission.子级) : undefined }) as Partial); + + // 关键代码行注释:存储到缓存 + routeCache.set(cacheKey, routes); + + // 关键代码行注释:限制缓存大小 + if (routeCache.size > 50) { + const firstKey = routeCache.keys().next().value; + if (firstKey) { + routeCache.delete(firstKey); + } + } + + return routes; }; // 默认props配置 @@ -50,7 +73,7 @@ const defaultProps = { }, }; -// 头部标题渲染组件 - 提取为独立组件避免重复渲染 +// 头部标题渲染组件 - 使用React.memo优化性能 const HeaderTitle: React.FC = React.memo(() => ( ( )); -// 菜单底部渲染组件 -const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo(({ - collapsed, - userInfo, - //onShowScriptLibrary +HeaderTitle.displayName = 'HeaderTitle'; + +// 用户信息显示组件 - 新增独立组件,使用React.memo优化 +const UserInfoDisplay: React.FC<{ userInfo: any; collapsed?: boolean; }> = React.memo(({ + userInfo, + collapsed }) => { - if (collapsed) return undefined; + if (collapsed) return null; return ( -
+ <> {/* 团队信息部分 */}
= React.memo 角色:{userInfo?.角色?.名称 || 'N/A'}
+ + ); +}); - {/* 任务控制器 */} +UserInfoDisplay.displayName = 'UserInfoDisplay'; + +// 菜单底部渲染组件 - 优化版本,使用React.memo +const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo(({ + collapsed, + userInfo, +}) => { + if (collapsed) return undefined; + + return ( +
+ {/* 用户信息部分 */} + {/* 话术库按钮 */}
+)); + +ThemeToggleButton.displayName = 'ThemeToggleButton'; + // Layout组件主体 const Layout: React.FC = ({ children }) => { // 状态管理 @@ -145,15 +214,14 @@ const Layout: React.FC = ({ children }) => { const [isClient, setIsClient] = useState(false); const [dynamicRoutes, setDynamicRoutes] = useState([]); const [showPersonalInfo, setShowPersonalInfo] = useState(false); - //const [showScriptLibrary, setShowScriptLibrary] = useState(false); const { navTheme, toggleTheme, changePrimaryColor, themeToken } = useTheme(() => { }); - // 使用 useMemo 优化 settings 计算 + // 使用 useMemo 优化 settings 计算 - 优化依赖项 const settings = useMemo>(() => ({ fixSiderbar: true, layout: "mix", splitMenus: false, - navTheme: navTheme, + navTheme: navTheme === 'dark' ? 'realDark' : 'light', contentWidth: "Fluid", colorPrimary: themeToken.colorPrimary, title: "私域管理系统V3", @@ -162,7 +230,7 @@ const Layout: React.FC = ({ children }) => { menuHeaderRender: false, }), [navTheme, themeToken.colorPrimary]); - // 使用 useCallback 优化事件处理函数 + // 使用 useCallback 优化事件处理函数 - 优化依赖项 const handleLogout = useCallback(() => { router.push('/'); userActions.clearUserInfoAndToken(); @@ -180,7 +248,7 @@ const Layout: React.FC = ({ children }) => { router.push(path || '/'); }, [router]); - // 用户信息更新逻辑 + // 用户信息更新逻辑 - 优化依赖项 const { fetchAndSetUserInfo } = useUserActions(); useEffect(() => { const updateUserInfo = async () => { @@ -199,7 +267,7 @@ const Layout: React.FC = ({ children }) => { setIsClient(true); }, []); - // 动态路由生成 - 使用 useMemo 优化 + // 动态路由生成 - 使用 useMemo 优化,添加更精确的依赖项 const memoizedRoutes = useMemo(() => { if (userInfo?.角色?.权限) { return generateDynamicRoutes(userInfo.角色.权限); @@ -207,12 +275,12 @@ const Layout: React.FC = ({ children }) => { return []; }, [userInfo?.角色?.权限]); - // 更新动态路由 + // 更新动态路由 - 优化依赖项 useEffect(() => { setDynamicRoutes(memoizedRoutes); }, [memoizedRoutes]); - // 下拉菜单配置 - 使用 useMemo 优化 + // 下拉菜单配置 - 使用 useMemo 优化,稳定化依赖项 const dropdownMenuItems = useMemo(() => [ { key: 'profile', @@ -220,7 +288,6 @@ const Layout: React.FC = ({ children }) => { label: '个人资料', onClick: handleShowPersonalInfo, }, - { key: 'logout', icon: , @@ -252,30 +319,11 @@ const Layout: React.FC = ({ children }) => {
), [handleMenuItemClick]); - // 头像渲染函数 - 使用 useCallback 优化 + // 头像渲染函数 - 使用 useCallback 优化,稳定化依赖项 const renderAvatar = useCallback((_props: any, dom: React.ReactNode) => ( <> {/* 主题切换按钮 */} - + {/* 主题色选择器 */} = ({ children }) => { ), [themeToken.colorPrimary, changePrimaryColor, toggleTheme, navTheme, dropdownMenuItems]); + // 菜单底部渲染函数 - 使用useCallback优化 + const renderMenuFooter = useCallback((props: any) => ( + + ), [userInfo]); + // 如果不在客户端,不渲染任何内容 if (!isClient) { return null; @@ -302,7 +358,6 @@ const Layout: React.FC = ({ children }) => { height: '100vh', display: 'flex', flexDirection: 'column', - //overflow: 'hidden' }}> = ({ children }) => { prefixCls="my-prefix" {...defaultProps} location={{ pathname: router.pathname }} - token={{ + token={{ // 头部菜单选中项的背景颜色 header: { colorBgMenuItemSelected: 'rgba(0,0,0,0.04)' }, // PageContainer 内边距控制 - 完全移除左右空白 @@ -336,13 +391,7 @@ const Layout: React.FC = ({ children }) => { render: renderAvatar, }} headerTitleRender={() => } - menuFooterRender={(props) => ( - - )} + menuFooterRender={renderMenuFooter} menuItemRender={renderMenuItem} {...settings} style={{ @@ -361,6 +410,23 @@ const Layout: React.FC = ({ children }) => { contentWidth="Fluid" locale="zh-CN" > + {/* + 内容区域边距移除方案说明: + + 为了完全移除页面内容的左右空白,采用了三层防护措施: + + 1. ProLayout 层级:通过 token.pageContainer.paddingInlinePageContainerContent = 0 + 移除 ProLayout 组件默认的左右内边距 + + 2. PageContainer 层级: + - pageHeaderRender={false} 禁用头部渲染避免额外空间 + - token.paddingInlinePageContainerContent = 0 再次确保移除左右内边距 + - style.padding = 0 移除组件自身样式内边距 + + 3. 最内层 div:padding = '0px' 作为最后一道防线 + + 这样的多层配置确保在不同版本的 Ant Design Pro 中都能正常工作 + */} = ({ children }) => { > - - {/* 话术库模态框 */} -
); }; -// 移除React.memo包装组件,避免不必要的重新渲染阻止 -export default Layout; +// 使用React.memo包装整个Layout组件 - 性能优化最后一步 +export default React.memo(Layout); diff --git a/src/components/layout/ThemeSwitcher.tsx b/src/components/layout/ThemeSwitcher.tsx index d10e404..58118f8 100644 --- a/src/components/layout/ThemeSwitcher.tsx +++ b/src/components/layout/ThemeSwitcher.tsx @@ -2,7 +2,7 @@ * 主题切换器组件 * 作者: 阿瑞 * 功能: 主题模式切换和主题色选择 - * 版本: v2.0 - 性能优化版本 + * 版本: v3.0 - 性能优化版本,移除realDark */ import React, { useCallback, useMemo } from 'react'; import { Button, Popover, Tag, Tooltip, Space, Typography } from 'antd'; @@ -14,110 +14,115 @@ interface ThemeSwitcherProps { value: string; onChange: (color: string) => void; toggleTheme: () => void; - navTheme: 'light' | 'realDark'; + navTheme: 'light' | 'dark'; } -// 预设主题色配置 - 使用更丰富的色彩 -const presetColors = [ - { color: '#078DEE', name: '天空蓝' }, - { color: '#7635DC', name: '紫罗兰' }, - { color: '#2065D1', name: '深海蓝' }, - { color: '#FDA92D', name: '橙黄色' }, - { color: '#FF4842', name: '珊瑚红' }, - { color: '#FFC107', name: '金黄色' }, - { color: '#00AB55', name: '翡翠绿' }, - { color: '#1890FF', name: 'Ant蓝' }, - { color: '#722ED1', name: 'Ant紫' }, - { color: '#EB2F96', name: 'Ant粉' }, +// 模块级注释:预定义主题色板 +const THEME_COLORS = [ + { name: '拂晓蓝', color: '#1677ff' }, + { name: '薄暮', color: '#fa541c' }, + { name: '火山', color: '#fa8c16' }, + { name: '日暮', color: '#faad14' }, + { name: '明青', color: '#13c2c2' }, + { name: '极光绿', color: '#52c41a' }, + { name: '极客蓝', color: '#2f54eb' }, + { name: '酱紫', color: '#722ed1' }, ]; -const ThemeSwitcher: React.FC = ({ +// 模块级注释:主题切换器主体组件 - 使用React.memo优化性能 +const ThemeSwitcher: React.FC = React.memo(({ value, - onChange }) => { - // 使用 useCallback 优化颜色选择处理函数 - const handleColorChange = useCallback((color: string) => { - onChange(color); - }, [onChange]); - - // 使用 useMemo 优化颜色选择器内容 - const colorPickerContent = useMemo(() => ( -
-
- - - 选择主题色 - -
-
- {presetColors.map(({ color, name }) => ( - - handleColorChange(color)} - style={{ - cursor: 'pointer', - borderRadius: '8px', - width: 48, - height: 32, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - margin: 0, - border: value === color ? '2px solid #1890ff' : '1px solid #d9d9d9', - boxShadow: value === color ? '0 2px 8px rgba(24, 144, 255, 0.3)' : 'none', - transition: 'all 0.2s ease', - }} - > - {value === color && ( - - ✓ - - )} - - - ))} -
-
- - 当前主题色: {presetColors.find(c => c.color === value)?.name || '自定义'} + onChange, + toggleTheme, + navTheme +}) => { + // 关键代码行注释:色板渲染函数,使用useCallback优化 + const renderColorPalette = useCallback(() => ( +
+ + + 选择主题色 -
+
+ {THEME_COLORS.map(({ name, color }) => ( + + onChange(color)} + /> + + ))} +
+ +
+ +
+
- ), [value, handleColorChange]); + ), [value, onChange, toggleTheme, navTheme]); + + // 关键代码行注释:弹出窗口配置,使用useMemo优化 + const popoverProps = useMemo(() => ({ + content: renderColorPalette(), + title: null, + trigger: 'click' as const, + placement: 'bottomRight' as const, + overlayStyle: { zIndex: 1050 }, + }), [renderColorPalette]); return ( - - {/* 主题色选择器 */} - - -
+ } + open={visible} + onOk={form.submit} + onCancel={onClose} + width="80%" + style={{ top: "15%" }} + okText="确认添加" + cancelText="取消" + > + {/* 客户信息表单 */} +
+ + + + + + value && value.replace(/[^+\d]/g, '')} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + 智能地址解析 + + + + {/* 智能地址解析区域 */} + + {/* API切换开关 */} +
+ + 解析引擎: + + + 快递100 + + } + unCheckedChildren={ + + + 备用 + + } + style={{ backgroundColor: useKuaidi100 ? '#52c41a' : '#1890ff' }} + /> + +
+ 快递100 (推荐) +
+ • 专业物流地址解析 +
+ • 准确率高达99% +
+ • 支持四级地址解析 +
+
+ 备用引擎 +
+ • 通用地址解析 +
+ • 免费使用 +
+ • 基础解析功能 +
+
+ } + trigger="hover" + > + + + +
+ + {/* 地址输入框 */} + + + {/* 解析按钮 */} + + + {/* 解析提示 */} +
+ + {useKuaidi100 + ? '🚀 快递100专业解析,支持复杂地址格式,解析失败将自动切换备用引擎' + : '🔧 备用解析引擎,提供基础地址识别功能' + } + +
+ + + ); +}; + +export default AddCustomerComponent; diff --git a/src/pages/team/sale/components/AddProductComponent.tsx b/src/pages/team/sale/components/AddProductComponent.tsx new file mode 100644 index 0000000..c731cc1 --- /dev/null +++ b/src/pages/team/sale/components/AddProductComponent.tsx @@ -0,0 +1,766 @@ +/** + * 作者: 阿瑞 + * 功能: 产品添加组件 + * 版本: 1.0.0 + */ +import React, { useState, ClipboardEvent, useEffect } from 'react'; +import { + Form, + Input, + Select, + Button, + Row, + Col, + Tag, + InputNumber, + Modal, + Card, + Space, + Typography, + Tooltip, + Spin, + App +} from 'antd'; +import { + CloseOutlined, + PictureOutlined, + TagOutlined, + BarcodeOutlined, + ShoppingOutlined, + DollarOutlined, + SaveOutlined, + InboxOutlined, + QuestionCircleOutlined, + ScissorOutlined, + CarOutlined, + UploadOutlined +} from '@ant-design/icons'; +import { Icon } from '@iconify/react'; +import { IBrand, ISupplier, IProduct, ICategory, ITeam } from '@/models/types'; +import { useUserInfo } from '@/store/userStore'; + +const { useApp } = App; + +const { Option } = Select; +const { Text } = Typography; + +interface AddProductProps { + visible?: boolean; + onClose?: () => void; + onSuccess: (product: IProduct) => void; +} + +/** + * 产品添加组件 + * 允许用户创建新产品,包括上传图片、填写产品信息和设置价格 + */ +const AddProductComponent: React.FC = ({ visible, onClose, onSuccess }) => { + const userInfo = useUserInfo(); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [imageLoading, setImageLoading] = useState(false); + const { message } = useApp(); // 使用 useApp hook 获取 message 实例 + + // 数据状态 + const [brands, setBrands] = useState([]); + const [suppliers, setSuppliers] = useState([]); + const [categories, setCategories] = useState([]); + const [productData, setProductData] = useState>({ + 团队: {} as ITeam, + 编码: '', + 图片: '', + 货号: '', + 级别: '', + 供应商: {} as ISupplier, + 品类: {} as ICategory, + 名称: '', + 描述: '', + 别名: [], + 成本: { + 成本价: 0, + 包装费: 0, + 运费: 0, + }, + 售价: 0, + 库存: 0, + 品牌: {} as IBrand, + }); + + /** + * 初始化数据加载 + */ + useEffect(() => { + if (visible) { + fetchInitialData(); + // 重置表单 + form.resetFields(); + setProductData({ + ...productData, + 图片: '', + }); + } + }, [visible]); + + /** + * 获取品牌、类别和供应商数据 + */ + const fetchInitialData = async () => { + setLoading(true); + try { + const teamId = userInfo.团队?._id; + if (!teamId) { + throw new Error('团队ID未找到'); + } + + const [brandsResponse, categoriesResponse, suppliersResponse] = await Promise.all([ + fetch(`/api/backstage/brands?teamId=${teamId}`), + fetch(`/api/backstage/categories?teamId=${teamId}`), + fetch(`/api/backstage/suppliers?teamId=${teamId}`) + ]); + + // 检查响应状态 + if (!brandsResponse.ok || !categoriesResponse.ok || !suppliersResponse.ok) { + throw new Error('获取数据失败'); + } + + const [brandsData, categoriesData, suppliersData] = await Promise.all([ + brandsResponse.json(), + categoriesResponse.json(), + suppliersResponse.json() + ]); + + setBrands(brandsData.brands); + setCategories(categoriesData.categories); + setSuppliers(suppliersData.suppliers); + } catch (error) { + message.error('加载基础数据失败'); + } finally { + setLoading(false); + } + }; + + /** + * 处理剪贴板图片粘贴 + */ + const handlePaste = async (event: ClipboardEvent) => { + event.preventDefault(); + const items = event.clipboardData.items; + + for (const item of items) { + if (item.type.indexOf('image') === 0) { + const file = item.getAsFile(); + if (file) { + setImageLoading(true); + + try { + const reader = new FileReader(); + reader.onload = (event: ProgressEvent) => { + const base64Image = event.target?.result; + + // 创建Image对象用于获取图片尺寸和压缩 + const img = new Image(); + img.onload = () => { + // 确定压缩后的尺寸 + const maxSize = 800; + const ratio = Math.min(maxSize / img.width, maxSize / img.height, 1); + const width = Math.floor(img.width * ratio); + const height = Math.floor(img.height * ratio); + + // 压缩图片 + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx?.drawImage(img, 0, 0, width, height); + + // 转换为较低质量的JPEG + const compressedImage = canvas.toDataURL('image/jpeg', 0.85); + + setProductData((prev) => ({ + ...prev, + 图片: compressedImage, + })); + + setImageLoading(false); + message.success('图片已添加并优化'); + }; + + img.onerror = () => { + setImageLoading(false); + message.error('图片加载失败'); + }; + + img.src = base64Image as string; + }; + + reader.readAsDataURL(file); + } catch (error) { + setImageLoading(false); + message.error('处理图片失败'); + } + + break; + } + } + } + }; + + /** + * 表单提交处理 + */ + const onFinish = async (values: any) => { + if (!productData.图片) { + message.warning('请添加产品图片'); + return; + } + + setLoading(true); + + try { + // 处理别名 - 从字符串转为数组 + const aliasArray = values.别名 ? + values.别名.split(',').map((item: string) => item.trim()).filter((item: string) => item) : + []; + + const completeData = { + ...values, + 图片: productData.图片, + 团队: userInfo.团队?._id, + 别名: aliasArray, + 成本: { + 成本价: values.成本价 || 0, + 包装费: values.包装费 || 0, + 运费: values.运费 || 0, + }, + }; + + const response = await fetch('/api/backstage/products', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(completeData) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const responseData = await response.json(); + const newProduct = responseData.product; + + message.success({ + content: `产品 ${newProduct.名称} 添加成功`, + icon: + }); + + onSuccess(newProduct); + form.resetFields(); + + // 清空图片 + setProductData(prev => ({ + ...prev, + 图片: '', + })); + + // 如果提供了关闭函数则调用 + if (onClose) { + onClose(); + } + } catch (error) { + console.error('添加产品失败', error); + message.error('添加产品失败,请检查数据'); + } finally { + setLoading(false); + } + }; + + /** + * 计算毛利率 + */ + const calculateGrossProfitMargin = () => { + const retailPrice = form.getFieldValue('售价') || 0; + const costPrice = form.getFieldValue('成本价') || 0; + const packagingCost = form.getFieldValue('包装费') || 0; + const shippingCost = form.getFieldValue('运费') || 0; + + const totalCost = costPrice + packagingCost + shippingCost; + + if (retailPrice <= 0 || totalCost <= 0) { + return '0.00%'; + } + + const margin = ((retailPrice - totalCost) / retailPrice) * 100; + return `${margin.toFixed(2)}%`; + }; + + /** + * 表单字段变化处理 + */ + const handleChange = (name: string, value: any) => { + setProductData(prev => ({ + ...prev, + [name]: value, + })); + }; + + // 毛利率显示 + const grossProfitMargin = Form.useWatch(['售价', '成本价', '包装费', '运费'], form); + + return ( + + + 添加新产品 + + } + open={visible} + onCancel={onClose} + width="90%" + footer={null} + destroyOnClose={true} + maskClosable={false} + zIndex={1100} + getContainer={false} + styles={{ + body: { padding: '16px 24px', maxHeight: '75vh', overflow: 'auto' } + }} + > + +
+ {loading &&
加载中...
} +
+ + {/* 左侧表单区域 */} + + {/* 基本信息 */} + + + 基本信息 + + } + size="small" + style={{ marginBottom: 16 }} + > + + + + } + maxLength={50} + showCount + /> + + + + + } + maxLength={100} + showCount + /> + + + + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + + {/* 分类信息 */} + + + 分类信息 + + } + size="small" + style={{ marginBottom: 16 }} + > + + + + + + + + + + + + + + + + + + + + {/* 价格信息 */} + + + 价格信息 + + } + size="small" + > + + + + + + + + + + 成本价 + + + + } + > + + + + + + + 包装费 + + + + } + > + + + + + + + 运费 + + + + } + > + + + + + + {/* 计算毛利率 */} + {grossProfitMargin && form.getFieldValue('售价') > 0 && ( +
+ + + 毛利率: + + {calculateGrossProfitMargin()} + + +
+ )} +
+ + + {/* 右侧图片上传和提交按钮 */} + + + + 产品图片 + + } + size="small" + style={{ marginBottom: 16 }} + > + + +
+ {imageLoading && ( +
+ 处理图片中... +
+ )} +
{ + e.preventDefault(); + e.currentTarget.style.borderColor = '#1890ff'; + e.currentTarget.style.background = '#e6f7ff'; + }} + onDragLeave={(e) => { + e.preventDefault(); + e.currentTarget.style.borderColor = '#d9d9d9'; + e.currentTarget.style.background = '#fafafa'; + }} + onMouseOver={(e) => { + e.currentTarget.style.borderColor = '#1890ff'; + e.currentTarget.style.background = '#e6f7ff'; + }} + onMouseOut={(e) => { + e.currentTarget.style.borderColor = '#d9d9d9'; + e.currentTarget.style.background = '#fafafa'; + }} + > + {productData.图片 ? ( +
+ 产品图片 +
+ ) : ( +
+ +

+ + + 点击或粘贴图片到此区域 + +

+

+ 建议尺寸: 800×800px, 支持 JPG、PNG 格式 +

+
+ )} +
+
+
+
+
+ + + +
+
+
+
+
+ ); +}; + +export default AddProductComponent; diff --git a/src/pages/team/sale/components/CustomerInfoComponent.tsx b/src/pages/team/sale/components/CustomerInfoComponent.tsx new file mode 100644 index 0000000..55f3855 --- /dev/null +++ b/src/pages/team/sale/components/CustomerInfoComponent.tsx @@ -0,0 +1,329 @@ +// src/pages/team/sale/components/CustomerInfoComponent.tsx +import React, { useEffect, useState } from 'react'; +import { Button, Card, Avatar, Descriptions, Spin, message, Typography, Modal, Tag } from 'antd'; +import { ICustomer } from '@/models/types'; +import dayjs from 'dayjs'; +import Recharge from './Recharge'; +import axios from 'axios'; +import CouponModal from './coupon-modal'; +import { + UserOutlined, + PhoneOutlined, + HomeOutlined, + WechatOutlined, + GiftOutlined, + DollarCircleOutlined, + PlusOutlined, + RedEnvelopeOutlined, + EditOutlined, + ShopOutlined, +} from '@ant-design/icons'; +import EditCustomerInfo from './EditCustomerInfo'; +import useCustomerTotalIncome from '@/hooks/useCustomerTotalIncome'; // 引入自定义 Hook +import SalesRecordPage from './sales-record'; // 引入销售记录页面 + +const { Text } = Typography; + +interface CustomerInfoComponentProps { + customer: ICustomer | null; + onAddCustomerClick: () => void; // 新增客户按钮的点击事件回调 + onCustomerUpdate: (updatedCustomer: ICustomer) => void; // 新增客户更新回调 +} + +const CustomerInfoComponent: React.FC = ({ + customer, + onAddCustomerClick, + onCustomerUpdate, +}) => { + const [rechargeVisible, setRechargeVisible] = useState(false); // 充值对话框是否可见 + const [couponModalVisible, setCouponModalVisible] = useState(false); // 优惠券对话框是否可见 + const [balance, setBalance] = useState(0); // 存储客户的余额 + const [coupons, setCoupons] = useState([]); // 存储客户的优惠券 + const [loading, setLoading] = useState(false); // 数据加载状态 + const [salesModalVisible, setSalesModalVisible] = useState(false); // 控制消费详情模态框的显示 + + //const { totalIncome, loading: incomeLoading } = useCustomerTotalIncome(customer?._id || null); // 使用自定义 Hook 获取消费总额 + //const { totalIncome, loading: incomeLoading } = useCustomerTotalIncome(customer?.电话 || null); // 使用自定义 Hook 获取消费总额 + const { totalIncome, storeAccounts, loading: incomeLoading } = useCustomerTotalIncome(customer?.电话 || null); + + const formatAddress = (address: ICustomer['地址']) => { + if (!address) return '暂无地址信息'; + const { 省份, 城市, 区县, 详细地址 } = address; + return `${省份 || ''}${城市 || ''}${区县 || ''}${详细地址 || ''}`; + }; + + const [editCustomerModalVisible, setEditCustomerModalVisible] = useState(false); + + const handleEditCustomerClick = () => { + setEditCustomerModalVisible(true); + }; + + // 修改 handleEditCustomerModalOk 函数,只关闭模态框并通知父组件 + const handleEditCustomerModalOk = (updatedCustomer: ICustomer) => { + setEditCustomerModalVisible(false); + onCustomerUpdate(updatedCustomer); // 通知父组件更新客户数据 + fetchCustomerData(); // 刷新客户数据 + }; + + // 获取客户余额和优惠券信息的函数 + const fetchCustomerData = async () => { + if (customer?._id) { + setLoading(true); + try { + const [balanceResponse, couponsResponse] = await Promise.all([ + axios.get(`/api/backstage/balance/${customer._id}`), + axios.get(`/api/backstage/coupons/assign`, { + params: { customerId: customer._id }, + }), + ]); + + if (balanceResponse.status === 200) { + setBalance(balanceResponse.data.balance ?? 0); + } else { + setBalance(0); + } + + if (couponsResponse.status === 200) { + setCoupons(couponsResponse.data.coupons ?? []); + } else { + setCoupons([]); + } + } catch (error) { + console.error('获取数据时出错:', error); + message.error('获取客户数据失败,请稍后重试'); + setBalance(0); + setCoupons([]); + } finally { + setLoading(false); + } + } + }; + + useEffect(() => { + if (customer?._id) { + fetchCustomerData(); + } + }, [customer]); + + // 充值成功后调用的函数 + const handleRechargeSuccess = () => { + fetchCustomerData(); // 重新获取客户数据 + setRechargeVisible(false); // 关闭充值对话框 + }; + + const handleCouponAssignSuccess = () => { + fetchCustomerData(); // 重新获取客户数据 + setCouponModalVisible(false); + }; + + + const handleSalesModalOpen = () => { + if (customer?.电话) { + setSalesModalVisible(true); // 打开模态框 + } else { + message.error('客户电话信息缺失,无法查询'); + } + }; + + const handleSalesModalClose = () => { + setSalesModalVisible(false); // 关闭模态框 + }; + + return ( + 客户信息} + style={{ minHeight: '380px' }} // 设定Card最小高度,确保无论客户是否选择,卡片高度保持一致 + extra={ + <> + {loading ? ( + <> + ) : customer ? ( + <> + + + + + + ) : ( +
+
+ )} + + } + > + {loading ? ( +
+ +
+ ) : customer ? ( + <> +
+
+ } + style={{ marginBottom: 8 }} + /> +
{customer.姓名 || '未提供'}
+
{customer.加粉日期 ? dayjs(customer.加粉日期).format('YYYY-MM-DD') : 'null'}
+
+ {storeAccounts.length > 0 ? storeAccounts.map((storeAccount) => + + }>{storeAccount} + ) : ''} +
+ +
+ +
+ + + + 电话 + + } + > + {customer.电话 || '未提供'} + + + + + 微信 + + } + > + {customer.微信 || '未提供'} + + + + + 生日 + + } + > + {customer.生日 ? dayjs(customer.生日).format('YYYY-MM-DD') : '未提供'} + + + + + 地址 + + } + > + {formatAddress(customer.地址)} + + +
+
+ + {/* 分割线 */} +
+ + ¥{balance.toFixed(2)} + {coupons.filter((coupon) => !coupon.已使用).length} 张 + {incomeLoading ? () : ({totalIncome.toFixed(0)})} + + + + + {/* 模态框 */} + + {/* 传入客户的手机号 */} + + + setRechargeVisible(false)} + customer={customer} + onRechargeSuccess={handleRechargeSuccess} + /> + setCouponModalVisible(false)} + onOk={handleCouponAssignSuccess} + customerId={customer._id} + /> + setEditCustomerModalVisible(false)} + /> + + ) : ( +
+ +

请选择一位客户 或

+ +

查看详情

+
+ )} +
+ ); +}; + +export default CustomerInfoComponent; diff --git a/src/pages/team/sale/components/EditCustomerInfo.tsx b/src/pages/team/sale/components/EditCustomerInfo.tsx new file mode 100644 index 0000000..a085922 --- /dev/null +++ b/src/pages/team/sale/components/EditCustomerInfo.tsx @@ -0,0 +1,119 @@ +// src/pages/team/sale/components/EditCustomerInfo.tsx +import React, { useEffect } from 'react'; +import { Modal, Form, Input, DatePicker, message } from 'antd'; +import { ICustomer } from '@/models/types'; +import axios from 'axios'; +import dayjs from 'dayjs'; + +interface EditCustomerInfoProps { + visible: boolean; + onCancel: () => void; + onOk: (updatedCustomer: ICustomer) => void; // 修改 onOk 接收更新后的客户数据 + customer: ICustomer | null; +} + +const EditCustomerInfo: React.FC = ({ visible, onOk, onCancel, customer }) => { + const [form] = Form.useForm(); + + // 当传入客户数据时,将其设置到表单中 + useEffect(() => { + if (customer) { + form.setFieldsValue({ + 姓名: customer.姓名, + 电话: customer.电话, + 地址: customer.地址, + 微信: customer.微信, + 生日: customer.生日 ? dayjs(customer.生日) : null, + 加粉日期: customer.加粉日期 ? dayjs(customer.加粉日期) : null, + }); + } else { + form.resetFields(); + } + }, [customer, form]); + + // 提交表单并发送更新请求 + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const method = customer ? 'PUT' : 'POST'; + const url = customer ? `/api/backstage/customers/${customer._id}` : '/api/backstage/customers'; + + const response = await axios({ + method, + url, + data: { + ...values, + }, + }); + + if (response.status === 200 || response.status === 201) { + //message.success('客户信息更新成功'); + onOk(response.data.customer); // 假设 API 返回更新后的客户数据 + } else { + message.error('客户信息更新失败'); + } + } catch (error) { + console.error('更新客户信息失败:', error); + message.error('客户信息更新失败'); + } + }; + + return ( + { + form.resetFields(); + onCancel(); + }} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default EditCustomerInfo; diff --git a/src/pages/team/sale/components/ProductInfoComponent.tsx b/src/pages/team/sale/components/ProductInfoComponent.tsx new file mode 100644 index 0000000..fd1d5d0 --- /dev/null +++ b/src/pages/team/sale/components/ProductInfoComponent.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import { Button, Card, Typography } from 'antd'; +import { IProduct, ISupplier } from '@/models/types'; +import ProductImage from '@/components/product/ProductImage'; +import { BarcodeOutlined, DollarCircleOutlined, PlusOutlined, SignatureOutlined, TagsOutlined, TrademarkCircleOutlined, TruckOutlined } from '@ant-design/icons'; + +const { Title } = Typography; + +interface ProductInfoComponentProps { + products: IProduct[]; + onAddProductClick: () => void; // 新增产品按钮的点击事件回调 +} + +/*数据模型 +// src/models/index.ts +//产品模型定义 +const ProductSchema: Schema = new Schema({ + 团队: { type: Schema.Types.ObjectId, ref: 'Team' }, // 将团队字段定义为引用 Team 模型的 ObjectId 表示产品所属团队 + 供应商: { type: Schema.Types.ObjectId, ref: 'Supplier' }, //关联供应商模型 + 品牌: { type: Schema.Types.ObjectId, ref: 'Brand' }, //关联品牌模型 + 品类: { type: Schema.Types.ObjectId, ref: 'Category' }, //关联品类模型 + 名称: String, + 描述: String, + 编码: String, + 图片: String, + 货号: String, + 别名: Array, //别名字段 Array表示数组 + 级别: String, + //成本,包含成本、包装费、运费 + 成本: { + 成本价: Number, + 包装费: Number, + 运费: Number, + }, + 售价: Number, + 库存: Number, +}, { timestamps: true }); // 自动添加创建时间和更新时间 +ProductSchema.index({ 团队: 1 }); // 对团队字段建立索引 + +// Supplier 供应商模型 +const SupplierSchema = new Schema( + { + 团队: { type: Schema.Types.ObjectId, ref: 'Team' }, //将团队字段定义为引用 Team 模型的 ObjectId + order: Number, //排序 + 供应商名称: String, + 联系方式: { + 电话: String, + 联系人: String, + 地址: String, + }, + 供应品类: [ + { + type: Schema.Types.ObjectId, + ref: 'Category', + }, + ], + // 供应商状态: 0 停用 1 正常 2 异常 3 备用,默认为 1 + status: { type: Number, default: 1 }, + 供应商等级: String, + 供应商类型: String, + 供应商备注: String, + }, + { timestamps: true }, +); + + +*/ + +const ProductInfoComponent: React.FC = ({ products, onAddProductClick }) => { + const defaultProduct = { + _id: 'default', + 供应商: { 供应商名称: '' } as ISupplier, + 品牌: { name: '' }, + 品类: { name: '' }, + 名称: '', + 描述: '', + 编码: '', + 图片: '', + 货号: '', + 别名: [], + 级别: '', + 售价: 0, + 库存: 0, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const productsToDisplay = products.length > 0 ? products : [defaultProduct]; + + // 计算产品售价总和 + const totalPrice = products.reduce((sum, product) => sum + (product.售价 || 0), 0); + + return ( + + 产品信息 + + (共 {products.length} 个产品,总价: ¥{totalPrice}) + + + } + style={{ minHeight: '380px' }} // 设定Card最小高度,确保无论是否选择产品,卡片高度保持一致 + extra={ + + } + > + {/* 使用flex布局 */} +
+ {productsToDisplay.map((product) => ( +
+ {/* 产品图片 */} + + + {/* 产品信息覆盖在图片上 */} +
+ {/* 产品名称 */} +
+ + {product.名称} + +

+ {product.描述 || ''} +

+

+ {product.品牌?.name || ''} +

+

+ {product.供应商?.联系方式?.联系人 || ''} +

+
+ + {/* 产品售价和货号信息 */} +
+

+ {product.货号 || ''} +

+

+ + {product.售价 > 0 ? ` ¥${product.售价}` : ''} +

+

+ {Array.isArray(product.别名) && product.别名.length > 0 ? product.别名.join(',') : ''} +

+
+
+
+ ))} +
+
+ ); +}; + +export default ProductInfoComponent; diff --git a/src/pages/team/sale/components/Recharge.tsx b/src/pages/team/sale/components/Recharge.tsx new file mode 100644 index 0000000..f64fcbf --- /dev/null +++ b/src/pages/team/sale/components/Recharge.tsx @@ -0,0 +1,152 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Button, Form, Input, InputNumber, message, List } from 'antd'; +import { useUserInfo } from '@/store/userStore'; +import { ICustomer } from '@/models/types'; +import axios from 'axios'; + +interface RechargeProps { + visible: boolean; + onCancel: () => void; + customer: ICustomer | null; + onRechargeSuccess: () => void; // 充值成功后的回调 +} + +const Recharge: React.FC = ({ visible, onCancel, customer, onRechargeSuccess }) => { + const [loading, setLoading] = useState(false); + const [balance, setBalance] = useState(null); + const [transactions, setTransactions] = useState([]); // 存储交易记录 + const [form] = Form.useForm(); + const { 团队 } = useUserInfo(); + + useEffect(() => { + const fetchBalanceAndTransactions = async () => { + if (customer?._id) { + try { + // 重置状态 + setBalance(null); + setTransactions([]); + form.resetFields(); + + // 获取客户余额 + const balanceResponse = await axios.get(`/api/backstage/balance/${customer._id}`); + if (balanceResponse.status === 200) { + setBalance(balanceResponse.data.balance ?? 0); // 默认值为 0 + } else { + setBalance(0); // 无法获取余额时设为 0 + } + + // 获取客户交易记录 + const transactionsResponse = await axios.get(`/api/backstage/transactions/${customer._id}`); + if (transactionsResponse.status === 200) { + setTransactions(transactionsResponse.data.transactions); + } else { + message.error('无法获取交易记录'); + } + } catch (error) { + console.error('获取数据时出错:', error); + setBalance(0); // 错误情况下设为 0 + message.error('无余额交易记录'); + } + } + }; + + if (visible) { + fetchBalanceAndTransactions(); + } + }, [customer, visible]); + + const handleRecharge = async () => { + if (!customer?._id) { + message.error('请先选择客户'); + return; + } + try { + setLoading(true); + const values = await form.validateFields(); + const { 金额, 描述 } = values; + + const response = await axios.post('/api/backstage/transactions', { + 团队: 团队?._id, + 客户: customer?._id, + 类型: '充值', + 金额, + 描述, + }); + + if (response.status === 200) { + message.success('充值成功'); + onRechargeSuccess(); + onCancel(); + } else { + message.error('充值失败,请重试'); + } + } catch (error) { + message.error('充值失败,请检查输入并重试'); + } finally { + setLoading(false); + } + }; + + return ( + 客户充值} + onCancel={onCancel} + footer={[ + , + , + ]} + > + {balance !== null && balance !== undefined ? ( +
+

当前余额: ¥ {balance.toFixed(2)}

+
+ ) : ( +
+ 当前余额: ¥ 0 +
+ )} + +
+ + + + + + +
+ +
+

余额交易记录

+
+ ( + + + + )} + /> +
+
+
+ ); +}; + +export default Recharge; diff --git a/src/pages/team/sale/components/SelectCustomer.tsx b/src/pages/team/sale/components/SelectCustomer.tsx new file mode 100644 index 0000000..a8390ab --- /dev/null +++ b/src/pages/team/sale/components/SelectCustomer.tsx @@ -0,0 +1,45 @@ +// src\pages\team\sale\components\SelectCustomer.tsx +import React from 'react'; +import { Select, Space } from 'antd'; +import { ICustomer } from '@/models/types'; + +interface SelectCustomerProps { + customers: ICustomer[]; + onSelectCustomer: (customerId: string) => void; +} + +const SelectCustomer: React.FC = ({ customers = [], onSelectCustomer }) => { + const handleChange = (value: string) => { + onSelectCustomer(value); + }; + + const filterOption = (input: string, option?: { value: string }) => { + const customer = customers.find(c => c._id === option?.value); + if (!customer) return false; + const nameMatch = customer.姓名 ? customer.姓名.toLowerCase().includes(input.toLowerCase()) : false; + const phoneMatch = customer.电话 ? customer.电话.toLowerCase().includes(input.toLowerCase()) : false; + return nameMatch || phoneMatch; + }; + + return ( + + + + ); +}; + +export default SelectCustomer; \ No newline at end of file diff --git a/src/pages/team/sale/components/SelectProduct.tsx b/src/pages/team/sale/components/SelectProduct.tsx new file mode 100644 index 0000000..d2beb02 --- /dev/null +++ b/src/pages/team/sale/components/SelectProduct.tsx @@ -0,0 +1,51 @@ +// src/pages/Component/SelectProduct.tsx +import React from 'react'; +import { Select, Space } from 'antd'; +import { IProduct } from '@/models/types'; + +interface SelectProductProps { + products: IProduct[]; + onSelectProduct: (productId: string) => void; +} + +const SelectProduct: React.FC = ({ products = [], onSelectProduct }) => { + const handleChange = (value: string) => { + onSelectProduct(value); + }; + + const filterOption = (input: string, option?: { value: string }) => { + if (!option) return false; // 如果没有选项对象提供,直接返回false + // 根据选项的value值(产品ID)在产品数组中找到对应的产品对象 + const product = products.find(p => p._id === option.value); + if (!product) return false; + // 将用户输入转换为小写,用于不区分大小写的搜索 + const lowerCaseInput = input.toLowerCase(); + // 检查产品的货号或编码是否包含用户的输入 + const matchesCodeOrNumber = (product.货号?.toLowerCase().includes(lowerCaseInput) || product.编码?.toLowerCase().includes(lowerCaseInput)) ?? false; + // 如果产品有别名,检查别名数组中是否至少有一个别名包含用户的输入 + const matchesAlias = product.别名?.some(alias => alias.toLowerCase().includes(lowerCaseInput)) ?? false; + return matchesCodeOrNumber || matchesAlias; + }; + + return ( + + + + ); +}; + +export default SelectProduct; \ No newline at end of file diff --git a/src/pages/team/sale/components/address-smart-parse.d.ts b/src/pages/team/sale/components/address-smart-parse.d.ts new file mode 100644 index 0000000..075146a --- /dev/null +++ b/src/pages/team/sale/components/address-smart-parse.d.ts @@ -0,0 +1,21 @@ +// src/types/address-smart-parse.d.ts +declare module 'address-smart-parse' { + interface AddressParseResult { + zipCode: string; + province: string; + provinceCode: string; + city: string; + cityCode: string; + county: string; + countyCode: string; + street: string; + streetCode: string; + address: string; + name: string; + phone: string; + idCard: string; + } + + // 假设模块默认导出了一个函数 + export default function parseAddress(address: string): AddressParseResult; + } \ No newline at end of file diff --git a/src/pages/team/sale/components/coupon-modal.tsx b/src/pages/team/sale/components/coupon-modal.tsx new file mode 100644 index 0000000..c0ec35b --- /dev/null +++ b/src/pages/team/sale/components/coupon-modal.tsx @@ -0,0 +1,159 @@ +//src\pages\team\sale\components\coupon-modal.tsx +import React, { useEffect, useState } from 'react'; +import { Modal, Form, message, Select, List, Typography, Tag } from 'antd'; +import axios from 'axios'; +import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 + +interface CouponModalProps { + visible: boolean; + onCancel: () => void; + onOk: () => void; + customerId: string | null; +} + +interface ICoupon { + _id: string; + 名称: string; + 描述: string; + 优惠券类型: string; + 金额: number; + 折扣: number; + 最低消费: number; + 生效日期: string; + 失效日期: string; +} + +interface ICustomerCoupon { + _id: ICoupon; // 这里指向的是完整的 Coupon 文档 + 券码: string; + 已使用: boolean; + 使用日期: string | null; +} + +const CouponModal: React.FC = ({ visible, onOk, onCancel, customerId }) => { + const [form] = Form.useForm(); + const userInfo = useUserInfo(); // 获取当前用户信息 + const [teamCoupons, setTeamCoupons] = useState([]); + const [customerCoupons, setCustomerCoupons] = useState([]); + const [defaultCouponIds, setDefaultCouponIds] = useState([]); // 默认选中的优惠券ID数组 + + useEffect(() => { + if (userInfo.团队?._id) { + fetchTeamCoupons(userInfo.团队._id); + } + if (customerId) { + fetchCustomerCoupons(customerId); + } + }, [userInfo, customerId]); + + const fetchTeamCoupons = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/coupons?teamId=${teamId}`); + setTeamCoupons(data.coupons); + } catch (error) { + console.error('Failed to load team coupons:', error); + message.error('加载团队优惠券失败'); + } + }; + + const fetchCustomerCoupons = async (customerId: string) => { + try { + const { data } = await axios.get(`/api/backstage/coupons/assign`, { + params: { customerId }, + }); + setCustomerCoupons(data.coupons); + if (data.coupons.length > 0) { + const couponIds = data.coupons.map((coupon: ICustomerCoupon) => coupon._id._id); // 使用嵌套的 _id + setDefaultCouponIds(couponIds); + form.setFieldsValue({ couponIds }); + } + } catch (error) { + console.error('Failed to load customer coupons:', error); + message.error('加载客户优惠券失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + await axios.post(`/api/backstage/coupons/assign/${customerId}`, { + couponIds: values.couponIds, + }); + + message.success('优惠券分配成功'); + form.resetFields(); + onOk(); + } catch (error) { + console.error('Failed to assign coupons:', error); + message.error('分配优惠券失败'); + } + }; + + return ( + 分配优惠券} + open={visible} + onOk={handleSubmit} + onCancel={() => { + form.resetFields(); + onCancel(); + }} + > +
+ + + +
+ + 已有优惠券: + ( + +
+
+ {coupon.券码} + + {coupon.已使用 ? '已使用' : '未使用'} + +
+
+ {coupon._id.名称} +
+ {coupon._id.描述 && ( +
+ {coupon._id.描述} +
+ )} +
+ 有效期:{new Date(coupon._id.生效日期).toLocaleDateString()} - {new Date(coupon._id.失效日期).toLocaleDateString()} +
+ {coupon.已使用 && coupon.使用日期 && ( +
+ 使用日期:{new Date(coupon.使用日期).toLocaleDateString()} +
+ )} +
+
+ )} + /> +
+ ); +}; + +export default CouponModal; diff --git a/src/pages/team/sale/components/sales-record.tsx b/src/pages/team/sale/components/sales-record.tsx new file mode 100644 index 0000000..d5785c5 --- /dev/null +++ b/src/pages/team/sale/components/sales-record.tsx @@ -0,0 +1,142 @@ +// src/pages/customer/sales-record.tsx +import React, { useState, useEffect } from 'react'; +import { Card, List, Descriptions, message } from 'antd'; +import axios from 'axios'; + +interface SalesRecordPageProps { + initialPhone?: string; // 用于传递初始手机号 +} + +const SalesRecordPage: React.FC = ({ initialPhone }) => { + const [loading, setLoading] = useState(false); + const [salesRecords, setSalesRecords] = useState([]); + const [afterSalesRecords, setAfterSalesRecords] = useState([]); + const [totalIncome, setTotalIncome] = useState(0); // 新增的本单收入状态 + + const handleSearch = async () => { + if (!initialPhone) { + message.error('手机号缺失'); + return; + } + + setLoading(true); + + try { + const { data } = await axios.get(`/api/backstage/customers/sales/${initialPhone}`); + + if (data.salesRecords) { + setSalesRecords(data.salesRecords); + } + + if (data.afterSalesRecords) { + setAfterSalesRecords(data.afterSalesRecords); + } + + message.success('查询成功'); + } catch (error) { + console.error(error); + message.error('查询失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + // 自动搜索初始手机号的销售记录 + useEffect(() => { + if (initialPhone) { + handleSearch(); + } + }, [initialPhone]); + + // 计算本单收入 + useEffect(() => { + const calculateTotalIncome = () => { + let totalSalesIncome = 0; // 收款金额总和 + let totalPendingIncome = 0; // 待收款总和 + let totalAfterSalesIncome = 0; // 售后收入总和 + let totalAfterSalesExpenditure = 0; // 售后支出总和 + + // 计算销售记录中的收款金额和待收款 + salesRecords.forEach((record) => { + totalSalesIncome += record.收款金额 || 0; + totalPendingIncome += record.待收款 || 0; + }); + + // 计算售后记录中的收入和支出 + afterSalesRecords.forEach((record) => { + if (record.收支类型 === '收入') { + totalAfterSalesIncome += record.收支金额 || 0; + } else if (record.收支类型 === '支出') { + totalAfterSalesExpenditure += record.收支金额 || 0; + } + }); + + // 本单收入 = 收款金额总和 + 待收款总和 + 售后收入 - 售后支出 + const calculatedIncome = totalSalesIncome + totalPendingIncome + totalAfterSalesIncome - totalAfterSalesExpenditure; + setTotalIncome(calculatedIncome); + }; + + calculateTotalIncome(); + }, [salesRecords, afterSalesRecords]); + + return ( +
+ {/* 销售记录展示 */} + + ( + + + {record.成交日期} + ¥{record.应收金额} + ¥{record.收款金额} + ¥{record.待收款} + {record.收款状态} + {record.订单状态.join(', ')} + {record.货款状态} + {record.备注 || '无'} + + + )} + /> + + + {/* 售后记录展示 */} + + ( + + + {record.类型} + {record.日期} + {record.售后进度} + {record.原因} + ¥{record.收支金额} + {record.收支平台 || '无'} + {record.备注 || '无'} + + + )} + /> + + + {/* 本单收入展示 */} + + + + ¥{totalIncome.toFixed(2)} + + + +
+ ); +}; + +export default SalesRecordPage; diff --git a/src/pages/team/sale/index copy 2.tsx b/src/pages/team/sale/index copy 2.tsx new file mode 100644 index 0000000..6eed0d1 --- /dev/null +++ b/src/pages/team/sale/index copy 2.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from 'react'; +import { Form, Input, Select, message, DatePicker, Button } from 'antd'; +import { ISalesRecord } from '@/models/types'; +import axios from 'axios'; +import { useUserInfo } from '@/store/userStore'; + +interface SalesRecordPageProps { + salesRecord: ISalesRecord | null; + onCancel: () => void; + onOk: () => void; +} + +const SalesRecordPage: React.FC = ({ salesRecord, onCancel }) => { + const [form] = Form.useForm(); + const [customers, setCustomers] = useState([]); // 客户 + const [products, setProducts] = useState([]); // 产品 + const [accounts, setAccounts] = useState([]); //支付平台 + const [payPlatforms, setPayPlatforms] = useState([]); //支付平台 + const [users, setUsers] = useState([]); + const userInfo = useUserInfo(); // 获取当前用户信息 + + useEffect(() => { + if (userInfo.团队?._id) { + const teamId = userInfo.团队._id; + fetchCustomers(teamId); + fetchProducts(teamId); + fetchAccounts(teamId); + fetchUsers(teamId); + fetchPayPlatforms(teamId); + } + }, [userInfo]); + // 获取客户数据 + const fetchCustomers = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/customers?teamId=${teamId}`); + setCustomers(data.customers); + } catch (error) { + message.error('加载客户数据失败'); + } + }; + // 获取产品数据 + const fetchProducts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/products?teamId=${teamId}`); + setProducts(data.products); + } catch (error) { + message.error('加载产品数据失败'); + } + }; + // 获取账户数据 + const fetchAccounts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`); + setAccounts(data.accounts); + } catch (error) { + message.error('加载账户数据失败'); + } + }; + // 获取用户数据 + const fetchUsers = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/users?teamId=${teamId}`); + setUsers(data.users); + } catch (error) { + message.error('加载用户数据失败'); + } + }; + //获取支付平台数据 + const fetchPayPlatforms = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/payment-platforms?teamId=${teamId}`); + setPayPlatforms(data.platforms || []); // 使用 data.platforms 而不是 data.paymentPlatforms + } catch (error) { + message.error('加载支付平台数据失败'); + setPayPlatforms([]); // 确保失败时也设置为一个空数组 + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const method = salesRecord ? 'PUT' : 'POST'; + const url = salesRecord ? `/api/backstage/sales/${salesRecord._id}` : '/api/backstage/sales'; + await axios({ + method: method, + url: url, + data: { + ...values, + 团队: userInfo.团队?._id, // 将团队ID包含在请求数据中 + }, + }); + message.success('销售记录操作成功'); + } catch (info) { + console.error('Validate Failed:', info); + message.error('销售记录操作失败'); + } + }; + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default SalesRecordPage; diff --git a/src/pages/team/sale/index copy.tsx b/src/pages/team/sale/index copy.tsx new file mode 100644 index 0000000..3bd5ff7 --- /dev/null +++ b/src/pages/team/sale/index copy.tsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Space, message, Card } from 'antd'; +import axios from 'axios'; +import { ISalesRecord } from '@/models/types'; // 确保有正确的类型定义 +import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 +import SalesRecordModal from './sales-record-modal'; // 引入销售记录模态框组件 +import dayjs from 'dayjs'; + +const SalesPage = () => { + const [salesRecords, setSalesRecords] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [currentSalesRecord, setCurrentSalesRecord] = useState(null); + const userInfo = useUserInfo(); // 获取当前用户信息 + + useEffect(() => { + if (userInfo.团队?._id) { + fetchSalesRecords(userInfo.团队._id); + } + }, [userInfo]); + + const fetchSalesRecords = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/sales?teamId=${teamId}`); + setSalesRecords(data.salesRecords); + } catch (error) { + message.error('加载销售数据失败'); + } + }; + + const handleModalOk = () => { + setIsModalVisible(false); + if (userInfo.团队?._id) { + fetchSalesRecords(userInfo.团队._id); + } + }; + + const handleCreate = () => { + setCurrentSalesRecord(null); + setIsModalVisible(true); + }; + + const handleDelete = async (id: string) => { + try { + await axios.delete(`/api/backstage/sales/${id}`); + if (userInfo.团队?._id) { + fetchSalesRecords(userInfo.团队._id); + } + message.success('销售记录删除成功'); + } catch (error) { + message.error('删除销售记录失败'); + } + }; + + const columns = [ + { + title: '成交日期', + dataIndex: '成交日期', + key: 'dealDate', + render: (date: Date) => dayjs(date).format('YYYY-MM-DD'), + }, + { + title: '客户', + dataIndex: '客户', + key: 'customer', + render: (customer: any) => customer ? customer.姓名 : '无' + }, + { + title: '产品', + dataIndex: '产品', + key: 'products', + render: (products: any[]) => products.map(p => p.名称).join(', ') + }, + { + title: '订单来源', + dataIndex: '订单来源', + key: 'orderSource', + render: (source: any) => source ? source.账号编号 : '无' + }, + { + title: '导购', + dataIndex: '导购', + key: 'salesperson', + render: (user: any) => user ? user.姓名 : '无' + }, + { + title: '收款状态', + dataIndex: '收款状态', + key: 'paymentStatus', + }, + { + title: '货款状态', + dataIndex: '货款状态', + key: 'paymentPlatform', + }, + { + title: '备注', + dataIndex: '备注', + key: 'remark', + }, + { + title: '操作', + key: 'action', + render: (_: any, record: ISalesRecord) => ( + + + + ), + }, + ]; + + return ( + + 添加销售记录 + + } + > + + {isModalVisible && ( + setIsModalVisible(false)} + salesRecord={currentSalesRecord} + /> + )} + + ); +}; + +export default SalesPage; diff --git a/src/pages/team/sale/index.tsx b/src/pages/team/sale/index.tsx new file mode 100644 index 0000000..cd00c83 --- /dev/null +++ b/src/pages/team/sale/index.tsx @@ -0,0 +1,546 @@ +//src\pages\team\sale\index.tsx +import React, { useEffect, useState } from 'react'; +import { Form, Input, Select, message, DatePicker, Button, Row, Col, Card, Divider, Tag } from 'antd'; +import { ICustomer, ICustomerCoupon, IProduct, ISalesRecord } from '@/models/types'; +import axios from 'axios'; +import { useUserInfo } from '@/store/userStore'; +import CustomerInfoComponent from './components/CustomerInfoComponent'; +import ProductInfoComponent from './components/ProductInfoComponent'; +import AddProductComponent from './components/AddProductComponent'; +import AddCustomerComponent from './components/AddCustomerComponent'; +import { BarcodeOutlined, ClusterOutlined, PhoneOutlined, ShopOutlined, SignatureOutlined, TagsOutlined, TrademarkCircleOutlined, TransactionOutlined, UserOutlined, WechatOutlined } from '@ant-design/icons'; +interface SalesRecordPageProps { + salesRecord: ISalesRecord | null; + onCancel: () => void; + onOk: () => void; +} + +const SalesRecordPage: React.FC = ({ salesRecord, onCancel }) => { + const [form] = Form.useForm(); + const [customers, setCustomers] = useState([]); // 客户 + const [products, setProducts] = useState([]); // 产品 + const [accounts, setAccounts] = useState([]); //支付平台 + const [payPlatforms, setPayPlatforms] = useState([]); //支付平台 + const userInfo = useUserInfo(); // 获取当前用户信息 + const [selectedCustomer, setSelectedCustomer] = useState(null);// 选中的客户 + const [selectedProducts, setSelectedProducts] = useState([]);// 选中的产品,是一个数组 + const [customerBalance, setCustomerBalance] = useState(0); // 客户余额 + const [deductionAmount, setDeductionAmount] = useState(0); // 余额抵扣 + const [selectedCoupons, setSelectedCoupons] = useState([]); // 选中的优惠券 + const [customerCoupons, setCustomerCoupons] = useState([]); + + // 定义filterOption函数 + const filterOption = (input: string, option?: { value: string }) => { + const customer = customers.find(c => c._id === option?.value); + if (!customer) return false; + const nameMatch = customer.姓名 ? customer.姓名.toLowerCase().includes(input.toLowerCase()) : false; + const phoneMatch = customer.电话 ? customer.电话.includes(input) : false; + return nameMatch || phoneMatch; + }; + // 定义productFilterOption函数 + const productFilterOption = (input: string, option?: { value: string }) => { + const product = products.find(p => p._id === option?.value); + if (!product) return false; + const inputLower = input.toLowerCase(); + const brandMatch = product.品牌?.name ? product.品牌.name.toLowerCase().includes(inputLower) : false; + const categoryMatch = product.品类?.name ? product.品类.name.toLowerCase().includes(inputLower) : false; + const supplierMatch = product.供应商?.供应商名称 ? product.供应商.供应商名称.toLowerCase().includes(inputLower) : false; + const nameMatch = product.名称 ? product.名称.toLowerCase().includes(inputLower) : false; + const aliasMatch = product.别名 && Array.isArray(product.别名) + ? product.别名.some((alias: string) => alias.toLowerCase().includes(inputLower)) + : false; + const descriptionMatch = product.描述 ? product.描述.toLowerCase().includes(inputLower) : false; + const codeMatch = product.货号 ? product.货号.toLowerCase().includes(inputLower) : false; + const levelMatch = product.级别 ? product.级别.toLowerCase().includes(inputLower) : false; + const priceMatch = product.售价 ? product.售价.toString().includes(input) : false; + + return ( + brandMatch || + categoryMatch || + supplierMatch || + nameMatch || + aliasMatch || + descriptionMatch || + codeMatch || + levelMatch || + priceMatch + ); + }; + + // 选中的客户 + //const handleCustomerChange = (value: string) => { + const handleCustomerChange = async (value: string) => { + const customer = customers.find(c => c._id === value) || null; + setSelectedCustomer(customer); + if (customer) { + try { + const response = await axios.get(`/api/backstage/balance/${customer._id}`); + if (response.status === 200) { + setCustomerBalance(response.data.balance ?? 0); + setDeductionAmount(0); // 重置抵扣金额 + } else { + setCustomerBalance(0); + } + // 获取客户优惠券 + const couponsResponse = await axios.get(`/api/backstage/coupons/assign`, { + params: { customerId: customer._id }, + }); + if (couponsResponse.status === 200) { + const customerCoupons = couponsResponse.data.coupons; + setCustomerCoupons(customerCoupons); + + if (customerCoupons.length > 0) { + const couponIds = customerCoupons.map((coupon: ICustomerCoupon) => coupon._id._id); // 使用嵌套的 _id + form.setFieldsValue({ couponIds }); + } + } else { + setCustomerCoupons([]); + } + } catch (error) { + console.error('获取客户余额时出错:', error); + setCustomerBalance(0); + } + } + }; + // 选中的产品,是一个数组 + const handleProductChange = (value: string[]) => { + const selectedProds = products.filter(p => value.includes(p._id)); + setSelectedProducts(selectedProds); + }; + // 从用户信息中获取团队ID,然后获取客户、产品、账户、支付平台数据 + useEffect(() => { + if (userInfo.团队?._id) { + const teamId = userInfo.团队._id; + fetchCustomers(teamId); + fetchProducts(teamId); + fetchAccounts(teamId); + fetchPayPlatforms(teamId); + } + }, [userInfo]); + // 获取客户数据 + const fetchCustomers = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/customers?teamId=${teamId}`); + setCustomers(data.customers); + } catch (error) { + message.error('加载客户数据失败'); + } + }; + // 获取产品数据 + const fetchProducts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/products?teamId=${teamId}`); + setProducts(data.products); + } catch (error) { + message.error('加载产品数据失败'); + } + }; + // 获取账户数据 + const fetchAccounts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`); + //按账号编号排序 + data.accounts.sort((a: any, b: any) => a.账号编号 - b.账号编号); + setAccounts(data.accounts); + } catch (error) { + message.error('加载账户数据失败'); + } + }; + //获取支付平台数据 + const fetchPayPlatforms = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/payment-platforms?teamId=${teamId}`); + setPayPlatforms(data.platforms || []); // 使用 data.platforms 而不是 data.paymentPlatforms + } catch (error) { + message.error('加载支付平台数据失败'); + setPayPlatforms([]); // 确保失败时也设置为一个空数组 + } + }; + // 提交表单数据 + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const method = salesRecord ? 'PUT' : 'POST'; + const url = salesRecord ? `/api/backstage/sales/${salesRecord._id}` : '/api/backstage/sales'; + let transactionId = null; // 初始化交易记录ID + if (deductionAmount > 0) { + // 创建消费记录来扣除余额,并获取交易记录ID + const transactionResponse = await axios.post('/api/backstage/transactions', { + 团队: userInfo.团队?._id, + 客户: selectedCustomer ? selectedCustomer._id : null, + 类型: '消费', + 金额: deductionAmount, + 描述: '销售余额抵扣', + }); + transactionId = transactionResponse.data._id; // 获取创建的交易记录的ID + console.log('生成的transactionId:', transactionId); // 打印transactionId + } + + const requestData = { + ...values, + 客户: selectedCustomer ? selectedCustomer._id : null, // 将客户ID包含在请求数据中 + 产品: selectedProducts.map(product => product._id), // 将产品ID数组包含在请求数据中 + 团队: userInfo.团队?._id, // 将团队ID包含在请求数据中 + 导购: userInfo._id, // 将导购ID包含在请求数据中 + 优惠券: selectedCoupons.map(coupon => coupon.券码), // 使用优惠券的券码数组 + 余额抵用: transactionId, // 将交易记录ID传递到后端 + }; + + // 将交易记录ID传递到后端 + await axios({ + method: method, + url: url, + data: requestData, + }); + + message.success('销售记录操作成功'); + // 清空表单 + form.resetFields(); // 重置表单 + setSelectedCustomer(null); //清空选中的客户 + setSelectedProducts([]); //清空选中的产品 + setSelectedCoupons([]); //清空选中的优惠券 + setDeductionAmount(0); //清空抵扣金额 + } catch (info) { + //console.error('Validate Failed:', info); + message.error('提交失败,请检查信息填写是否完整!'); + } + }; + + // 创建两个独立的状态变量 + const [isCustomerModalVisible, setIsCustomerModalVisible] = useState(false); + const [isProductModalVisible, setIsProductModalVisible] = useState(false); + // 新增产品成功后的回调函数 + const handleAddProductClick = () => { + setIsProductModalVisible(true); + }; + // 新增产品成功后的回调函数 + const handleAddProductSuccess = (newProduct: IProduct) => { + setProducts([...products, newProduct]); + setSelectedProducts([...selectedProducts, newProduct]); + form.setFieldsValue({ 产品: [...selectedProducts.map(p => p._id), newProduct._id] }); // 自动选中新添加的产品 + setIsProductModalVisible(false); // 关闭产品模态框 + }; + + // 新增客户按钮的点击事件回调 + const handleAddCustomerClick = () => { + setIsCustomerModalVisible(true); + }; + + // 新增客户成功后的回调函数 + const handleAddCustomerSuccess = (newCustomer: ICustomer) => { + setCustomers([...customers, newCustomer]); // 将新客户添加到客户列表中 + setSelectedCustomer(newCustomer); // 将新客户设置为选中的客户 + //打印newCustomer + console.log(newCustomer); + form.setFieldsValue({ 客户: newCustomer._id }); // 自动选中新添加的客户 + setIsCustomerModalVisible(false); // 关闭客户模态框 + }; + + // 计算选中产品的总售价 + const totalPrice = selectedProducts.reduce((acc, cur) => acc + cur.售价, 0); + + + // 定义处理编辑客户后的回调 + const handleEditCustomerSuccess = (updatedCustomer: ICustomer) => { + setSelectedCustomer(updatedCustomer); // 更新选中的客户 + message.success('客户信息已更新'); + }; + + + return ( +
+ + + +
+ + setIsCustomerModalVisible(false)} + onSuccess={handleAddCustomerSuccess} + /> + + + + setIsProductModalVisible(false)} + onSuccess={handleAddProductSuccess} + /> + + + + + + + +
订单信息
+ + +
+
总应收: ¥{totalPrice - deductionAmount}
+
总抵扣: ¥{deductionAmount}
+
+ + setDeductionAmount(Math.min(Number(e.target.value), customerBalance))} + placeholder={`可用余额:¥${customerBalance.toFixed(2)}`} + /> + +
+
+ + + + +
+
+ + + } + extra={ + <> + + + + } + bordered style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/*应收金额 */} + + + + + + {/*收款金额 */} + + + + + + {/*待收款 */} + + + + + + + + + + + + + + + + ); +}; + +export default SalesRecordPage; diff --git a/src/pages/team/sale/sales-record-modal.tsx b/src/pages/team/sale/sales-record-modal.tsx new file mode 100644 index 0000000..f972d40 --- /dev/null +++ b/src/pages/team/sale/sales-record-modal.tsx @@ -0,0 +1,216 @@ +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Input, Select, message, DatePicker } from 'antd'; +import { ISalesRecord } from '@/models/types'; +import axios from 'axios'; +import { useUserInfo } from '@/store/userStore'; +import dayjs from 'dayjs'; + +interface SalesRecordModalProps { + visible: boolean; + onCancel: () => void; + onOk: () => void; + salesRecord: ISalesRecord | null; +} + +const SalesRecordModal: React.FC = ({ visible, onCancel, onOk, salesRecord }) => { + const [form] = Form.useForm(); + const [customers, setCustomers] = useState([]); + const [products, setProducts] = useState([]); + const [accounts, setAccounts] = useState([]); + const [users, setUsers] = useState([]); + const userInfo = useUserInfo(); // 获取当前用户信息 + + useEffect(() => { + if (userInfo.团队?._id) { + const teamId = userInfo.团队._id; + fetchCustomers(teamId); + fetchProducts(teamId); + fetchAccounts(teamId); + fetchUsers(teamId); + } + }, [userInfo]); + + useEffect(() => { + if (salesRecord) { + form.setFieldsValue({ + 客户: salesRecord.客户?._id, + 产品: salesRecord.产品.map(p => p._id), + 订单来源: salesRecord.订单来源?._id, + 导购: salesRecord.导购?._id, + 成交日期: salesRecord.成交日期 ? dayjs(salesRecord.成交日期) : null, + 收款状态: salesRecord.收款状态, + 货款状态: salesRecord.货款状态, + 备注: salesRecord.备注, + }); + } else { + form.resetFields(); // 当没有销售记录数据时重置表单 + } + }, [salesRecord, form]); + + const fetchCustomers = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/customers?teamId=${teamId}`); + setCustomers(data.customers); + } catch (error) { + message.error('加载客户数据失败'); + } + }; + + const fetchProducts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/products?teamId=${teamId}`); + setProducts(data.products); + } catch (error) { + message.error('加载产品数据失败'); + } + }; + + const fetchAccounts = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`); + setAccounts(data.accounts); + } catch (error) { + message.error('加载账户数据失败'); + } + }; + + const fetchUsers = async (teamId: string) => { + try { + const { data } = await axios.get(`/api/backstage/users?teamId=${teamId}`); + setUsers(data.users); + } catch (error) { + message.error('加载用户数据失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + const method = salesRecord ? 'PUT' : 'POST'; + const url = salesRecord ? `/api/backstage/sales/${salesRecord._id}` : '/api/backstage/sales'; + + await axios({ + method: method, + url: url, + data: { + ...values, + 团队: userInfo.团队?._id, // 将团队ID包含在请求数据中 + }, + }); + + message.success('销售记录操作成功'); + onOk(); // 直接调用 onOk 通知外部重新加载 + } catch (info) { + console.error('Validate Failed:', info); + message.error('销售记录操作失败'); + } + }; + + return ( + { + form.resetFields(); + onCancel(); + }} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default SalesRecordModal; diff --git a/src/styles/antd.css b/src/styles/antd.css index 7596ca6..c0f12e7 100644 --- a/src/styles/antd.css +++ b/src/styles/antd.css @@ -8,3 +8,20 @@ */ /* ==================== 仅必要的样式调整 ==================== */ +/* ==================== Ant Design 组件样式优化 ==================== */ +/* 关键代码行注释:针对Ant Design组件的毛玻璃风格优化 */ + +/* 模态框样式 */ +.ant-modal-header { + background: transparent !important; + border-bottom: 1px solid var(--glass-border) !important; + border-radius: 20px 20px 0 0 !important; + } + + .ant-modal-title { + color: var(--text-primary) !important; + } + + .ant-modal-body { + color: var(--text-secondary) !important; + } \ No newline at end of file diff --git a/src/utils/ConnectDB.ts b/src/utils/ConnectDB.ts index 87793dc..8e4d5f1 100644 --- a/src/utils/ConnectDB.ts +++ b/src/utils/ConnectDB.ts @@ -1,240 +1,63 @@ -/** - * 文件: src/utils/ConnectDB.ts - * 作者: 阿瑞 - * 功能: MongoDB数据库连接管理工具 - 高阶函数装饰器 - * 版本: v2.0.0 - * @description 为Next.js API路由提供数据库连接管理,支持连接复用、错误重试、连接超时等功能 - */ - +// src/utils/connectDB.ts import mongoose from 'mongoose'; import { NextApiRequest, NextApiResponse } from 'next'; -// =========================================== -// 类型定义区域 -// =========================================== - -/** - * 数据库连接配置接口 - */ -interface DatabaseConfig { - maxRetries?: number; // 最大重试次数 - retryDelay?: number; // 重试延迟(毫秒) - connectTimeout?: number; // 连接超时(毫秒) - enableLogging?: boolean; // 是否启用日志 -} - -/** - * API处理函数类型定义 - */ -type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise; - -// =========================================== -// 配置常量区域 -// =========================================== - -/** - * 默认数据库连接配置 - */ -const DEFAULT_CONFIG: Required = { - maxRetries: 3, // 默认重试3次 - retryDelay: 1000, // 默认延迟1秒 - connectTimeout: 10000, // 默认10秒超时 - enableLogging: process.env.NODE_ENV === 'development', // 开发环境启用日志 +// 数据库连接状态枚举 +const MONGO_CONNECTION_STATES = { + disconnected: 0, + connected: 1, + connecting: 2, + disconnecting: 3, }; -/** - * Mongoose连接状态枚举 - * 0 = 断开连接, 1 = 已连接, 2 = 正在连接, 3 = 正在断开连接 - */ -const CONNECTION_STATES = { - DISCONNECTED: 0, - CONNECTED: 1, - CONNECTING: 2, - DISCONNECTING: 3, -} as const; - -// =========================================== -// 工具函数区域 -// =========================================== - -/** - * 安全的日志记录函数 - * @param message 日志消息 - * @param data 可选的数据对象 - * @param isError 是否为错误日志 - */ -const safeLog = (message: string, data?: any, isError = false): void => { - if (DEFAULT_CONFIG.enableLogging) { - if (isError) { - console.error(`[DB Error] ${message}`, data); - } else { - console.log(`[DB Info] ${message}`, data); - } +// 定义一个高阶函数 connectDB,它接受一个处理请求的 handler,并返回一个新的处理函数 +const connectDB = ( + handler: (req: NextApiRequest, res: NextApiResponse) => Promise +) => async (req: NextApiRequest, res: NextApiResponse): Promise => { + // 检查数据库连接状态 + const connectionState = mongoose.connections[0]?.readyState; + + // 如果已经连接,直接调用 handler + if (connectionState === MONGO_CONNECTION_STATES.connected) { + return handler(req, res); } -}; - -/** - * 延迟函数 - * @param ms 延迟毫秒数 - */ -const delay = (ms: number): Promise => - new Promise(resolve => setTimeout(resolve, ms)); - -/** - * 检查数据库连接状态 - * @returns 是否已连接 - */ -const isDbConnected = (): boolean => { - const connection = mongoose.connections[0]; - return connection && connection.readyState === CONNECTION_STATES.CONNECTED; -}; - -/** - * 获取数据库连接URI - * @returns 数据库URI或null - */ -const getDatabaseUri = (): string | null => { + + // 如果正在连接中,等待连接完成 + if (connectionState === MONGO_CONNECTION_STATES.connecting) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒 + return connectDB(handler)(req, res); // 递归调用 + } + + // 从环境变量中获取数据库连接 URI const dbUri = process.env.MONGODB_URI; if (!dbUri) { - safeLog('数据库URI未在环境变量中配置', null, true); - return null; + console.error('MONGODB_URI environment variable is not set'); + return res.status(500).json({ error: 'Database URI not provided.' }); } - return dbUri; -}; -// =========================================== -// 核心连接功能区域 -// =========================================== - -/** - * 执行数据库连接(带重试机制) - * @param dbUri 数据库连接URI - * @param config 连接配置 - * @returns Promise 连接是否成功 - */ -const connectWithRetry = async ( - dbUri: string, - config: Required -): Promise => { - for (let attempt = 1; attempt <= config.maxRetries; attempt++) { - try { - safeLog(`尝试连接数据库 (第${attempt}/${config.maxRetries}次)`); - - // 设置mongoose连接选项 - const connectOptions = { - serverSelectionTimeoutMS: config.connectTimeout, - socketTimeoutMS: config.connectTimeout, - family: 4, // 使用IPv4 - maxPoolSize: 10, // 连接池大小 - retryWrites: true, - w: 'majority' as const, // 写关注级别 - 修复类型错误 - }; - - await mongoose.connect(dbUri, connectOptions); - safeLog('数据库连接成功'); - return true; - - } catch (error) { - const isLastAttempt = attempt === config.maxRetries; - safeLog( - `数据库连接失败 (第${attempt}/${config.maxRetries}次)`, - { error: error instanceof Error ? error.message : 'Unknown error' }, - true - ); - - if (!isLastAttempt) { - safeLog(`等待${config.retryDelay}ms后重试...`); - await delay(config.retryDelay); - } - } - } - - return false; -}; - -// =========================================== -// 主要导出功能区域 -// =========================================== - -/** - * 数据库连接高阶函数装饰器 - * @description 为API路由提供数据库连接管理,包含连接复用、错误重试、超时处理等功能 - * @param handler API处理函数 - * @param config 可选的数据库连接配置 - * @returns 包装后的API处理函数 - * - * @example - * ```typescript - * export default connectDB(async (req, res) => { - * // 你的API逻辑 - * }); - * ``` - */ -const connectDB = ( - handler: ApiHandler, - config: DatabaseConfig = {} -) => async (req: NextApiRequest, res: NextApiResponse): Promise => { - - // 合并配置 - const finalConfig: Required = { ...DEFAULT_CONFIG, ...config }; - try { - // 检查现有连接状态 - if (isDbConnected()) { - safeLog('使用现有数据库连接'); - return await handler(req, res); - } - - // 获取数据库URI - const dbUri = getDatabaseUri(); - if (!dbUri) { - return res.status(500).json({ - error: 'Database configuration error', - message: process.env.NODE_ENV === 'development' - ? 'MONGODB_URI not found in environment variables' - : 'Database configuration is missing' - }); - } - - // 尝试连接数据库 - const connected = await connectWithRetry(dbUri, finalConfig); + // 设置 mongoose 连接选项 - 使用兼容的选项 + const options = { + maxPoolSize: 10, // 连接池最大连接数 + serverSelectionTimeoutMS: 10000, // 服务器选择超时时间 + socketTimeoutMS: 45000, // Socket 超时时间 + connectTimeoutMS: 10000, // 连接超时时间 + }; - if (!connected) { - safeLog('所有数据库连接尝试均失败', null, true); - return res.status(500).json({ - error: 'Database connection failed', - message: 'Unable to establish database connection after multiple attempts' - }); - } - - // 成功连接后执行API处理函数 - return await handler(req, res); + // 连接到数据库 + await mongoose.connect(dbUri, options); + console.log('Database connected successfully'); + // 成功连接后,处理原始请求 + return handler(req, res); } catch (error) { - // 全局错误捕获 - safeLog('数据库连接装饰器发生未预期错误', error, true); - - return res.status(500).json({ - error: 'Internal server error', - message: process.env.NODE_ENV === 'development' - ? (error instanceof Error ? error.message : 'Unknown error occurred') - : 'An unexpected error occurred' + console.error('Error connecting to database:', error); + // 如果连接失败,返回 500 错误 + return res.status(500).json({ + error: 'Failed to connect to the database.', + details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined }); } }; -// =========================================== -// 导出区域 -// =========================================== - export default connectDB; - -/** - * 导出配置常量供外部使用 - */ -export { DEFAULT_CONFIG, CONNECTION_STATES }; - -/** - * 导出类型定义供外部使用 - */ -export type { DatabaseConfig, ApiHandler }; diff --git a/src/utils/cleanupIndexes.ts b/src/utils/cleanupIndexes.ts new file mode 100644 index 0000000..ea3fa0f --- /dev/null +++ b/src/utils/cleanupIndexes.ts @@ -0,0 +1,120 @@ +/** + * 文件: src/utils/cleanupIndexes.ts + * 作者: 阿瑞 + * 功能: 清理数据库中的重复索引 + * 版本: 1.0.0 + */ + +import mongoose from 'mongoose'; +import { User, Account } from '@/models'; + +/** + * 清理重复索引的函数 + * 主要解决 Mongoose 重复索引警告问题 + */ +export async function cleanupDuplicateIndexes(): Promise { + try { + console.log('开始清理重复索引...'); + + // 获取数据库连接 + const db = mongoose.connection.db; + if (!db) { + throw new Error('数据库连接未建立'); + } + + // 清理 User 集合的重复索引 + try { + const userCollection = db.collection('users'); + const userIndexes = await userCollection.indexes(); + + console.log('User 集合现有索引:', userIndexes.map(idx => idx.name)); + + // 删除可能的重复索引(如果存在) + for (const index of userIndexes) { + if (index.name && index.name !== '_id_' && index.name.includes('电话') && index.name !== '电话_1') { + try { + await userCollection.dropIndex(index.name); + console.log(`删除 User 集合重复索引: ${index.name}`); + } catch (error) { + console.log(`索引 ${index.name} 可能不存在或已被删除`); + } + } + + if (index.name && index.name !== '_id_' && index.name.includes('unionid') && index.name !== 'unionid_1') { + try { + await userCollection.dropIndex(index.name); + console.log(`删除 User 集合重复索引: ${index.name}`); + } catch (error) { + console.log(`索引 ${index.name} 可能不存在或已被删除`); + } + } + } + } catch (error) { + console.error('清理 User 集合索引时出错:', error); + } + + // 清理 Account 集合的重复索引 + try { + const accountCollection = db.collection('accounts'); + const accountIndexes = await accountCollection.indexes(); + + console.log('Account 集合现有索引:', accountIndexes.map(idx => idx.name)); + + // 删除可能的重复索引(如果存在) + for (const index of accountIndexes) { + if (index.name && index.name !== '_id_' && index.name.includes('unionid') && index.name !== 'unionid_1') { + try { + await accountCollection.dropIndex(index.name); + console.log(`删除 Account 集合重复索引: ${index.name}`); + } catch (error) { + console.log(`索引 ${index.name} 可能不存在或已被删除`); + } + } + } + } catch (error) { + console.error('清理 Account 集合索引时出错:', error); + } + + console.log('重复索引清理完成'); + } catch (error) { + console.error('清理索引过程中发生错误:', error); + throw error; + } +} + +/** + * 重建必要的索引 + * 确保数据库有正确的索引结构 + */ +export async function rebuildIndexes(): Promise { + try { + console.log('开始重建索引...'); + + // 重建 User 模型的索引 + await User.syncIndexes(); + console.log('User 模型索引重建完成'); + + // 重建 Account 模型的索引 + await Account.syncIndexes(); + console.log('Account 模型索引重建完成'); + + console.log('所有索引重建完成'); + } catch (error) { + console.error('重建索引时发生错误:', error); + throw error; + } +} + +/** + * 完整的索引清理和重建流程 + */ +export async function fullIndexCleanup(): Promise { + try { + await cleanupDuplicateIndexes(); + await rebuildIndexes(); + console.log('索引清理和重建流程完成'); + } catch (error) { + console.error('索引清理和重建流程失败:', error); + throw error; + } +} \ No newline at end of file diff --git a/src/utils/connectDB.ts.bak b/src/utils/connectDB.ts.bak new file mode 100644 index 0000000..87793dc --- /dev/null +++ b/src/utils/connectDB.ts.bak @@ -0,0 +1,240 @@ +/** + * 文件: src/utils/ConnectDB.ts + * 作者: 阿瑞 + * 功能: MongoDB数据库连接管理工具 - 高阶函数装饰器 + * 版本: v2.0.0 + * @description 为Next.js API路由提供数据库连接管理,支持连接复用、错误重试、连接超时等功能 + */ + +import mongoose from 'mongoose'; +import { NextApiRequest, NextApiResponse } from 'next'; + +// =========================================== +// 类型定义区域 +// =========================================== + +/** + * 数据库连接配置接口 + */ +interface DatabaseConfig { + maxRetries?: number; // 最大重试次数 + retryDelay?: number; // 重试延迟(毫秒) + connectTimeout?: number; // 连接超时(毫秒) + enableLogging?: boolean; // 是否启用日志 +} + +/** + * API处理函数类型定义 + */ +type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise; + +// =========================================== +// 配置常量区域 +// =========================================== + +/** + * 默认数据库连接配置 + */ +const DEFAULT_CONFIG: Required = { + maxRetries: 3, // 默认重试3次 + retryDelay: 1000, // 默认延迟1秒 + connectTimeout: 10000, // 默认10秒超时 + enableLogging: process.env.NODE_ENV === 'development', // 开发环境启用日志 +}; + +/** + * Mongoose连接状态枚举 + * 0 = 断开连接, 1 = 已连接, 2 = 正在连接, 3 = 正在断开连接 + */ +const CONNECTION_STATES = { + DISCONNECTED: 0, + CONNECTED: 1, + CONNECTING: 2, + DISCONNECTING: 3, +} as const; + +// =========================================== +// 工具函数区域 +// =========================================== + +/** + * 安全的日志记录函数 + * @param message 日志消息 + * @param data 可选的数据对象 + * @param isError 是否为错误日志 + */ +const safeLog = (message: string, data?: any, isError = false): void => { + if (DEFAULT_CONFIG.enableLogging) { + if (isError) { + console.error(`[DB Error] ${message}`, data); + } else { + console.log(`[DB Info] ${message}`, data); + } + } +}; + +/** + * 延迟函数 + * @param ms 延迟毫秒数 + */ +const delay = (ms: number): Promise => + new Promise(resolve => setTimeout(resolve, ms)); + +/** + * 检查数据库连接状态 + * @returns 是否已连接 + */ +const isDbConnected = (): boolean => { + const connection = mongoose.connections[0]; + return connection && connection.readyState === CONNECTION_STATES.CONNECTED; +}; + +/** + * 获取数据库连接URI + * @returns 数据库URI或null + */ +const getDatabaseUri = (): string | null => { + const dbUri = process.env.MONGODB_URI; + if (!dbUri) { + safeLog('数据库URI未在环境变量中配置', null, true); + return null; + } + return dbUri; +}; + +// =========================================== +// 核心连接功能区域 +// =========================================== + +/** + * 执行数据库连接(带重试机制) + * @param dbUri 数据库连接URI + * @param config 连接配置 + * @returns Promise 连接是否成功 + */ +const connectWithRetry = async ( + dbUri: string, + config: Required +): Promise => { + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + safeLog(`尝试连接数据库 (第${attempt}/${config.maxRetries}次)`); + + // 设置mongoose连接选项 + const connectOptions = { + serverSelectionTimeoutMS: config.connectTimeout, + socketTimeoutMS: config.connectTimeout, + family: 4, // 使用IPv4 + maxPoolSize: 10, // 连接池大小 + retryWrites: true, + w: 'majority' as const, // 写关注级别 - 修复类型错误 + }; + + await mongoose.connect(dbUri, connectOptions); + safeLog('数据库连接成功'); + return true; + + } catch (error) { + const isLastAttempt = attempt === config.maxRetries; + safeLog( + `数据库连接失败 (第${attempt}/${config.maxRetries}次)`, + { error: error instanceof Error ? error.message : 'Unknown error' }, + true + ); + + if (!isLastAttempt) { + safeLog(`等待${config.retryDelay}ms后重试...`); + await delay(config.retryDelay); + } + } + } + + return false; +}; + +// =========================================== +// 主要导出功能区域 +// =========================================== + +/** + * 数据库连接高阶函数装饰器 + * @description 为API路由提供数据库连接管理,包含连接复用、错误重试、超时处理等功能 + * @param handler API处理函数 + * @param config 可选的数据库连接配置 + * @returns 包装后的API处理函数 + * + * @example + * ```typescript + * export default connectDB(async (req, res) => { + * // 你的API逻辑 + * }); + * ``` + */ +const connectDB = ( + handler: ApiHandler, + config: DatabaseConfig = {} +) => async (req: NextApiRequest, res: NextApiResponse): Promise => { + + // 合并配置 + const finalConfig: Required = { ...DEFAULT_CONFIG, ...config }; + + try { + // 检查现有连接状态 + if (isDbConnected()) { + safeLog('使用现有数据库连接'); + return await handler(req, res); + } + + // 获取数据库URI + const dbUri = getDatabaseUri(); + if (!dbUri) { + return res.status(500).json({ + error: 'Database configuration error', + message: process.env.NODE_ENV === 'development' + ? 'MONGODB_URI not found in environment variables' + : 'Database configuration is missing' + }); + } + + // 尝试连接数据库 + const connected = await connectWithRetry(dbUri, finalConfig); + + if (!connected) { + safeLog('所有数据库连接尝试均失败', null, true); + return res.status(500).json({ + error: 'Database connection failed', + message: 'Unable to establish database connection after multiple attempts' + }); + } + + // 成功连接后执行API处理函数 + return await handler(req, res); + + } catch (error) { + // 全局错误捕获 + safeLog('数据库连接装饰器发生未预期错误', error, true); + + return res.status(500).json({ + error: 'Internal server error', + message: process.env.NODE_ENV === 'development' + ? (error instanceof Error ? error.message : 'Unknown error occurred') + : 'An unexpected error occurred' + }); + } +}; + +// =========================================== +// 导出区域 +// =========================================== + +export default connectDB; + +/** + * 导出配置常量供外部使用 + */ +export { DEFAULT_CONFIG, CONNECTION_STATES }; + +/** + * 导出类型定义供外部使用 + */ +export type { DatabaseConfig, ApiHandler }; diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 965b222..ff0dc6b 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -2,12 +2,12 @@ * 主题工具函数 * @author 阿瑞 * @description 主题管理工具,与项目的主题系统集成,支持Ant Design主题配置和CSS变量 - * @version 3.0.0 + * @version 4.0.0 - 性能优化版本 * @created 2024-12-19 - * @updated 重构以适配当前项目的主题架构 + * @updated 重构以提升性能,添加缓存机制,移除realDark */ -import { useCallback, useEffect, useState, useMemo } from 'react'; +import { useCallback, useEffect, useState, useMemo, useRef } from 'react'; import { useSettingActions, useThemeMode } from '@/store/settingStore'; import { ThemeMode as ProjectThemeMode } from '@/types/enum'; import { lightTheme, darkTheme } from '@/styles/theme/themeConfig'; @@ -55,9 +55,9 @@ const defaultDarkToken: ThemeToken = { borderRadius: 12, }; -// 模块级注释:主题Hook接口 +// 模块级注释:主题Hook接口 - 移除realDark,只保留light和dark export interface UseThemeReturn { - navTheme: 'light' | 'realDark'; + navTheme: 'light' | 'dark'; themeToken: ThemeToken; toggleTheme: () => void; changePrimaryColor: (color: string) => void; @@ -66,7 +66,32 @@ export interface UseThemeReturn { mounted: boolean; } -// 模块级注释:应用CSS主题类的函数 +// 模块级注释:主题配置缓存系统 - 性能优化核心 +interface ThemeCache { + antdTheme: ThemeConfig; + themeToken: ThemeToken; + cacheKey: string; +} + +const themeCache = new Map(); +const MAX_CACHE_SIZE = 10; // 限制缓存大小避免内存泄露 + +// 关键代码行注释:生成缓存键值 +const generateCacheKey = (isDark: boolean, customPrimaryColor: string): string => { + return `${isDark ? 'dark' : 'light'}-${customPrimaryColor || 'default'}`; +}; + +// 关键代码行注释:清理过期缓存 +const cleanCache = (): void => { + if (themeCache.size > MAX_CACHE_SIZE) { + const keys = Array.from(themeCache.keys()); + keys.slice(0, themeCache.size - MAX_CACHE_SIZE).forEach(key => { + themeCache.delete(key); + }); + } +}; + +// 模块级注释:应用CSS主题类的函数 - 优化版本 const applyThemeClasses = (isDark: boolean): void => { if (typeof document !== 'undefined') { const root = document.documentElement; @@ -87,7 +112,7 @@ const applyThemeClasses = (isDark: boolean): void => { } }; -// 模块级注释:从Ant Design主题配置提取Token +// 模块级注释:从Ant Design主题配置提取Token - 优化版本 const extractTokenFromTheme = (theme: ThemeConfig, isDark: boolean): ThemeToken => { const token = theme.token || {}; const baseToken = isDark ? defaultDarkToken : defaultLightToken; @@ -107,9 +132,52 @@ const extractTokenFromTheme = (theme: ThemeConfig, isDark: boolean): ThemeToken }; }; -// 模块级注释:主题Hook实现 +// 模块级注释:获取缓存的主题配置 - 性能优化核心函数 +const getCachedThemeConfig = (isDark: boolean, customPrimaryColor: string): ThemeCache => { + const cacheKey = generateCacheKey(isDark, customPrimaryColor); + + // 关键代码行注释:尝试从缓存获取 + const cached = themeCache.get(cacheKey); + if (cached) { + return cached; + } + + // 关键代码行注释:缓存未命中,重新计算 + const baseTheme = isDark ? darkTheme : lightTheme; + const antdTheme: ThemeConfig = customPrimaryColor ? { + ...baseTheme, + token: { + ...baseTheme.token, + colorPrimary: customPrimaryColor, + colorPrimaryHover: customPrimaryColor, + colorLink: customPrimaryColor, + }, + } : baseTheme; + + const themeToken = extractTokenFromTheme(antdTheme, isDark); + + // 关键代码行注释:如果有自定义主色,覆盖相关颜色 + if (customPrimaryColor) { + themeToken.colorPrimary = customPrimaryColor; + themeToken.colorLink = customPrimaryColor; + } + + const themeConfig: ThemeCache = { + antdTheme, + themeToken, + cacheKey, + }; + + // 关键代码行注释:存储到缓存 + themeCache.set(cacheKey, themeConfig); + cleanCache(); + + return themeConfig; +}; + +// 模块级注释:主题Hook实现 - 性能优化版本 export const useTheme = ( - _updateLayoutSettings?: (newTheme: 'light' | 'realDark', newColorPrimary?: string) => void + _updateLayoutSettings?: (newTheme: 'light' | 'dark', newColorPrimary?: string) => void ): UseThemeReturn => { // 关键代码行注释:使用zustand store管理主题状态 const themeMode = useThemeMode(); @@ -119,55 +187,33 @@ export const useTheme = ( const [mounted, setMounted] = useState(false); const [customPrimaryColor, setCustomPrimaryColor] = useState(''); + // 关键代码行注释:使用ref避免不必要的重新渲染 + const lastThemeRef = useRef(''); + // 关键代码行注释:计算当前主题状态 const isDark = themeMode === ProjectThemeMode.Dark; - const navTheme = isDark ? 'realDark' : 'light'; + const navTheme = isDark ? 'dark' : 'light'; - // 关键代码行注释:获取当前Ant Design主题配置 - const currentAntdTheme = useMemo(() => { - const baseTheme = isDark ? darkTheme : lightTheme; - - // 如果有自定义主色,应用到主题配置 - if (customPrimaryColor) { - return { - ...baseTheme, - token: { - ...baseTheme.token, - colorPrimary: customPrimaryColor, - colorPrimaryHover: customPrimaryColor, - colorLink: customPrimaryColor, - }, - }; - } - - return baseTheme; + // 关键代码行注释:使用缓存获取主题配置 - 大幅提升性能 + const cachedThemeConfig = useMemo(() => { + return getCachedThemeConfig(isDark, customPrimaryColor); }, [isDark, customPrimaryColor]); - // 关键代码行注释:提取主题Token - const themeToken = useMemo(() => { - const token = extractTokenFromTheme(currentAntdTheme, isDark); - - // 如果有自定义主色,覆盖相关颜色 - if (customPrimaryColor) { - token.colorPrimary = customPrimaryColor; - token.colorLink = customPrimaryColor; - } - - return token; - }, [currentAntdTheme, isDark, customPrimaryColor]); + // 关键代码行注释:从缓存中提取配置 + const { antdTheme: currentAntdTheme, themeToken } = cachedThemeConfig; - // 关键代码行注释:主题切换函数 + // 关键代码行注释:主题切换函数 - 优化版本 const toggleTheme = useCallback(() => { toggleThemeMode(); // 关键代码行注释:调用旧的回调函数以保持兼容性 if (_updateLayoutSettings) { - const newTheme = isDark ? 'light' : 'realDark'; + const newTheme = isDark ? 'light' : 'dark'; _updateLayoutSettings(newTheme, themeToken.colorPrimary); } }, [toggleThemeMode, isDark, themeToken.colorPrimary, _updateLayoutSettings]); - // 关键代码行注释:主色更改函数 + // 关键代码行注释:主色更改函数 - 优化版本 const changePrimaryColor = useCallback((color: string) => { setCustomPrimaryColor(color); @@ -195,14 +241,19 @@ export const useTheme = ( } }, []); - // 关键代码行注释:应用CSS主题类 + // 关键代码行注释:应用CSS主题类 - 优化防抖版本 useEffect(() => { if (mounted) { - applyThemeClasses(isDark); + const currentThemeKey = `${isDark}-${mounted}`; + // 关键代码行注释:防止重复应用相同主题 + if (lastThemeRef.current !== currentThemeKey) { + applyThemeClasses(isDark); + lastThemeRef.current = currentThemeKey; + } } }, [mounted, isDark]); - // 关键代码行注释:监听存储变化(多标签页同步) + // 关键代码行注释:监听存储变化(多标签页同步)- 优化版本 useEffect(() => { if (!mounted) return; @@ -227,11 +278,16 @@ export const useTheme = ( }; }; -// 模块级注释:导出类型别名以保持兼容性 -export type ThemeMode = 'light' | 'realDark'; +// 模块级注释:导出类型别名以保持兼容性 - 移除realDark +export type ThemeMode = 'light' | 'dark'; // 模块级注释:导出主题配置 export { lightTheme, darkTheme }; // 模块级注释:导出工具函数 -export { applyThemeClasses }; \ No newline at end of file +export { applyThemeClasses }; + +// 模块级注释:导出缓存清理函数供测试使用 +export const clearThemeCache = (): void => { + themeCache.clear(); +}; \ No newline at end of file