0610.1
All checks were successful
Next.js CI/CD 流水线 / deploy (push) Successful in 4m44s

This commit is contained in:
2025-06-10 16:14:49 +08:00
parent d8398afa12
commit 27733bd91e
92 changed files with 6731 additions and 273 deletions

View File

@@ -20,6 +20,9 @@
"bcryptjs": "^3.0.2",
"color": "^5.0.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"file-saver": "^2.0.5",
"geist": "^1.4.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
@@ -31,14 +34,20 @@
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"styled-components": "^6.0.9",
"uuid": "^11.1.0",
"xlsx": "^0.18.5",
"zustand": "^5.0.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/file-saver": "^2.0.7",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20",
"@types/ramda": "^0.30.2",

1171
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

414
public/md2.md Normal file
View File

@@ -0,0 +1,414 @@
<think>
我现在需要帮助用户解决他们在使用Next.js和MongoDB构建客户管理系统时遇到的问题。他们提供了一些代码片段包括CustomerModal、CustomersPage、以及相关的API处理文件。
首先我需要理解他们的系统结构。看起来他们有一个用于展示客户的页面一个用于编辑或添加客户的模态窗口还有一个处理CRUD操作的API端点。用户可能在使用过程中遇到了一些问题比如数据更新不生效、页面刷新后状态丢
失、或者错误提示等。
接下来,我会详细分析每个文件的功能和潜在的问题:
1. **CustomerModal**:这是一个用于添加或编辑客户的模态组件。它通过`useContext`获取客户列表上下文,并调用`fetchCustomers`方法来初始化数据。在提交表单时它会根据是否存在客户ID决定是更新还是创建新客户。
2. **CustomersPage**:这个页面负责显示所有客户的数据,并允许用户进行搜索、分页浏览以及添加或编辑客户。使用了`useState``useContext`来管理状态和数据 fetching。
3. **API处理文件**这是一个Next.js API路由用于处理客户的CRUD操作。看起来已经实现了GET获取单个客户、PUT更新客户和DELETE删除客户方法并且使用了连接数据库的中间件。
现在,我需要考虑用户可能遇到的具体问题。假设他们遇到了数据无法正确提交或状态管理的问题,那么我会检查以下几个方面:
- **上下文的使用**:在`CustomerModal`中,是否正确地传递了客户列表的状态?如果没有正确更新,可能会导致页面显示不正确的数据。
- **API请求处理**在提交表单时Axios请求是否正确构造特别是对于PUT请求是否需要发送完整的地址对象而不仅仅是部分属性
- **错误处理和状态管理**如果Axios请求失败是否有适当的错误处理机制如果没有可能会导致用户不知道发生了什么问题。
- **页面刷新后的数据丢失**:由于`useState`只能在组件内部保持状态,页面刷新会导致状态丢失。这可能意味着用户需要将客户数据存储到后端数据库中,而不是仅仅依赖前端的状态管理。
通过分析这些点我可以推断出用户的问题可能与API请求的处理、上下文更新或错误处理有关。因此在帮助用户解决问题时我应该建议他们检查这些方面并提供相应的解决方案和代码示例。
</think>
根据您提供的代码片段我可以看出您正在使用Next.js和MongoDB构建一个客户管理系统并且已经实现了基本的CRUD创建、读取、更新、删除功能。以下是针对您的问题和可能遇到的挑战的一些建议和解答
### 1. 管理客户数据的状态
`CustomersPage`中,您使用了`useState`来管理客户数据的状态。为了确保状态能够正确反映最新的数据,建议将所有对客户的操作(添加、编辑、删除)包装在一个事务中。这样可以防止部分更新导致的数据不一致。
**改进代码示例:**
```typescript
const updateCustomer = async (customer: Customer) => {
try {
// 使用事务包裹CRUD操作确保数据一致性
const session = await mongoose.startSession();
session.startTransaction();
// 更新客户数据
await customer.findByIdAndUpdate(
id,
{ $set: customer },
{ session, new: true }
);
// 提交事务
await session.commitTransaction();
// 刷新客户列表以显示更新
refetchCustomers();
} catch (error) {
console.error('更新客户时出错:', error);
throw error;
}
};
```
### 2. 在`CustomerModal`中正确处理文件上传
在您的模态表单中,您已经提到了处理文件上传的需求。以下是使用`react-dropzone`和Axios实现文件上传的示例
**改进代码示例:**
```typescript
import { useDropzone } from 'react-dropzone';
import axios from 'axios';
const CustomerModal = ({ customer }: { customer?: Customer }) => {
const [files, setFiles] = useState<File[]>([]);
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles(acceptedFiles);
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpeg', '.jpg', '.png']
}
});
const uploadFiles = async () => {
try {
// 假设files是一个包含上传文件的数组
if (files.length === 0) return;
// 创建一个 FormData 对象
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
// 发送 POST 请求到后端 API
await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// 上传成功后更新客户数据
setCustomer(prev => ({ ...prev!, avatar: ' uploaded' }));
} catch (error) {
console.error('文件上传失败:', error);
throw error;
}
};
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{children}
</div>
);
};
```
### 3. 处理复杂的数据结构
在您的`CustomerModal`中,您可能需要处理更复杂的数据结构。例如,如果您有一个嵌套的地址对象或其他关联数据,可以使用`react-hook-form`来管理表单状态,提高表单处理效率。
**改进代码示例:**
```typescript
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
interface CustomerSchema {
name: string;
email: string;
address: {
street: string;
city: string;
country: string;
};
}
const schema = yup.object().shape({
name: yup.string().required(),
email: yup.string().email().required(),
address: yup.object().shape({
street: yup.string().required(),
city: yup.string().required(),
country: yup.string().required()
})
});
export const CustomerModal = ({ customer }: { customer?: Customer }) => {
const { register, handleSubmit, formState } = useForm<CustomerSchema>({
resolver: yupResolver,
defaultValues: customer || {
name: '',
email: '',
address: {
street: '',
city: '',
country: ''
}
}
});
const onSubmit = async (data: CustomerSchema) => {
try {
if (customer?._id) {
// 更新客户
await axios.put(`/api/customers/${customer._id}`, data);
} else {
// 创建新客户
await axios.post('/api/customers', data);
}
refetchCustomers();
onClose();
} catch (error) {
console.error('表单提交失败:', error);
throw error;
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 表单字段在此 */}
</form>
);
};
```
### 4. 实现分页和排序
`CustomersPage`中,您已经实现了基本的搜索功能。为了进一步提高用户体验,可以添加分页和排序功能。
**改进代码示例:**
```typescript
interface CustomerListState extends CustomersState {
currentPage: number;
itemsPerPage: number;
sortField: keyof Customer;
sortOrder: 'asc' | 'desc';
}
const CustomersPage = () => {
const [state, setState] = useState<CustomerListState>({
customers: [],
searchTerm: '',
currentPage: 1,
itemsPerPage: 10,
sortField: 'name',
sortOrder: 'asc'
});
const handleSortChange = (sortField: keyof Customer) => {
let newSortOrder: 'asc' | 'desc';
if (state.sortField === sortField && state.sortOrder === 'asc') {
newSortOrder = 'desc';
} else {
newSortOrder = 'asc';
}
setState(prev => ({
...prev,
sortField,
sortOrder: newSortOrder
}));
};
const handlePageChange = (pageNumber: number) => {
setState(prev => ({ ...prev, currentPage: pageNumber }));
};
return (
<>
<input
type="text"
placeholder="搜索客户..."
value={state.searchTerm}
onChange={(e) => setState({...state, searchTerm: e.target.value})}
/>
<div className="sort-container">
{Object.keys(Customer).map((field) => (
<button
key={field}
onClick={() => handleSortChange(field as keyof Customer)}
>
{field} {state.sortOrder === 'asc' ? '↑' : '↓'}
</button>
))}
</div>
<Table>
{/* 表格内容 */}
</Table>
<Pagination
currentPage={state.currentPage}
itemsPerPage={state.itemsPerPage}
totalItems={totalCustomers} // 假设您已经计算了总客户数
onPageChange={handlePageChange}
/>
</>
);
};
```
### 5. 错误处理与日志记录
在您的API和前端代码中错误处理是非常重要的。确保所有网络请求都有适当的错误处理并将错误信息记录到控制台或集中式日志系统中。
**改进代码示例:**
```typescript
// 在您的 axios 配置中添加拦截器
axios.interceptors.response.use(
(response) => response,
(error) => {
console.error('请求失败:', error);
// 您可以在这里处理特定错误状态,例如重试逻辑或显示用户友好的错误消息
return Promise.reject(error);
}
);
// 在您的组件中,确保您处理了所有可能的网络错误
const onSubmit = async (data: CustomerSchema) => {
try {
if (customer?._id) {
await axios.put(`/api/customers/${customer._id}`, data)
.catch((error) => {
console.error('更新客户失败:', error);
throw error;
});
} else {
await axios.post('/api/customers', data)
.catch((error) => {
console.error('创建客户失败:', error);
throw error;
});
}
refetchCustomers();
onClose();
} catch (error) {
// 处理全局错误
console.error('表单提交失败:', error);
// 您可以在这里显示用户友好的错误消息
}
};
```
### 6. 性能优化
为了提高您的应用性能,可以考虑以下措施:
- **数据缓存**:使用本地存储或缓存解决方案(如 `redux``mobx` 或简单的 `localStorage`) 来减少对后端的请求次数。
- **懒加载**:对于表格中的大量数据,可以实现懒加载,只在需要时加载更多数据。
- **优化API调用**确保您的API端点进行了适当的优化例如分页、过滤和排序都在服务器端处理。
### 7. 测试
不要忘记测试您的代码!单元测试、集成测试和用户界面测试都是必不可少的。使用工具如 `Jest``React Testing Library``Cypress` 来帮助您进行测试。
**测试示例:**
```typescript
import { render, fireEvent } from '@testing-library/react';
import CustomersPage from './CustomersPage';
describe('CustomersPage', () => {
it('should display customers when search term is empty', () => {
const mockCustomers = [
{ _id: '1', name: 'John Doe', email: 'john@example.com' },
// 其他客户...
];
render(<CustomersPage customers={mockCustomers} />);
// 断言表格中显示了所有客户
// 您可以在此处添加具体的断言
});
it('should filter customers based on search term', () => {
const mockCustomers = [
{ _id: '1', name: 'John Doe', email: 'john@example.com' },
{ _id: '2', name: 'Jane Smith', email: 'jane@example.com' },
];
const component = render(<CustomersPage customers={mockCustomers} />);
// 输入搜索词
fireEvent.change(component.getByPlaceholderText('搜索客户...'), {
target: { value: 'John' }
});
// 断言表格中只显示包含 'John' 的客户
});
});
```
### 8. 部署与监控
最后,确保您的应用可以顺利部署到生产环境,并且有适当的监控和错误报告机制。使用工具如 `Docker` 进行容器化部署,`Prometheus` 进行监控,以及 `Sentry``New Relic` 进行错误跟踪。
### 总结
通过以上改进措施您的React和TypeScript应用将更加完善、高效并且易于维护。确保您遵循最佳实践定期审查和优化代码并根据用户反馈进行调整以提供最佳的用户体验。
</think>
以下是逐步说明如何实现上述技术改进:
1. **安装必要的依赖**
首先,安装所需的库,如 `typescript``axios``react-hook-form``yup` 等。
```bash
npm install typescript axios react-hook-form yup @hookform/resolvers/yup
```
2. **设置 TypeScript 项目**
创建一个 `tsconfig.json` 文件以配置 TypeScript 设置,确保您的项目使用最新的 ES6+ 特性,并正确处理模块解析。
3. **集成 Axios**
在项目中引入 `axios` 来管理 HTTP 请求。您可以在项目的入口点或在需要的地方按需导入和使用 `axios`。
4. **实现 Form 表单验证**
使用 `react-hook-form` 和 `yup` 进行表单数据验证。定义您的模式,并将其与 `useForm` 钩子结合使用,以确保表单输入的正确性。
5. **添加分页和排序功能**
在表格组件中实现分页和排序逻辑。您可以通过添加下拉列表或按钮来允许用户选择排序方式(如按名称、电子邮件等),然后根据选定的条件调整数据展示。
6. **实施数据懒加载**
对于大量数据,使用无限滚动或其他技术实现懒加载,确保在需要时才从服务器获取更多数据,提升应用性能。
7. **设置错误处理和日志记录**
在您的 API 调用中添加错误拦截器,并将错误信息记录到控制台或集中式日志服务中。同时,在组件级别处理错误状态,以提供友好的用户体验。
8. **部署应用**
使用容器化技术如 Docker 将您的应用打包,并配置持续集成/持续部署CI/CD管道确保代码顺利交付到生产环境。
9. **监控和维护**
部署监控工具跟踪应用性能和错误。定期审查日志,优化代码,并根据用户反馈进行调整,以保持应用的最佳状态。
通过遵循这些步骤,您可以显著提升 React 和 TypeScript 应用的质量和性能,确保其在各种使用场景下的稳定性和高效性。

