This commit is contained in:
@@ -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
1171
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
414
public/md2.md
Normal file
414
public/md2.md
Normal 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
59
public/test2.md
Normal 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/>
|
||||
|
||||

|
||||
|
||||
> A block quote with ~~strikethrough~~ and a URL: [https://reactjs.org](https://reactjs.org).
|
||||
33
src/components/markdown/index.tsx
Normal file
33
src/components/markdown/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
161
src/components/markdown/styles.ts
Normal file
161
src/components/markdown/styles.ts
Normal 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;
|
||||
30
src/components/markdown2/index.tsx
Normal file
30
src/components/markdown2/index.tsx
Normal 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>
|
||||
|
||||
);
|
||||
}
|
||||
161
src/components/markdown2/styles.ts
Normal file
161
src/components/markdown2/styles.ts
Normal 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;
|
||||
@@ -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 }, // 可在前端计算
|
||||
});
|
||||
|
||||
@@ -231,6 +231,7 @@ export interface IAccount {
|
||||
// 定义日增长数据接口类型
|
||||
export interface IDailyGrowth {
|
||||
日期: Date;
|
||||
消耗: number; // 推广引流消费
|
||||
总人数: number;
|
||||
扣除人数: number;
|
||||
日增长人数?: number;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import connectDB from '@/lib/connectDB';
|
||||
import { fullIndexCleanup } from '@/utils/cleanupIndexes';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
102
src/pages/api/backstage/accounts/accountgrowth/clear.ts
Normal file
102
src/pages/api/backstage/accounts/accountgrowth/clear.ts
Normal 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 : '未知错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 处理函数
|
||||
@@ -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);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 工具函数
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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'; // 引入类型定义
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import connectDB from '@/lib/connectDB';
|
||||
import { LogisticsRecord } from '@/models';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import connectDB from '@/lib/connectDB';
|
||||
import { LogisticsRecord } from '@/models';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import connectDB from '@/lib/connectDB';
|
||||
import { LogisticsRecord } from '@/models';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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('店铺账号操作失败');
|
||||
}
|
||||
|
||||
@@ -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('删除店铺账号失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 }}
|
||||
>
|
||||
导出筛选
|
||||
|
||||
766
src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx
Normal file
766
src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx
Normal 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;
|
||||
1077
src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx
Normal file
1077
src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
@@ -141,17 +143,19 @@ const SalesPage = () => {
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
// 状态管理
|
||||
const [modals, setModals] = useState({
|
||||
edit: false,
|
||||
ship: false,
|
||||
afterSales: false,
|
||||
customerAnalysis: false,
|
||||
salesAnalysis: false,
|
||||
});
|
||||
const [currentRecord, setCurrentRecord] = useState<ISalesRecord | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -173,7 +177,7 @@ const SalesPage = () => {
|
||||
// 立即显示发货模态框,不等待状态更新
|
||||
setCurrentRecord(record);
|
||||
setModals(prev => ({ ...prev, ship: true }));
|
||||
|
||||
|
||||
// 在后台异步更新状态(非阻塞)
|
||||
if (record.处理状态 !== '已处理') {
|
||||
updateProcessingStatus(record._id, '已处理')
|
||||
@@ -193,11 +197,17 @@ const SalesPage = () => {
|
||||
setAfterSalesType(type);
|
||||
setModals(prev => ({ ...prev, afterSales: true }));
|
||||
},
|
||||
closeAll: () => {
|
||||
setModals({ edit: false, ship: false, afterSales: false });
|
||||
setCurrentRecord(null);
|
||||
setAfterSalesType(null);
|
||||
showCustomerAnalysis: () => {
|
||||
setModals(prev => ({ ...prev, customerAnalysis: true }));
|
||||
},
|
||||
showSalesAnalysis: () => {
|
||||
setModals(prev => ({ ...prev, salesAnalysis: true }));
|
||||
},
|
||||
closeAll: () => {
|
||||
setModals({ edit: false, ship: false, afterSales: false, customerAnalysis: false, salesAnalysis: false });
|
||||
setCurrentRecord(null);
|
||||
setAfterSalesType(null);
|
||||
},
|
||||
handleSuccess: () => {
|
||||
if (userInfo.团队?._id) {
|
||||
fetchSalesRecords(userInfo.团队._id);
|
||||
@@ -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]);
|
||||
|
||||
@@ -324,12 +310,12 @@ const SalesPage = () => {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
// 更新本地状态
|
||||
setSalesRecords(prevRecords =>
|
||||
prevRecords.map(record =>
|
||||
record._id === recordId
|
||||
setSalesRecords(prevRecords =>
|
||||
prevRecords.map(record =>
|
||||
record._id === recordId
|
||||
? { ...record, 处理状态: status }
|
||||
: record
|
||||
)
|
||||
@@ -506,7 +492,7 @@ const SalesPage = () => {
|
||||
align: "center",
|
||||
render: (record: ISalesRecord) => {
|
||||
const currentStatus = record.处理状态 || '未处理';
|
||||
|
||||
|
||||
// 状态配置
|
||||
const statusConfig = {
|
||||
'未处理': {
|
||||
@@ -536,10 +522,10 @@ const SalesPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '8px 4px'
|
||||
}}>
|
||||
@@ -548,84 +534,84 @@ const SalesPage = () => {
|
||||
onChange={(e) => updateProcessingStatus(record._id, e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
width: '100%'
|
||||
}}>
|
||||
{(['未处理', '处理中', '已处理'] as const).map((status) => {
|
||||
const config = statusConfig[status];
|
||||
const isSelected = currentStatus === status;
|
||||
|
||||
return (
|
||||
<Radio
|
||||
key={status}
|
||||
value={status}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={config.description}
|
||||
placement="topRight"
|
||||
color={config.color}
|
||||
overlayStyle={{
|
||||
fontSize: '12px',
|
||||
zIndex: 9999
|
||||
}}
|
||||
mouseEnterDelay={0.3}
|
||||
mouseLeaveDelay={0.1}
|
||||
>
|
||||
<div
|
||||
className={`status-radio-item ${isSelected ? 'selected' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px',
|
||||
padding: '6px 10px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
fontWeight: isSelected ? '600' : '500',
|
||||
color: isSelected ? config.textColor : '#666',
|
||||
backgroundColor: isSelected ? config.bgColor : '#fafafa',
|
||||
border: `1.5px solid ${isSelected ? config.borderColor : '#e8e8e8'}`,
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
cursor: 'pointer',
|
||||
minWidth: '75px',
|
||||
boxShadow: isSelected ? `0 3px 6px ${config.color}25, 0 1px 3px ${config.color}15` : '0 1px 2px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: config.color,
|
||||
fontSize: '10px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
{config.icon}
|
||||
</span>
|
||||
<span style={{ lineHeight: 1 }}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Radio>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
return (
|
||||
<Radio
|
||||
key={status}
|
||||
value={status}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={config.description}
|
||||
placement="topRight"
|
||||
color={config.color}
|
||||
overlayStyle={{
|
||||
fontSize: '12px',
|
||||
zIndex: 9999
|
||||
}}
|
||||
mouseEnterDelay={0.3}
|
||||
mouseLeaveDelay={0.1}
|
||||
>
|
||||
<div
|
||||
className={`status-radio-item ${isSelected ? 'selected' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px',
|
||||
padding: '6px 10px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '12px',
|
||||
fontWeight: isSelected ? '600' : '500',
|
||||
color: isSelected ? config.textColor : '#666',
|
||||
backgroundColor: isSelected ? config.bgColor : '#fafafa',
|
||||
border: `1.5px solid ${isSelected ? config.borderColor : '#e8e8e8'}`,
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
cursor: 'pointer',
|
||||
minWidth: '75px',
|
||||
boxShadow: isSelected ? `0 3px 6px ${config.color}25, 0 1px 3px ${config.color}15` : '0 1px 2px rgba(0,0,0,0.05)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: config.color,
|
||||
fontSize: '10px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
{config.icon}
|
||||
</span>
|
||||
<span style={{ lineHeight: 1 }}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Radio>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
//备注
|
||||
{
|
||||
title: "备注",
|
||||
@@ -905,9 +891,9 @@ const SalesPage = () => {
|
||||
))}
|
||||
</div>
|
||||
<div style={COMMON_STYLES.flexRow}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => modalHandlers.showShip(record)}
|
||||
>
|
||||
发货
|
||||
@@ -923,7 +909,7 @@ const SalesPage = () => {
|
||||
|
||||
return baseColumns;
|
||||
}, [isNotFinanceRole, isAdmin, userInfo]);
|
||||
|
||||
|
||||
return (
|
||||
// 外层容器 - 占据全部可用空间
|
||||
<div style={{
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
909
src/pages/team/SaleRecord/test.tsx
Normal file
909
src/pages/team/SaleRecord/test.tsx
Normal 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;
|
||||
1191
src/pages/team/account/index.tsx
Normal file
1191
src/pages/team/account/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
32
src/pages/test/markdown/22.tsx
Normal file
32
src/pages/test/markdown/22.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
src/pages/test/markdown/index.tsx
Normal file
31
src/pages/test/markdown/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
257
src/pages/test/userstore-debug.tsx
Normal file
257
src/pages/test/userstore-debug.tsx
Normal 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;
|
||||
@@ -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
17
src/utils/highlight.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user