0607.1
Some checks failed
Next.js CI/CD 流水线 / deploy (push) Failing after 36s

This commit is contained in:
2025-06-07 00:20:00 +08:00
parent d098d58018
commit 6362673ccb
51 changed files with 5542 additions and 515 deletions

View File

@@ -17,6 +17,7 @@
"@types/lodash": "^4.17.17", "@types/lodash": "^4.17.17",
"antd": "^5.25.4", "antd": "^5.25.4",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"dayjs": "^1.11.13",
"geist": "^1.4.2", "geist": "^1.4.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",

3
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
bcryptjs: bcryptjs:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2 version: 3.0.2
dayjs:
specifier: ^1.11.13
version: 1.11.13
geist: geist:
specifier: ^1.4.2 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)) version: 1.4.2(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))

View File

@@ -2,7 +2,7 @@
* Layout主布局组件 * Layout主布局组件
* 作者: 阿瑞 * 作者: 阿瑞
* 功能: 应用主布局,包含导航菜单、用户信息、主题切换等功能 * 功能: 应用主布局,包含导航菜单、用户信息、主题切换等功能
* 版本: v2.0 - 性能优化版本 * 版本: v3.0 - 性能优化版本移除realDark添加React.memo优化
*/ */
import React, { useEffect, useMemo, useState, useCallback } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -22,21 +22,44 @@ interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
// 生成动态路由的函数 - 优化版本 // 生成动态路由的函数 - 优化版本,添加缓存机制
const routeCache = new Map<string, MenuDataItem[]>();
const generateDynamicRoutes = (permissions: IPermission[]): MenuDataItem[] => { const generateDynamicRoutes = (permissions: IPermission[]): MenuDataItem[] => {
if (!permissions?.length) return []; 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)); const sortedPermissions = permissions.sort((a, b) => (a. ?? 0) - (b. ?? 0));
// 映射权限数据到菜单项 // 映射权限数据到菜单项
return sortedPermissions.map(permission => ({ const routes = sortedPermissions.map(permission => ({
path: permission.路径, path: permission.路径 || '/',
name: permission.名称, name: permission.名称 || '',
icon: <Icon icon={permission.Icon} width="20" height="20" />, icon: <Icon icon={(permission.Icon as string) || 'ant-design:home-outlined'} width="20" height="20" />,
component: './DynamicComponent', component: './DynamicComponent',
routes: permission.子级 ? generateDynamicRoutes(permission.) : undefined routes: permission.子级 ? generateDynamicRoutes(permission.) : undefined
}) as Partial<MenuDataItem>); }) as Partial<MenuDataItem>);
// 关键代码行注释:存储到缓存
routeCache.set(cacheKey, routes);
// 关键代码行注释:限制缓存大小
if (routeCache.size > 50) {
const firstKey = routeCache.keys().next().value;
if (firstKey) {
routeCache.delete(firstKey);
}
}
return routes;
}; };
// 默认props配置 // 默认props配置
@@ -50,7 +73,7 @@ const defaultProps = {
}, },
}; };
// 头部标题渲染组件 - 提取为独立组件避免重复渲染 // 头部标题渲染组件 - 使用React.memo优化性能
const HeaderTitle: React.FC = React.memo(() => ( const HeaderTitle: React.FC = React.memo(() => (
<a> <a>
<img <img
@@ -67,16 +90,17 @@ const HeaderTitle: React.FC = React.memo(() => (
</a> </a>
)); ));
// 菜单底部渲染组件 HeaderTitle.displayName = 'HeaderTitle';
const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo(({
collapsed, // 用户信息显示组件 - 新增独立组件使用React.memo优化
userInfo, const UserInfoDisplay: React.FC<{ userInfo: any; collapsed?: boolean; }> = React.memo(({
//onShowScriptLibrary userInfo,
collapsed
}) => { }) => {
if (collapsed) return undefined; if (collapsed) return null;
return ( return (
<div style={{ textAlign: 'center', padding: '0px', borderRadius: '8px' }}> <>
{/* 团队信息部分 */} {/* 团队信息部分 */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
@@ -102,14 +126,28 @@ const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo
{userInfo?.?. || 'N/A'} {userInfo?.?. || 'N/A'}
</span> </span>
</div> </div>
</>
);
});
{/* 任务控制器 */} UserInfoDisplay.displayName = 'UserInfoDisplay';
// 菜单底部渲染组件 - 优化版本使用React.memo
const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo(({
collapsed,
userInfo,
}) => {
if (collapsed) return undefined;
return (
<div style={{ textAlign: 'center', padding: '0px', borderRadius: '8px' }}>
{/* 用户信息部分 */}
<UserInfoDisplay userInfo={userInfo} collapsed={collapsed} />
{/* 话术库按钮 */} {/* 话术库按钮 */}
<div style={{ marginTop: '12px', marginBottom: '12px' }}> <div style={{ marginTop: '12px', marginBottom: '12px' }}>
<Button <Button
type="primary" type="primary"
//onClick={onShowScriptLibrary}
style={{ style={{
width: '100%', width: '100%',
height: '36px', height: '36px',
@@ -136,6 +174,37 @@ const MenuFooter: React.FC<{ collapsed?: boolean; userInfo: any; }> = React.memo
); );
}); });
MenuFooter.displayName = 'MenuFooter';
// 主题切换按钮组件 - 新增独立组件使用React.memo优化
const ThemeToggleButton: React.FC<{
toggleTheme: () => void;
navTheme: 'light' | 'dark';
}> = React.memo(({ toggleTheme, navTheme }) => (
<Button
type="text"
shape="circle"
onClick={toggleTheme}
className="theme-switch-ripple"
title={navTheme === 'dark' ? '切换到明亮模式' : '切换到暗黑模式'}
aria-label={navTheme === 'dark' ? '切换到明亮模式' : '切换到暗黑模式'}
style={{
marginRight: '8px',
border: 'none',
background: 'var(--glass-bg)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
transition: 'all 0.3s ease',
fontSize: '16px',
color: navTheme === 'dark' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(10, 17, 40, 0.8)',
}}
>
{navTheme === 'dark' ? <MdLightMode /> : <MdDarkMode />}
</Button>
));
ThemeToggleButton.displayName = 'ThemeToggleButton';
// Layout组件主体 // Layout组件主体
const Layout: React.FC<LayoutProps> = ({ children }) => { const Layout: React.FC<LayoutProps> = ({ children }) => {
// 状态管理 // 状态管理
@@ -145,15 +214,14 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [dynamicRoutes, setDynamicRoutes] = useState<MenuDataItem[]>([]); const [dynamicRoutes, setDynamicRoutes] = useState<MenuDataItem[]>([]);
const [showPersonalInfo, setShowPersonalInfo] = useState<boolean>(false); const [showPersonalInfo, setShowPersonalInfo] = useState<boolean>(false);
//const [showScriptLibrary, setShowScriptLibrary] = useState<boolean>(false);
const { navTheme, toggleTheme, changePrimaryColor, themeToken } = useTheme(() => { }); const { navTheme, toggleTheme, changePrimaryColor, themeToken } = useTheme(() => { });
// 使用 useMemo 优化 settings 计算 // 使用 useMemo 优化 settings 计算 - 优化依赖项
const settings = useMemo<Partial<ProSettings>>(() => ({ const settings = useMemo<Partial<ProSettings>>(() => ({
fixSiderbar: true, fixSiderbar: true,
layout: "mix", layout: "mix",
splitMenus: false, splitMenus: false,
navTheme: navTheme, navTheme: navTheme === 'dark' ? 'realDark' : 'light',
contentWidth: "Fluid", contentWidth: "Fluid",
colorPrimary: themeToken.colorPrimary, colorPrimary: themeToken.colorPrimary,
title: "私域管理系统V3", title: "私域管理系统V3",
@@ -162,7 +230,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
menuHeaderRender: false, menuHeaderRender: false,
}), [navTheme, themeToken.colorPrimary]); }), [navTheme, themeToken.colorPrimary]);
// 使用 useCallback 优化事件处理函数 // 使用 useCallback 优化事件处理函数 - 优化依赖项
const handleLogout = useCallback(() => { const handleLogout = useCallback(() => {
router.push('/'); router.push('/');
userActions.clearUserInfoAndToken(); userActions.clearUserInfoAndToken();
@@ -180,7 +248,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
router.push(path || '/'); router.push(path || '/');
}, [router]); }, [router]);
// 用户信息更新逻辑 // 用户信息更新逻辑 - 优化依赖项
const { fetchAndSetUserInfo } = useUserActions(); const { fetchAndSetUserInfo } = useUserActions();
useEffect(() => { useEffect(() => {
const updateUserInfo = async () => { const updateUserInfo = async () => {
@@ -199,7 +267,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
setIsClient(true); setIsClient(true);
}, []); }, []);
// 动态路由生成 - 使用 useMemo 优化 // 动态路由生成 - 使用 useMemo 优化,添加更精确的依赖项
const memoizedRoutes = useMemo(() => { const memoizedRoutes = useMemo(() => {
if (userInfo?.?.) { if (userInfo?.?.) {
return generateDynamicRoutes(userInfo..); return generateDynamicRoutes(userInfo..);
@@ -207,12 +275,12 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
return []; return [];
}, [userInfo?.?.]); }, [userInfo?.?.]);
// 更新动态路由 // 更新动态路由 - 优化依赖项
useEffect(() => { useEffect(() => {
setDynamicRoutes(memoizedRoutes); setDynamicRoutes(memoizedRoutes);
}, [memoizedRoutes]); }, [memoizedRoutes]);
// 下拉菜单配置 - 使用 useMemo 优化 // 下拉菜单配置 - 使用 useMemo 优化,稳定化依赖项
const dropdownMenuItems = useMemo(() => [ const dropdownMenuItems = useMemo(() => [
{ {
key: 'profile', key: 'profile',
@@ -220,7 +288,6 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
label: '个人资料', label: '个人资料',
onClick: handleShowPersonalInfo, onClick: handleShowPersonalInfo,
}, },
{ {
key: 'logout', key: 'logout',
icon: <LogoutOutlined />, icon: <LogoutOutlined />,
@@ -252,30 +319,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
</div> </div>
), [handleMenuItemClick]); ), [handleMenuItemClick]);
// 头像渲染函数 - 使用 useCallback 优化 // 头像渲染函数 - 使用 useCallback 优化,稳定化依赖项
const renderAvatar = useCallback((_props: any, dom: React.ReactNode) => ( const renderAvatar = useCallback((_props: any, dom: React.ReactNode) => (
<> <>
{/* 主题切换按钮 */} {/* 主题切换按钮 */}
<Button <ThemeToggleButton toggleTheme={toggleTheme} navTheme={navTheme} />
type="text"
shape="circle"
onClick={toggleTheme}
className="theme-switch-ripple"
title={navTheme === 'realDark' ? '切换到明亮模式' : '切换到暗黑模式'}
aria-label={navTheme === 'realDark' ? '切换到明亮模式' : '切换到暗黑模式'}
style={{
marginRight: '8px',
border: 'none',
background: 'var(--glass-bg)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
transition: 'all 0.3s ease',
fontSize: '16px',
color: navTheme === 'realDark' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(10, 17, 40, 0.8)',
}}
>
{navTheme === 'realDark' ? <MdLightMode /> : <MdDarkMode />}
</Button>
{/* 主题色选择器 */} {/* 主题色选择器 */}
<ThemeSwitcher <ThemeSwitcher
@@ -292,6 +340,14 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
</> </>
), [themeToken.colorPrimary, changePrimaryColor, toggleTheme, navTheme, dropdownMenuItems]); ), [themeToken.colorPrimary, changePrimaryColor, toggleTheme, navTheme, dropdownMenuItems]);
// 菜单底部渲染函数 - 使用useCallback优化
const renderMenuFooter = useCallback((props: any) => (
<MenuFooter
collapsed={props?.collapsed}
userInfo={userInfo}
/>
), [userInfo]);
// 如果不在客户端,不渲染任何内容 // 如果不在客户端,不渲染任何内容
if (!isClient) { if (!isClient) {
return null; return null;
@@ -302,7 +358,6 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
height: '100vh', height: '100vh',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
//overflow: 'hidden'
}}> }}>
<ProConfigProvider hashed={false}> <ProConfigProvider hashed={false}>
<ConfigProvider <ConfigProvider
@@ -312,7 +367,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
prefixCls="my-prefix" prefixCls="my-prefix"
{...defaultProps} {...defaultProps}
location={{ pathname: router.pathname }} location={{ pathname: router.pathname }}
token={{ token={{
// 头部菜单选中项的背景颜色 // 头部菜单选中项的背景颜色
header: { colorBgMenuItemSelected: 'rgba(0,0,0,0.04)' }, header: { colorBgMenuItemSelected: 'rgba(0,0,0,0.04)' },
// PageContainer 内边距控制 - 完全移除左右空白 // PageContainer 内边距控制 - 完全移除左右空白
@@ -336,13 +391,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
render: renderAvatar, render: renderAvatar,
}} }}
headerTitleRender={() => <HeaderTitle />} headerTitleRender={() => <HeaderTitle />}
menuFooterRender={(props) => ( menuFooterRender={renderMenuFooter}
<MenuFooter
collapsed={props?.collapsed}
userInfo={userInfo}
//onShowScriptLibrary={handleShowScriptLibrary}
/>
)}
menuItemRender={renderMenuItem} menuItemRender={renderMenuItem}
{...settings} {...settings}
style={{ style={{
@@ -361,6 +410,23 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
contentWidth="Fluid" contentWidth="Fluid"
locale="zh-CN" locale="zh-CN"
> >
{/*
内容区域边距移除方案说明:
为了完全移除页面内容的左右空白,采用了三层防护措施:
1. ProLayout 层级:通过 token.pageContainer.paddingInlinePageContainerContent = 0
移除 ProLayout 组件默认的左右内边距
2. PageContainer 层级:
- pageHeaderRender={false} 禁用头部渲染避免额外空间
- token.paddingInlinePageContainerContent = 0 再次确保移除左右内边距
- style.padding = 0 移除组件自身样式内边距
3. 最内层 divpadding = '0px' 作为最后一道防线
这样的多层配置确保在不同版本的 Ant Design Pro 中都能正常工作
*/}
<PageContainer <PageContainer
// 移除默认的页面标题栏 // 移除默认的页面标题栏
header={{ title: null }} header={{ title: null }}
@@ -414,12 +480,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
> >
<PersonalInfo /> <PersonalInfo />
</Modal> </Modal>
{/* 话术库模态框 */}
</div> </div>
); );
}; };
// 移除React.memo包装组件,避免不必要的重新渲染阻止 // 使用React.memo包装整个Layout组件 - 性能优化最后一步
export default Layout; export default React.memo(Layout);

View File

@@ -2,7 +2,7 @@
* 主题切换器组件 * 主题切换器组件
* 作者: 阿瑞 * 作者: 阿瑞
* 功能: 主题模式切换和主题色选择 * 功能: 主题模式切换和主题色选择
* 版本: v2.0 - 性能优化版本 * 版本: v3.0 - 性能优化版本移除realDark
*/ */
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Button, Popover, Tag, Tooltip, Space, Typography } from 'antd'; import { Button, Popover, Tag, Tooltip, Space, Typography } from 'antd';
@@ -14,110 +14,115 @@ interface ThemeSwitcherProps {
value: string; value: string;
onChange: (color: string) => void; onChange: (color: string) => void;
toggleTheme: () => void; toggleTheme: () => void;
navTheme: 'light' | 'realDark'; navTheme: 'light' | 'dark';
} }
// 预设主题色配置 - 使用更丰富的色彩 // 模块级注释:预定义主题色板
const presetColors = [ const THEME_COLORS = [
{ color: '#078DEE', name: '天空蓝' }, { name: '拂晓蓝', color: '#1677ff' },
{ color: '#7635DC', name: '紫罗兰' }, { name: '薄暮', color: '#fa541c' },
{ color: '#2065D1', name: '深海蓝' }, { name: '火山', color: '#fa8c16' },
{ color: '#FDA92D', name: '橙黄色' }, { name: '日暮', color: '#faad14' },
{ color: '#FF4842', name: '珊瑚红' }, { name: '明青', color: '#13c2c2' },
{ color: '#FFC107', name: '金黄色' }, { name: '极光绿', color: '#52c41a' },
{ color: '#00AB55', name: '翡翠绿' }, { name: '极客蓝', color: '#2f54eb' },
{ color: '#1890FF', name: 'Ant蓝' }, { name: '酱紫', color: '#722ed1' },
{ color: '#722ED1', name: 'Ant紫' },
{ color: '#EB2F96', name: 'Ant粉' },
]; ];
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ // 模块级注释:主题切换器主体组件 - 使用React.memo优化性能
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = React.memo(({
value, value,
onChange }) => { onChange,
// 使用 useCallback 优化颜色选择处理函数 toggleTheme,
const handleColorChange = useCallback((color: string) => { navTheme
onChange(color); }) => {
}, [onChange]); // 关键代码行注释色板渲染函数使用useCallback优化
const renderColorPalette = useCallback(() => (
// 使用 useMemo 优化颜色选择器内容 <div style={{ padding: '12px 8px' }}>
const colorPickerContent = useMemo(() => ( <Space direction="vertical" style={{ width: '100%' }}>
<div style={{ width: 280 }}> <Text >
<div style={{ marginBottom: 12 }}>
<Space>
<BgColorsOutlined style={{ color: '#1890ff' }} />
<Text strong></Text>
</Space>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 8 }}>
{presetColors.map(({ color, name }) => (
<Tooltip key={color} title={name} placement="top">
<Tag
color={color}
onClick={() => 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 && (
<span style={{
color: '#fff',
fontSize: '12px',
fontWeight: 'bold',
textShadow: '0 1px 2px rgba(0,0,0,0.5)'
}}>
</span>
)}
</Tag>
</Tooltip>
))}
</div>
<div style={{ marginTop: 12, textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
: {presetColors.find(c => c.color === value)?.name || '自定义'}
</Text> </Text>
</div> <div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '8px',
width: '200px'
}}>
{THEME_COLORS.map(({ name, color }) => (
<Tooltip key={color} title={name}>
<Tag
color={color}
style={{
width: '40px',
height: '32px',
cursor: 'pointer',
border: value === color ? '2px solid #000' : 'none',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
transform: value === color ? 'scale(1.1)' : 'scale(1)',
}}
onClick={() => onChange(color)}
/>
</Tooltip>
))}
</div>
<div style={{ marginTop: '8px' }}>
<Button
type="text"
size="small"
onClick={toggleTheme}
style={{
width: '100%',
height: '32px',
border: '1px dashed rgba(0,0,0,0.2)',
color: navTheme === 'dark' ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',
fontSize: '12px'
}}
>
{navTheme === 'dark' ? '🌞 切换到明亮模式' : '🌙 切换到暗黑模式'}
</Button>
</div>
</Space>
</div> </div>
), [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 ( return (
<Space size="middle"> <Popover {...popoverProps}>
{/* 主题色选择器 */} <Button
<Popover type="text"
trigger="click" shape="circle"
placement="bottomRight" icon={<BgColorsOutlined />}
content={colorPickerContent} title="主题设置"
title={null} aria-label="主题设置"
overlayStyle={{ padding: 0 }} style={{
> marginRight: '8px',
<Tooltip title="选择主题色" placement="bottom"> border: 'none',
<Button background: 'var(--glass-bg)',
shape="circle" backdropFilter: 'blur(10px)',
size="small" WebkitBackdropFilter: 'blur(10px)',
icon={<BgColorsOutlined />} transition: 'all 0.3s ease',
style={{ fontSize: '16px',
backgroundColor: value, color: navTheme === 'dark' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(10, 17, 40, 0.8)',
borderColor: value, }}
color: '#fff', />
boxShadow: `0 2px 4px ${value}40`, </Popover>
transition: 'all 0.2s ease',
}}
/>
</Tooltip>
</Popover>
</Space>
); );
}; });
// 使用 React.memo 优化组件性能 ThemeSwitcher.displayName = 'ThemeSwitcher';
export default React.memo(ThemeSwitcher);
export default ThemeSwitcher;

View File

@@ -11,8 +11,8 @@ const UserSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'Role' }, // 将角色字段定义为引用 Role 模型的 ObjectId : { type: Schema.Types.ObjectId, ref: 'Role' }, // 将角色字段定义为引用 Role 模型的 ObjectId
: { type: String }, // 用户微信昵称字段 : { type: String }, // 用户微信昵称字段
: { type: String }, // 用户头像字段 : { type: String }, // 用户头像字段
unionid: { type: String, unique: true }, unionid: { type: String, unique: true, sparse: true }, // 添加sparse以允许null值
openid: { type: String, unique: true }, // 添加openid字段 openid: { type: String, unique: true, sparse: true }, // 添加openid字段sparse允许null值
}, { timestamps: true }); // 自动添加创建时间和更新时间 }, { timestamps: true }); // 自动添加创建时间和更新时间
UserSchema.index({ 团队: 1 }); // 对团队字段建立索引 UserSchema.index({ 团队: 1 }); // 对团队字段建立索引
@@ -235,7 +235,7 @@ const CustomerSchema: Schema = new Schema({
: [CouponUsageSchema], // 使用子文档结构来存储优惠券信息 : [CouponUsageSchema], // 使用子文档结构来存储优惠券信息
}, { timestamps: true }); // 自动添加创建时间和更新时间 }, { timestamps: true }); // 自动添加创建时间和更新时间
CustomerSchema.index({ 团队: 1 }); // 对团队字段建立索引 CustomerSchema.index({ 团队: 1 }); // 对团队字段建立索引
CustomerSchema.index({ 电话: 1 }); // 电话字段索引 // CustomerSchema.index({ 电话: 1 }); // 注释掉重复索引因为已在schema定义中设置了unique: true
//销售记录模型定义 //销售记录模型定义
const SalesRecordSchema: Schema = new Schema({ const SalesRecordSchema: Schema = new Schema({
@@ -265,6 +265,10 @@ const SalesRecordSchema: Schema = new Schema({
default: '待结算', // 默认值 default: '待结算', // 默认值
enum: ['可结算', '不可结算', '待结算', '已结算'], // 枚举限制 enum: ['可结算', '不可结算', '待结算', '已结算'], // 枚举限制
}, },
: {
type: String,
enum: ['未处理', '处理中', '已处理'], // 处理状态枚举限制
},
//余额抵用,关联交易记录 //余额抵用,关联交易记录
: { type: Schema.Types.ObjectId, ref: 'Transaction' }, : { type: Schema.Types.ObjectId, ref: 'Transaction' },
回访日期: Date, 回访日期: Date,
@@ -394,7 +398,7 @@ const AccountSchema: Schema = new Schema({
: { type: Schema.Types.ObjectId, ref: 'User' }, : { type: Schema.Types.ObjectId, ref: 'User' },
: { type: Schema.Types.ObjectId, ref: 'User' }, : { type: Schema.Types.ObjectId, ref: 'User' },
: [{ type: Schema.Types.ObjectId, ref: 'Category' }], : [{ type: Schema.Types.ObjectId, ref: 'Category' }],
unionid: { type: String, sparse: true }, //sparse: true 表示该字段可以为空,但如果有值,那么值必须是唯一的 unionid: { type: String, unique: true, sparse: true }, //sparse: true 表示该字段可以为空,但如果有值,那么值必须是唯一的
openid: { type: String }, // 添加openid字段 openid: { type: String }, // 添加openid字段
: { type: String }, : { type: String },
: { type: String }, : { type: String },
@@ -407,7 +411,7 @@ const AccountSchema: Schema = new Schema({
: { type: String }, : { type: String },
: [DailyGrowthSchema], // 日增长数据数组 : [DailyGrowthSchema], // 日增长数据数组
}, { timestamps: true }); // 自动添加创建时间和更新时间 }, { timestamps: true }); // 自动添加创建时间和更新时间
AccountSchema.index({ unionid: 1 }, { unique: true, sparse: true });// 创建索引,确保 unionid 只在不为 null 时唯一 // AccountSchema.index({ unionid: 1 }, { unique: true, sparse: true }); // 注释掉重复索引因为在schema中已经定义了unique
AccountSchema.index({ 团队: 1 }); // 对团队字段建立索引 AccountSchema.index({ 团队: 1 }); // 对团队字段建立索引
AccountSchema.index({ 账号负责人: 1 }); // 对账号负责人字段建立索引 AccountSchema.index({ 账号负责人: 1 }); // 对账号负责人字段建立索引
AccountSchema.index({ 前端引流人员: 1 }); // 对前端引流人员字段建立索引 AccountSchema.index({ 前端引流人员: 1 }); // 对前端引流人员字段建立索引

View File

@@ -272,6 +272,7 @@ export interface ISalesRecord {
二次审计: boolean; 二次审计: boolean;
收款状态: string; 收款状态: string;
货款状态: string; 货款状态: string;
?: '未处理' | '处理中' | '已处理';
售后收支: number; 售后收支: number;
回访日期: Date; 回访日期: Date;
备注: string; 备注: string;

View File

@@ -0,0 +1,42 @@
/**
* 文件: src/pages/api/admin/cleanup-indexes.ts
* 作者: 阿瑞
* 功能: 管理员API - 清理数据库重复索引
* 版本: 1.0.0
*/
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import { fullIndexCleanup } from '@/utils/cleanupIndexes';
/**
* 清理数据库重复索引的API端点
* 仅限管理员使用
*/
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// 仅允许 POST 请求
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({ error: `不允许 ${req.method} 方法` });
}
try {
// 执行索引清理
await fullIndexCleanup();
res.status(200).json({
success: true,
message: '数据库索引清理完成',
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('索引清理失败:', error);
res.status(500).json({
success: false,
error: '索引清理失败',
details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
});
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,50 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Brand } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const brand = await Brand.findById(id).populate('团队');
if (!brand) {
return res.status(404).json({ message: '未找到品牌' });
}
res.status(200).json(brand);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { name, description, order, } = req.body;
const updateData = { name, description, order, };
const updatedBrand = await Brand.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedBrand) {
return res.status(404).json({ message: '未找到品牌' });
}
res.status(200).json({ message: '品牌更新成功', brand: updatedBrand });
} catch (error) {
res.status(400).json({ message: '更新品牌失败' });
}
break;
case 'DELETE':
try {
const deletedBrand = await Brand.findByIdAndDelete(id);
if (!deletedBrand) {
return res.status(404).json({ message: '未找到品牌' });
}
res.status(200).json({ message: '品牌删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,29 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Brand } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
const brands = await Brand.find({ 团队: teamId }).populate('团队');
res.status(200).json({ brands });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { name, description, order, } = req.body;
const newBrand = new Brand({ name, description, order, });
await newBrand.save();
res.status(201).json({ message: '品牌创建成功', brand: newBrand });
} catch (error) {
res.status(400).json({ message: '创建品牌失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,50 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Category } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const category = await Category.findById(id).populate('团队');
if (!category) {
return res.status(404).json({ message: '未找到品类' });
}
res.status(200).json(category);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { name, description, icon } = req.body;
const updateData = { name, description, icon };
const updatedCategory = await Category.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedCategory) {
return res.status(404).json({ message: '未找到品类' });
}
res.status(200).json({ message: '品类更新成功', category: updatedCategory });
} catch (error) {
res.status(400).json({ message: '更新品类失败' });
}
break;
case 'DELETE':
try {
const deletedCategory = await Category.findByIdAndDelete(id);
if (!deletedCategory) {
return res.status(404).json({ message: '未找到品类' });
}
res.status(200).json({ message: '品类删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Category } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
const categories = await Category.find({ 团队: teamId }).populate('团队');
res.status(200).json({ categories });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { name, description, icon, } = req.body;
const newCategory = new Category({
name, description, icon,
});
await newCategory.save();
res.status(201).json({ message: '品类创建成功', category: newCategory });
} catch (error) {
res.status(400).json({ message: '创建品类失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,68 @@
//src\pages\api\backstage\customers\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const customer = await Customer.findById(id)
.populate('团队')
.populate({
path: '优惠券._id', // 使用优惠券子文档中的ID字段进行关联
model: 'Coupon' // 关联的模型名称
});
if (!customer) {
return res.status(404).json({ message: '未找到客户' });
}
res.status(200).json(customer);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , , , , } = req.body;
const updatedCustomer = await Customer.findByIdAndUpdate(id, {
,
,
: {
省份: 地址.省份,
城市: 地址.城市,
区县: 地址.区县,
详细地址: 地址.详细地址
},
,
,
,
}, { new: true });
if (!updatedCustomer) {
return res.status(404).json({ message: '未找到客户' });
}
res.status(200).json({ message: '客户更新成功', customer: updatedCustomer });
} catch (error) {
res.status(400).json({ message: '更新客户失败' });
}
break;
case 'DELETE':
try {
const deletedCustomer = await Customer.findByIdAndDelete(id);
if (!deletedCustomer) {
return res.status(404).json({ message: '未找到客户' });
}
res.status(200).json({ message: '客户删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,53 @@
//src\pages\api\backstage\customers\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
//按创建时间从近到远排序
const customers = await Customer.find({ 团队: teamId })
//.populate('团队')
.sort({ createdAt: -1 });
res.status(200).json({ customers });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , , } = req.body;
const newCustomer = new Customer({
, ,
: {
省份: 地址.省份,
城市: 地址.城市,
区县: 地址.区县,
详细地址: 地址.详细地址
},
, , ,
});
await newCustomer.save();
res.status(201).json({ message: '客户创建成功', customer: newCustomer });
} catch (error: any) {
// 如果错误代码是 11000表示重复键错误
if (error.code === 11000) {
const duplicateField = Object.keys(error.keyPattern)[0]; // 重复字段
const duplicateValue = error.keyValue[duplicateField]; // 重复的值
res.status(400).json({
message: `客户 ${duplicateValue} 已存在,请使用不同的${duplicateField}`
});
} else {
// 处理其他类型的错误
console.error('创建客户失败:', error);
res.status(400).json({ message: '创建客户失败', error: error.message });
}
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,59 @@
// src/pages/api/backstage/customers/sales/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import { Customer, SalesRecord, AfterSalesRecord } from '@/models'; // 导入模型
import { ICustomer } from '@/models/types';
type Data = {
message: string;
salesRecords?: any[];
afterSalesRecords?: any[];
};
const handler = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
const { id } = req.query;
if (req.method === 'GET') {
try {
// 查找客户,通过手机号
const customer: ICustomer | null = await Customer.findOne({ 电话: id });
if (!customer) {
return res.status(404).json({ message: '客户未找到' });
}
// 查询客户的销售记录
const salesRecords = await SalesRecord.find({ 客户: customer._id })
.populate({
path: '产品',
select: '-图片' // 排除产品中的“图片”字段
})
.populate('导购')
.populate('订单来源')
.populate('售后记录');
// 查询客户的售后记录
const afterSalesRecords = await AfterSalesRecord.find({ : { $in: salesRecords.map(record => record._id) } })
.populate({
path: '原产品',
select: '-图片' // 排除原产品中的“图片”字段
})
.populate({
path: '替换产品',
select: '-图片' // 排除替换产品中的“图片”字段
});
return res.status(200).json({
message: '查询成功',
salesRecords,
afterSalesRecords
});
} catch (error) {
console.error(error);
return res.status(500).json({ message: '服务器内部错误' });
}
} else {
return res.status(405).json({ message: `不允许的请求方法 ${req.method}` });
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,51 @@
// src/pages/api/backstage/payment-platforms/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PaymentPlatform } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const platform = await PaymentPlatform.findById(id);
if (!platform) {
return res.status(404).json({ message: '未找到收支平台' });
}
res.status(200).json(platform);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , } = req.body;
const updateData = { , , , };
const updatedPlatform = await PaymentPlatform.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedPlatform) {
return res.status(404).json({ message: '未找到收支平台' });
}
res.status(200).json({ message: '收支平台更新成功', platform: updatedPlatform });
} catch (error) {
res.status(400).json({ message: '更新收支平台失败' });
}
break;
case 'DELETE':
try {
const deletedPlatform = await PaymentPlatform.findByIdAndDelete(id);
if (!deletedPlatform) {
return res.status(404).json({ message: '未找到收支平台' });
}
res.status(200).json({ message: '收支平台删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,30 @@
// src/pages/api/backstage/payment-platforms/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PaymentPlatform } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
const platforms = await PaymentPlatform.find({ 团队: teamId });
res.status(200).json({ platforms });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , } = req.body;
const newPlatform = new PaymentPlatform({ , , , , });
await newPlatform.save();
res.status(201).json({ message: '收支平台创建成功', platform: newPlatform });
} catch (error) {
res.status(400).json({ message: '创建收支平台失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,85 @@
//src\pages\api\backstage\products\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Product } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const product = await Product.findById(id)
.populate('团队')
.populate('品牌')
.populate('品类')
//.populate('供应商')
.populate({
path: '供应商',
select: '联系方式'
})
;
if (!product) {
return res.status(404).json({ message: '未找到产品' });
}
res.status(200).json(product);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , , , , , , , , , , , , } = req.body;
// 构建更新数据对象
const updateData: any = {
,
,
,
,
,
,
: {
,
,
,
},
,
,
,
,
,
};
// 只有当图片数据存在且不为空时,才更新图片字段
if (typeof !== 'undefined' && !== '') {
updateData. = ;
}
const updatedProduct = await Product.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedProduct) {
return res.status(404).json({ message: '未找到产品' });
}
res.status(200).json({ message: '产品更新成功', product: updatedProduct });
} catch (error) {
res.status(400).json({ message: '更新产品失败' });
}
break;
case 'DELETE':
try {
const deletedProduct = await Product.findByIdAndDelete(id);
if (!deletedProduct) {
return res.status(404).json({ message: '未找到产品' });
}
res.status(200).json({ message: '产品删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,56 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Product } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
//const products = await Product.find({ 团队: teamId })
//排除图片字段
const products = await Product.find({ 团队: teamId }).select('-图片')
//.populate('团队')
//.populate('品牌')
//.populate('品类')
//.populate('供应商')
.populate({
path: '供应商',
select: '联系方式'
})
.populate({
path: '品牌',
select: 'name'
})
.populate({
path: '品类',
select: 'name'
})
.sort({ createdAt: -1 });
res.status(200).json({ products });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , , , , , , , , , , } = req.body;
const newProduct = new Product({
, , , , , , ,
: {
,
,
,
},
, , , ,
});
await newProduct.save();
res.status(201).json({ message: '产品创建成功', product: newProduct });
} catch (error) {
res.status(400).json({ message: '创建产品失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -56,7 +56,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
.sort({ createdAt: -1 }); // 按成交日期倒序排列 .sort({ createdAt: -1 }); // 按成交日期倒序排列
res.status(200).json({ salesRecords }); res.status(200).json({ salesRecords });
} catch (error) { } catch (error) {
res.status(500).json({ message: '服务器错误' }); console.error('Error fetching sales records:', error);
res.status(500).json({
message: '服务器错误',
details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
});
} }
} else if (req.method === 'POST') { } else if (req.method === 'POST') {
try { try {
@@ -65,7 +69,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await newSalesRecord.save(); await newSalesRecord.save();
res.status(201).json({ message: '销售记录创建成功', salesRecord: newSalesRecord }); res.status(201).json({ message: '销售记录创建成功', salesRecord: newSalesRecord });
} catch (error) { } catch (error) {
res.status(400).json({ message: '创建销售记录失败' }); console.error('Error creating sales record:', error);
res.status(400).json({
message: '创建销售记录失败',
details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
});
} }
} else { } else {
res.setHeader('Allow', ['GET', 'POST']); res.setHeader('Allow', ['GET', 'POST']);

View File

@@ -0,0 +1,98 @@
/**
* 作者: 阿瑞
* 功能: 更新销售记录的处理状态API
* 版本: 1.0.0
*/
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import { SalesRecord } from '@/models';
/**
* 更新销售记录的处理状态
* @param req - HTTP请求对象
* @param res - HTTP响应对象
*/
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// 只允许PUT方法
if (req.method !== 'PUT') {
return res.status(405).json({
success: false,
message: '不支持的请求方法只允许PUT请求'
});
}
try {
// 从请求体中获取参数
const { recordId, } = req.body;
// 参数验证
if (!recordId) {
return res.status(400).json({
success: false,
message: '缺少必要参数recordId'
});
}
// 验证处理状态的有效性
const validStatuses = ['未处理', '处理中', '已处理'];
if ( && !validStatuses.includes()) {
return res.status(400).json({
success: false,
message: '无效的处理状态,只允许:未处理、处理中、已处理'
});
}
// 查找并更新销售记录
const updatedRecord = await SalesRecord.findByIdAndUpdate(
recordId,
{ },
{ new: true, runValidators: true }
);
// 检查记录是否存在
if (!updatedRecord) {
return res.status(404).json({
success: false,
message: '未找到指定的销售记录'
});
}
// 返回成功响应
return res.status(200).json({
success: true,
message: '处理状态更新成功',
data: {
recordId: updatedRecord._id,
处理状态: updatedRecord.处理状态
}
});
} catch (error: any) {
console.error('更新销售记录处理状态失败:', error);
// 处理不同类型的错误
if (error.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: '数据验证失败',
details: error.message
});
}
if (error.name === 'CastError') {
return res.status(400).json({
success: false,
message: '无效的记录ID格式'
});
}
// 其他未知错误
return res.status(500).json({
success: false,
message: '服务器内部错误,请稍后重试'
});
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,50 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Supplier } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const supplier = await Supplier.findById(id).populate('团队');
if (!supplier) {
return res.status(404).json({ message: '未找到供应商' });
}
res.status(200).json(supplier);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , , , status, } = req.body;
const updateData = { , , , , , status, };
const updatedSupplier = await Supplier.findByIdAndUpdate(id, updateData, { new: true });
if (!updatedSupplier) {
return res.status(404).json({ message: '未找到供应商' });
}
res.status(200).json({ message: '供应商更新成功', supplier: updatedSupplier });
} catch (error) {
res.status(400).json({ message: '更新供应商失败' });
}
break;
case 'DELETE':
try {
const deletedSupplier = await Supplier.findByIdAndDelete(id);
if (!deletedSupplier) {
return res.status(404).json({ message: '未找到供应商' });
}
res.status(200).json({ message: '供应商删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Supplier } from '@/models';
import connectDB from '@/utils/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
const suppliers = await Supplier.find({ 团队: teamId })
.populate('团队')
.populate('供应品类'); // 使用 populate 填充供应品类信息
res.status(200).json({ suppliers });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , status, , } = req.body;
const newSupplier = new Supplier({
, , , , , status, ,
});
await newSupplier.save();
res.status(201).json({ message: '供应商创建成功', supplier: newSupplier });
} catch (error) {
res.status(400).json({ message: '创建供应商失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,56 @@
//src\pages\api\backstage\users\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import { IUser } from '@/models/types'; // 导入 IUser 接口类型
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;
switch (method) {
case 'GET':
try {
const user = await User.findById(id).populate('团队').populate('角色') as IUser; // 断言为 IUser 类型
if (!user) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json(user);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
case 'PUT':
try {
const { , , , , , , , unionid, openid, } = req.body as IUser; // 断言为 IUser 类型
const updateData = { , , , , , , , unionid, openid } as IUser; // 断言为 IUser 类型
if () {
updateData. = await bcrypt.hash(, 10);
}
const updatedUser = await User.findByIdAndUpdate(id, updateData, { new: true }) as IUser; // 断言为 IUser 类型
if (!updatedUser) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json({ message: '用户更新成功', user: updatedUser });
} catch (error) {
res.status(400).json({ message: '更新用户失败' });
}
break;
case 'DELETE':
try {
const deletedUser = await User.findByIdAndDelete(id);
if (!deletedUser) {
return res.status(404).json({ message: '未找到用户' });
}
res.status(200).json({ message: '用户删除成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`不允许 ${method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -0,0 +1,41 @@
//src\pages\api\backstage\users\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
try {
const { teamId } = req.query; // 获取团队ID
const users = await User.find({ 团队: teamId })
//排除密码字段
.select('-密码')
//.populate('团队')
//.populate('角色')
.populate({
path: '角色',
select: '名称 描述',
})
;
res.status(200).json({ users });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
} else if (req.method === 'POST') {
try {
const { , , , , , , , unionid, openid, } = req.body;
const hashedPassword = await bcrypt.hash( || '123456', 10); // 使用默认密码如果没有提供密码
const newUser = new User({ , , , 密码: hashedPassword, , , , , unionid, openid });
await newUser.save();
res.status(201).json({ message: '用户创建成功', user: newUser });
} catch (error) {
res.status(400).json({ message: '创建用户失败' });
}
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`不允许 ${req.method} 方法`);
}
};
export default connectDB(handler);

View File

@@ -13,11 +13,8 @@ const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => {
// 查询所有相关的物流记录 // 查询所有相关的物流记录
const logisticsRecords = await LogisticsRecord.find({ }); const logisticsRecords = await LogisticsRecord.find({ });
if (logisticsRecords.length > 0) { // 始终返回200状态码没有记录时返回空数组
return res.status(200).json(logisticsRecords); // 返回多个记录 return res.status(200).json(logisticsRecords);
} else {
return res.status(404).json({ message: '未找到对应的物流记录' });
}
} catch (error: any) { } catch (error: any) {
console.error('查询物流记录失败:', error); console.error('查询物流记录失败:', error);
return res.status(500).json({ message: '查询物流记录失败,请检查服务器状态', error: error.message }); return res.status(500).json({ message: '查询物流记录失败,请检查服务器状态', error: error.message });

View File

@@ -50,6 +50,9 @@ export default connectDB(async (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ userInfo }); res.status(200).json({ userInfo });
} catch (error) { } catch (error) {
console.error('Error fetching user info:', error); console.error('Error fetching user info:', error);
res.status(500).json({ error: '服务器错误' }); res.status(500).json({
error: '服务器错误',
details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
});
} }
}); });

View File

@@ -12,7 +12,6 @@ import {
DatePicker, DatePicker,
Button, Button,
InputNumber, InputNumber,
message,
Row, Row,
Col, Col,
Divider, Divider,
@@ -22,8 +21,10 @@ import {
Card, Card,
Empty, Empty,
Tag, Tag,
Badge Badge,
App
} from 'antd'; } from 'antd';
import dayjs from 'dayjs';
import { import {
CloseOutlined, CloseOutlined,
UserOutlined, UserOutlined,
@@ -45,10 +46,11 @@ import {
import { ISalesRecord, IPaymentPlatform, IProduct } from '@/models/types'; import { ISalesRecord, IPaymentPlatform, IProduct } from '@/models/types';
import { useUserInfo } from '@/store/userStore'; import { useUserInfo } from '@/store/userStore';
import ProductImage from '@/components/product/ProductImage'; import ProductImage from '@/components/product/ProductImage';
//import AddProductComponent from '../sale/components/AddProductComponent'; import AddProductComponent from '../sale/components/AddProductComponent';
const { Option } = Select; const { Option } = Select;
const { Text } = Typography; const { Text } = Typography;
const { useApp } = App;
/** /**
* 售后记录模态框组件属性定义 * 售后记录模态框组件属性定义
@@ -93,6 +95,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const typeConfig = getTypeConfig(type); const typeConfig = getTypeConfig(type);
const { message } = useApp(); // 使用 useApp hook 获取 message 实例
/** /**
* 模块级注释:初始化数据加载 * 模块级注释:初始化数据加载
@@ -117,7 +120,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
form.setFieldsValue({ form.setFieldsValue({
销售记录: record._id, 销售记录: record._id,
类型: type, 类型: type,
日期: new Date(), 日期: dayjs(),
收支金额: type === '退货' ? record.收款金额 : 0, 收支金额: type === '退货' ? record.收款金额 : 0,
收支类型: type === '退货' ? '支出' : (type === '补差' ? '收入' : ''), 收支类型: type === '退货' ? '支出' : (type === '补差' ? '收入' : ''),
}); });
@@ -132,11 +135,11 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
const fetchPayPlatforms = async (teamId: string) => { const fetchPayPlatforms = async (teamId: string) => {
try { try {
const response = await fetch(`/api/backstage/payment-platforms?teamId=${teamId}`); const response = await fetch(`/api/backstage/payment-platforms?teamId=${teamId}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
setPaymentPlatforms(data.platforms || []); setPaymentPlatforms(data.platforms || []);
return data.platforms; return data.platforms;
@@ -154,11 +157,11 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
const fetchProducts = async (teamId: string) => { const fetchProducts = async (teamId: string) => {
try { try {
const response = await fetch(`/api/backstage/products?teamId=${teamId}`); const response = await fetch(`/api/backstage/products?teamId=${teamId}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
setProducts(data.products || []); setProducts(data.products || []);
return data.products; return data.products;
@@ -201,7 +204,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
}, },
body: JSON.stringify({ 收款码: paymentCode }) body: JSON.stringify({ 收款码: paymentCode })
}); });
if (paymentCodeResponse.ok) { if (paymentCodeResponse.ok) {
const paymentCodeData = await paymentCodeResponse.json(); const paymentCodeData = await paymentCodeResponse.json();
paymentCodeId = paymentCodeData.paymentCodeId; paymentCodeId = paymentCodeData.paymentCodeId;
@@ -218,7 +221,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
替换产品: replacementProductIds, 替换产品: replacementProductIds,
类型: type, 类型: type,
团队: userInfo.团队?._id, 团队: userInfo.团队?._id,
日期: values.日期.toISOString(), 日期: values.日期 ? values..toISOString() : null,
收款码: paymentCodeId, 收款码: paymentCodeId,
: '待处理', : '待处理',
}; };
@@ -493,10 +496,10 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
<Row style={{ marginBottom: 12 }}> <Row style={{ marginBottom: 12 }}>
<Col span={12}> <Col span={12}>
<Space> <Space>
<CalendarOutlined style={{ color: '#1890ff' }} /> <CalendarOutlined style={{ color: '#1890ff' }} />
<Text>: {new Date(record.).toLocaleDateString('zh-CN')}</Text> <Text>: {dayjs(record.).format('YYYY-MM-DD')}</Text>
</Space> </Space>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Space> <Space>
@@ -528,7 +531,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
<Text> <Text>
: :
<Text strong style={{ marginLeft: 4 }}> <Text strong style={{ marginLeft: 4 }}>
{Math.ceil((new Date().getTime() - new Date(record.).getTime()) / (1000 * 60 * 60 * 24))} {dayjs().diff(dayjs(record.), 'day')}
</Text> </Text>
</Text> </Text>
</Space> </Space>
@@ -589,7 +592,7 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
name="原因" name="原因"
rules={[{ required: true, message: '请选择售后原因' }]} rules={[{ required: true, message: '请选择售后原因' }]}
> >
<Select <Select
placeholder="请选择售后原因" placeholder="请选择售后原因"
getPopupContainer={triggerNode => triggerNode.parentElement || document.body} getPopupContainer={triggerNode => triggerNode.parentElement || document.body}
> >
@@ -834,12 +837,12 @@ const AfterSalesModal: React.FC<AfterSalesModalProps> = ({ visible, onOk, onCanc
</Row> </Row>
</Form> </Form>
{/* 产品添加模态框 {/* 产品添加模态框 */}
<AddProductComponent <AddProductComponent
visible={isProductModalVisible} visible={isProductModalVisible}
onClose={() => setIsProductModalVisible(false)} onClose={() => setIsProductModalVisible(false)}
onSuccess={handleAddProductSuccess} onSuccess={handleAddProductSuccess}
/> */} />
</Modal> </Modal>
); );
}; };

View File

@@ -13,6 +13,7 @@ import {
Skeleton, Skeleton,
Typography, Typography,
App, App,
Radio,
} from "antd"; } from "antd";
const { Paragraph } = Typography; const { Paragraph } = Typography;
@@ -112,6 +113,39 @@ const ShipModal = lazy(() => import("./ship-modal"));
const SalesPage = () => { const SalesPage = () => {
const { message } = useApp(); const { message } = useApp();
const userInfo = useUserInfo(); const userInfo = useUserInfo();
// 添加样式到head中用于处理状态列的hover效果
React.useEffect(() => {
const style = document.createElement('style');
style.textContent = `
.status-radio-item:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.status-radio-item.selected:hover {
transform: translateY(-1px);
}
.ant-radio-wrapper {
margin: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.ant-radio {
position: absolute !important;
opacity: 0 !important;
pointer-events: none !important;
}
.ant-radio-wrapper-checked .status-radio-item {
/* 选中状态的额外样式已在内联样式中处理 */
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
// 状态管理 // 状态管理
const [modals, setModals] = useState({ const [modals, setModals] = useState({
@@ -136,8 +170,23 @@ const SalesPage = () => {
setModals(prev => ({ ...prev, edit: true })); setModals(prev => ({ ...prev, edit: true }));
}, },
showShip: (record: ISalesRecord) => { showShip: (record: ISalesRecord) => {
// 立即显示发货模态框,不等待状态更新
setCurrentRecord(record); setCurrentRecord(record);
setModals(prev => ({ ...prev, ship: true })); setModals(prev => ({ ...prev, ship: true }));
// 在后台异步更新状态(非阻塞)
if (record. !== '已处理') {
updateProcessingStatus(record._id, '已处理')
.then(() => {
// 状态更新成功,静默处理或记录日志
console.log('后台更新状态为已处理成功');
})
.catch((error) => {
console.error('后台更新状态失败:', error);
// 可选:给用户一个轻微的提示,但不影响主流程
message.warning('状态更新失败,请手动更新为已处理');
});
}
}, },
showAfterSales: (record: ISalesRecord, type: typeof AFTER_SALES_TYPES[number]) => { showAfterSales: (record: ISalesRecord, type: typeof AFTER_SALES_TYPES[number]) => {
setCurrentRecord(record); setCurrentRecord(record);
@@ -180,7 +229,7 @@ const SalesPage = () => {
}); });
// 批量预加载产品图片 // 批量预加载产品图片
if (productIds.size > 0) { /*if (productIds.size > 0) {
fetch('/api/products/batchImages', { fetch('/api/products/batchImages', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -202,7 +251,7 @@ const SalesPage = () => {
}).catch((error: Error) => { }).catch((error: Error) => {
console.error('预加载产品图片失败', error); console.error('预加载产品图片失败', error);
}); });
} }*/
// 批量预加载物流状态 // 批量预加载物流状态
if (recordIds.size > 0) { if (recordIds.size > 0) {
@@ -256,6 +305,45 @@ const SalesPage = () => {
} }
}; };
// 更新处理状态
const updateProcessingStatus = async (recordId: string, status: '未处理' | '处理中' | '已处理') => {
try {
const response = await fetch('/api/backstage/sales/updateStatus', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recordId,
处理状态: status,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
// 更新本地状态
setSalesRecords(prevRecords =>
prevRecords.map(record =>
record._id === recordId
? { ...record, 处理状态: status }
: record
)
);
message.success(`状态已更新为:${status}`);
} else {
throw new Error(data.message || '更新失败');
}
} catch (error) {
console.error('更新处理状态失败:', error);
message.error('更新状态失败');
}
};
// 已迁移到modalHandlers中 // 已迁移到modalHandlers中
//const columns: TableColumnType<ISalesRecord>[] = useMemo(() => [ //const columns: TableColumnType<ISalesRecord>[] = useMemo(() => [
const columns: TableColumnType<ISalesRecord>[] = useMemo(() => { const columns: TableColumnType<ISalesRecord>[] = useMemo(() => {
@@ -410,6 +498,134 @@ const SalesPage = () => {
); );
}, },
}, },
// 处理状态列
{
title: "处理状态",
key: "处理状态",
width: 110,
align: "center",
render: (record: ISalesRecord) => {
const currentStatus = record. || '未处理';
// 状态配置
const statusConfig = {
'未处理': {
color: '#ff4d4f',
bgColor: '#fff2f0',
borderColor: '#ffccc7',
icon: '○',
textColor: '#cf1322',
description: '还未报单,请尽快报单'
},
'处理中': {
color: '#faad14',
bgColor: '#fffbf0',
borderColor: '#ffe58f',
icon: '◐',
textColor: '#d48806',
description: '已报单,请尽快处理'
},
'已处理': {
color: '#52c41a',
bgColor: '#f6ffed',
borderColor: '#b7eb8f',
icon: '●',
textColor: '#389e0d',
description: '已发货,请跟踪物流'
}
};
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '6px',
padding: '8px 4px'
}}>
<Radio.Group
value={currentStatus}
onChange={(e) => updateProcessingStatus(record._id, e.target.value)}
style={{ width: '100%' }}
>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
width: '100%'
}}>
{(['未处理', '处理中', '已处理'] as const).map((status) => {
const config = statusConfig[status];
const isSelected = currentStatus === status;
return (
<Radio
key={status}
value={status}
style={{
margin: 0,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Tooltip
title={config.description}
placement="topRight"
color={config.color}
overlayStyle={{
fontSize: '12px',
zIndex: 9999
}}
mouseEnterDelay={0.3}
mouseLeaveDelay={0.1}
>
<div
className={`status-radio-item ${isSelected ? 'selected' : ''}`}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '4px',
padding: '6px 10px',
borderRadius: '8px',
fontSize: '12px',
fontWeight: isSelected ? '600' : '500',
color: isSelected ? config.textColor : '#666',
backgroundColor: isSelected ? config.bgColor : '#fafafa',
border: `1.5px solid ${isSelected ? config.borderColor : '#e8e8e8'}`,
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'pointer',
minWidth: '75px',
boxShadow: isSelected ? `0 3px 6px ${config.color}25, 0 1px 3px ${config.color}15` : '0 1px 2px rgba(0,0,0,0.05)',
position: 'relative',
overflow: 'hidden',
}}
>
<span
style={{
color: config.color,
fontSize: '10px',
lineHeight: 1
}}
>
{config.icon}
</span>
<span style={{ lineHeight: 1 }}>
{status}
</span>
</div>
</Tooltip>
</Radio>
);
})}
</div>
</Radio.Group>
</div>
);
},
},
//备注 //备注
{ {
title: "备注", title: "备注",
@@ -470,31 +686,61 @@ const SalesPage = () => {
style={COMMON_STYLES.copyButton} style={COMMON_STYLES.copyButton}
onClick={async () => { onClick={async () => {
try { try {
if (products.length > 0) { // 并行执行:立即开始复制操作,同时在后台更新状态
try { const copyPromise = (async () => {
// 尝试复制第一款产品的图片 if (products.length > 0) {
const productId = products[0]._id; try {
const blob = await fetchBase64ImageAsBlob(productId); // 尝试复制第一款产品的图片
const productId = products[0]._id;
const blob = await fetchBase64ImageAsBlob(productId);
const clipboardItems: Record<string, Blob | string> = { const clipboardItems: Record<string, Blob | string> = {
"text/plain": new Blob([combinedText], { type: "text/plain" }), "text/plain": new Blob([combinedText], { type: "text/plain" }),
}; };
clipboardItems[blob.type] = blob; clipboardItems[blob.type] = blob;
const clipboardItem = new ClipboardItem(clipboardItems); const clipboardItem = new ClipboardItem(clipboardItems);
await navigator.clipboard.write([clipboardItem]); await navigator.clipboard.write([clipboardItem]);
message.success("客户信息、所有产品名称、备注和图片已复制"); return "已复制发货信息和图片";
} catch (imageError) { } catch (imageError) {
// 图片复制失败,降级到文本复制 // 图片复制失败,降级到文本复制
console.error("图片复制失败,仅复制文本信息:", imageError); console.error("图片复制失败,仅复制文本信息:", imageError);
await navigator.clipboard.writeText(combinedText);
return "已复制发货信息";
}
} else {
// 没有产品时只复制文本
await navigator.clipboard.writeText(combinedText); await navigator.clipboard.writeText(combinedText);
message.success("客户信息、所有产品名称和备注已复制"); return "已复制客户信息";
} }
} else { })();
// 没有产品时只复制文本
await navigator.clipboard.writeText(combinedText); // 后台异步更新状态(不阻塞复制操作)
message.success("客户信息、备注已复制"); const statusPromise = (async () => {
} if (record. !== '处理中') {
try {
await updateProcessingStatus(record._id, '处理中');
return "状态已更新为处理中";
} catch (error) {
console.error('后台更新状态失败:', error);
return "状态更新失败";
}
}
return "状态无需更新";
})();
// 等待复制完成,立即给用户反馈
const copyResult = await copyPromise;
message.success(copyResult);
// 状态更新在后台继续进行,完成后可选择性给出提示
statusPromise.then((statusResult) => {
if (statusResult.includes("已更新")) {
// 可选:给出状态更新的提示,或者静默处理
console.log(statusResult);
}
});
} catch (err) { } catch (err) {
console.error("复制失败:", err); console.error("复制失败:", err);
message.error("复制信息失败"); message.error("复制信息失败");
@@ -659,7 +905,11 @@ const SalesPage = () => {
))} ))}
</div> </div>
<div style={COMMON_STYLES.flexRow}> <div style={COMMON_STYLES.flexRow}>
<Button size="small" type="primary" onClick={() => modalHandlers.showShip(record)}> <Button
size="small"
type="primary"
onClick={() => modalHandlers.showShip(record)}
>
</Button> </Button>
<Button size="small" type="primary" onClick={() => modalHandlers.showEdit(record)}> <Button size="small" type="primary" onClick={() => modalHandlers.showEdit(record)}>

View File

@@ -11,7 +11,6 @@ import {
Select, Select,
DatePicker, DatePicker,
Button, Button,
message,
InputNumber, InputNumber,
Row, Row,
Col, Col,
@@ -19,7 +18,9 @@ import {
Spin, Spin,
Typography, Typography,
Tooltip, Tooltip,
Card} from 'antd'; Card,
App
} from 'antd';
import { import {
UserOutlined, UserOutlined,
ShoppingCartOutlined, ShoppingCartOutlined,
@@ -31,9 +32,10 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ISalesRecord, ICustomer, IProduct, IPaymentPlatform } from '@/models/types'; import { ISalesRecord, ICustomer, IProduct, IPaymentPlatform } from '@/models/types';
import axios from 'axios';
import { useUserInfo } from '@/store/userStore'; import { useUserInfo } from '@/store/userStore';
const { useApp } = App;
const { Title, Text } = Typography; const { Title, Text } = Typography;
const { Option } = Select; const { Option } = Select;
@@ -57,12 +59,15 @@ const ProductSelectOption = ({ product }: { product: IProduct }) => {
const fetchImage = async () => { const fetchImage = async () => {
try { try {
const response = await axios.get(`/api/products/images/${product._id}`); const response = await fetch(`/api/products/images/${product._id}`);
if (response.data && response.data.image) { if (response.ok) {
setImageSrc(response.data.image); const data = await response.json();
setImageLoaded(true); if (data && data.image) {
setImageSrc(data.image);
setImageLoaded(true);
}
} }
} catch (error) { } catch (error: unknown) {
console.error('获取产品图片失败:', error); console.error('获取产品图片失败:', error);
} }
}; };
@@ -130,6 +135,7 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
const [paymentPlatforms, setPaymentPlatforms] = useState<IPaymentPlatform[]>([]); const [paymentPlatforms, setPaymentPlatforms] = useState<IPaymentPlatform[]>([]);
const userInfo = useUserInfo(); // 获取当前用户信息 const userInfo = useUserInfo(); // 获取当前用户信息
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<any[]>([]);
const { message } = useApp(); // 使用 useApp hook 获取 message 实例
// 加载状态 // 加载状态
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -210,10 +216,15 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
*/ */
const fetchUsers = async (teamId: string) => { const fetchUsers = async (teamId: string) => {
try { try {
const { data } = await axios.get(`/api/backstage/users?teamId=${teamId}`); const response = await fetch(`/api/backstage/users?teamId=${teamId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setUsers(data.users); setUsers(data.users);
return data.users; return data.users;
} catch (error) { } catch (error: unknown) {
console.error('加载用户数据失败:', error);
message.error('加载用户数据失败'); message.error('加载用户数据失败');
return []; return [];
} }
@@ -221,10 +232,15 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
const fetchCustomers = async (teamId: string) => { const fetchCustomers = async (teamId: string) => {
try { try {
const { data } = await axios.get(`/api/backstage/customers?teamId=${teamId}`); const response = await fetch(`/api/backstage/customers?teamId=${teamId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setCustomers(data.customers); setCustomers(data.customers);
return data.customers; return data.customers;
} catch (error) { } catch (error: unknown) {
console.error('加载客户数据失败:', error);
message.error('加载客户数据失败'); message.error('加载客户数据失败');
return []; return [];
} }
@@ -232,10 +248,15 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
const fetchProducts = async (teamId: string) => { const fetchProducts = async (teamId: string) => {
try { try {
const { data } = await axios.get(`/api/backstage/products?teamId=${teamId}`); const response = await fetch(`/api/backstage/products?teamId=${teamId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setProducts(data.products); setProducts(data.products);
return data.products; return data.products;
} catch (error) { } catch (error: unknown) {
console.error('加载产品数据失败:', error);
message.error('加载产品数据失败'); message.error('加载产品数据失败');
return []; return [];
} }
@@ -243,10 +264,15 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
const fetchPaymentPlatforms = async (teamId: string) => { const fetchPaymentPlatforms = async (teamId: string) => {
try { try {
const { data } = await axios.get(`/api/backstage/payment-platforms?teamId=${teamId}`); const response = await fetch(`/api/backstage/payment-platforms?teamId=${teamId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setPaymentPlatforms(data.platforms); setPaymentPlatforms(data.platforms);
return data.platforms; return data.platforms;
} catch (error) { } catch (error: unknown) {
console.error('加载收款平台数据失败:', error);
message.error('加载收款平台数据失败'); message.error('加载收款平台数据失败');
return []; return [];
} }
@@ -268,7 +294,18 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
if (record?._id) { if (record?._id) {
// 更新已有记录 // 更新已有记录
await axios.put(`/api/backstage/sales/Records/${record._id}`, updatedRecord); const response = await fetch(`/api/backstage/sales/Records/${record._id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedRecord)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
message.success('订单更新成功'); message.success('订单更新成功');
} else { } else {
// 创建新记录 (实际未实现) // 创建新记录 (实际未实现)
@@ -314,7 +351,7 @@ const SalesModal: React.FC<SalesModalProps> = ({ visible, onOk, onCancel, record
</Button>, </Button>,
]} ]}
maskClosable={false} maskClosable={false}
destroyOnClose={true} destroyOnHidden={true}
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ minHeight: '200px' }}> <div style={{ minHeight: '200px' }}>

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, Button, message } from 'antd'; import { Modal, Form, Input, Button, App } from 'antd';
import axios from 'axios';
import { ISalesRecord } from '@/models/types'; import { ISalesRecord } from '@/models/types';
const { useApp } = App;
import { useUserInfo } from '@/store/userStore'; import { useUserInfo } from '@/store/userStore';
interface ShipModalProps { interface ShipModalProps {
@@ -15,6 +16,7 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
const [form] = Form.useForm(); const [form] = Form.useForm();
const [logisticsNumbers, setLogisticsNumbers] = useState<{ [key: string]: string }>({}); // 保存每个产品的物流单号 const [logisticsNumbers, setLogisticsNumbers] = useState<{ [key: string]: string }>({}); // 保存每个产品的物流单号
const userInfo = useUserInfo(); // 获取当前用户信息 const userInfo = useUserInfo(); // 获取当前用户信息
const { message } = useApp(); // 使用 useApp hook 获取 message 实例
useEffect(() => { useEffect(() => {
if (record) { if (record) {
@@ -32,11 +34,15 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
setLogisticsNumbers(initialLogisticsNumbers); setLogisticsNumbers(initialLogisticsNumbers);
// 获取已有的物流记录并填充单号 // 获取已有的物流记录并填充单号
axios fetch(`/api/tools/logistics?关联记录=${record._id}`)
.get('/api/tools/logistics', { params: { 关联记录: record._id } }) .then(async (response) => {
.then((res) => { if (!response.ok) {
const logisticsRecords = res.data; throw new Error(`网络错误或服务器错误: ${response.status}`);
if (logisticsRecords && Array.isArray(logisticsRecords)) { }
const logisticsRecords = await response.json();
// 处理返回的物流记录(可能是空数组)
if (logisticsRecords && Array.isArray(logisticsRecords) && logisticsRecords.length > 0) {
const updatedLogisticsNumbers: { [key: string]: string } = { ...initialLogisticsNumbers }; const updatedLogisticsNumbers: { [key: string]: string } = { ...initialLogisticsNumbers };
// 遍历物流记录,将已有的单号填充到对应的产品 // 遍历物流记录,将已有的单号填充到对应的产品
@@ -47,13 +53,17 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
} }
}); });
setLogisticsNumbers(updatedLogisticsNumbers); // 更新状态以填充表单 setLogisticsNumbers(updatedLogisticsNumbers);
console.log('已加载现有物流记录');
} else {
// 没有物流记录是正常情况,不需要错误提示
console.log('暂无物流记录,将创建新记录');
} }
}) })
//查不到物流记录时,提示警告而不是报错,警告信息为“暂无物流记录” .catch((err: unknown) => {
.catch((err) => {
console.error('获取物流记录失败:', err); console.error('获取物流记录失败:', err);
message.error('暂无物流记录'); // 只在真正的网络错误时才提示用户
message.warning('获取现有物流记录失败,将创建新记录');
}); });
} }
}, [record, form]); }, [record, form]);
@@ -83,10 +93,22 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
产品: productsWithLogisticsNumbers // 只提交填写了物流单号的产品 产品: productsWithLogisticsNumbers // 只提交填写了物流单号的产品
}; };
await axios.post('/api/tools/logistics', logisticsData); const response = await fetch('/api/tools/logistics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(logisticsData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
message.success('发货信息提交成功'); message.success('发货信息提交成功');
onOk(); // 关闭模态框 onOk(); // 关闭模态框
} catch (error) { } catch (error: unknown) {
console.error('发货信息提交失败:', error);
message.error('发货信息提交失败'); message.error('发货信息提交失败');
} }
}; };
@@ -121,7 +143,7 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
<Input placeholder="自动填入客户电话尾号" disabled /> <Input placeholder="自动填入客户电话尾号" disabled />
</Form.Item> </Form.Item>
{/* 动态生成每个产品的物流单号输入框 */} {/* 动态生成每个产品的物流单号输入框 */}
{record?.?.map(product => ( {record?.?.map(product => (
<Form.Item <Form.Item
key={product._id} key={product._id}
@@ -134,7 +156,7 @@ const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }
onChange={e => { onChange={e => {
const cleanedValue = e.target.value.replace(/[\s.,/#!$%\^&\*;:{}=\-_`~()<>[\]'"|\\?@+]/g, '');//过滤特殊字符和空格 const cleanedValue = e.target.value.replace(/[\s.,/#!$%\^&\*;:{}=\-_`~()<>[\]'"|\\?@+]/g, '');//过滤特殊字符和空格
handleLogisticsNumberChange(product._id, cleanedValue); handleLogisticsNumberChange(product._id, cleanedValue);
}} }}
/> />
</Form.Item> </Form.Item>
))} ))}

View File

@@ -0,0 +1,388 @@
/**
* 新增客户组件
* 作者: 阿瑞
* 功能: 客户信息录入支持智能地址解析快递100 API + 备用API
* 版本: v2.0
*/
import React, { useEffect, useState } from 'react';
import {
Modal,
Button,
Form,
Input,
DatePicker,
Row,
Col,
message,
Popover,
Switch,
Space,
Typography,
Divider,
Card
} from 'antd';
import axios from 'axios';
import { ICustomer } from '@/models/types';
import { useUserInfo } from '@/store/userStore';
import {
ThunderboltOutlined,
ApiOutlined,
InfoCircleOutlined,
UserAddOutlined
} from '@ant-design/icons';
const { Text, Title } = Typography;
interface AddCustomerComponentProps {
visible: boolean;
onClose: () => void;
onSuccess: (customer: ICustomer) => void;
}
interface CombinedResponse {
name: string | null;
phone: string | null;
address: {
province: string | null;
city: string | null;
county: string | null;
detail: string | null;
full_location: string | null;
orig_location: string | null;
town: string | null;
village: string | null;
} | null;
}
const AddCustomerComponent: React.FC<AddCustomerComponentProps> = ({
visible,
onClose,
onSuccess
}) => {
const [form] = Form.useForm();
const userInfo = useUserInfo();
const [loading, setLoading] = useState(false);
const [address, setAddress] = useState<string>('');
const [useKuaidi100, setUseKuaidi100] = useState(true); // 默认使用快递100
// 表单提交处理
const handleSubmit = async (values: any) => {
const formattedValues = {
...values,
生日: values.生日 ? values..format('YYYY-MM-DD') : undefined,
加粉日期: values.加粉日期 ? values..format('YYYY-MM-DD') : undefined,
团队: userInfo.团队?._id,
};
try {
const response = await axios.post('/api/backstage/customers', formattedValues);
const newCustomer = response.data.customer;
message.success('客户添加成功');
onSuccess(newCustomer);
form.resetFields();
} catch (error: any) {
if (error.response && error.response.data && error.response.data.message) {
message.error(error.response.data.message);
} else {
message.error('添加客户失败,请检查输入的数据');
}
console.error('请求处理失败', error);
}
};
// 显示模态框时重置表单
useEffect(() => {
if (visible) {
form.resetFields();
setAddress('');
}
}, [visible, form]);
// 地址输入框变化处理
const handleAddressChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setAddress(event.target.value);
};
// 智能地址解析处理
const handleAddressParse = async () => {
if (!address.trim()) {
message.error('请输入地址后再进行解析');
return;
}
setLoading(true);
try {
// 根据切换状态选择API端点
const apiEndpoint = useKuaidi100
? '/api/tools/parseAddressKuaidi100'
: '/api/tools/parseAddress';
console.log(`使用${useKuaidi100 ? '快递100' : '备用'}API解析地址:`, address);
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: address }),
});
const data: CombinedResponse & { error?: string } = await response.json();
if (!response.ok) {
// 如果快递100失败自动切换到备用API
if (useKuaidi100 && response.status !== 400) {
message.warning('快递100解析失败正在尝试备用解析...');
setUseKuaidi100(false);
// 递归调用备用API
setTimeout(() => {
handleAddressParse();
}, 500);
return;
}
message.error(data.error || '解析失败');
return;
}
// 检查解析结果完整性
const missingFields = [];
if (!data.name) missingFields.push('姓名');
if (!data.phone) missingFields.push('电话');
if (!data.address?.province) missingFields.push('省份');
if (!data.address?.city) missingFields.push('城市');
if (!data.address?.county) missingFields.push('区县');
if (missingFields.length > 0) {
message.warning(`未能自动解析 ${missingFields.join('、')},请手动填写`);
} else {
message.success(`${useKuaidi100 ? '快递100' : '备用'}解析完成`);
}
// 自动填充表单字段
form.setFieldsValue({
姓名: data.name || '',
电话: data.phone || '',
: {
省份: data.address?.province || '',
城市: data.address?.city || '',
区县: data.address?.county || '',
详细地址: data.address?.detail || '',
},
});
} catch (error) {
console.error('地址解析失败', error);
// 如果快递100出现网络错误尝试备用API
if (useKuaidi100) {
message.warning('快递100服务异常正在尝试备用解析...');
setUseKuaidi100(false);
setTimeout(() => {
handleAddressParse();
}, 500);
} else {
message.error('地址解析失败,请检查网络连接');
}
} finally {
setLoading(false);
}
};
// API切换处理
const handleApiSwitch = (checked: boolean) => {
setUseKuaidi100(checked);
message.info(`已切换到${checked ? '快递100' : '备用'}解析API`);
};
return (
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<UserAddOutlined style={{ color: '#1890ff' }} />
<Title level={3} style={{ margin: 0, color: '#1890ff' }}>
</Title>
</div>
}
open={visible}
onOk={form.submit}
onCancel={onClose}
width="80%"
style={{ top: "15%" }}
okText="确认添加"
cancelText="取消"
>
{/* 客户信息表单 */}
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="姓名" label="姓名" rules={[{ required: true, message: '请输入客户姓名' }]}>
<Input placeholder="请输入客户姓名" />
</Form.Item>
<Form.Item
name="电话"
label="电话"
rules={[
{ required: true, message: '请输入电话号码' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码' }
]}
normalize={(value) => value && value.replace(/[^+\d]/g, '')}
>
<Input placeholder="请输入手机号码" />
</Form.Item>
<Form.Item name="加粉日期" label="加粉日期" rules={[{ required: true, message: '请选择加粉日期' }]}>
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD"
placeholder="选择加粉日期"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="地址" required>
<Input.Group compact>
<Form.Item
name={['地址', '省份']}
noStyle
>
<Input style={{ width: '15%' }} placeholder="省份" />
</Form.Item>
<Form.Item
name={['地址', '城市']}
noStyle
>
<Input style={{ width: '15%' }} placeholder="城市" />
</Form.Item>
<Form.Item
name={['地址', '区县']}
noStyle
>
<Input style={{ width: '15%' }} placeholder="区县" />
</Form.Item>
<Form.Item
name={['地址', '详细地址']}
noStyle
>
<Input style={{ width: '55%' }} placeholder="详细地址" />
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item name="微信" label="微信">
<Input placeholder="微信号(可选)" />
</Form.Item>
<Form.Item name="生日" label="生日">
<DatePicker
style={{ width: '100%' }}
format="YYYY-MM-DD"
placeholder="选择生日(可选)"
/>
</Form.Item>
</Col>
</Row>
</Form>
<Divider orientation="left">
<Space>
<ThunderboltOutlined style={{ color: '#52c41a' }} />
<Text strong></Text>
</Space>
</Divider>
{/* 智能地址解析区域 */}
<Card size="small" style={{ backgroundColor: '#fafafa' }}>
{/* API切换开关 */}
<div style={{ marginBottom: 12 }}>
<Space align="center">
<Text strong>:</Text>
<Switch
checked={useKuaidi100}
onChange={handleApiSwitch}
checkedChildren={
<Space size={4}>
<ThunderboltOutlined />
<span>100</span>
</Space>
}
unCheckedChildren={
<Space size={4}>
<ApiOutlined />
<span></span>
</Space>
}
style={{ backgroundColor: useKuaidi100 ? '#52c41a' : '#1890ff' }}
/>
<Popover
title="解析引擎说明"
content={
<div style={{ maxWidth: 300 }}>
<div style={{ marginBottom: 8 }}>
<Text strong style={{ color: '#52c41a' }}>100 ()</Text>
<br />
<Text type="secondary"> </Text>
<br />
<Text type="secondary"> 99%</Text>
<br />
<Text type="secondary"> </Text>
</div>
<div>
<Text strong style={{ color: '#1890ff' }}></Text>
<br />
<Text type="secondary"> </Text>
<br />
<Text type="secondary"> 使</Text>
<br />
<Text type="secondary"> </Text>
</div>
</div>
}
trigger="hover"
>
<InfoCircleOutlined style={{ color: '#1890ff', cursor: 'help' }} />
</Popover>
</Space>
</div>
{/* 地址输入框 */}
<Input.TextArea
rows={3}
placeholder="请输入完整的收件人信息,例如:张三 13800138000 广东省深圳市南山区科技园南区..."
value={address}
onChange={handleAddressChange}
style={{ marginBottom: 12 }}
autoComplete="off"
/>
{/* 解析按钮 */}
<Button
type="primary"
block
onClick={handleAddressParse}
loading={loading}
icon={useKuaidi100 ? <ThunderboltOutlined /> : <ApiOutlined />}
style={{
backgroundColor: useKuaidi100 ? '#52c41a' : '#1890ff',
borderColor: useKuaidi100 ? '#52c41a' : '#1890ff'
}}
>
{loading
? `正在使用${useKuaidi100 ? '快递100' : '备用'}引擎解析...`
: `使用${useKuaidi100 ? '快递100' : '备用'}引擎解析地址`
}
</Button>
{/* 解析提示 */}
<div style={{ marginTop: 8, textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
{useKuaidi100
? '🚀 快递100专业解析支持复杂地址格式解析失败将自动切换备用引擎'
: '🔧 备用解析引擎,提供基础地址识别功能'
}
</Text>
</div>
</Card>
</Modal>
);
};
export default AddCustomerComponent;

View File

@@ -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<AddProductProps> = ({ 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<IBrand[]>([]);
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
const [categories, setCategories] = useState<ICategory[]>([]);
const [productData, setProductData] = useState<Omit<IProduct, '_id' | '供应链报价'>>({
: {} 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<HTMLDivElement>) => {
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<FileReader>) => {
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: <SaveOutlined style={{ color: '#52c41a' }} />
});
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 (
<Modal
title={
<Space>
<ShoppingOutlined />
<span></span>
</Space>
}
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' }
}}
>
<Spin spinning={loading}>
<div style={{ minHeight: '300px' }}>
{loading && <div style={{ textAlign: 'center', padding: '50px', color: '#999' }}>...</div>}
<Form
form={form}
layout="vertical"
onFinish={onFinish}
requiredMark="optional"
>
<Row gutter={24}>
{/* 左侧表单区域 */}
<Col span={16}>
{/* 基本信息 */}
<Card
title={
<Space>
<ShoppingOutlined />
<span></span>
</Space>
}
size="small"
style={{ marginBottom: 16 }}
>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="名称"
label="产品名称"
rules={[{ required: true, message: '请输入产品名称' }]}
>
<Input
placeholder="输入产品名称"
prefix={<TagOutlined />}
maxLength={50}
showCount
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="描述" label="产品描述">
<Input
placeholder="输入产品描述或备注"
prefix={<QuestionCircleOutlined />}
maxLength={100}
showCount
/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item name="别名" label="别名" help="多个别名请用英文逗号,分隔">
<Input
placeholder="输入产品别名,例如:红色款,特价款"
prefix={<TagOutlined />}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="编码" label="产品编码">
<Input
placeholder="输入产品编码"
prefix={<BarcodeOutlined />}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="货号" name="货号">
<Input
placeholder="输入产品货号"
prefix={<BarcodeOutlined />}
/>
</Form.Item>
</Col>
</Row>
</Card>
{/* 分类信息 */}
<Card
title={
<Space>
<InboxOutlined />
<span></span>
</Space>
}
size="small"
style={{ marginBottom: 16 }}
>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label="产品类别"
name="品类"
rules={[{ required: true, message: '请选择产品类别' }]}
>
<Select
onChange={(value) => handleChange('品类', value)}
placeholder="选择产品类别"
showSearch
optionFilterProp="children"
getPopupContainer={triggerNode => triggerNode.parentElement || document.body}
>
{categories.map((category) => (
<Option key={category._id} value={category._id}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{category.icon && (
<Icon icon={category.icon} width={16} height={16} style={{ marginRight: 8 }} />
)}
{category.name}
</div>
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="品牌"
name="品牌"
rules={[{ required: true, message: '请选择品牌' }]}
>
<Select
onChange={(value) => handleChange('品牌', value)}
placeholder="选择品牌"
showSearch
optionFilterProp="children"
getPopupContainer={triggerNode => triggerNode.parentElement || document.body}
>
{brands.map((brand) => (
<Option key={brand._id} value={brand._id}>
{brand.name}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="供应商"
name="供应商"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
onChange={(value) => handleChange('供应商', value)}
placeholder="选择供应商"
showSearch
optionFilterProp="children"
getPopupContainer={triggerNode => triggerNode.parentElement || document.body}
>
{suppliers.map((supplier) => (
<Option
key={supplier._id}
value={supplier._id}
disabled={supplier.status === 0}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{supplier.}</span>
{supplier..map((category) => (
<Tag
key={category._id}
color="blue"
style={{
display: 'flex',
alignItems: 'center',
marginLeft: 8
}}
>
{category.icon && (
<Icon
icon={category.icon}
width={12}
height={12}
style={{ marginRight: 4 }}
/>
)}
{category.name}
</Tag>
))}
</div>
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Card>
{/* 价格信息 */}
<Card
title={
<Space>
<DollarOutlined />
<span></span>
</Space>
}
size="small"
>
<Row gutter={24}>
<Col span={6}>
<Form.Item
label="售价"
name="售价"
rules={[{ required: true, message: '请输入产品售价' }]}
>
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
placeholder="输入产品售价"
prefix="¥"
addonAfter="元"
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="成本价"
label={
<Tooltip title="产品本身的采购成本">
<Space>
<span></span>
<QuestionCircleOutlined style={{ fontSize: 14 }} />
</Space>
</Tooltip>
}
>
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
placeholder="输入成本价格"
prefix="¥"
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="包装费"
label={
<Tooltip title="外包装、防震材料等费用">
<Space>
<span></span>
<ScissorOutlined style={{ fontSize: 14 }} />
</Space>
</Tooltip>
}
>
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
placeholder="输入包装费"
prefix="¥"
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="运费"
label={
<Tooltip title="物流运输费用">
<Space>
<span></span>
<CarOutlined style={{ fontSize: 14 }} />
</Space>
</Tooltip>
}
>
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
placeholder="输入运费"
prefix="¥"
/>
</Form.Item>
</Col>
</Row>
{/* 计算毛利率 */}
{grossProfitMargin && form.getFieldValue('售价') > 0 && (
<div style={{
padding: '8px 12px',
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
marginTop: '8px'
}}>
<Space align="center">
<DollarOutlined style={{ color: '#52c41a' }} />
<Text>: </Text>
<Text strong style={{ color: '#52c41a' }}>
{calculateGrossProfitMargin()}
</Text>
</Space>
</div>
)}
</Card>
</Col>
{/* 右侧图片上传和提交按钮 */}
<Col span={8}>
<Card
title={
<Space>
<PictureOutlined />
<span></span>
</Space>
}
size="small"
style={{ marginBottom: 16 }}
>
<Form.Item
label="图片"
required
help="粘贴图片(Ctrl+V)或拖拽图片到此区域"
>
<Spin spinning={imageLoading}>
<div style={{ position: 'relative' }}>
{imageLoading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10,
color: '#999'
}}>
...
</div>
)}
<div
onPaste={handlePaste}
style={{
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '16px',
minHeight: '360px',
border: '1px dashed #d9d9d9',
borderRadius: '8px',
background: '#fafafa',
transition: 'all 0.3s',
position: 'relative',
}}
onDragOver={(e) => {
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. ? (
<div style={{ position: 'relative', width: '100%', textAlign: 'center' }}>
<img
src={productData.}
alt="产品图片"
style={{
maxWidth: '100%',
maxHeight: '360px',
borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}
/>
<Button
type="primary"
danger
shape="circle"
icon={<CloseOutlined />}
onClick={(e) => {
e.stopPropagation();
setProductData(prev => ({ ...prev, : '' }));
}}
style={{
position: 'absolute',
top: -10,
right: -10,
}}
size="small"
/>
</div>
) : (
<div style={{ textAlign: 'center' }}>
<PictureOutlined style={{ fontSize: 48, color: '#bfbfbf' }} />
<p style={{ marginTop: 16, color: '#666' }}>
<Space>
<UploadOutlined />
<span></span>
</Space>
</p>
<p style={{ color: '#bfbfbf', fontSize: 12 }}>
建议尺寸: 800×800px, JPGPNG
</p>
</div>
)}
</div>
</div>
</Spin>
</Form.Item>
</Card>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
onClick={() => form.submit()}
loading={loading}
block
style={{ height: 48 }}
>
</Button>
</Col>
</Row>
</Form>
</div>
</Spin>
</Modal>
);
};
export default AddProductComponent;

View File

@@ -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<CustomerInfoComponentProps> = ({
customer,
onAddCustomerClick,
onCustomerUpdate,
}) => {
const [rechargeVisible, setRechargeVisible] = useState(false); // 充值对话框是否可见
const [couponModalVisible, setCouponModalVisible] = useState(false); // 优惠券对话框是否可见
const [balance, setBalance] = useState<number>(0); // 存储客户的余额
const [coupons, setCoupons] = useState<any[]>([]); // 存储客户的优惠券
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 (
<Card
title={<div className="text-2xl"></div>}
style={{ minHeight: '380px' }} // 设定Card最小高度确保无论客户是否选择卡片高度保持一致
extra={
<>
{loading ? (
<></>
) : customer ? (
<>
<Button
icon={<EditOutlined />}
style={{
marginRight: 8,
}}
onClick={handleEditCustomerClick}></Button>
<Button
//type="primary"
icon={<DollarCircleOutlined />}
style={{
marginRight: 8,
}}
onClick={() => setRechargeVisible(true)}
//block // 按钮宽度占满父容器
>
</Button>
<Button
//type="primary"
icon={<RedEnvelopeOutlined />}
onClick={() => setCouponModalVisible(true)}
//block
>
</Button>
</>
) : (
<div>
</div>
)}
</>
}
>
{loading ? (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
padding: '40px 0'
}}>
<Spin />
</div>
) : customer ? (
<>
<div style={{ display: 'flex', marginBottom: 24 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginRight: 24 }}>
<Avatar
size={64}
icon={<UserOutlined />}
style={{ marginBottom: 8 }}
/>
<div>{customer. || '未提供'}</div>
<div
//灰色字体、字号稍小
style={{ color: '#999', fontSize: 14 }}
>{customer. ? dayjs(customer.).format('YYYY-MM-DD') : 'null'}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', marginTop: 8 }}
>
{storeAccounts.length > 0 ? storeAccounts.map((storeAccount) =>
<span key={storeAccount} style={{ marginRight: 8 }}>
<Tag icon={<ShopOutlined />}>{storeAccount}</Tag>
</span>) : ''}
</div>
</div>
<div style={{ flex: 1 }}>
<Descriptions
column={1}
layout="horizontal"
labelStyle={{ fontWeight: 'bold', }}
contentStyle={{ color: '#555', whiteSpace: 'normal' }}
>
<Descriptions.Item
label={
<span>
<PhoneOutlined style={{ marginRight: 8 }} />
</span>
}
>
<Text copyable>{customer. || '未提供'}</Text>
</Descriptions.Item>
<Descriptions.Item
label={
<span>
<WechatOutlined style={{ marginRight: 8 }} />
</span>
}
>
<Text copyable>{customer. || '未提供'}</Text>
</Descriptions.Item>
<Descriptions.Item
label={
<span>
<GiftOutlined style={{ marginRight: 8 }} />
</span>
}
>
{customer. ? dayjs(customer.).format('YYYY-MM-DD') : '未提供'}
</Descriptions.Item>
<Descriptions.Item
label={
<span>
<HomeOutlined style={{ marginRight: 8 }} />
</span>
}
>
{formatAddress(customer.)}
</Descriptions.Item>
</Descriptions>
</div>
</div>
{/* 分割线 */}
<div style={{ margin: '24px 0', borderTop: '1px solid #eee' }}></div>
<Descriptions
column={2}
//labelStyle={{ color: '#555', fontSize: '14px' }}
//contentStyle={{ color: '#333',}}
labelStyle={{ fontWeight: 'bold' }}
contentStyle={{ color: '#555' }}
>
<Descriptions.Item label="余额"><span >¥{balance.toFixed(2)}</span></Descriptions.Item>
<Descriptions.Item label="券码"><span >{coupons.filter((coupon) => !coupon.使).length} </span></Descriptions.Item>
<Descriptions.Item label="积分">{incomeLoading ? (<Spin size="small" />) : (<span >{totalIncome.toFixed(0)}</span>)}</Descriptions.Item>
<Descriptions.Item><Button size='small'
//灰色字体、字号稍小
style={{ color: '#999', fontSize: 14 }}
onClick={handleSalesModalOpen} ></Button></Descriptions.Item>
</Descriptions>
{/* 模态框 */}
<Modal
title="消费详情"
open={salesModalVisible}
onCancel={handleSalesModalClose}
footer={null}
width={'90%'} // 自定义模态框的宽度
//顶部距离
style={{ top: 6 }}
>
<SalesRecordPage initialPhone={customer?.} /> {/* 传入客户的手机号 */}
</Modal>
<Recharge
visible={rechargeVisible}
onCancel={() => setRechargeVisible(false)}
customer={customer}
onRechargeSuccess={handleRechargeSuccess}
/>
<CouponModal
visible={couponModalVisible}
onCancel={() => setCouponModalVisible(false)}
onOk={handleCouponAssignSuccess}
customerId={customer._id}
/>
<EditCustomerInfo
visible={editCustomerModalVisible}
customer={customer} // 传入当前客户
onOk={handleEditCustomerModalOk} // 传入父组件的回调
onCancel={() => setEditCustomerModalVisible(false)}
/>
</>
) : (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<UserOutlined style={{ fontSize: 48, color: '#999' }} />
<p style={{ marginTop: 16, color: '#999' }}> </p>
<Button type="primary" icon={<PlusOutlined />} onClick={onAddCustomerClick}>
</Button>
<p style={{ marginTop: 16, color: '#999' }}></p>
</div>
)}
</Card>
);
};
export default CustomerInfoComponent;

View File

@@ -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<EditCustomerInfoProps> = ({ 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 (
<Modal
title={customer ? '编辑客户信息' : '新增客户'}
open={visible}
onOk={handleSubmit}
onCancel={() => {
form.resetFields();
onCancel();
}}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="姓名"
label="姓名"
rules={[{ required: true, message: '请输入客户姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item
name="电话"
label="电话"
rules={[{ required: true, message: '请输入客户电话' }]}
>
<Input placeholder="请输入电话" />
</Form.Item>
<Form.Item label="地址" required>
<Input.Group compact>
<Form.Item name={['地址', '省份']} noStyle>
<Input style={{ width: '15%' }} placeholder="省份" />
</Form.Item>
<Form.Item name={['地址', '城市']} noStyle>
<Input style={{ width: '15%' }} placeholder="城市" />
</Form.Item>
<Form.Item name={['地址', '区县']} noStyle>
<Input style={{ width: '15%' }} placeholder="区县" />
</Form.Item>
<Form.Item name={['地址', '详细地址']} noStyle>
<Input style={{ width: '55%' }} placeholder="详细地址" />
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item name="微信" label="微信">
<Input placeholder="请输入微信" />
</Form.Item>
<Form.Item name="生日" label="生日">
<DatePicker />
</Form.Item>
<Form.Item name="加粉日期" label="加粉日期">
<DatePicker />
</Form.Item>
</Form>
</Modal>
);
};
export default EditCustomerInfo;

View File

@@ -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<ProductInfoComponentProps> = ({ 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 (
<Card
title={
<div className="text-2xl">
<span
className="text-sm font-normal text-gray-400">
{products.length} : ¥{totalPrice}
</span>
</div>
}
style={{ minHeight: '380px' }} // 设定Card最小高度确保无论是否选择产品卡片高度保持一致
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={onAddProductClick}>
</Button>
}
>
{/* 使用flex布局 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', justifyContent: 'center' }}>
{productsToDisplay.map((product) => (
<div
key={product._id}
style={{
position: 'relative',
//width: '260px',
//height: '260px',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
//backgroundColor: '#f9f9f9',
}}
>
{/* 产品图片 */}
<ProductImage
productId={product._id}
alt={product.}
style={{
//width: '100%',
//height: '100%',
width: '260px',
height: '260px',
objectFit: 'cover',
filter: 'brightness(0.7)', // 图片调暗,凸显文字
}}
/>
{/* 产品信息覆盖在图片上 */}
<div
style={{
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
color: '#fff',
padding: '10px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.5)',
}}
>
{/* 产品名称 */}
<div>
<Title level={4} style={{ margin: 0, color: '#fff' }}>
{product.}
</Title>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<SignatureOutlined /> {product. || ''}
</p>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<TrademarkCircleOutlined /> {product.?.name || ''}
</p>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<TruckOutlined /> {product.?.?. || ''}
</p>
</div>
{/* 产品售价和货号信息 */}
<div>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<BarcodeOutlined /> {product. || ''}
</p>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<DollarCircleOutlined />
{product. > 0 ? ` ¥${product.}` : ''}
</p>
<p style={{ fontSize: '12px', margin: '4px 0' }}>
<TagsOutlined /> {Array.isArray(product.) && product..length > 0 ? product..join('') : ''}
</p>
</div>
</div>
</div>
))}
</div>
</Card>
);
};
export default ProductInfoComponent;

View File

@@ -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<RechargeProps> = ({ visible, onCancel, customer, onRechargeSuccess }) => {
const [loading, setLoading] = useState(false);
const [balance, setBalance] = useState<number | null>(null);
const [transactions, setTransactions] = useState<any[]>([]); // 存储交易记录
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 (
<Modal
open={visible}
//title="充值"
title={<div className="text-2xl"></div>}
onCancel={onCancel}
footer={[
<Button key="back" onClick={onCancel}>
</Button>,
<Button key="submit" type="primary" loading={loading} onClick={handleRecharge}>
</Button>,
]}
>
{balance !== null && balance !== undefined ? (
<div style={{ marginBottom: '16px' }}>
<h2>: ¥ {balance.toFixed(2)}</h2>
</div>
) : (
<div style={{ marginBottom: '16px' }}>
<strong>: ¥ 0</strong>
</div>
)}
<Form form={form} layout="vertical">
<Form.Item
name="金额"
label="充值金额"
rules={[{ required: true, message: '请输入充值金额' }]}
>
<InputNumber min={0.01} step={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="描述"
label="描述"
>
<Input placeholder="可选,填写描述信息" />
</Form.Item>
</Form>
<div style={{ marginTop: '24px' }}>
<h3></h3>
<div style={{ maxHeight: '320px', overflowY: 'auto' }}>
<List
itemLayout="horizontal"
dataSource={transactions}
renderItem={item => (
<List.Item>
<List.Item.Meta
title={`类型: ${item.}`}
description={`金额: ¥${item..toFixed(2)} - 日期: ${new Date(item.).toLocaleDateString()} - 描述: ${item.}`}
/>
</List.Item>
)}
/>
</div>
</div>
</Modal>
);
};
export default Recharge;

View File

@@ -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<SelectCustomerProps> = ({ 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 (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Select
showSearch
allowClear
size="large"
style={{ width: '100%' }}
placeholder="选择客户"
onChange={handleChange}
filterOption={filterOption}
>
{customers.map(customer => (
<Select.Option key={customer._id} value={customer._id}>
{customer.} - {customer.}
</Select.Option>
))}
</Select>
</Space>
);
};
export default SelectCustomer;

View File

@@ -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<SelectProductProps> = ({ 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 (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Select
showSearch
allowClear
size='large'
style={{ width: '100%' }}
placeholder="选择产品"
onChange={handleChange}
filterOption={filterOption}
>
{products.map(product => (
<Select.Option key={product._id} value={product._id}>
{product.?.name} - {product.?.} - {product.?.name} - {product.}
</Select.Option>
))}
</Select>
</Space>
);
};
export default SelectProduct;

View File

@@ -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;
}

View File

@@ -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<CouponModalProps> = ({ visible, onOk, onCancel, customerId }) => {
const [form] = Form.useForm();
const userInfo = useUserInfo(); // 获取当前用户信息
const [teamCoupons, setTeamCoupons] = useState<ICoupon[]>([]);
const [customerCoupons, setCustomerCoupons] = useState<ICustomerCoupon[]>([]);
const [defaultCouponIds, setDefaultCouponIds] = useState<string[]>([]); // 默认选中的优惠券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 (
<Modal
title={<div className="text-2xl"></div>}
open={visible}
onOk={handleSubmit}
onCancel={() => {
form.resetFields();
onCancel();
}}
>
<Form form={form} layout="vertical">
<Form.Item
name="couponIds"
label="选择优惠券"
rules={[{ required: true, message: '请选择一个或多个优惠券!' }]}
>
<Select
mode="multiple"
placeholder="请选择一个或多个优惠券"
value={defaultCouponIds}
>
{teamCoupons.map((coupon) => (
<Select.Option key={coupon._id} value={coupon._id}>
{`${coupon.} (${coupon.})`}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>
<Typography.Title level={5}></Typography.Title>
<List
bordered
dataSource={customerCoupons}
renderItem={(coupon) => (
<List.Item>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#888', fontSize: '0.9em' }}>{coupon.}</span>
<Tag color={coupon.使 ? 'red' : 'green'} style={{ marginLeft: '10px' }}>
{coupon.使 ? '已使用' : '未使用'}
</Tag>
</div>
<div style={{ marginTop: '5px', fontSize: '1.1em', fontWeight: 'bold' }}>
{coupon._id.}
</div>
{coupon._id. && (
<div style={{ marginTop: '3px', fontStyle: 'italic', color: '#666' }}>
{coupon._id.}
</div>
)}
<div style={{ marginTop: '5px', color: '#666' }}>
{new Date(coupon._id.).toLocaleDateString()} - {new Date(coupon._id.).toLocaleDateString()}
</div>
{coupon.使 && coupon.使 && (
<div style={{ marginTop: '3px', color: '#888' }}>
使{new Date(coupon.使).toLocaleDateString()}
</div>
)}
</div>
</List.Item>
)}
/>
</Modal>
);
};
export default CouponModal;

View File

@@ -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<SalesRecordPageProps> = ({ initialPhone }) => {
const [loading, setLoading] = useState(false);
const [salesRecords, setSalesRecords] = useState<any[]>([]);
const [afterSalesRecords, setAfterSalesRecords] = useState<any[]>([]);
const [totalIncome, setTotalIncome] = useState<number>(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 (
<div
//固定高度,超出部分滚动
style={{height: '90vh', overflow: 'auto'}}
className="container mx-auto p-4"
>
{/* 销售记录展示 */}
<Card title="销售记录" loading={loading}>
<List
dataSource={salesRecords}
renderItem={(record: any) => (
<List.Item>
<Descriptions title={`订单号: ${record._id}`} column={2} bordered>
<Descriptions.Item label="成交日期">{record.}</Descriptions.Item>
<Descriptions.Item label="应收金额">¥{record.}</Descriptions.Item>
<Descriptions.Item label="收款金额">¥{record.}</Descriptions.Item>
<Descriptions.Item label="待收款">¥{record.}</Descriptions.Item>
<Descriptions.Item label="收款状态">{record.}</Descriptions.Item>
<Descriptions.Item label="订单状态">{record..join(', ')}</Descriptions.Item>
<Descriptions.Item label="货款状态">{record.}</Descriptions.Item>
<Descriptions.Item label="备注">{record. || '无'}</Descriptions.Item>
</Descriptions>
</List.Item>
)}
/>
</Card>
{/* 售后记录展示 */}
<Card title="售后记录" className="mt-4" loading={loading}>
<List
dataSource={afterSalesRecords}
renderItem={(record: any) => (
<List.Item>
<Descriptions title={`售后记录号: ${record._id}`} column={2} bordered>
<Descriptions.Item label="售后类型">{record.}</Descriptions.Item>
<Descriptions.Item label="售后日期">{record.}</Descriptions.Item>
<Descriptions.Item label="售后进度">{record.}</Descriptions.Item>
<Descriptions.Item label="原因">{record.}</Descriptions.Item>
<Descriptions.Item label="收支金额">¥{record.}</Descriptions.Item>
<Descriptions.Item label="收支平台">{record. || '无'}</Descriptions.Item>
<Descriptions.Item label="备注">{record. || '无'}</Descriptions.Item>
</Descriptions>
</List.Item>
)}
/>
</Card>
{/* 本单收入展示 */}
<Card title="本单收入" className="mt-4">
<Descriptions bordered>
<Descriptions.Item label="本单收入总和" span={2}>
¥{totalIncome.toFixed(2)}
</Descriptions.Item>
</Descriptions>
</Card>
</div>
);
};
export default SalesRecordPage;

View File

@@ -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<SalesRecordPageProps> = ({ salesRecord, onCancel }) => {
const [form] = Form.useForm();
const [customers, setCustomers] = useState<any[]>([]); // 客户
const [products, setProducts] = useState<any[]>([]); // 产品
const [accounts, setAccounts] = useState<any[]>([]); //支付平台
const [payPlatforms, setPayPlatforms] = useState<any[]>([]); //支付平台
const [users, setUsers] = useState<any[]>([]);
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 (
<div style={{ padding: '24px', maxWidth: '800px', margin: '0 auto' }}>
<Form
form={form}
layout="vertical"
initialValues={salesRecord ? { ...salesRecord } : {}}
>
<Form.Item
name="客户"
label="客户"
rules={[{ required: true, message: '请选择客户!' }]}
>
<Select
allowClear
showSearch
placeholder="请选择客户"
>
{customers.map(customer => (
<Select.Option key={customer._id} value={customer._id}>
{customer.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="产品"
label="产品"
rules={[{ required: true, message: '请选择产品!' }]}
>
<Select
mode="multiple"
placeholder="请选择产品"
allowClear
showSearch
>
{products.map(product => (
<Select.Option key={product._id} value={product._id}>
{product.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="订单来源"
label="订单来源"
rules={[{ required: true, message: '请选择订单来源!' }]}
>
<Select placeholder="请选择订单来源">
{accounts.map(account => (
<Select.Option key={account._id} value={account._id}>
{account.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="导购"
label="导购"
rules={[{ required: true, message: '请选择导购!' }]}
>
<Select placeholder="请选择导购">
{users.map(user => (
<Select.Option key={user._id} value={user._id}>
{user.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="成交日期"
label="成交日期"
rules={[{ required: true, message: '请选择成交日期!' }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="收款状态"
label="收款状态"
rules={[{ required: true, message: '请选择收款状态!' }]}
>
<Select placeholder="请选择收款状态">
<Select.Option value="未付款"></Select.Option>
<Select.Option value="已付款"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="收款平台"
label="收支平台"
rules={[{ required: true, message: '请选择收支平台!' }]}
>
<Select placeholder="收款平台">
{payPlatforms && Array.isArray(payPlatforms) && payPlatforms.map(platform => (
<Select.Option key={platform._id} value={platform._id}>
{platform.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="货款状态"
label="货款状态"
rules={[{ required: true, message: '请选择货款状态!' }]}
>
<Select placeholder="请选择货款状态">
<Select.Option value="可结算"></Select.Option>
<Select.Option value="不可结算"></Select.Option>
<Select.Option value="待结算"></Select.Option>
<Select.Option value="已结算"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="备注"
label="备注"
>
<Input.TextArea placeholder="请输入备注" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSubmit}>
</Button>
<Button onClick={onCancel} style={{ marginLeft: '10px' }}>
</Button>
</Form.Item>
</Form>
</div>
);
};
export default SalesRecordPage;

View File

@@ -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<ISalesRecord[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentSalesRecord, setCurrentSalesRecord] = useState<ISalesRecord | null>(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) => (
<Space size="middle">
<Button onClick={() => handleDelete(record._id)}></Button>
</Space>
),
},
];
return (
<Card
title="销售记录列表"
extra={
<Button type="primary" onClick={handleCreate}>
</Button>
}
>
<Table
columns={columns}
dataSource={salesRecords}
rowKey="_id"
/>
{isModalVisible && (
<SalesRecordModal
visible={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
salesRecord={currentSalesRecord}
/>
)}
</Card>
);
};
export default SalesPage;

View File

@@ -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<SalesRecordPageProps> = ({ salesRecord, onCancel }) => {
const [form] = Form.useForm();
const [customers, setCustomers] = useState<any[]>([]); // 客户
const [products, setProducts] = useState<any[]>([]); // 产品
const [accounts, setAccounts] = useState<any[]>([]); //支付平台
const [payPlatforms, setPayPlatforms] = useState<any[]>([]); //支付平台
const userInfo = useUserInfo(); // 获取当前用户信息
const [selectedCustomer, setSelectedCustomer] = useState<ICustomer | null>(null);// 选中的客户
const [selectedProducts, setSelectedProducts] = useState<IProduct[]>([]);// 选中的产品,是一个数组
const [customerBalance, setCustomerBalance] = useState<number>(0); // 客户余额
const [deductionAmount, setDeductionAmount] = useState<number>(0); // 余额抵扣
const [selectedCoupons, setSelectedCoupons] = useState<any[]>([]); // 选中的优惠券
const [customerCoupons, setCustomerCoupons] = useState<ICustomerCoupon[]>([]);
// 定义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 (
<div >
<Row gutter={24} style={{
//padding: '20px',
borderRadius: '8px',
}}>
<Card bordered style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
<Row gutter={24} >
<Col span={8} >
<CustomerInfoComponent
customer={selectedCustomer}
onAddCustomerClick={handleAddCustomerClick}
onCustomerUpdate={handleEditCustomerSuccess} // 传递更新回调
/>
<AddCustomerComponent
visible={isCustomerModalVisible}
onClose={() => setIsCustomerModalVisible(false)}
onSuccess={handleAddCustomerSuccess}
/>
</Col>
<Col span={16}>
<ProductInfoComponent products={selectedProducts} onAddProductClick={handleAddProductClick} />
<AddProductComponent
visible={isProductModalVisible}
onClose={() => setIsProductModalVisible(false)}
onSuccess={handleAddProductSuccess}
/>
</Col>
</Row>
</Card>
<Divider
style={{
margin: '10px 0',
borderTop: '1px solid #d9d9d9',
}}
></Divider>
<Card
title={
<Row align="middle" justify="space-between" style={{ width: '100%' }}>
<Col>
<div className="text-2xl"></div>
</Col>
<Col>
<div style={{
display: 'flex',
//右边距
marginRight: '20px',
//上下居中
alignItems: 'center',
gap: '16px'
}}>
<div>: ¥{totalPrice - deductionAmount}</div>
<div>: ¥{deductionAmount}</div>
<div>
<Form.Item
name="余额抵扣"
label="余额抵扣"
//上下居中
style={{ margin: '0' }}
>
<Input
type="number"
style={{ width: '160px' }}
value={deductionAmount}
onChange={e => setDeductionAmount(Math.min(Number(e.target.value), customerBalance))}
placeholder={`可用余额:¥${customerBalance.toFixed(2)}`}
/>
</Form.Item>
</div>
<div>
<Form.Item
name="优惠券"
label="使用优惠券"
style={{ margin: '0', width: '100%' }}
>
<Select
mode="multiple"
placeholder="请选择优惠券"
//宽度100%
style={{ width: '160px' }}
value={selectedCoupons.map(coupon => coupon.)} // 使用券码数组作为值
onChange={codes => {
// 根据选择的券码从customerCoupons中找到对应的优惠券对象
const selected = codes.map(code => customerCoupons.find(coupon => coupon. === code));
setSelectedCoupons(selected);
}}
>
{customerCoupons.map(coupon => (
<Select.Option
key={coupon.} // 使用券码作为key
value={coupon.} // 使用券码作为value
disabled={coupon.使} // 根据已使用状态禁用选项
>
{`${coupon._id.}···(${coupon._id.})···${coupon.使 ? '已使用' : '未使用'}`}
</Select.Option>
))}
</Select>
</Form.Item>
</div>
</div>
</Col>
</Row>
}
extra={
<>
<Button type="primary" onClick={handleSubmit}>
</Button>
<Button onClick={onCancel} style={{ marginLeft: '10px' }}>
</Button>
</>
}
bordered style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
<Form
form={form}
layout="vertical"
initialValues={salesRecord ? { ...salesRecord } : {}}
onFinish={handleSubmit}
>
<Row gutter={24} >
<Col span={8} >
<Form.Item
name="客户"
label="客户"
rules={[{ required: true, message: '请选择客户!' }]}
>
<Select
allowClear
showSearch
onChange={handleCustomerChange}
placeholder="请选择客户"
filterOption={filterOption} // 添加自定义筛选函数
>
{customers.map(customer => (
<Select.Option key={customer._id} value={customer._id}>
<Tag icon={<UserOutlined />} color="volcano">{customer.}</Tag>
<Tag icon={<PhoneOutlined />} color="blue">{customer.}</Tag>
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={16}>
<Form.Item
name="产品"
label="产品"
rules={[{ required: true, message: '请选择产品!' }]}
>
<Select
mode="multiple"
placeholder="请选择产品"
allowClear
showSearch
onChange={handleProductChange}
filterOption={productFilterOption} // 产品筛选函数
>
{products.map(product => (
<Select.Option key={product._id} value={product._id}>
<Tag icon={<TrademarkCircleOutlined />} color="magenta">{product.?.name || ''}</Tag>
<Tag icon={<ClusterOutlined />} color="volcano">{product.?.name || ''}</Tag>
<Tag icon={<SignatureOutlined />} color="purple">{product.}</Tag>
<Tag icon={<TransactionOutlined />} color="green">{product.}</Tag>
{/*<Tag icon={<UnlockOutlined />} color="geekblue">{product.库存}</Tag>*/}
<Tag icon={<BarcodeOutlined />} color="cyan">{product.}</Tag>
<Tag icon={<TagsOutlined />} color="gold">{product.}</Tag>
{/*<Tag icon={<LikeOutlined />} color="magenta">{product.级别}</Tag>*/}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={6}>
<Form.Item
name="订单来源"
label="来源店铺"
rules={[{ required: true, message: '请选择订单来源!' }]}
>
<Select
allowClear
showSearch
placeholder="请选择订单来源"
optionFilterProp="children"
filterOption={(input, option) => {
// 检查微信号和微信昵称是否匹配输入的内容
const account = accounts.find(acc => acc._id === option?.value);
if (!account) return false;
const { , } = account;
return (
.toLowerCase().includes(input.toLowerCase()) ||
.toLowerCase().includes(input.toLowerCase())
);
}}
>
{accounts.map(account => (
<Select.Option key={account._id} value={account._id}>
<Tag icon={<ShopOutlined />} color="blue">{account.}</Tag>
<Tag icon={<WechatOutlined />} color="success">{account.}{account.}</Tag>
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="成交日期"
label="成交日期"
rules={[{ required: true, message: '请选择成交日期!' }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="收款状态"
label="收款状态"
rules={[{ required: true, message: '请选择收款状态!' }]}
>
<Select placeholder="请选择收款状态">
<Select.Option value="全款"></Select.Option>
<Select.Option value="定金"></Select.Option>
<Select.Option value="未付"></Select.Option>
<Select.Option value="赠送"></Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="收款平台"
label="收款平台"
rules={[{ required: true, message: '请选择收款平台!' }]}
>
<Select placeholder="收款平台">
{payPlatforms && Array.isArray(payPlatforms) && payPlatforms.map(platform => (
<Select.Option key={platform._id} value={platform._id}>
{platform.}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
{/*应收金额 */}
<Col span={4}>
<Form.Item
name="应收金额"
label="应收金额"
rules={[{ required: true, message: '请输入应收金额!' }]}
>
<Input type="number" />
</Form.Item>
</Col>
{/*收款金额 */}
<Col span={4}>
<Form.Item
name="收款金额"
label="收款金额"
rules={[{ required: true, message: '请输入收款金额!' }]}
>
<Input type="number" />
</Form.Item>
</Col>
{/*待收款 */}
<Col span={4}>
<Form.Item
name="待收款"
label="待收款"
rules={[{ required: true, message: '请输入待收款!' }]}
>
<Input type="number" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="备注"
label="备注"
>
<Input.TextArea
placeholder="请输入备注"
autoSize={{ minRows: 1, maxRows: 3 }}//默认显示的行数
/>
</Form.Item>
</Col>
</Row>
</Form>
</Card>
</Row>
</div>
);
};
export default SalesRecordPage;

View File

@@ -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<SalesRecordModalProps> = ({ visible, onCancel, onOk, salesRecord }) => {
const [form] = Form.useForm();
const [customers, setCustomers] = useState<any[]>([]);
const [products, setProducts] = useState<any[]>([]);
const [accounts, setAccounts] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
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 (
<Modal
title={salesRecord ? '编辑销售记录' : '添加新销售记录'}
open={visible}
onOk={handleSubmit}
onCancel={() => {
form.resetFields();
onCancel();
}}
>
<Form
form={form}
layout="vertical"
initialValues={salesRecord ? { ...salesRecord } : {}}
>
<Form.Item
name="客户"
label="客户"
rules={[{ required: true, message: '请选择客户!' }]}
>
<Select placeholder="请选择客户">
{customers.map(customer => (
<Select.Option key={customer._id} value={customer._id}>
{customer.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="产品"
label="产品"
rules={[{ required: true, message: '请选择产品!' }]}
>
<Select mode="multiple" placeholder="请选择产品">
{products.map(product => (
<Select.Option key={product._id} value={product._id}>
{product.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="订单来源"
label="订单来源"
rules={[{ required: true, message: '请选择订单来源!' }]}
>
<Select placeholder="请选择订单来源">
{accounts.map(account => (
<Select.Option key={account._id} value={account._id}>
{account.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="导购"
label="导购"
rules={[{ required: true, message: '请选择导购!' }]}
>
<Select placeholder="请选择导购">
{users.map(user => (
<Select.Option key={user._id} value={user._id}>
{user.}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="成交日期"
label="成交日期"
rules={[{ required: true, message: '请选择成交日期!' }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="收款状态"
label="收款状态"
rules={[{ required: true, message: '请选择收款状态!' }]}
>
<Select placeholder="请选择收款状态">
<Select.Option value="未付款"></Select.Option>
<Select.Option value="已付款"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="货款状态"
label="货款状态"
rules={[{ required: true, message: '请选择货款状态!' }]}
>
<Select placeholder="请选择货款状态">
<Select.Option value="可结算"></Select.Option>
<Select.Option value="不可结算"></Select.Option>
<Select.Option value="待结算"></Select.Option>
<Select.Option value="已结算"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="备注"
label="备注"
>
<Input.TextArea placeholder="请输入备注" />
</Form.Item>
</Form>
</Modal>
);
};
export default SalesRecordModal;

View File

@@ -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;
}

View File

@@ -1,240 +1,63 @@
/** // src/utils/connectDB.ts
* 文件: src/utils/ConnectDB.ts
* 作者: 阿瑞
* 功能: MongoDB数据库连接管理工具 - 高阶函数装饰器
* 版本: v2.0.0
* @description 为Next.js API路由提供数据库连接管理支持连接复用、错误重试、连接超时等功能
*/
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
// =========================================== // 数据库连接状态枚举
// 类型定义区域 const MONGO_CONNECTION_STATES = {
// =========================================== disconnected: 0,
connected: 1,
/** connecting: 2,
* 数据库连接配置接口 disconnecting: 3,
*/
interface DatabaseConfig {
maxRetries?: number; // 最大重试次数
retryDelay?: number; // 重试延迟(毫秒)
connectTimeout?: number; // 连接超时(毫秒)
enableLogging?: boolean; // 是否启用日志
}
/**
* API处理函数类型定义
*/
type ApiHandler = (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
// ===========================================
// 配置常量区域
// ===========================================
/**
* 默认数据库连接配置
*/
const DEFAULT_CONFIG: Required<DatabaseConfig> = {
maxRetries: 3, // 默认重试3次
retryDelay: 1000, // 默认延迟1秒
connectTimeout: 10000, // 默认10秒超时
enableLogging: process.env.NODE_ENV === 'development', // 开发环境启用日志
}; };
/** // 定义一个高阶函数 connectDB它接受一个处理请求的 handler并返回一个新的处理函数
* Mongoose连接状态枚举 const connectDB = (
* 0 = 断开连接, 1 = 已连接, 2 = 正在连接, 3 = 正在断开连接 handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>
*/ ) => async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
const CONNECTION_STATES = { // 检查数据库连接状态
DISCONNECTED: 0, const connectionState = mongoose.connections[0]?.readyState;
CONNECTED: 1,
CONNECTING: 2, // 如果已经连接,直接调用 handler
DISCONNECTING: 3, if (connectionState === MONGO_CONNECTION_STATES.connected) {
} as const; return handler(req, res);
// ===========================================
// 工具函数区域
// ===========================================
/**
* 安全的日志记录函数
* @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);
}
} }
};
// 如果正在连接中,等待连接完成
/** if (connectionState === MONGO_CONNECTION_STATES.connecting) {
* 延迟函数 await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒
* @param ms 延迟毫秒数 return connectDB(handler)(req, res); // 递归调用
*/ }
const delay = (ms: number): Promise<void> =>
new Promise(resolve => setTimeout(resolve, ms)); // 从环境变量中获取数据库连接 URI
/**
* 检查数据库连接状态
* @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; const dbUri = process.env.MONGODB_URI;
if (!dbUri) { if (!dbUri) {
safeLog('数据库URI未在环境变量中配置', null, true); console.error('MONGODB_URI environment variable is not set');
return null; return res.status(500).json({ error: 'Database URI not provided.' });
} }
return dbUri;
};
// ===========================================
// 核心连接功能区域
// ===========================================
/**
* 执行数据库连接(带重试机制)
* @param dbUri 数据库连接URI
* @param config 连接配置
* @returns Promise<boolean> 连接是否成功
*/
const connectWithRetry = async (
dbUri: string,
config: Required<DatabaseConfig>
): Promise<boolean> => {
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<void> => {
// 合并配置
const finalConfig: Required<DatabaseConfig> = { ...DEFAULT_CONFIG, ...config };
try { try {
// 检查现有连接状态 // 设置 mongoose 连接选项 - 使用兼容的选项
if (isDbConnected()) { const options = {
safeLog('使用现有数据库连接'); maxPoolSize: 10, // 连接池最大连接数
return await handler(req, res); serverSelectionTimeoutMS: 10000, // 服务器选择超时时间
} socketTimeoutMS: 45000, // Socket 超时时间
connectTimeoutMS: 10000, // 连接超时时间
// 获取数据库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); await mongoose.connect(dbUri, options);
return res.status(500).json({ console.log('Database connected successfully');
error: 'Database connection failed',
message: 'Unable to establish database connection after multiple attempts'
});
}
// 成功连接后执行API处理函数
return await handler(req, res);
// 成功连接后,处理原始请求
return handler(req, res);
} catch (error) { } catch (error) {
// 全局错误捕获 console.error('Error connecting to database:', error);
safeLog('数据库连接装饰器发生未预期错误', error, true); // 如果连接失败,返回 500 错误
return res.status(500).json({
return res.status(500).json({ error: 'Failed to connect to the database.',
error: 'Internal server error', details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
message: process.env.NODE_ENV === 'development'
? (error instanceof Error ? error.message : 'Unknown error occurred')
: 'An unexpected error occurred'
}); });
} }
}; };
// ===========================================
// 导出区域
// ===========================================
export default connectDB; export default connectDB;
/**
* 导出配置常量供外部使用
*/
export { DEFAULT_CONFIG, CONNECTION_STATES };
/**
* 导出类型定义供外部使用
*/
export type { DatabaseConfig, ApiHandler };

120
src/utils/cleanupIndexes.ts Normal file
View File

@@ -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<void> {
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<void> {
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<void> {
try {
await cleanupDuplicateIndexes();
await rebuildIndexes();
console.log('索引清理和重建流程完成');
} catch (error) {
console.error('索引清理和重建流程失败:', error);
throw error;
}
}

240
src/utils/connectDB.ts.bak Normal file
View File

@@ -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<void>;
// ===========================================
// 配置常量区域
// ===========================================
/**
* 默认数据库连接配置
*/
const DEFAULT_CONFIG: Required<DatabaseConfig> = {
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<void> =>
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<boolean> 连接是否成功
*/
const connectWithRetry = async (
dbUri: string,
config: Required<DatabaseConfig>
): Promise<boolean> => {
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<void> => {
// 合并配置
const finalConfig: Required<DatabaseConfig> = { ...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 };

View File

@@ -2,12 +2,12 @@
* 主题工具函数 * 主题工具函数
* @author 阿瑞 * @author 阿瑞
* @description 主题管理工具与项目的主题系统集成支持Ant Design主题配置和CSS变量 * @description 主题管理工具与项目的主题系统集成支持Ant Design主题配置和CSS变量
* @version 3.0.0 * @version 4.0.0 - 性能优化版本
* @created 2024-12-19 * @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 { useSettingActions, useThemeMode } from '@/store/settingStore';
import { ThemeMode as ProjectThemeMode } from '@/types/enum'; import { ThemeMode as ProjectThemeMode } from '@/types/enum';
import { lightTheme, darkTheme } from '@/styles/theme/themeConfig'; import { lightTheme, darkTheme } from '@/styles/theme/themeConfig';
@@ -55,9 +55,9 @@ const defaultDarkToken: ThemeToken = {
borderRadius: 12, borderRadius: 12,
}; };
// 模块级注释主题Hook接口 // 模块级注释主题Hook接口 - 移除realDark只保留light和dark
export interface UseThemeReturn { export interface UseThemeReturn {
navTheme: 'light' | 'realDark'; navTheme: 'light' | 'dark';
themeToken: ThemeToken; themeToken: ThemeToken;
toggleTheme: () => void; toggleTheme: () => void;
changePrimaryColor: (color: string) => void; changePrimaryColor: (color: string) => void;
@@ -66,7 +66,32 @@ export interface UseThemeReturn {
mounted: boolean; mounted: boolean;
} }
// 模块级注释:应用CSS主题类的函数 // 模块级注释:主题配置缓存系统 - 性能优化核心
interface ThemeCache {
antdTheme: ThemeConfig;
themeToken: ThemeToken;
cacheKey: string;
}
const themeCache = new Map<string, ThemeCache>();
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 => { const applyThemeClasses = (isDark: boolean): void => {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
const root = document.documentElement; 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 extractTokenFromTheme = (theme: ThemeConfig, isDark: boolean): ThemeToken => {
const token = theme.token || {}; const token = theme.token || {};
const baseToken = isDark ? defaultDarkToken : defaultLightToken; 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 = ( export const useTheme = (
_updateLayoutSettings?: (newTheme: 'light' | 'realDark', newColorPrimary?: string) => void _updateLayoutSettings?: (newTheme: 'light' | 'dark', newColorPrimary?: string) => void
): UseThemeReturn => { ): UseThemeReturn => {
// 关键代码行注释使用zustand store管理主题状态 // 关键代码行注释使用zustand store管理主题状态
const themeMode = useThemeMode(); const themeMode = useThemeMode();
@@ -119,55 +187,33 @@ export const useTheme = (
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [customPrimaryColor, setCustomPrimaryColor] = useState<string>(''); const [customPrimaryColor, setCustomPrimaryColor] = useState<string>('');
// 关键代码行注释使用ref避免不必要的重新渲染
const lastThemeRef = useRef<string>('');
// 关键代码行注释:计算当前主题状态 // 关键代码行注释:计算当前主题状态
const isDark = themeMode === ProjectThemeMode.Dark; const isDark = themeMode === ProjectThemeMode.Dark;
const navTheme = isDark ? 'realDark' : 'light'; const navTheme = isDark ? 'dark' : 'light';
// 关键代码行注释:获取当前Ant Design主题配置 // 关键代码行注释:使用缓存获取主题配置 - 大幅提升性能
const currentAntdTheme = useMemo(() => { const cachedThemeConfig = useMemo(() => {
const baseTheme = isDark ? darkTheme : lightTheme; return getCachedThemeConfig(isDark, customPrimaryColor);
// 如果有自定义主色,应用到主题配置
if (customPrimaryColor) {
return {
...baseTheme,
token: {
...baseTheme.token,
colorPrimary: customPrimaryColor,
colorPrimaryHover: customPrimaryColor,
colorLink: customPrimaryColor,
},
};
}
return baseTheme;
}, [isDark, customPrimaryColor]); }, [isDark, customPrimaryColor]);
// 关键代码行注释:提取主题Token // 关键代码行注释:从缓存中提取配置
const themeToken = useMemo(() => { const { antdTheme: currentAntdTheme, themeToken } = cachedThemeConfig;
const token = extractTokenFromTheme(currentAntdTheme, isDark);
// 如果有自定义主色,覆盖相关颜色
if (customPrimaryColor) {
token.colorPrimary = customPrimaryColor;
token.colorLink = customPrimaryColor;
}
return token;
}, [currentAntdTheme, isDark, customPrimaryColor]);
// 关键代码行注释:主题切换函数 // 关键代码行注释:主题切换函数 - 优化版本
const toggleTheme = useCallback(() => { const toggleTheme = useCallback(() => {
toggleThemeMode(); toggleThemeMode();
// 关键代码行注释:调用旧的回调函数以保持兼容性 // 关键代码行注释:调用旧的回调函数以保持兼容性
if (_updateLayoutSettings) { if (_updateLayoutSettings) {
const newTheme = isDark ? 'light' : 'realDark'; const newTheme = isDark ? 'light' : 'dark';
_updateLayoutSettings(newTheme, themeToken.colorPrimary); _updateLayoutSettings(newTheme, themeToken.colorPrimary);
} }
}, [toggleThemeMode, isDark, themeToken.colorPrimary, _updateLayoutSettings]); }, [toggleThemeMode, isDark, themeToken.colorPrimary, _updateLayoutSettings]);
// 关键代码行注释:主色更改函数 // 关键代码行注释:主色更改函数 - 优化版本
const changePrimaryColor = useCallback((color: string) => { const changePrimaryColor = useCallback((color: string) => {
setCustomPrimaryColor(color); setCustomPrimaryColor(color);
@@ -195,14 +241,19 @@ export const useTheme = (
} }
}, []); }, []);
// 关键代码行注释应用CSS主题类 // 关键代码行注释应用CSS主题类 - 优化防抖版本
useEffect(() => { useEffect(() => {
if (mounted) { if (mounted) {
applyThemeClasses(isDark); const currentThemeKey = `${isDark}-${mounted}`;
// 关键代码行注释:防止重复应用相同主题
if (lastThemeRef.current !== currentThemeKey) {
applyThemeClasses(isDark);
lastThemeRef.current = currentThemeKey;
}
} }
}, [mounted, isDark]); }, [mounted, isDark]);
// 关键代码行注释:监听存储变化(多标签页同步) // 关键代码行注释:监听存储变化(多标签页同步)- 优化版本
useEffect(() => { useEffect(() => {
if (!mounted) return; if (!mounted) return;
@@ -227,11 +278,16 @@ export const useTheme = (
}; };
}; };
// 模块级注释:导出类型别名以保持兼容性 // 模块级注释:导出类型别名以保持兼容性 - 移除realDark
export type ThemeMode = 'light' | 'realDark'; export type ThemeMode = 'light' | 'dark';
// 模块级注释:导出主题配置 // 模块级注释:导出主题配置
export { lightTheme, darkTheme }; export { lightTheme, darkTheme };
// 模块级注释:导出工具函数 // 模块级注释:导出工具函数
export { applyThemeClasses }; export { applyThemeClasses };
// 模块级注释:导出缓存清理函数供测试使用
export const clearThemeCache = (): void => {
themeCache.clear();
};