init
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@
|
|||||||
/src/.umi-test
|
/src/.umi-test
|
||||||
/dist
|
/dist
|
||||||
.swc
|
.swc
|
||||||
yarn.local
|
yarn.local
|
||||||
|
yarn.lock
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { defineConfig } from "umi";
|
import { defineConfig } from "@umijs/max";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
routes: [
|
// 最小化有效配置
|
||||||
{ path: "/", component: "index" },
|
history: { type: 'hash' },
|
||||||
{ path: "/docs", component: "docs" },
|
plugins: [],
|
||||||
],
|
// 禁用MFSU功能
|
||||||
npmClient: 'yarn',
|
mfsu: false,
|
||||||
});
|
});
|
||||||
|
|||||||
12
config/proxy.js
Normal file
12
config/proxy.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const proxy = {
|
||||||
|
'/api': {
|
||||||
|
'target': 'http://jsonplaceholder.typicode.com/',
|
||||||
|
'changeOrigin': true,
|
||||||
|
'pathRewrite': { '^/api' : '' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default proxy;
|
||||||
12
config/routes.js
Normal file
12
config/routes.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// 简化的路由配置
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', component: './index' },
|
||||||
|
{ path: '/login', component: './login' },
|
||||||
|
{ path: '/docs', component: './docs' },
|
||||||
|
// 404 页面
|
||||||
|
{ path: '/*', component: './404' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
{
|
{
|
||||||
"extends": "./src/.umi/tsconfig.json"
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@@/*": ["./src/.umi/*"],
|
||||||
|
"@@@/*": ["./public/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -2,18 +2,36 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"author": "liuhanhua <1>",
|
"author": "liuhanhua <1>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "umi dev",
|
"dev": "cross-env UMI_ENV=dev max dev",
|
||||||
|
"dev:pre": "cross-env UMI_ENV=pre umi dev",
|
||||||
"build": "umi build",
|
"build": "umi build",
|
||||||
"postinstall": "umi setup",
|
"postinstall": "umi setup",
|
||||||
"setup": "umi setup",
|
"setup": "umi setup",
|
||||||
"start": "npm run dev"
|
"start": "npm run dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"umi": "^4.5.3"
|
"@ant-design/icons": "^6.0.0",
|
||||||
|
"@ant-design/pro-components": "^2.8.7",
|
||||||
|
"@ebay/nice-modal-react": "^1.2.13",
|
||||||
|
"@umijs/plugin-initial-state": "^2.4.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-react": "^1.0.6",
|
||||||
|
"accounting": "^0.4.1",
|
||||||
|
"antd-style": "^3.7.1",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"less": "^4.4.0",
|
||||||
|
"less-loader": "^12.3.0",
|
||||||
|
"rc-tween-one": "^3.0.6",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"antd": "^5.27.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.33",
|
"@types/react": "^18.0.33",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"typescript": "^5.0.3"
|
"@umijs/plugins": "^4.5.3",
|
||||||
|
"typescript": "^5.0.3",
|
||||||
|
"@umijs/max": "^4.5.3",
|
||||||
|
"cross-env": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
src/app.jsx
10
src/app.jsx
@@ -0,0 +1,10 @@
|
|||||||
|
// 简化的 Umi 运行时配置文件
|
||||||
|
|
||||||
|
// 简单的路由切换处理
|
||||||
|
export function onRouteChange({ matchedRoutes }) {
|
||||||
|
if (matchedRoutes && matchedRoutes.length > 0) {
|
||||||
|
document.title = matchedRoutes[matchedRoutes.length - 1].route.title || 'LeasePal 商户管理系统';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除默认对象导出,避免渲染错误
|
||||||
106
src/global.less
106
src/global.less
@@ -0,0 +1,106 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorWeak {
|
||||||
|
filter: invert(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
|
||||||
|
left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ant-table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
&-thead > tr,
|
||||||
|
&-tbody > tr {
|
||||||
|
> th,
|
||||||
|
> td {
|
||||||
|
white-space: pre;
|
||||||
|
> span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-layout-content {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首页样式
|
||||||
|
.home-container {
|
||||||
|
padding: 24px;
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-container {
|
||||||
|
.home-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文档页面样式
|
||||||
|
.docs-container {
|
||||||
|
padding: 24px;
|
||||||
|
.intro-card {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.docs-list {
|
||||||
|
.ant-list-item {
|
||||||
|
padding: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-radius: 8px;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contact-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
import { Link, Outlet } from 'umi';
|
// 简化的布局组件
|
||||||
|
import React from 'react';
|
||||||
|
import { Layout as AntdLayout } from 'antd';
|
||||||
|
import { Outlet } from 'umi';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
export default function Layout() {
|
const { Content } = AntdLayout;
|
||||||
|
|
||||||
|
const AdminLayout = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.navs}>
|
<AntdLayout className={styles.layout}>
|
||||||
<ul>
|
<Content className={styles.content}>
|
||||||
<li>
|
<Outlet />
|
||||||
<Link to="/">Home</Link>
|
</Content>
|
||||||
</li>
|
</AntdLayout>
|
||||||
<li>
|
|
||||||
<Link to="/docs">Docs</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/umijs/umi">Github</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default AdminLayout;
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
.navs {
|
// 简化的布局样式
|
||||||
ul {
|
.layout {
|
||||||
padding: 0;
|
min-height: 100vh;
|
||||||
list-style: none;
|
background: #f0f2f5;
|
||||||
display: flex;
|
}
|
||||||
}
|
|
||||||
li {
|
.content {
|
||||||
margin-right: 1em;
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本响应式设计
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/pages/404.jsx
Normal file
17
src/pages/404.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// 简单的404页面
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'umi';
|
||||||
|
|
||||||
|
const NotFoundPage = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '50px 0' }}>
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>抱歉,您访问的页面不存在</h2>
|
||||||
|
<p>
|
||||||
|
<Link to="/">返回首页</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundPage;
|
||||||
7
src/pages/404.less
Normal file
7
src/pages/404.less
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
const DocsPage = () => {
|
const DocsPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ padding: '24px' }}>
|
||||||
<p>This is umi docs.</p>
|
<h1>系统文档中心</h1>
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<h2>欢迎使用LeasePal商户管理系统</h2>
|
||||||
|
<p>本系统旨在帮助管理员高效管理商户信息、租赁合同和相关业务流程。</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<h3>快速导航</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>系统概述</strong> - LeasePal商户管理系统的整体架构和功能介绍</li>
|
||||||
|
<li><strong>商户管理</strong> - 如何添加、编辑和管理商户信息</li>
|
||||||
|
<li><strong>租赁业务</strong> - 租赁合同的创建、管理和状态跟踪</li>
|
||||||
|
<li><strong>API接口文档</strong> - 系统提供的所有API接口详细说明</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style={{ color: '#666' }}>如有问题或需要帮助,请联系系统管理员</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import yayJpg from '../assets/yay.jpg';
|
import React from 'react';
|
||||||
|
|
||||||
export default function HomePage() {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ padding: '24px' }}>
|
||||||
<h2>Yay! Welcome to umi!</h2>
|
<h1>LeasePal 商户管理系统</h1>
|
||||||
<p>
|
<p>欢迎使用商户管理系统</p>
|
||||||
<img src={yayJpg} width="388" />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
To get started, edit <code>pages/index.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
|
|||||||
81
src/pages/login.jsx
Normal file
81
src/pages/login.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './login.less';
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [username, setUsername] = useState('admin');
|
||||||
|
const [password, setPassword] = useState('admin123');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟登录请求
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// 模拟登录成功,存储token
|
||||||
|
localStorage.setItem('token', 'mock-token-123456');
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
alert('登录成功');
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch (error) {
|
||||||
|
alert('登录失败,请检查用户名和密码');
|
||||||
|
console.error('Login error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="loginContainer">
|
||||||
|
<div className="loginCard">
|
||||||
|
<h2 className="loginTitle">LeasePal 商户管理系统</h2>
|
||||||
|
<h5 className="loginSubtitle">登录</h5>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="loginForm">
|
||||||
|
<div className="formItem">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="用户名"
|
||||||
|
required
|
||||||
|
className="formInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formItem">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="密码"
|
||||||
|
required
|
||||||
|
className="formInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formItem">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="loginButton"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="loginTips">
|
||||||
|
<p><strong>提示:</strong>用户名: admin, 密码: admin123</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
59
src/pages/login.less
Normal file
59
src/pages/login.less
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.loginContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginCard {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginTitle {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginSubtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginForm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginTips {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.loginCard {
|
||||||
|
width: 90%;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-form-item-icon {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
30
src/utils/downloadCSV.js
Normal file
30
src/utils/downloadCSV.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 将文本内容下载为CSV文件
|
||||||
|
* @param {string} text - CSV文本内容
|
||||||
|
* @param {string} filename - 文件名,默认为export.csv
|
||||||
|
*/
|
||||||
|
export const downloadCSVFromText = (text, filename = 'export.csv') => {
|
||||||
|
try {
|
||||||
|
// 创建Blob对象
|
||||||
|
const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
let link = document.createElement('a');
|
||||||
|
|
||||||
|
// 处理不同浏览器的URL创建方式
|
||||||
|
if (link.download !== undefined) {
|
||||||
|
// 支持HTML5的浏览器
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url); // 释放URL对象以避免内存泄漏
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载CSV文件失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
198
src/utils/request.js
Normal file
198
src/utils/request.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { message } from "antd";
|
||||||
|
import { history } from "@umijs/max";
|
||||||
|
import { getLocale } from "@@/exports";
|
||||||
|
import { downloadCSVFromText } from "@/utils/downloadCSV";
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: process.env.API_BASE_URL || '/api', // 使用UMI风格的环境变量或默认值
|
||||||
|
timeout: 30000, // 请求超时时间
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 存储请求的取消令牌
|
||||||
|
const cancelTokenSourceMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成请求唯一标识
|
||||||
|
* @param {Object} config - 请求配置
|
||||||
|
* @returns {string} 唯一标识
|
||||||
|
*/
|
||||||
|
function generateRequestKey(config) {
|
||||||
|
const { method, url, params, data } = config;
|
||||||
|
return [
|
||||||
|
method.toLowerCase(),
|
||||||
|
url,
|
||||||
|
JSON.stringify(params),
|
||||||
|
JSON.stringify(data)
|
||||||
|
].join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消重复请求
|
||||||
|
* @param {Object} config - 请求配置
|
||||||
|
*/
|
||||||
|
function cancelDuplicateRequest(config) {
|
||||||
|
const requestKey = generateRequestKey(config);
|
||||||
|
|
||||||
|
// 如果存在重复请求,取消之前的
|
||||||
|
if (cancelTokenSourceMap.has(requestKey)) {
|
||||||
|
const source = cancelTokenSourceMap.get(requestKey);
|
||||||
|
source.cancel('取消重复请求');
|
||||||
|
cancelTokenSourceMap.delete(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的取消令牌
|
||||||
|
const source = axios.CancelToken.source();
|
||||||
|
config.cancelToken = source.token;
|
||||||
|
cancelTokenSourceMap.set(requestKey, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// 取消重复请求
|
||||||
|
cancelDuplicateRequest(config);
|
||||||
|
|
||||||
|
const { method, headers } = config;
|
||||||
|
|
||||||
|
if ('POST' === method.toUpperCase() || 'PUT' === method.toUpperCase()) {
|
||||||
|
headers["Content-Type"] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
headers["token"] = token;
|
||||||
|
}
|
||||||
|
headers["Accept-Language"] = getLocale();
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
console.log('请求已取消:', error.message);
|
||||||
|
return Promise.resolve({ data: { success: false, message: error.message } });
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorHandler = (error) => {
|
||||||
|
const { response } = error || {};
|
||||||
|
const { status, data } = response || {};
|
||||||
|
|
||||||
|
// 根据不同错误类型显示不同提示
|
||||||
|
const errorMessage = data?.message || "请求失败,请稍后重试";
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
message.error("登录已过期,请重新登录");
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
if (history.location.pathname !== '/login') {
|
||||||
|
setTimeout(() => {
|
||||||
|
history.push("/login");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
message.error("没有权限执行此操作");
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
message.error("请求的资源不存在");
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
message.error("服务器内部错误");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({ success: false, message: errorMessage, error });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// 移除取消令牌,避免内存泄漏
|
||||||
|
const requestKey = generateRequestKey(response.config);
|
||||||
|
cancelTokenSourceMap.delete(requestKey);
|
||||||
|
|
||||||
|
const { headers } = response || {};
|
||||||
|
|
||||||
|
// 假设后端统一返回格式为 { code, data, message }
|
||||||
|
if (response.status === 200) {
|
||||||
|
if (headers["content-type"] && headers["content-type"].includes('text/csv')) {
|
||||||
|
downloadCSVFromText(response.data);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分页数据结构
|
||||||
|
const responseData = response.data;
|
||||||
|
if (responseData.data && responseData.data.paging && responseData.data.data) {
|
||||||
|
const data = responseData.data;
|
||||||
|
data['list'] = data.data;
|
||||||
|
data['total'] = data.paging.total;
|
||||||
|
delete data['data'];
|
||||||
|
}
|
||||||
|
return responseData; // 直接返回业务数据
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 确保在错误情况下也移除取消令牌
|
||||||
|
if (error.config) {
|
||||||
|
const requestKey = generateRequestKey(error.config);
|
||||||
|
cancelTokenSourceMap.delete(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消请求的情况
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
console.log('请求已取消:', error.message);
|
||||||
|
return Promise.resolve({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorHandler(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装请求方法
|
||||||
|
* @param {string} method - 请求方法
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {Object} data - 请求数据
|
||||||
|
* @param {Object} config - 额外配置
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
function request(method, url, data = {}, config = {}) {
|
||||||
|
return service({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
[method.toLowerCase() === 'get' ? 'params' : 'data']: data,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出常用请求方法
|
||||||
|
export const http = {
|
||||||
|
get: (url, params, config) => request('get', url, params, config),
|
||||||
|
post: (url, data, config) => request('post', url, data, config),
|
||||||
|
put: (url, data, config) => request('put', url, data, config),
|
||||||
|
delete: (url, data, config) => request('delete', url, data, config),
|
||||||
|
patch: (url, data, config) => request('patch', url, data, config),
|
||||||
|
|
||||||
|
// 取消所有请求
|
||||||
|
cancelAllRequests() {
|
||||||
|
cancelTokenSourceMap.forEach((source) => {
|
||||||
|
source.cancel('取消所有请求');
|
||||||
|
});
|
||||||
|
cancelTokenSourceMap.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default service;
|
||||||
1
typings.d.ts
vendored
1
typings.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
import 'umi/typings';
|
|
||||||
Reference in New Issue
Block a user