This commit is contained in:
liuhanhua
2025-12-01 10:09:46 +08:00
parent 6cdc748f9e
commit f8069035a8
21 changed files with 4006 additions and 103 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
PORT=8888
COMPRESS=none

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@
/dist
.swc
yarn.local
yarn.lock

View File

@@ -1,9 +1,9 @@
import { defineConfig } from "umi";
import { defineConfig } from "@umijs/max";
export default defineConfig({
routes: [
{ path: "/", component: "index" },
{ path: "/docs", component: "docs" },
],
npmClient: 'yarn',
// 最小化有效配置
history: { type: 'hash' },
plugins: [],
// 禁用MFSU功能
mfsu: false,
});

12
config/proxy.js Normal file
View 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
View 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;

View File

@@ -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/**/*"]
}

View File

@@ -2,18 +2,36 @@
"private": true,
"author": "liuhanhua <1>",
"scripts": {
"dev": "umi dev",
"dev": "cross-env UMI_ENV=dev max dev",
"dev:pre": "cross-env UMI_ENV=pre umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"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": {
"@types/react": "^18.0.33",
"@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"
}
}

View File

@@ -0,0 +1,10 @@
// 简化的 Umi 运行时配置文件
// 简单的路由切换处理
export function onRouteChange({ matchedRoutes }) {
if (matchedRoutes && matchedRoutes.length > 0) {
document.title = matchedRoutes[matchedRoutes.length - 1].route.title || 'LeasePal 商户管理系统';
}
}
//

View File

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

View File

@@ -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';
export default function Layout() {
const { Content } = AntdLayout;
const AdminLayout = () => {
return (
<div className={styles.navs}>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/docs">Docs</Link>
</li>
<li>
<a href="https://github.com/umijs/umi">Github</a>
</li>
</ul>
<AntdLayout className={styles.layout}>
<Content className={styles.content}>
<Outlet />
</div>
</Content>
</AntdLayout>
);
}
};
export default AdminLayout;

View File

@@ -1,10 +1,17 @@
.navs {
ul {
padding: 0;
list-style: none;
display: flex;
// 简化的布局样式
.layout {
min-height: 100vh;
background: #f0f2f5;
}
li {
margin-right: 1em;
.content {
padding: 24px;
min-height: 100vh;
}
// 基本响应式设计
@media (max-width: 576px) {
.content {
padding: 16px;
}
}

17
src/pages/404.jsx Normal file
View 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
View File

@@ -0,0 +1,7 @@
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 24px;
}

View File

@@ -1,7 +1,25 @@
import React from 'react';
const DocsPage = () => {
return (
<div style={{ padding: '24px' }}>
<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>This is umi docs.</p>
<p style={{ color: '#666' }}>如有问题或需要帮助请联系系统管理员</p>
</div>
</div>
);
};

View File

@@ -1,15 +1,12 @@
import yayJpg from '../assets/yay.jpg';
import React from 'react';
export default function HomePage() {
const HomePage = () => {
return (
<div>
<h2>Yay! Welcome to umi!</h2>
<p>
<img src={yayJpg} width="388" />
</p>
<p>
To get started, edit <code>pages/index.tsx</code> and save to reload.
</p>
<div style={{ padding: '24px' }}>
<h1>LeasePal 商户管理系统</h1>
<p>欢迎使用商户管理系统</p>
</div>
);
}
};
export default HomePage;

81
src/pages/login.jsx Normal file
View 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
View 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
View 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
View 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
View File

@@ -1 +0,0 @@
import 'umi/typings';

3417
yarn.lock

File diff suppressed because it is too large Load Diff