This commit is contained in:
@@ -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
3
pnpm-lock.yaml
generated
@@ -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))
|
||||||
|
|||||||
@@ -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. 最内层 div:padding = '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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }); // 对前端引流人员字段建立索引
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ export interface ISalesRecord {
|
|||||||
二次审计: boolean;
|
二次审计: boolean;
|
||||||
收款状态: string;
|
收款状态: string;
|
||||||
货款状态: string;
|
货款状态: string;
|
||||||
|
处理状态?: '未处理' | '处理中' | '已处理';
|
||||||
售后收支: number;
|
售后收支: number;
|
||||||
回访日期: Date;
|
回访日期: Date;
|
||||||
备注: string;
|
备注: string;
|
||||||
|
|||||||
42
src/pages/api/admin/cleanup-indexes.ts
Normal file
42
src/pages/api/admin/cleanup-indexes.ts
Normal 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);
|
||||||
50
src/pages/api/backstage/brands/[id].ts
Normal file
50
src/pages/api/backstage/brands/[id].ts
Normal 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);
|
||||||
29
src/pages/api/backstage/brands/index.ts
Normal file
29
src/pages/api/backstage/brands/index.ts
Normal 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);
|
||||||
50
src/pages/api/backstage/categories/[id].ts
Normal file
50
src/pages/api/backstage/categories/[id].ts
Normal 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);
|
||||||
31
src/pages/api/backstage/categories/index.ts
Normal file
31
src/pages/api/backstage/categories/index.ts
Normal 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);
|
||||||
68
src/pages/api/backstage/customers/[id].ts
Normal file
68
src/pages/api/backstage/customers/[id].ts
Normal 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);
|
||||||
53
src/pages/api/backstage/customers/index.ts
Normal file
53
src/pages/api/backstage/customers/index.ts
Normal 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);
|
||||||
59
src/pages/api/backstage/customers/sales/[id].ts
Normal file
59
src/pages/api/backstage/customers/sales/[id].ts
Normal 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);
|
||||||
51
src/pages/api/backstage/payment-platforms/[id].ts
Normal file
51
src/pages/api/backstage/payment-platforms/[id].ts
Normal 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);
|
||||||
30
src/pages/api/backstage/payment-platforms/index.ts
Normal file
30
src/pages/api/backstage/payment-platforms/index.ts
Normal 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);
|
||||||
85
src/pages/api/backstage/products/[id].ts
Normal file
85
src/pages/api/backstage/products/[id].ts
Normal 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);
|
||||||
56
src/pages/api/backstage/products/index.ts
Normal file
56
src/pages/api/backstage/products/index.ts
Normal 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);
|
||||||
@@ -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']);
|
||||||
|
|||||||
98
src/pages/api/backstage/sales/updateStatus.ts
Normal file
98
src/pages/api/backstage/sales/updateStatus.ts
Normal 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);
|
||||||
50
src/pages/api/backstage/suppliers/[id].ts
Normal file
50
src/pages/api/backstage/suppliers/[id].ts
Normal 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);
|
||||||
33
src/pages/api/backstage/suppliers/index.ts
Normal file
33
src/pages/api/backstage/suppliers/index.ts
Normal 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);
|
||||||
56
src/pages/api/backstage/users/[id].ts
Normal file
56
src/pages/api/backstage/users/[id].ts
Normal 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);
|
||||||
41
src/pages/api/backstage/users/index.ts
Normal file
41
src/pages/api/backstage/users/index.ts
Normal 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);
|
||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
388
src/pages/team/sale/components/AddCustomerComponent.tsx
Normal file
388
src/pages/team/sale/components/AddCustomerComponent.tsx
Normal 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;
|
||||||
766
src/pages/team/sale/components/AddProductComponent.tsx
Normal file
766
src/pages/team/sale/components/AddProductComponent.tsx
Normal 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, 支持 JPG、PNG 格式
|
||||||
|
</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;
|
||||||
329
src/pages/team/sale/components/CustomerInfoComponent.tsx
Normal file
329
src/pages/team/sale/components/CustomerInfoComponent.tsx
Normal 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;
|
||||||
119
src/pages/team/sale/components/EditCustomerInfo.tsx
Normal file
119
src/pages/team/sale/components/EditCustomerInfo.tsx
Normal 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;
|
||||||
193
src/pages/team/sale/components/ProductInfoComponent.tsx
Normal file
193
src/pages/team/sale/components/ProductInfoComponent.tsx
Normal 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;
|
||||||
152
src/pages/team/sale/components/Recharge.tsx
Normal file
152
src/pages/team/sale/components/Recharge.tsx
Normal 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;
|
||||||
45
src/pages/team/sale/components/SelectCustomer.tsx
Normal file
45
src/pages/team/sale/components/SelectCustomer.tsx
Normal 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;
|
||||||
51
src/pages/team/sale/components/SelectProduct.tsx
Normal file
51
src/pages/team/sale/components/SelectProduct.tsx
Normal 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;
|
||||||
21
src/pages/team/sale/components/address-smart-parse.d.ts
vendored
Normal file
21
src/pages/team/sale/components/address-smart-parse.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
159
src/pages/team/sale/components/coupon-modal.tsx
Normal file
159
src/pages/team/sale/components/coupon-modal.tsx
Normal 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;
|
||||||
142
src/pages/team/sale/components/sales-record.tsx
Normal file
142
src/pages/team/sale/components/sales-record.tsx
Normal 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;
|
||||||
228
src/pages/team/sale/index copy 2.tsx
Normal file
228
src/pages/team/sale/index copy 2.tsx
Normal 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;
|
||||||
137
src/pages/team/sale/index copy.tsx
Normal file
137
src/pages/team/sale/index copy.tsx
Normal 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;
|
||||||
546
src/pages/team/sale/index.tsx
Normal file
546
src/pages/team/sale/index.tsx
Normal 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;
|
||||||
216
src/pages/team/sale/sales-record-modal.tsx
Normal file
216
src/pages/team/sale/sales-record-modal.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
120
src/utils/cleanupIndexes.ts
Normal 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
240
src/utils/connectDB.ts.bak
Normal 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 };
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user