59
public/test2.md Normal file
View File

@@ -0,0 +1,59 @@
# h1
<br/>
## h2
<br/>
**Paragraph** Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups.
<br/>
[Link (https://www.google.com/)](https://www.google.com/)
<br/>
###### Lists
<br/>
- [x] Write the press release
- [ ] Update the website
- [ ] Contact the media
<br/>
---
<br/>
###### A table:
<br/>
| Syntax | Description | Test Text |
| :--- | :----: | ---: |
| Header | Title | Here's this |
| Paragraph | Text | And more |
<br/>
```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
ReactDOM.render(
<ReactMarkdown rehypePlugins={[rehypeHighlight]}>{'# Your markdown here'}</ReactMarkdown>,
document.querySelector('#content')
);
```
<br/>
![aoun](/aoun.png)
> A block quote with ~~strikethrough~~ and a URL: [https://reactjs.org](https://reactjs.org).

View File

@@ -0,0 +1,33 @@
//src\components\markdown\index.tsx
import ReactMarkdown from 'react-markdown';
// markdown plugins
import rehypeHighlight from 'rehype-highlight';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; //为删除线、表格、任务列表和 URL 直接添加支持
import '@/utils/highlight';
import StyledMarkdown from './styles';
import { ComponentProps } from 'react';
import { useThemeToken } from '@/theme/hooks';
import { useSettings } from '@/store/settingStore';
// 使用 ComponentProps 提取 ReactMarkdown 的 Props 类型
type Props = ComponentProps<typeof ReactMarkdown>;
export default function Markdown({ children }: Props) {
const token = useThemeToken();
const { themeMode } = useSettings();
return (
<StyledMarkdown $token={token}
//亮色模式
$thememode={themeMode}
>
<ReactMarkdown
rehypePlugins={[rehypeHighlight, rehypeRaw]}
remarkPlugins={[[remarkGfm, { singleTilde: false }]]}
>
{children}
</ReactMarkdown>
</StyledMarkdown>
);
}

View File

@@ -0,0 +1,161 @@
import { ThemeMode } from '@/types/enum';
import { GlobalToken } from 'antd';
import styled from 'styled-components';
const StyledMarkdown = styled.div<{
$token: GlobalToken;
//$thememode: ThemeMode
$thememode: any
}>`
display: grid;
// Text
h1 {
font-size: 1.75rem;
font-weight: 800;
}
h2 {
font-size: 1.35rem;
font-weight: 700;
}
h3 {
font-size: 1.25rem;
font-weight: 600;
}
h4 {
font-size: 1.075rem;
font-weight: 500;
}
h5 {
font-size: 0.95rem;
font-weight: 400;
}
h6 {
font-size: 0.825rem;
font-weight: 300;
}
a {
color: ${(props) => props.$token.colorPrimary};
}
img {
border-radius: 4px;
}
br {
display: grid;
content: '';
margin-top: 0.75em;
}
// Divider
hr {
margin: 0;
border-width: 1;
border-style: solid;
}
// List
ul,
ol {
margin: 0;
li {
line-height: 2;
display: flex;
align-items: center;
}
}
// Blockquote
blockquote {
line-height: 1.5;
font-size: 1.5em;
margin: 40px auto;
position: relative;
padding: 24px 24px 24px 64px;
border-radius: 16px;
background-color: #f4f6f8;
color: #637381;
p,
span {
margin-bottom: 0;
font-size: inherit;
font-family: inherit;
}
&::before {
left: 16px;
top: -8px;
display: block;
font-size: 3em;
position: absolute;
content: '“';
}
}
// Code Block
pre,
pre > code {
font-size: 16px;
overflow-x: auto;
white-space: pre;
border-radius: 8px;
}
code {
font-size: 14px;
border-radius: 4px;
white-space: pre;
padding: 0px;
background-color: ${(props) =>
props.$thememode === ThemeMode.Light ? '#161c24' : '#919eab29'};
}
// Table
table {
width: 100%;
border-collapse: collapse;
border: 1px solid #919eab33;
th,
td {
padding: 8px;
border: 1px solid #919eab33;
}
tbody tr:nth-of-type(odd) {
//background-color: #f4f6f8; //改为根据主题明暗度设置,暗色颜色为#161c24明色颜色为#f4f6f8
}
}
// Checkbox
input {
margin-right: 10px;
&[type='checkbox'] {
position: relative;
cursor: pointer;
&::before {
content: '';
top: -2px;
left: -2px;
width: 17px;
height: 17px;
border-radius: 3px;
position: absolute;
background-color: #f4f6f8;
}
&:checked {
&::before {
background-color: ${(props) => props.$token.colorPrimary};
}
&::after {
content: '';
top: 1px;
left: 5px;
width: 4px;
height: 9px;
position: absolute;
transform: rotate(45deg);
border: solid white;
border-width: 0 2px 2px 0;
}
}
}
}
`;
export default StyledMarkdown;

View File

@@ -0,0 +1,30 @@
//src\components\markdown\index.tsx
import ReactMarkdown from 'react-markdown';
// markdown plugins
import rehypeHighlight from 'rehype-highlight';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; //为删除线、表格、任务列表和 URL 直接添加支持
import '@/utils/highlight';
//import StyledMarkdown from './styles';
import { ComponentProps } from 'react';
// import { useThemeToken } from '@/theme/hooks';
//import { useSettings } from '@/store/settingStore';
// 使用 ComponentProps 提取 ReactMarkdown 的 Props 类型
type Props = ComponentProps<typeof ReactMarkdown>;
export default function Markdown({ children }: Props) {
//const token = useThemeToken();
//const { themeMode } = useSettings();
return (
<ReactMarkdown
rehypePlugins={[rehypeHighlight, rehypeRaw]}
remarkPlugins={[[remarkGfm, { singleTilde: false }]]}
>
{children}
</ReactMarkdown>
);
}

View File

@@ -0,0 +1,161 @@
import { ThemeMode } from '@/types/enum';
import { GlobalToken } from 'antd';
import styled from 'styled-components';
const StyledMarkdown = styled.div<{
$token: GlobalToken;
//$thememode: ThemeMode
$thememode: any
}>`
display: grid;
// Text
h1 {
font-size: 1.75rem;
font-weight: 800;
}
h2 {
font-size: 1.35rem;
font-weight: 700;
}
h3 {
font-size: 1.25rem;
font-weight: 600;
}
h4 {
font-size: 1.075rem;
font-weight: 500;
}
h5 {
font-size: 0.95rem;
font-weight: 400;
}
h6 {
font-size: 0.825rem;
font-weight: 300;
}
a {
color: ${(props) => props.$token.colorPrimary};
}
img {
border-radius: 4px;
}
br {
display: grid;
content: '';
margin-top: 0.75em;
}
// Divider
hr {
margin: 0;
border-width: 1;
border-style: solid;
}
// List
ul,
ol {
margin: 0;
li {
line-height: 2;
display: flex;
align-items: center;
}
}
// Blockquote
blockquote {
line-height: 1.5;
font-size: 1.5em;
margin: 40px auto;
position: relative;
padding: 24px 24px 24px 64px;
border-radius: 16px;
background-color: #f4f6f8;
color: #637381;
p,
span {
margin-bottom: 0;
font-size: inherit;
font-family: inherit;
}
&::before {
left: 16px;
top: -8px;
display: block;
font-size: 3em;
position: absolute;
content: '“';
}
}
// Code Block
pre,
pre > code {
font-size: 16px;
overflow-x: auto;
white-space: pre;
border-radius: 8px;
}
code {
font-size: 14px;
border-radius: 4px;
white-space: pre;
padding: 0px;
background-color: ${(props) =>
props.$thememode === ThemeMode.Light ? '#161c24' : '#919eab29'};
}
// Table
table {
width: 100%;
border-collapse: collapse;
border: 1px solid #919eab33;
th,
td {
padding: 8px;
border: 1px solid #919eab33;
}
tbody tr:nth-of-type(odd) {
//background-color: #f4f6f8; //改为根据主题明暗度设置,暗色颜色为#161c24明色颜色为#f4f6f8
}
}
// Checkbox
input {
margin-right: 10px;
&[type='checkbox'] {
position: relative;
cursor: pointer;
&::before {
content: '';
top: -2px;
left: -2px;
width: 17px;
height: 17px;
border-radius: 3px;
position: absolute;
background-color: #f4f6f8;
}
&:checked {
&::before {
background-color: ${(props) => props.$token.colorPrimary};
}
&::after {
content: '';
top: 1px;
left: 5px;
width: 4px;
height: 9px;
position: absolute;
transform: rotate(45deg);
border: solid white;
border-width: 0 2px 2px 0;
}
}
}
}
`;
export default StyledMarkdown;

View File

@@ -387,6 +387,7 @@ LogisticsRecordSchema.index({ 关联记录: 1 }); // 对关联记录字段建
const DailyGrowthSchema = new Schema({
: { type: Date, required: true },
: { type: Number, required: true },
: { type: Number, required: true },
: { type: Number, required: true },
: { type: Number, required: true }, // 可在前端计算
});

View File

@@ -231,6 +231,7 @@ export interface IAccount {
// 定义日增长数据接口类型
export interface IDailyGrowth {
日期: Date;
消耗: number; // 推广引流消费
总人数: number;
扣除人数: number;
日增长人数?: number;

View File

@@ -6,7 +6,7 @@
*/
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { fullIndexCleanup } from '@/utils/cleanupIndexes';
/**

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Account } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -0,0 +1,102 @@
/**
* 文件名: src/pages/api/backstage/accounts/accountgrowth/clear.ts
* 作者: 阿瑞
* 功能: 清除指定账号的日增长数据 (开发调试功能)
* 版本: v1.0.0
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { Account } from '@/models';
import { Types } from 'mongoose';
interface ClearGrowthDataRequest {
accountId: string;
teamId: string;
}
interface ApiResponse {
success: boolean;
message: string;
error?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ApiResponse>
) {
// ▸ 方法验证模块
if (req.method !== 'DELETE') {
return res.status(405).json({
success: false,
message: '方法不被允许'
});
}
// ▸ 开发环境检查模块
if (process.env.NODE_ENV !== 'development') {
return res.status(403).json({
success: false,
message: '此功能仅在开发环境下可用'
});
}
try {
// ▸ 请求数据验证模块
const { accountId, teamId }: ClearGrowthDataRequest = req.body;
if (!accountId || !teamId) {
return res.status(400).json({
success: false,
message: '缺少必要参数accountId 和 teamId'
});
}
// 验证 ObjectId 格式
if (!Types.ObjectId.isValid(accountId) || !Types.ObjectId.isValid(teamId)) {
return res.status(400).json({
success: false,
message: '无效的 ID 格式'
});
}
// ▸ 账号验证模块
const account = await Account.findOne({
_id: new Types.ObjectId(accountId),
团队: new Types.ObjectId(teamId)
});
if (!account) {
return res.status(404).json({
success: false,
message: '未找到指定账号'
});
}
// ▸ 数据清除模块
const result = await Account.updateOne(
{ _id: new Types.ObjectId(accountId) },
{ $set: { : [] } }
);
if (result.modifiedCount === 0) {
return res.status(400).json({
success: false,
message: '清除操作失败'
});
}
// ▸ 成功响应模块
return res.status(200).json({
success: true,
message: `账号 ${account.} 的日增长数据已成功清除`
});
} catch (error) {
console.error('清除日增长数据错误:', error);
return res.status(500).json({
success: false,
message: '服务器内部错误',
error: error instanceof Error ? error.message : '未知错误'
});
}
}

View File

@@ -1,6 +1,6 @@
//src\pages\api\backstage\accounts\accountgrowth\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB'; // 使用高阶函数
import connectDB from '@/lib/connectDB'; // 使用高阶函数
import { Account } from '@/models'; // 使用之前的 AccountSchema 模型
interface AccountData {

View File

@@ -1,56 +0,0 @@
//src\pages\api\backstage\accounts\accountgrowth\register.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB'; // 使用高阶函数确保数据库连接
import { Account } from '@/models'; // 使用 Account 模型
import dayjs from 'dayjs';
interface DailyGrowth {
日期: Date;
总人数: number;
扣除人数: number;
日增长人数: number;
}
type Data = {
success?: boolean;
error?: string;
};
const handler = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
const { accountId, date, totalPeople, deductedPeople } = req.body;
try {
// 找到目标账号
const account = await Account.findById(accountId);
if (!account) {
return res.status(404).json({ error: '账号未找到' });
}
// 查找是否已经存在当天的记录
const existingRecord = account..find((record: DailyGrowth) =>
dayjs(record.).isSame(dayjs(date), 'day')
);
if (existingRecord) {
// 更新已有记录
existingRecord. = totalPeople;
existingRecord. = deductedPeople;
existingRecord. = totalPeople - deductedPeople;
} else {
// 添加新记录
account..push({
日期: dayjs(date).toDate(),
总人数: totalPeople,
扣除人数: deductedPeople,
日增长人数: totalPeople - deductedPeople,
});
}
await account.save();
res.status(200).json({ success: true });
} catch (error) {
res.status(500).json({ error: '保存增长记录失败' });
}
};
export default connectDB(handler); // 使用 connectDB 包装 API 处理函数

View File

@@ -1,13 +1,27 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { broadcastUpdate } from './sse'; // 引入广播功能
import { Account } from '@/models';
import connectDB from '@/lib/connectDB';
import dayjs from 'dayjs';
import { IDailyGrowth } from '@/models/types'; // 引入 DailyGrowth 类型
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { accountId, date, totalPeople, deductedPeople } = req.body;
if (req.method !== 'POST') {
return res.status(405).json({ error: '仅支持POST请求' });
}
const { accountId, date, consumption, totalPeople, deductedPeople } = req.body;
// 验证必要参数
if (!accountId || !date || totalPeople === undefined || deductedPeople === undefined) {
return res.status(400).json({
error: '缺少必要参数',
received: { accountId, date, consumption, totalPeople, deductedPeople }
});
}
try {
console.log('接收到的数据:', { accountId, date, consumption, totalPeople, deductedPeople });
const account = await Account.findById(accountId);
if (!account) {
return res.status(404).json({ error: '账号未找到' });
@@ -39,12 +53,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// 更新现有记录或添加新记录
if (existingRecord) {
existingRecord. = consumption || 0;
existingRecord. = totalPeople;
existingRecord. = deductedPeople;
existingRecord. = growthPeople;
} else {
account..push({
日期: selectedDate.toDate(),
消耗: consumption || 0,
总人数: totalPeople,
扣除人数: deductedPeople,
日增长人数: growthPeople,
@@ -58,8 +74,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ success: true });
} catch (error) {
res.status(500).json({ error: '保存增长记录失败' });
console.error('保存增长记录失败:', error);
res.status(500).json({
error: '保存增长记录失败',
details: error instanceof Error ? error.message : '未知错误'
});
}
};
export default handler;
export default connectDB(handler);

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Account } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Transaction } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Brand } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Brand } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Category } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Category } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Coupon } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\coupons\assign\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer, Coupon } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一券码
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\coupons\assign\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\coupons\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Coupon } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\customers\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\customers\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Customer } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,6 +1,6 @@
// src/pages/api/backstage/customers/sales/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Customer, SalesRecord, AfterSalesRecord } from '@/models'; // 导入模型
import { ICustomer } from '@/models/types';

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/mine/info/[id].ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { User } from '@/models';
import { IPermission, IUser } from '@/models/types';
import { buildPermissionTree } from '@/pages/api/buildPermissionTree';

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/mine/sales/index.ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { SalesRecord } from '@/models';
//import { ISalesRecord } from '@/models/types';

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/payment-platforms/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PaymentPlatform } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/payment-platforms/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { PaymentPlatform } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\products\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Product } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Product } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/sales/Records/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { SalesRecord } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { id } = req.query; // 从请求中获取销售记录的 ID

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\sales\Records\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { SalesRecord } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { SalesRecord } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,7 +1,7 @@
// src/pages/api/backstage/sales/aftersale/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { AfterSalesRecord, SalesRecord } from '@/models'; // 售后记录模型文件名为 AfterSalesRecord
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import mongoose from 'mongoose';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB'; // 确保数据库连接
import connectDB from '@/lib/connectDB'; // 确保数据库连接
import { AfterSalesRecord } from '@/models'; // 导入售后记录模型
//import { IAfterSalesRecord } from '@/models/types'; // 导入售后记录类型定义
import { isValidObjectId } from 'mongoose'; // Mongoose 工具函数

View File

@@ -1,7 +1,7 @@
//src/pages/api/backstage/sales/aftersale/records/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { AfterSalesRecord } from '@/models'; // 导入售后记录模型
import connectDB from '@/utils/connectDB'; // 确保数据库已连接
import connectDB from '@/lib/connectDB'; // 确保数据库已连接
import mongoose from 'mongoose';
interface IQuery {

View File

@@ -1,6 +1,6 @@
//src\pages\api\backstage\sales\aftersale\records\updateProgress.ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { AfterSalesRecord, SalesRecord } from '@/models';
async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { CustomerPaymentCode } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\sales\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { SalesRecord, Customer } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
interface CouponUsage {
_id: string;
券码: string;

View File

@@ -4,7 +4,7 @@
* 版本: 1.0.0
*/
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { SalesRecord } from '@/models';
/**

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Supplier } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Supplier } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -1,6 +1,6 @@
//src\pages\api\backstage\transactions\[id].ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Transaction } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Transaction, Customer } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\users\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { IUser } from '@/models/types'; // 导入 IUser 接口类型
import bcrypt from 'bcryptjs';

View File

@@ -1,7 +1,7 @@
//src\pages\api\backstage\users\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import bcrypt from 'bcryptjs';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { buildPermissionTree } from './buildPermissionTree';
import type { IPermission } from '@/models/types';

View File

@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
import mongoose from 'mongoose';

View File

@@ -5,7 +5,7 @@
* 版本: v2.0
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
import mongoose from 'mongoose';

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { CustomerPaymentCode } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Product } from '@/models';
const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -5,7 +5,7 @@
* 版本: v2.0
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { Product } from '@/models';
const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,7 +1,7 @@
//src\pages\api\roles\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Role } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { query: { id }, method } = req;

View File

@@ -1,7 +1,7 @@
//src\pages\api\roles\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Role } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { buildPermissionTree } from '@/pages/api/buildPermissionTree';
import { IPermission } from '@/models/types';

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { ScriptCategory, Script } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { ScriptCategory } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { Script, TagInfo } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { Script, ScriptCategory, TagInfo } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,6 +1,6 @@
//src\pages\api\team\create.ts
import { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB'; // 确保数据库连接
import connectDB from '@/lib/connectDB'; // 确保数据库连接
import { Team, User } from '@/models'; // 引入 Team 和 User 模型
import { ITeam } from '@/models/types'; // 引入类型定义

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { Team } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API响应接口
interface ApiResponse<T = any> {

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
/**

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
/**

View File

@@ -6,7 +6,7 @@
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
/**

View File

@@ -7,7 +7,7 @@
import type { NextApiRequest } from 'next';
import type { NextApiResponseServerIO } from '@/types/socket';
import { Todo } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponseServerIO) => {

View File

@@ -7,7 +7,7 @@
import type { NextApiRequest } from 'next';
import type { NextApiResponseServerIO } from '@/types/socket';
import { Todo } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
// API处理函数
const handler = async (req: NextApiRequest, res: NextApiResponseServerIO) => {

View File

@@ -1,6 +1,6 @@
//src\pages/api/tools/SFExpress/updateLogisticsDetails.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
import { querySFExpress } from '@/utils/querySFExpress';

View File

@@ -1,6 +1,6 @@
// src/pages/api/logistics/detail/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { id } = req.query; // 从 URL 中获取订单 ID

View File

@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { LogisticsRecord } from '@/models';
const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models'; // 用户模型存储在 models 目录中
import connectDB from '@/utils/connectDB'; // 数据库连接工具
import connectDB from '@/lib/connectDB'; // 数据库连接工具
import { buildPermissionTree } from './buildPermissionTree'; // 存在的权限树构建工具
export default connectDB(async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,7 +1,7 @@
//src\pages\api\users\[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import { IUser } from '@/models/types'; // 导入 IUser 接口类型
import bcrypt from 'bcryptjs';

View File

@@ -1,7 +1,7 @@
//src\pages\api\users\index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { User } from '@/models';
import connectDB from '@/utils/connectDB';
import connectDB from '@/lib/connectDB';
import bcrypt from 'bcryptjs';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, message, Select } from 'antd';
import axios from 'axios';
import { Modal, Form, Input, Select, App } from 'antd';
import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息
import { IAccount, IUser, ICategory } from '@/models/types';
@@ -16,6 +15,7 @@ const AccountModal: React.FC<AccountModalProps> = ({ visible, onOk, onCancel, ac
const [users, setUsers] = useState<IUser[]>([]);
const [categories, setCategories] = useState<ICategory[]>([]);
const userInfo = useUserInfo(); // 获取当前用户信息
const { message } = App.useApp(); // 使用 useApp hook 获取 message
/*useEffect(() => {
fetchUsersAndCategories(userInfo.团队?._id);
@@ -60,12 +60,21 @@ const AccountModal: React.FC<AccountModalProps> = ({ visible, onOk, onCancel, ac
const fetchUsersAndCategories = async (teamId: string) => {
try {
const [usersResponse, categoriesResponse] = await Promise.all([
axios.get(`/api/backstage/users?teamId=${teamId}`),
axios.get(`/api/backstage/categories?teamId=${teamId}`)
fetch(`/api/backstage/users?teamId=${teamId}`),
fetch(`/api/backstage/categories?teamId=${teamId}`)
]);
setUsers(usersResponse.data.users);
setCategories(categoriesResponse.data.categories);
} catch (error) {
if (!usersResponse.ok || !categoriesResponse.ok) {
throw new Error('获取数据失败');
}
const usersData = await usersResponse.json();
const categoriesData = await categoriesResponse.json();
setUsers(usersData.users);
setCategories(categoriesData.categories);
} catch (error: unknown) {
console.error('获取用户或品类数据失败:', error);
message.error('加载用户或品类数据失败');
}
};
@@ -76,18 +85,24 @@ const AccountModal: React.FC<AccountModalProps> = ({ visible, onOk, onCancel, ac
const method = account ? 'PUT' : 'POST';
const url = account ? `/api/backstage/accounts/${account._id}` : '/api/backstage/accounts';
await axios({
const response = await fetch(url, {
method,
url,
data: {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...values,
团队: userInfo.团队?._id,
},
}),
});
if (!response.ok) {
throw new Error('店铺账号操作失败');
}
message.success('店铺账号操作成功');
onOk();
} catch (error) {
} catch (error: unknown) {
console.error('Validate Failed:', error);
message.error('店铺账号操作失败');
}

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Table, Button, message, Card, Tag, Typography, Popconfirm } from 'antd';
import axios from 'axios';
import { Table, Button, Card, Tag, Typography, Popconfirm, App } from 'antd';
import AccountModal from './account-modal'; // 引入店铺账号模态框组件
import { IAccount, ICategory } from '@/models/types'; // 确保有正确的类型定义
import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息
@@ -15,6 +14,7 @@ const AccountsPage = () => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentAccount, setCurrentAccount] = useState<IAccount | null>(null);
const userInfo = useUserInfo(); // 获取当前用户信息
const { message } = App.useApp(); // 使用 useApp hook 获取 message
useEffect(() => {
if (userInfo.?._id) {
@@ -24,9 +24,16 @@ const AccountsPage = () => {
const fetchAccounts = async (teamId: string) => {
try {
const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`);
const response = await fetch(`/api/backstage/accounts?teamId=${teamId}`);
if (!response.ok) {
throw new Error('获取店铺账号数据失败');
}
const data = await response.json();
setAccounts(data.accounts);
} catch (error) {
} catch (error: unknown) {
console.error('加载店铺账号数据失败:', error);
message.error('加载店铺账号数据失败');
}
};
@@ -45,12 +52,20 @@ const AccountsPage = () => {
const handleDelete = async (id: string) => {
try {
await axios.delete(`/api/backstage/accounts/${id}`);
const response = await fetch(`/api/backstage/accounts/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('删除店铺账号失败');
}
if (userInfo.?._id) {
fetchAccounts(userInfo.._id);
}
message.success('店铺账号删除成功');
} catch (error) {
} catch (error: unknown) {
console.error('删除店铺账号失败:', error);
message.error('删除店铺账号失败');
}
};

View File

@@ -54,8 +54,8 @@ const buildCustomerAddress = (address: any): string => {
};
// 导出Excel相关库
//import * as XLSX from 'xlsx';
//import { saveAs } from 'file-saver';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
const AfterSaleRecordPage = () => {
const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例
@@ -662,7 +662,6 @@ const AfterSaleRecordPage = () => {
};
// 导出Excel的函数
/*
const exportToExcel = () => {
if (filteredRecords.length === 0) {
message.warning('没有可导出的数据');
@@ -737,9 +736,9 @@ const AfterSaleRecordPage = () => {
}
});
// 创建工作簿和工作表
//const ws = XLSX.utils.json_to_sheet(exportData);
//const wb = XLSX.utils.book_new();
//XLSX.utils.book_append_sheet(wb, ws, '售后记录');
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '售后记录');
// 设置列宽 (大致估计,可能需要调整)
const colWidths = [
@@ -763,7 +762,7 @@ const AfterSaleRecordPage = () => {
{ wch: 10 }, // 待收
{ wch: 20 }, // 备注
];
//ws['!cols'] = colWidths;
ws['!cols'] = colWidths;
// 生成Excel文件并下载
const now = new Date();
@@ -781,7 +780,6 @@ const AfterSaleRecordPage = () => {
message.error({ content: '导出失败,请重试', key: 'exporting' });
}
};
*/
return (
<Card>
@@ -862,7 +860,7 @@ const AfterSaleRecordPage = () => {
size="small"
type="primary"
icon={<DownloadOutlined />}
//onClick={exportToExcel}
onClick={exportToExcel}
style={{ marginLeft: 8 }}
>

View File

@@ -0,0 +1,766 @@
/**
* 客户分析模态框组件
* 作者: 阿瑞
* 功能: 展示团队客户列表和分析数据
* 版本: v1.0.0
*/
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { Modal, Table, message, Spin, Tabs, Card, Row, Col, DatePicker, Button, Space, Divider, Statistic } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { FilterOutlined, ReloadOutlined, CalendarOutlined, UserOutlined, EnvironmentOutlined } from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
// 客户数据接口定义
interface ICustomer {
_id: string;
姓名: string;
电话: string;
: {
省份: string;
城市: string;
区县: string;
详细地址: string;
};
加粉日期: string;
createdAt: string;
updatedAt: string;
}
// 组件Props接口定义
interface CustomerAnalysisModalProps {
visible: boolean;
onCancel: () => void;
teamId: string;
}
/**
* 客户分析模态框组件
* 展示客户列表和基础分析信息
*/
const CustomerAnalysisModal: React.FC<CustomerAnalysisModalProps> = ({
visible,
onCancel,
teamId
}) => {
// 状态管理
const [customers, setCustomers] = useState<ICustomer[]>([]);
const [loading, setLoading] = useState<boolean>(false);
// 筛选状态管理
const [filters, setFilters] = useState({
createTimeRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null,
addTimeRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null,
});
// ECharts实例引用
const chartRef = useRef<any>(null);
// 记录是否已经初始化过尺寸
const [chartInitialized, setChartInitialized] = useState<boolean>(false);
/**
* 获取客户数据
* 从API获取团队的所有客户信息
*/
const fetchCustomers = async () => {
if (!teamId) return;
setLoading(true);
try {
const response = await fetch(`/api/backstage/customers?teamId=${teamId}`);
const data = await response.json();
if (response.ok) {
setCustomers(data.customers || []);
} else {
message.error('获取客户数据失败');
}
} catch (error) {
console.error('获取客户数据出错:', error);
message.error('获取客户数据出错');
} finally {
setLoading(false);
}
};
/**
* 格式化地址显示
* 将地址对象转换为完整的地址字符串
*/
const formatAddress = (address: ICustomer['地址']): string => {
if (!address) return '-';
const { , , , } = address;
return [, , , ].filter(Boolean).join(' ');
};
/**
* 格式化日期显示
* 将ISO日期字符串格式化为可读格式
*/
const formatDate = (dateString: string): string => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('zh-CN');
};
/**
* 筛选条件处理函数
* 管理各种筛选条件的变更
*/
const handleFilterChange = {
// 创建时间范围筛选
createTimeRange: (dates: any) => {
setFilters(prev => ({ ...prev, createTimeRange: dates }));
},
// 加粉日期范围筛选
addTimeRange: (dates: any) => {
setFilters(prev => ({ ...prev, addTimeRange: dates }));
},
// 重置所有筛选
reset: () => {
setFilters({
createTimeRange: null,
addTimeRange: null,
});
}
};
/**
* 根据筛选条件过滤客户数据
* 应用时间范围筛选逻辑
*/
const filteredCustomers = useMemo(() => {
let filtered = [...customers];
// 筛选创建时间范围
if (filters.createTimeRange) {
const [start, end] = filters.createTimeRange;
filtered = filtered.filter(customer => {
const createTime = dayjs(customer.createdAt);
return createTime.isAfter(start.startOf('day')) && createTime.isBefore(end.endOf('day'));
});
}
// 筛选加粉日期范围
if (filters.addTimeRange) {
const [start, end] = filters.addTimeRange;
filtered = filtered.filter(customer => {
if (!customer.) return false;
const addTime = dayjs(customer.);
return addTime.isAfter(start.startOf('day')) && addTime.isBefore(end.endOf('day'));
});
}
return filtered;
}, [customers, filters]);
/**
* 统计客户地区分布数据
* 按省份统计客户数量,用于地图展示
*/
const customerLocationStats = useMemo(() => {
const stats: { [key: string]: number } = {};
const details: { [key: string]: ICustomer[] } = {};
filteredCustomers.forEach(customer => {
const province = customer.?.;
if (province) {
// 统计数量
stats[province] = (stats[province] || 0) + 1;
// 保存详细信息
if (!details[province]) {
details[province] = [];
}
details[province].push(customer);
}
});
// 转换为echarts需要的数据格式
const mapData = Object.entries(stats).map(([name, value]) => ({
name,
value,
customers: details[name]
}));
return {
stats,
mapData,
details
};
}, [filteredCustomers]);
/**
* 地区分布图表配置
* 使用柱状图展示客户地区分布
*/
const getChartOption = () => {
const { mapData } = customerLocationStats;
// 按客户数量排序显示前10个地区
const sortedData = mapData
.sort((a, b) => b.value - a.value)
.slice(0, 10);
return {
title: {
text: '客户地区分布TOP10',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
const data = params[0];
return `${data.name}<br/>客户数量: ${data.value}`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: sortedData.map(item => item.name),
axisTick: {
alignWithLabel: true
},
axisLabel: {
rotate: 45,
fontSize: 12
}
},
yAxis: {
type: 'value',
name: '客户数量(人)',
nameTextStyle: {
fontSize: 12
}
},
series: [
{
name: '客户数量',
type: 'bar',
data: sortedData.map(item => ({
value: item.value,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
})),
emphasis: {
focus: 'series'
},
animationDelay: (idx: number) => idx * 80,
animationDuration: 800,
animationEasing: 'cubicOut'
}
],
// 全局动画配置
animation: true,
animationDuration: 800,
animationEasing: 'cubicOut',
animationDelayUpdate: (idx: number) => idx * 30
};
};
// 表格列配置
const columns: ColumnsType<ICustomer> = [
{
title: '姓名',
dataIndex: '姓名',
key: '姓名',
width: 120,
fixed: 'left',
},
{
title: '电话',
dataIndex: '电话',
key: '电话',
width: 140,
},
{
title: '地址',
dataIndex: '地址',
key: '地址',
width: 300,
render: (address: ICustomer['地址']) => formatAddress(address),
},
{
title: '加粉日期',
dataIndex: '加粉日期',
key: '加粉日期',
width: 120,
render: (date: string) => formatDate(date),
sorter: (a, b) => new Date(a.).getTime() - new Date(b.).getTime(),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 150,
render: (date: string) => formatDate(date),
sorter: (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
defaultSortOrder: 'descend',
},
];
// 监听模态框打开事件,获取客户数据
useEffect(() => {
if (visible && teamId) {
fetchCustomers();
}
}, [visible, teamId]);
// 处理图表容器尺寸问题 - 仅在Modal首次打开时执行
useEffect(() => {
if (visible && !chartInitialized) {
// Modal完全显示后延迟触发图表重新调整大小
const timer = setTimeout(() => {
if (chartRef.current) {
const echartsInstance = chartRef.current.getEchartsInstance();
if (echartsInstance) {
echartsInstance.resize();
setChartInitialized(true);
}
}
}, 300);
return () => clearTimeout(timer);
}
}, [visible, chartInitialized]);
// 重置初始化状态当Modal关闭时
useEffect(() => {
if (!visible) {
setChartInitialized(false);
}
}, [visible]);
// 创建筛选工具栏组件
const FilterToolbar = () => (
<Card
size="small"
style={{
marginBottom: '16px',
background: 'linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)',
border: '1px solid #e8f4f8',
borderRadius: '8px'
}}
>
<Row gutter={[16, 8]} align="middle">
<Col>
<FilterOutlined style={{ color: '#1890ff', fontSize: '16px' }} />
<span style={{
marginLeft: '8px',
fontWeight: '600',
color: '#262626',
fontSize: '14px'
}}>
</span>
</Col>
<Col>
<Divider type="vertical" style={{ height: '20px', margin: '0 8px' }} />
</Col>
<Col>
<Space direction="vertical" size={4}>
<span style={{ fontSize: '12px', color: '#8c8c8c' }}></span>
<RangePicker
size="small"
placeholder={['开始日期', '结束日期']}
value={filters.createTimeRange}
onChange={handleFilterChange.createTimeRange}
style={{ width: '200px' }}
allowClear
/>
</Space>
</Col>
<Col>
<Space direction="vertical" size={4}>
<span style={{ fontSize: '12px', color: '#8c8c8c' }}></span>
<RangePicker
size="small"
placeholder={['开始日期', '结束日期']}
value={filters.addTimeRange}
onChange={handleFilterChange.addTimeRange}
style={{ width: '200px' }}
allowClear
/>
</Space>
</Col>
<Col>
<Button
size="small"
icon={<ReloadOutlined />}
onClick={handleFilterChange.reset}
style={{
marginTop: '18px',
height: '24px'
}}
type="dashed"
>
</Button>
</Col>
<Col flex="auto">
<div style={{ textAlign: 'right', fontSize: '12px' }}>
{filters.createTimeRange || filters.addTimeRange ? (
<div>
<div style={{ color: '#52c41a', fontWeight: '500', marginBottom: '2px' }}>
🔍
</div>
<div style={{ color: '#8c8c8c' }}>
{filteredCustomers.length} / {customers.length}
</div>
</div>
) : (
<div style={{ color: '#8c8c8c' }}>
{new Date().toLocaleString('zh-CN')}
</div>
)}
</div>
</Col>
</Row>
</Card>
);
// 创建数据统计卡片组件
const StatisticsCards = () => (
<Row gutter={[16, 16]} style={{ marginBottom: '16px' }}>
<Col span={8}>
<Card
size="small"
style={{
textAlign: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '8px'
}}
>
<Statistic
title={<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: '13px' }}></span>}
value={filteredCustomers.length}
suffix={<span style={{ color: 'rgba(255,255,255,0.6)' }}></span>}
valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }}
prefix={<UserOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
/>
</Card>
</Col>
<Col span={8}>
<Card
size="small"
style={{
textAlign: 'center',
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
border: 'none',
borderRadius: '8px'
}}
>
<Statistic
title={<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: '13px' }}></span>}
value={Object.keys(customerLocationStats.stats).length}
suffix={<span style={{ color: 'rgba(255,255,255,0.6)' }}></span>}
valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }}
prefix={<EnvironmentOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
/>
</Card>
</Col>
<Col span={8}>
<Card
size="small"
style={{
textAlign: 'center',
background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
border: 'none',
borderRadius: '8px'
}}
>
<Statistic
title={<span style={{ color: 'rgba(255,255,255,0.8)', fontSize: '13px' }}></span>}
value={
filteredCustomers.length > 0
? dayjs(Math.max(...filteredCustomers.map(c => new Date(c. || c.createdAt).getTime()))).format('MM-DD')
: '-'
}
valueStyle={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}
prefix={<CalendarOutlined style={{ color: 'rgba(255,255,255,0.8)' }} />}
/>
</Card>
</Col>
</Row>
);
// 创建Tabs标签页数据
const tabItems = [
{
key: 'overview',
label: (
<span>
<EnvironmentOutlined style={{ marginRight: '4px' }} />
</span>
),
children: (
<div>
<FilterToolbar />
<StatisticsCards />
<Row gutter={[16, 16]}>
<Col span={16}>
<Card
title={
<span style={{ fontSize: '14px', fontWeight: '600' }}>
📊
</span>
}
size="small"
style={{
height: '480px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}
bodyStyle={{ padding: '16px' }}
>
<ReactECharts
ref={chartRef}
option={getChartOption()}
style={{ height: '400px', width: '100%' }}
opts={{
renderer: 'canvas'
}}
notMerge={false}
lazyUpdate={true}
onChartReady={() => {
// 图表初始化完成后,确保尺寸正确(仅在未初始化时)
if (!chartInitialized) {
setTimeout(() => {
if (chartRef.current) {
const echartsInstance = chartRef.current.getEchartsInstance();
if (echartsInstance) {
echartsInstance.resize();
setChartInitialized(true);
}
}
}, 100);
}
}}
/>
</Card>
</Col>
<Col span={8}>
<Card
title={
<span style={{ fontSize: '14px', fontWeight: '600' }}>
📋
</span>
}
size="small"
style={{
height: '480px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}
bodyStyle={{ padding: '12px' }}
>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
{Object.entries(customerLocationStats.stats)
.sort(([,a], [,b]) => b - a)
.map(([province, count], index) => (
<div
key={province}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '10px 12px',
borderBottom: '1px solid #f0f0f0',
borderRadius: '4px',
marginBottom: '4px',
background: index % 2 === 0 ? '#fafafa' : '#fff',
transition: 'all 0.2s ease',
cursor: 'pointer'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e6f7ff';
e.currentTarget.style.transform = 'translateX(2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = index % 2 === 0 ? '#fafafa' : '#fff';
e.currentTarget.style.transform = 'translateX(0)';
}}
>
<span style={{
fontWeight: '500',
fontSize: '13px',
color: '#262626'
}}>
{province}
</span>
<span style={{
color: '#1890ff',
fontWeight: 'bold',
padding: '3px 10px',
background: 'linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)',
borderRadius: '12px',
fontSize: '12px',
border: '1px solid #91d5ff'
}}>
{count}
</span>
</div>
))
}
{Object.keys(customerLocationStats.stats).length === 0 && (
<div style={{
textAlign: 'center',
color: '#bfbfbf',
padding: '60px 0',
fontSize: '14px'
}}>
<EnvironmentOutlined style={{ fontSize: '24px', marginBottom: '8px', display: 'block' }} />
</div>
)}
</div>
</Card>
</Col>
</Row>
</div>
),
},
{
key: 'list',
label: (
<span>
<UserOutlined style={{ marginRight: '4px' }} />
</span>
),
children: (
<div>
<div style={{
marginBottom: '16px',
padding: '12px 16px',
background: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef'
}}>
<Row justify="space-between" align="middle">
<Col>
<span style={{ fontSize: '14px', fontWeight: '600', color: '#495057' }}>
📋
</span>
</Col>
<Col>
<span style={{ fontSize: '12px', color: '#6c757d' }}>
{filteredCustomers.length}
{(filters.createTimeRange || filters.addTimeRange) && (
<span style={{ color: '#28a745', marginLeft: '8px' }}>
()
</span>
)}
</span>
</Col>
</Row>
</div>
<Table
columns={columns}
dataSource={filteredCustomers}
rowKey="_id"
size="small"
scroll={{
x: 'max-content',
y: 400
}}
pagination={{
pageSize: 15,
showSizeChanger: true,
pageSizeOptions: ['10', '15', '20', '50'],
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`,
size: 'small'
}}
// 行样式优化
rowClassName={(_, index) => (index % 2 === 0 ? 'even-row' : 'odd-row')}
style={{
border: '1px solid #e9ecef',
borderRadius: '8px',
overflow: 'hidden'
}}
/>
</div>
),
},
];
return (
<Modal
title={
<div style={{
fontSize: '18px',
fontWeight: '600',
color: '#262626',
display: 'flex',
alignItems: 'center'
}}>
📊
</div>
}
open={visible}
onCancel={onCancel}
footer={null}
width={1400}
centered
destroyOnClose
style={{ top: 20 }}
bodyStyle={{
padding: '20px',
background: '#fafafa',
maxHeight: '85vh',
overflow: 'auto'
}}
>
<Spin spinning={loading}>
<Tabs
defaultActiveKey="overview"
items={tabItems}
size="small"
style={{
background: '#fff',
borderRadius: '8px',
padding: '16px'
}}
onChange={(activeKey) => {
// Tab切换时如果切换到overview且未初始化重新调整图表大小
if (activeKey === 'overview' && !chartInitialized) {
setTimeout(() => {
if (chartRef.current) {
const echartsInstance = chartRef.current.getEchartsInstance();
if (echartsInstance) {
echartsInstance.resize();
setChartInitialized(true);
}
}
}, 100);
}
}}
/>
</Spin>
</Modal>
);
};
export default CustomerAnalysisModal;

File diff suppressed because it is too large Load Diff

View File

@@ -109,6 +109,8 @@ const buildCustomerAddress = (address: any): string => {
const EditSalesModal = lazy(() => import("./sales-modal"));
const AfterSalesModal = lazy(() => import("./AfterSalesModal"));
const ShipModal = lazy(() => import("./ship-modal"));
const CustomerAnalysisModal = lazy(() => import("./components/CustomerAnalysisModal"));
const SalesAnalysisModal = lazy(() => import("./components/SalesAnalysisModal"));
const SalesPage = () => {
const { message } = useApp();
@@ -152,6 +154,8 @@ const SalesPage = () => {
edit: false,
ship: false,
afterSales: false,
customerAnalysis: false,
salesAnalysis: false,
});
const [currentRecord, setCurrentRecord] = useState<ISalesRecord | null>(null);
const [loading, setLoading] = useState(false);
@@ -192,9 +196,15 @@ const SalesPage = () => {
setCurrentRecord(record);
setAfterSalesType(type);
setModals(prev => ({ ...prev, afterSales: true }));
},
showCustomerAnalysis: () => {
setModals(prev => ({ ...prev, customerAnalysis: true }));
},
showSalesAnalysis: () => {
setModals(prev => ({ ...prev, salesAnalysis: true }));
},
closeAll: () => {
setModals({ edit: false, ship: false, afterSales: false });
setModals({ edit: false, ship: false, afterSales: false, customerAnalysis: false, salesAnalysis: false });
setCurrentRecord(null);
setAfterSalesType(null);
},
@@ -227,33 +237,8 @@ const SalesPage = () => {
});
}
});
// 批量预加载产品图片
/*if (productIds.size > 0) {
fetch('/api/products/batchImages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: Array.from(productIds)
})
}).then(async (response) => {
if (response.ok) {
const data = await response.json();
// 存储图片到本地缓存
const imageData = data?.images || {};
localStorage.setItem('productImageCache', JSON.stringify(imageData));
console.log('预加载产品图片成功');
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}).catch((error: Error) => {
console.error('预加载产品图片失败', error);
});
}*/
// 批量预加载物流状态
/*
if (recordIds.size > 0) {
fetch('/api/logistics/batchStatus', {
method: 'POST',
@@ -277,6 +262,7 @@ const SalesPage = () => {
console.error('预加载物流状态失败', error);
});
}
*/
}
}, [salesRecords]);
@@ -934,10 +920,50 @@ const SalesPage = () => {
}}>
<Table
sticky
title={undefined}
title={() => (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: '-16px -24px 16px -24px',
padding: '16px 24px',
background: '#fafafa',
borderRadius: '6px 6px 0 0'
}}>
<span style={{ fontWeight: 'bold', fontSize: '16px' }}></span>
<div style={{ display: 'flex', gap: '8px' }}>
<Button size="small" type="primary" onClick={modalHandlers.showCustomerAnalysis}>
</Button>
<Tooltip title="功能开发中,敬请期待">
<Button size="small" disabled>
Excel
</Button>
</Tooltip>
<Button
size="small"
onClick={modalHandlers.showSalesAnalysis}
type="primary"
icon={<Iconify icon="solar:chart-square-bold" />}
>
</Button>
<Tooltip title="功能开发中,敬请期待">
<Button size="small" disabled>
</Button>
</Tooltip>
<Tooltip title="功能开发中,敬请期待">
<Button size="small" disabled>
</Button>
</Tooltip>
</div>
</div>
)}
scroll={{
// 表格高度 - 自动计算减去分页器高度,确保表格占满整个容器
y: 'calc(100vh - 160px)', // 减去头部、分页器等组件的高度
// 表格高度 - 自动计算减去分页器高度和标题高度,确保表格占满整个容器
y: 'calc(100vh - 240px)', // 减去头部、标题栏、分页器等组件的高度
// 表格宽度 - 支持水平滚动,内容超出时可滚动
x: 'max-content'
}}
@@ -994,6 +1020,20 @@ const SalesPage = () => {
record={currentRecord}
/>
)}
{modals.customerAnalysis && (
<CustomerAnalysisModal
visible={modals.customerAnalysis}
onCancel={modalHandlers.closeAll}
teamId={userInfo?.?._id || ''}
/>
)}
{modals.salesAnalysis && (
<SalesAnalysisModal
visible={modals.salesAnalysis}
onCancel={modalHandlers.closeAll}
salesRecords={salesRecords}
/>
)}
</Suspense>
</div>
);

View File

@@ -0,0 +1,909 @@
//src\pages\team\SaleRecord\index.tsx
import React, { useState, useEffect, useMemo } from "react";
import {
Table,
Button,
TableColumnType,
Tag,
Tooltip,
DatePicker,
App,
} from "antd";
import { ISalesRecord, ICoupon } from "@/models/types"; // 确保有正确的类型定义
import { useUserInfo } from "@/store/userStore"; // 使用 Zustand 获取用户信息
import {
FieldTimeOutlined,
MobileOutlined,
UserOutlined,
WechatOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import MyTooltip from "@/components/tooltip/MyTooltip";
import ProductCardList from "@/components/product/ProductCardList";
// 导出Excel相关库
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
// 移除了模态框相关的懒加载组件
const SalesPage = () => {
const { message } = App.useApp(); // 使用 useApp hook 获取 message
const [loading, setLoading] = useState<boolean>(false);
const [salesRecords, setSalesRecords] = useState<ISalesRecord[]>([]);
const [filteredRecords, setFilteredRecords] = useState<ISalesRecord[]>([]);
const [transactionDateRange, setTransactionDateRange] = useState<[Date | null, Date | null]>([null, null]);
const [addFansDateRange, setAddFansDateRange] = useState<[Date | null, Date | null]>([null, null]);
const userInfo = useUserInfo(); // 获取当前用户信息
useEffect(() => {
if (userInfo.?._id) {
fetchSalesRecords(userInfo.._id);
}
}, [userInfo]); // 恢复原来的依赖项只在userInfo变化时重新获取数据
// 当日期范围筛选条件改变时应用筛选
useEffect(() => {
if (salesRecords.length > 0) {
const filtered = applyFilters(salesRecords);
setFilteredRecords(filtered);
}
}, [transactionDateRange, addFansDateRange]);
// 筛选函数:应用日期范围筛选条件
const applyFilters = (records: ISalesRecord[]) => {
let filtered = [...records];
// 根据成交日期范围筛选
if (transactionDateRange[0] && transactionDateRange[1]) {
filtered = filtered.filter(record => {
if (!record.) return false;
const recordDate = new Date(record.);
return recordDate >= transactionDateRange[0]! &&
recordDate <= transactionDateRange[1]!;
});
}
// 根据加粉日期范围筛选
if (addFansDateRange[0] && addFansDateRange[1]) {
filtered = filtered.filter(record => {
if (!record.?.) return false;
const addFansDate = new Date(record..);
return addFansDate >= addFansDateRange[0]! &&
addFansDate <= addFansDateRange[1]!;
});
}
return filtered;
};
useEffect(() => {
if (salesRecords.length > 0) {
// 预加载产品图片和物流状态
const productIds = new Set<string>();
const recordIds = new Set<string>();
salesRecords.forEach(record => {
if (record._id) recordIds.add(record._id);
if (record. && record..length > 0) {
record..forEach(product => {
if (product._id) productIds.add(product._id);
});
}
});
// 批量预加载产品图片
if (productIds.size > 0) {
fetch('/api/products/batchImages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productIds: Array.from(productIds)
})
}).then(async (response) => {
if (response.ok) {
const data = await response.json();
const imageData = data?.images || {};
localStorage.setItem('productImageCache', JSON.stringify(imageData));
console.log('预加载产品图片成功');
}
}).catch((error: unknown) => {
console.error('预加载产品图片失败', error);
});
}
// 批量预加载物流状态
if (recordIds.size > 0) {
fetch('/api/logistics/batchStatus', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recordIds: Array.from(recordIds)
})
}).then(async (response) => {
if (response.ok) {
const data = await response.json();
const statusData = data?.statuses || {};
localStorage.setItem('logisticsStatusCache', JSON.stringify(statusData));
console.log('预加载物流状态成功');
}
}).catch((error: unknown) => {
console.error('预加载物流状态失败', error);
});
}
}
}, [salesRecords]);
// 获取销售记录
const fetchSalesRecords = async (teamId: string) => {
setLoading(true); // 请求开始前设置加载状态为 true
try {
const response = await fetch(
`/api/backstage/sales/Records?teamId=${teamId}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const records = data.salesRecords || [];
setSalesRecords(records); // 设置销售记录
setFilteredRecords(records); // 初始化filteredRecords
setLoading(false);
} catch (error: unknown) {
console.error('获取销售记录失败:', error);
message.error("加载销售记录失败"); // 处理错误
} finally {
setLoading(false); // 无论成功还是失败都确保加载状态被设置为 false
}
};
// 提取唯一的导购姓名用于筛选
// 提取唯一的成交日期用于筛选
// 合并导购姓名和成交日期筛选
//const columns: TableColumnType<ISalesRecord>[] = useMemo(() => [
const columns: TableColumnType<ISalesRecord>[] = useMemo(() => {
const baseColumns: TableColumnType<ISalesRecord>[] = [
{
title: "来源",
width: 60,
key: "订单来源信息",
align: "center",
render: (record: any) => {
const wechatId = record.?. ?? "未知";
const accountNumber = record.?. ?? "未知";
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Tag
icon={<MobileOutlined />}
color="blue"
style={{ marginBottom: 4 }}
>
{accountNumber}
</Tag>
<Tag icon={<WechatOutlined />} color="green">
{wechatId}
</Tag>
</div>
);
},
},
{
title: "日期/导购",
key: "日期和导购信息",
align: "center",
width: 160, // 减小宽度因为我们优化了控件大小
render: (record: any) => {
// 格式化日期为更简洁的形式 (年-月-日)
const formatDate = (dateString: string) => {
if (!dateString) return "未知";
const date = new Date(dateString);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '-');
};
const transactionDate = formatDate(record.);
const createdDate = formatDate(record.createdAt);
const guideName = record.?. ?? "未知";
// 为完整信息创建tooltip内容
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "4px"
}}>
{/* 导购信息 */}
<Tag
icon={<UserOutlined />}
color="green"
style={{
marginBottom: 4,
textAlign: 'center',
fontWeight: 'bold'
}}
>
{guideName}
</Tag>
{/* 成交日期 */}
<Tag
icon={<FieldTimeOutlined />}
color="blue"
style={{
margin: 0,
textAlign: 'center',
fontSize: '11px'
}}
>
: {transactionDate}
</Tag>
{/* 创建日期 */}
<Tag
icon={<FieldTimeOutlined />}
color="default"
style={{
margin: 0,
textAlign: 'center',
fontSize: '11px'
}}
>
: {createdDate}
</Tag>
</div>
);
},
},
{
title: "客户信息",
width: 60,
align: "center",
key: "客户信息",
render: (record: any) => {
const customerName = record.?. ?? "未知";
// 计算成交周期
const transactionDate = record. ? new Date(record.) : new Date();
const addFansDate = record.?. ? new Date(record..) : null;
const diffDays = addFansDate ? Math.ceil(
Math.abs(transactionDate.getTime() - addFansDate.getTime()) / (1000 * 60 * 60 * 24)
) : '未知';
// 从 record 获取待收金额
const unreceivedAmount = record.
? parseFloat(record..toFixed(2))
: 0;
// 如果待收金额大于0使用红色否则使用蓝色
const customerTagColor = unreceivedAmount > 0 ? "#cd201f" : "blue";
return (
<MyTooltip color="white" title={customerName} placement="topLeft">
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "4px" }}>
{/* 第一行:客户姓名 */}
<Tag
icon={<UserOutlined />}
color={customerTagColor}
style={{ margin: 0 }}
>
{record.?. ?? "未知"}
</Tag>
{/* 第二行:电话尾号 */}
<Tag icon={<MobileOutlined />} color="green" style={{ margin: 0 }}>
{record.?.
? `${record...slice(-4)}`
: "未知"}
</Tag>
{/* 第三行:加粉日期 */}
<Tag
icon={<FieldTimeOutlined />}
color="purple"
style={{ margin: 0 }}
>
{record.?.
? new Date(record..).toLocaleDateString()
: "未知"}
</Tag>
{/* 第四行:成交周期 */}
<Tag color="orange" style={{ margin: 0 }}>
{diffDays}
</Tag>
</div>
</MyTooltip>
);
},
},
//备注
{
title: "备注",
key: "备注",
dataIndex: "备注",
width: 260,
render: (text: string, _record: any) => {
return (
<MyTooltip title={text} color="white" placement="topLeft">
<div
style={{
lineHeight: "1.2",
fontSize: "1.0em",
}}
>
{text || "无备注"}
</div>
</MyTooltip>
);
},
},
{
title: "产品信息",
width: 420, // 增加宽度以适应3个产品 (130px * 3 + 间距)
dataIndex: "产品",
key: "productImage",
render: (products: any[], record: ISalesRecord) => {
return (
<div
style={{
width: '100%',
maxWidth: '410px', // 限制最大宽度,超出时显示滚动条
overflowX: 'auto', // 横向滚动
overflowY: 'hidden', // 隐藏纵向滚动
// 自定义滚动条样式 - 使用CSS变量
scrollbarWidth: 'thin',
scrollbarColor: '#d9d9d9 transparent',
}}
className="product-scroll-container"
>
<ProductCardList products={products} record={record} />
</div>
);
},
},
{
title: "财务信息",
key: "financialInfo",
width: 180, // 适当调整宽度
render: (record: any) => {
const paymentPlatform = record.?. ?? "未知";
const receivedAmount = record.
? record..toFixed(2)
: "0.00";
const paymentStatus = record. ?? "未知";
const receivableAmount = record.
? record..toFixed(2)
: "0.00";
const unreceivedAmount = record.
? record..toFixed(2)
: "0.00";
const receivedPendingAmount = record.
? record..toFixed(2)
: "0.00"; // 待收已收字段
return (
<div
style={{ display: "flex", flexDirection: "column", gap: "4px" }}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span>{paymentPlatform}</span>
<span style={{ fontWeight: "bold" }}>
: ¥{receivableAmount}
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span>{paymentStatus}</span>
<span style={{ fontWeight: "bold" }}>
: ¥{receivedAmount}
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span>:</span>
<span style={{ fontWeight: "bold" }}>
¥{record.?. ?? 0}
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span>:</span>
<div>
{record.?.map((coupon: ICoupon) => (
<MyTooltip
//title={`类型:${coupon._id.优惠券类型}\n金额${coupon._id.金额}\n折扣${coupon._id.折扣}`}
color="white"
title={
<>
{coupon._id.}
<br />
{coupon._id.}
<br />
{coupon._id.}
</>
}
>
<Tag key={coupon._id._id} color="blue">
{coupon._id.}
</Tag>
</MyTooltip>
))}
</div>
</div>
<div
style={{
display: "flex",
//小号字体
fontSize: "12px",
justifyContent: "space-between",
}}
>
<span>
<span
style={{
fontWeight: "bold",
//1.6倍字体
fontSize: "1.3em",
color:
parseFloat(unreceivedAmount) > 0
? "#ff4d4f"
: "inherit", // 只有大于0时才显示红色
}}
>
¥{unreceivedAmount}
</span>
</span>
<div style={{ display: "flex", gap: "8px" }}>
<span>: </span>
<span style={{ fontWeight: "bold" }}>
¥{receivedPendingAmount}
</span>
</div>
</div>
</div>
);
},
},
];
return baseColumns;
}, [userInfo]);
// 导出Excel的函数
const exportToExcel = () => {
if (filteredRecords.length === 0) {
message.warning('没有可导出的数据');
return;
}
message.loading({ content: '正在准备导出数据...', key: 'exporting', duration: 0 });
try {
// 准备导出数据
const exportData = filteredRecords.map((record, index) => {
// 处理产品信息 - 合并成一个字符串
const productsText = record.?.map(product =>
`${product. || '未知'}`
).join(', ') || '无产品';
// 计算成交周期
const transactionDate = record. ? new Date(record.) : new Date();
const addFansDate = record.?. ? new Date(record..) : null;
const diffDays = addFansDate ? Math.ceil(
Math.abs(transactionDate.getTime() - addFansDate.getTime()) / (1000 * 60 * 60 * 24)
) : '未知';
// 格式化日期
const formatDate = (dateStr: string | undefined | Date): string => {
if (!dateStr) return '未知';
try {
const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
if (isNaN(date.getTime())) return '无效日期';
return date.toLocaleDateString('zh-CN');
} catch {
return '无效日期';
}
};
// 处理电话号码 - 只保留尾号
const phoneNumber = record.?.
? `${record...slice(-4)}`
: '未知';
// 计算售后产品金额
let afterSalesAmount = 0;
if (record. && record..length > 0 && record. && record..length > 0) {
const afterSalesProductIds = new Set(
record..flatMap(afterSales =>
afterSales..map(productId => String(productId))
)
);
record..forEach(product => {
if (afterSalesProductIds.has(String(product._id))) {
afterSalesAmount += (product. || 0);
}
});
}
// 计算产品总成本(成本价+包装费+运费)
let totalProductCost = 0;
if (record. && record..length > 0) {
totalProductCost = record..reduce((sum, product) => {
const costPrice = product.?. || 0;
const packagingFee = product.?. || 0;
const shippingFee = product.?. || 0;
return sum + costPrice + packagingFee + shippingFee;
}, 0);
}
// 返回导出的行数据
return {
'序号': index + 1,
'客户姓名': record.?. || '未知',
'电话尾号': phoneNumber,
'成交日期': formatDate(record.),
'加粉日期': formatDate(record.?.),
'成交周期(天)': diffDays,
'导购': record.?. || '未知',
'账号编号': record.?. || '未知',
'产品信息': productsText,
'应收金额': record. || 0,
'收款金额': record. || 0,
'待收金额': record. || 0,
'待收已收': record. || 0,
'收款平台': record.?. || '未知',
'收款状态': record. || '未知',
'总成本': totalProductCost,
'售后金额': afterSalesAmount,
'备注': record. || '无',
'创建时间': record.createdAt ? formatDate(new Date(record.createdAt)) : '未知',
};
});
// 创建工作簿和工作表
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '销售记录');
// 设置列宽 (大致估计,可能需要调整)
const colWidths = [
{ wch: 5 }, // 序号
{ wch: 10 }, // 客户姓名
{ wch: 10 }, // 电话尾号
{ wch: 10 }, // 成交日期
{ wch: 10 }, // 加粉日期
{ wch: 8 }, // 成交周期
{ wch: 10 }, // 导购
{ wch: 10 }, // 账号编号
{ wch: 40 }, // 产品信息
{ wch: 10 }, // 应收金额
{ wch: 10 }, // 收款金额
{ wch: 10 }, // 待收金额
{ wch: 10 }, // 待收已收
{ wch: 10 }, // 收款平台
{ wch: 10 }, // 收款状态
{ wch: 10 }, // 总成本
{ wch: 10 }, // 售后金额
{ wch: 20 }, // 备注
{ wch: 20 }, // 创建时间
];
ws['!cols'] = colWidths;
// 生成Excel文件并下载
const now = new Date();
const dateStr = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`;
const fileName = `销售记录导出_${dateStr}.xlsx`;
// 使用file-saver保存文件
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });
saveAs(blob, fileName);
message.success({ content: '导出成功!', key: 'exporting' });
} catch (error) {
console.error('导出Excel失败:', error);
message.error({ content: '导出失败,请重试', key: 'exporting' });
}
};
return (
<div>
<Table
sticky
title={undefined}
footer={() => {
// 计算统计数据
const calculateStats = () => {
// 总记录数量
const recordCount = filteredRecords.length;
// 总出单数量(所有产品数量的总和)
const orderCount = filteredRecords.reduce((sum, record) => {
// 如果产品数组存在且有长度,就累加产品数量
// 如果产品数组不存在或为空就按1计算
if (record. && record..length > 0) {
return sum + record..length;
}
return sum + 1; // 如果没有产品信息,按一单计算
}, 0);
// 计算不重复的客户数量(根据客户电话号码去重)
const uniqueCustomers = new Set();
filteredRecords.forEach(record => {
if (record.?.) {
uniqueCustomers.add(record..);
}
});
const customerCount = uniqueCustomers.size;
// 计算总销售额(应收金额)
const totalSales = filteredRecords.reduce((sum, record) => {
return sum + (record. || 0);
}, 0);
// 计算总成本
const totalCost = filteredRecords.reduce((sum, record) => {
if (record. && record..length > 0) {
// 累加每个产品的总成本(成本价+包装费+运费)
const productsCost = record..reduce((costSum, product) => {
// 计算单个产品的总成本 = 成本价 + 包装费 + 运费
const costPrice = product.?. || 0;
const packagingFee = product.?. || 0;
const shippingFee = product.?. || 0;
const totalProductCost = costPrice + packagingFee + shippingFee;
// 假设每个产品数量为1如果有产品数量字段应该乘以数量
return costSum + totalProductCost;
}, 0);
return sum + productsCost;
}
return sum;
}, 0);
// 计算毛利率 = (销售额-成本)/销售额 * 100%
const profitRate = totalSales > 0
? ((totalSales - totalCost) / totalSales * 100).toFixed(1)
: '0.0';
// 计算客单价 = 总销售额/客户数量
const customerPrice = customerCount > 0
? (totalSales / customerCount).toFixed(2)
: '0.00';
// 计算件单价 = 总销售额/总出单数量
const unitPrice = orderCount > 0
? (totalSales / orderCount).toFixed(2)
: '0.00';
// 计算售后产品金额
let afterSalesAmount = 0;
filteredRecords.forEach(record => {
if (record. && record..length > 0 && record. && record..length > 0) {
const afterSalesProductIds = new Set(
record..flatMap(afterSales =>
afterSales..map(productId => String(productId))
)
);
record..forEach(product => {
if (afterSalesProductIds.has(String(product._id))) {
afterSalesAmount += (product. || 0);
}
});
}
});
return {
orderCount,
customerCount,
totalSales,
totalCost,
profitRate,
recordCount,
customerPrice,
unitPrice,
afterSalesAmount
};
};
// 获取统计数据
const stats = calculateStats();
// 渲染筛选状态标签
const renderFilterStatus = () => {
if (transactionDateRange[0] || addFansDateRange[0]) {
return (
<Tooltip title={`${salesRecords.length} 条记录中,筛选出${stats.recordCount}条符合条件的记录`}>
<Tag color="blue">
: {stats.recordCount} / {salesRecords.length}
</Tag>
</Tooltip>
);
}
return null;
};
return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
marginBottom: 8
}}>
<div style={{
display: 'flex',
gap: 8,
flexWrap: 'wrap',
alignItems: 'center'
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: 4, fontSize: '0.9em' }}>:</span>
<DatePicker.RangePicker
size="small"
style={{ width: 160 }}
onChange={(_, dateStrings) => {
if (dateStrings[0] && dateStrings[1]) {
// 设置开始日期为当天的00:00:00
const startDate = new Date(dateStrings[0]);
startDate.setHours(0, 0, 0, 0);
// 设置结束日期为当天的23:59:59确保包含当天的所有记录
const endDate = new Date(dateStrings[1]);
endDate.setHours(23, 59, 59, 999);
setTransactionDateRange([startDate, endDate]);
} else {
setTransactionDateRange([null, null]);
}
}}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: 4, fontSize: '0.9em' }}>:</span>
<DatePicker.RangePicker
size="small"
style={{ width: 160 }}
onChange={(_, dateStrings) => {
if (dateStrings[0] && dateStrings[1]) {
// 设置开始日期为当天的00:00:00
const startDate = new Date(dateStrings[0]);
startDate.setHours(0, 0, 0, 0);
// 设置结束日期为当天的23:59:59确保包含当天的所有记录
const endDate = new Date(dateStrings[1]);
endDate.setHours(23, 59, 59, 999);
setAddFansDateRange([startDate, endDate]);
} else {
setAddFansDateRange([null, null]);
}
}}
/>
</div>
<Button
size="small"
type="primary"
onClick={() => {
setTransactionDateRange([null, null]);
setAddFansDateRange([null, null]);
}}
>
</Button>
{renderFilterStatus()}
{/* 添加统计信息 */}
<div style={{
display: 'flex',
//gap: 8,
marginLeft: 16,
borderLeft: '1px solid #f0f0f0',
paddingLeft: 16
}}>
<Tooltip title="成交客户数量">
<Tag color="green" icon={<UserOutlined />}>
: {stats.customerCount}
</Tag>
</Tooltip>
<Tooltip title="总记录数 / 总出单数量(按产品计算)">
<Tag color="purple" icon={<FieldTimeOutlined />}>
/: {stats.recordCount}/{stats.orderCount}
</Tag>
</Tooltip>
<Tooltip title="总销售额(应收金额)">
<Tag color="orange">
: ¥{stats.totalSales.toFixed(2)}
</Tag>
</Tooltip>
<Tooltip title="总成本金额(成本价+包装费+运费)">
<Tag color="red">
: ¥{stats.totalCost.toFixed(2)}
</Tag>
</Tooltip>
<Tooltip title="毛利(销售额-成本)">
<Tag color="green">
: ¥{(stats.totalSales - stats.totalCost).toFixed(2)}
</Tag>
</Tooltip>
<Tooltip title="毛利率((销售额-成本)/销售额 * 100%)">
<Tag color="lime">
: {stats.profitRate}%
</Tag>
</Tooltip>
<Tooltip title="客单价(总销售额/客户数量)">
<Tag color="cyan" icon={<UserOutlined />}>
: ¥{stats.customerPrice}
</Tag>
</Tooltip>
<Tooltip title="件单价(总销售额/总出单数量)">
<Tag color="blue" icon={<FieldTimeOutlined />}>
: ¥{stats.unitPrice}
</Tag>
</Tooltip>
<Tooltip title="售后产品金额">
<Tag color="magenta">
: ¥{stats.afterSalesAmount.toFixed(2)}
</Tag>
</Tooltip>
{/* 添加导出按钮 */}
<Button
size="small"
type="primary"
icon={<DownloadOutlined />}
onClick={exportToExcel}
style={{ marginLeft: 8 }}
>
</Button>
</div>
</div>
</div>
);
}}
scroll={{
y: `calc(100vh - 250px)`, // 适当调整高度
x: 'max-content'
}}
pagination={{
position: ['bottomRight'],
pageSize: 100,
showSizeChanger: true,
pageSizeOptions: ["10", "20", "50", "100"],
showTotal: (total) => `${total} 条记录`,
size: 'small'
}}
size="small"
loading={loading}
columns={columns}
dataSource={filteredRecords} // 使用筛选后的记录
rowKey="_id"
// 添加行渲染优化
rowClassName={(_, index) => (index % 2 === 0 ? 'even-row' : 'odd-row')}
/>
</div>
);
};
export default SalesPage;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
//src\pages\test\markdown\index.tsx
import { useEffect, useState } from 'react';
import Markdown from '@/components/markdown2'; // 引入你刚刚创建的 Markdown 组件
export default function MarkdownPage() {
const [markdownContent, setMarkdownContent] = useState<string | null>(null);
// 使用 useEffect 钩子来加载 public 文件夹中的 Markdown 文件内容
useEffect(() => {
// 从 public 文件夹中获取文件内容
fetch('/md2.md')
.then((response) => response.text())
.then((text) => {
setMarkdownContent(text);
})
.catch((error) => {
console.error('Error loading Markdown file:', error);
});
}, []);
// 当文件内容加载后,显示 Markdown 组件
return (
<div style={{ padding: '20px' }}>
{markdownContent ? (
<Markdown>{markdownContent}</Markdown>
) : (
<p>Loading Markdown content...</p>
)}
</div>
);
}

View File

@@ -0,0 +1,31 @@
//src\pages\test\markdown\index.tsx
import { useEffect, useState } from 'react';
import Markdown from '@/components/markdown'; // 引入你刚刚创建的 Markdown 组件
export default function MarkdownPage() {
const [markdownContent, setMarkdownContent] = useState<string | null>(null);
// 使用 useEffect 钩子来加载 public 文件夹中的 Markdown 文件内容
useEffect(() => {
// 从 public 文件夹中获取文件内容
fetch('/test2.md')
.then((response) => response.text())
.then((text) => {
setMarkdownContent(text);
})
.catch((error) => {
console.error('Error loading Markdown file:', error);
});
}, []);
// 当文件内容加载后,显示 Markdown 组件
return (
<div style={{ padding: '20px' }}>
{markdownContent ? (
<Markdown>{markdownContent}</Markdown>
) : (
<p>Loading Markdown content...</p>
)}
</div>
);
}

View File

@@ -0,0 +1,257 @@
/**
* 文件: src/pages/test/userstore-debug.tsx
* 作者: 阿瑞
* 功能: 用户状态管理调试页面
* 版本: v1.0.1
*/
import React, { useState, useEffect, useMemo } from 'react';
import { Card, Typography, Row, Col, Alert, Tag, Descriptions } from 'antd';
import {
HomeOutlined,
InfoCircleOutlined
} from '@ant-design/icons';
// 导入用户状态相关的钩子函数
import {
useUserInfo,
useAccessToken,
useRefreshToken,
//useUserActions,
useUserToken,
useUserHomePath
} from '@/store/userStore';
const { Title, Text } = Typography;
// 定义 localStorage 状态接口
interface LocalStorageState {
hasUserInfo: boolean;
hasAccessToken: boolean;
hasRefreshToken: boolean;
}
/**
* 用户状态调试页面主组件
* 用于展示和测试用户状态管理相关功能
*/
const UserStoreDebugPage: React.FC = () => {
// 获取用户状态相关数据
const userInfo = useUserInfo();
const accessToken = useAccessToken();
const refreshToken = useRefreshToken();
//const userActions = useUserActions();
const userToken = useUserToken();
const userHomePath = useUserHomePath();
// 本地存储状态
const [localStorageState, setLocalStorageState] = useState<LocalStorageState>({
hasUserInfo: false,
hasAccessToken: false,
hasRefreshToken: false,
});
// 使用 useMemo 优化 JSON 字符串化
const userInfoJson = useMemo(() => JSON.stringify(userInfo, null, 2), [userInfo]);
// 安全检查 localStorage 状态
useEffect(() => {
const checkLocalStorage = () => {
if (typeof window !== 'undefined') {
setLocalStorageState({
hasUserInfo: !!localStorage.getItem('userInfo'),
hasAccessToken: !!localStorage.getItem('userAccessToken'),
hasRefreshToken: !!localStorage.getItem('userRefreshToken'),
});
}
};
checkLocalStorage();
// 每2秒检查一次 localStorage 状态变化(减少频率)
const interval = setInterval(checkLocalStorage, 2000);
return () => clearInterval(interval);
}, []);
return (
<div style={{ padding: '24px', backgroundColor: '#f5f5f5', minHeight: '100vh' }}>
{/* 页面标题和说明 */}
<div style={{ marginBottom: '24px' }}>
<Title level={2}>
<InfoCircleOutlined style={{ marginRight: '8px' }} />
</Title>
<Alert
message="调试说明"
description="此页面用于调试和测试用户状态管理相关功能,包括各种钩子函数的返回值和操作方法。"
type="info"
showIcon
style={{ marginBottom: '24px' }}
/>
</div>
<Row gutter={[16, 16]}>
{/* 用户信息展示 */}
<Col xs={24} lg={12}>
<Card
title="useUserInfo() - 用户信息"
extra={<Tag color="green">Hook</Tag>}
>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="用户ID">
{userInfo._id || <Text type="secondary"></Text>}
</Descriptions.Item>
<Descriptions.Item label="姓名">
{userInfo. || <Text type="secondary"></Text>}
</Descriptions.Item>
<Descriptions.Item label="邮箱">
{userInfo. || <Text type="secondary"></Text>}
</Descriptions.Item>
<Descriptions.Item label="电话">
{userInfo. || <Text type="secondary"></Text>}
</Descriptions.Item>
<Descriptions.Item label="角色名称">
{userInfo.?. || <Text type="secondary"></Text>}
</Descriptions.Item>
<Descriptions.Item label="微信昵称">
{userInfo. || <Text type="secondary"></Text>}
</Descriptions.Item>
</Descriptions>
<div style={{ marginTop: '16px' }}>
<Text strong>JSON :</Text>
<pre style={{
backgroundColor: '#f5f5f5',
padding: '12px',
borderRadius: '4px',
fontSize: '12px',
overflow: 'auto',
maxHeight: '200px'
}}>
{userInfoJson}
</pre>
</div>
</Card>
</Col>
{/* 令牌信息展示 */}
<Col xs={24} lg={12}>
<Card
title="Token 相关钩子"
extra={<Tag color="orange">Tokens</Tag>}
>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="useAccessToken()">
<Text code copyable={!!accessToken} style={{ fontSize: '12px' }}>
{accessToken || '未设置'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="useRefreshToken()">
<Text code copyable={!!refreshToken} style={{ fontSize: '12px' }}>
{refreshToken || '未设置'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="useUserToken().accessToken">
<Text code copyable={!!userToken.accessToken} style={{ fontSize: '12px' }}>
{userToken.accessToken || '未设置'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="useUserToken().refreshToken">
<Text code copyable={!!userToken.refreshToken} style={{ fontSize: '12px' }}>
{userToken.refreshToken || '未设置'}
</Text>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
{/* 用户主页路径展示 */}
<Col xs={24} lg={12}>
<Card
title="useUserHomePath() - 用户主页路径"
extra={<Tag color="purple">Navigation</Tag>}
>
<div style={{ textAlign: 'center', padding: '20px' }}>
<HomeOutlined style={{ fontSize: '24px', marginBottom: '12px', color: '#1890ff' }} />
<br />
<Text strong style={{ fontSize: '18px' }}>
{userHomePath}
</Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
</Text>
</div>
</Card>
</Col>
{/* 用户操作方法展示 */}
<Col xs={24} lg={12}>
<Card
title="useUserActions() - 操作方法"
extra={<Tag color="red">Actions</Tag>}
>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="setUserInfo">
<Tag color="blue">function</Tag>
</Descriptions.Item>
<Descriptions.Item label="setUserToken">
<Tag color="green">function</Tag>
</Descriptions.Item>
<Descriptions.Item label="clearUserInfoAndToken">
<Tag color="red">function</Tag>
</Descriptions.Item>
<Descriptions.Item label="fetchAndSetUserInfo">
<Tag color="orange">async function</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
{/* 调试信息 */}
<Card
title="调试信息"
style={{ marginTop: '24px' }}
extra={<Tag color="cyan">Debug Info</Tag>}
>
<Row gutter={[16, 16]}>
<Col span={12}>
<Text strong>localStorage :</Text>
<ul style={{ marginTop: '8px' }}>
<li>
userInfo: {localStorageState.hasUserInfo ?
<Tag color="success"> </Tag> :
<Tag color="error"> </Tag>
}
</li>
<li>
userAccessToken: {localStorageState.hasAccessToken ?
<Tag color="success"> </Tag> :
<Tag color="error"> </Tag>
}
</li>
<li>
userRefreshToken: {localStorageState.hasRefreshToken ?
<Tag color="success"> </Tag> :
<Tag color="error"> </Tag>
}
</li>
</ul>
</Col>
<Col span={12}>
<Text strong>:</Text>
<ul style={{ marginTop: '8px' }}>
<li>typeof window: <Tag color="blue">{typeof window}</Tag></li>
<li>Node.js : <Tag color={typeof window === 'undefined' ? 'success' : 'error'}>
{typeof window === 'undefined' ? '是' : '否'}
</Tag></li>
</ul>
</Col>
</Row>
</Card>
</div>
);
};
export default UserStoreDebugPage;

View File

@@ -117,5 +117,9 @@ export const useUserInfo = () => useUserStore((state: UserStore) => state.userIn
export const useAccessToken = () => useUserStore((state: UserStore) => state.accessToken);
export const useRefreshToken = () => useUserStore((state: UserStore) => state.refreshToken);
export const useUserActions = () => useUserStore((state: UserStore) => state.actions);
export const useUserToken = () => useUserStore((state: UserStore) => ({ accessToken: state.accessToken, refreshToken: state.refreshToken }));
export const useUserToken = () => {
const accessToken = useUserStore((state: UserStore) => state.accessToken);
const refreshToken = useUserStore((state: UserStore) => state.refreshToken);
return { accessToken, refreshToken };
};
export default useUserStore;

17
src/utils/highlight.ts Normal file
View File

@@ -0,0 +1,17 @@
import 'highlight.js/styles/base16/tomorrow-night.css';
import hljs from 'highlight.js';
declare global {
interface Window {
hljs: any;
}
}
hljs.configure({
languages: ['javascript', 'sh', 'bash', 'html', 'scss', 'css', 'json'],
});
if (typeof window !== 'undefined') {
window.hljs = hljs;
}