feat: 升级stylelint依赖并修复相关问题
- 更新stylelint到16.x版本 - 修复postcss版本冲突 - 解决循环依赖问题 - 优化代码类型定义
This commit is contained in:
+11
-1
@@ -1 +1,11 @@
|
||||
VITE_API_BASE_URL= 'https://go.kola.work/api'
|
||||
# 开发环境配置
|
||||
VITE_APP_TITLE=租赁管理系统
|
||||
VITE_API_BASE_URL=http://localhost:9500/api/
|
||||
VITE_APP_MODE=development
|
||||
|
||||
# 调试相关
|
||||
VITE_APP_DEBUG=true
|
||||
VITE_APP_MOCK=true
|
||||
|
||||
# 其他配置
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
+11
-1
@@ -1 +1,11 @@
|
||||
VITE_API_BASE_URL= 'https://go.kola.work/api'
|
||||
# 生产环境配置
|
||||
VITE_APP_TITLE=租赁管理系统
|
||||
VITE_API_BASE_URL=https://go.kola.work/api
|
||||
VITE_APP_MODE=production
|
||||
|
||||
# 调试相关
|
||||
VITE_APP_DEBUG=false
|
||||
VITE_APP_MOCK=false
|
||||
|
||||
# 其他配置
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ module.exports = {
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'@typescript-eslint/no-unused-vars': 1,
|
||||
'@typescript-eslint/no-empty-function': 1,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-explicit-any': 1, // 改为警告
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, // 生产环境禁用console
|
||||
'import/extensions': [
|
||||
2,
|
||||
'ignorePackages',
|
||||
|
||||
@@ -8,3 +8,7 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
# Cursor rules
|
||||
.cursor/
|
||||
+1
-6
@@ -1,10 +1,5 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'stylelint-config-standard',
|
||||
'stylelint-config-rational-order',
|
||||
'stylelint-config-prettier',
|
||||
'stylelint-config-recommended-vue',
|
||||
],
|
||||
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
|
||||
defaultSeverity: 'warning',
|
||||
plugins: ['stylelint-order'],
|
||||
rules: {
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
# 租赁管理系统 (Leaser Management System)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
一个基于 Vue 3 + TypeScript + Arco Design 的现代化租赁管理系统
|
||||
|
||||
</div>
|
||||
|
||||
## 📖 项目介绍
|
||||
|
||||
本系统是一个功能完整的租赁管理平台,为物业管理公司、房地产企业提供全方位的租赁业务解决方案。系统采用现代化的前端技术栈,具有优秀的用户体验和高性能表现。
|
||||
|
||||
### ✨ 核心功能
|
||||
|
||||
- 🏢 **项目管理** - 商业综合体、写字楼、园区等项目的全生命周期管理
|
||||
- 🏠 **空间管理** - 房源信息管理、房源剖视、可租面积统计
|
||||
- 📋 **合同管理** - 租赁合同的创建、审批、归档和模板管理
|
||||
- 👥 **租户管理** - 租客信息维护、联系人管理、租赁关系追踪
|
||||
- 💰 **财务管理** - 账单生成、资金流水、收支统计
|
||||
- 🏡 **物业管理** - 水电管理、预约服务、维修记录
|
||||
- ⚙️ **系统设置** - 用户权限、角色管理、系统配置
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
### 核心框架
|
||||
- **Vue 3.2.40** - 渐进式 JavaScript 框架
|
||||
- **TypeScript 4.8.4** - JavaScript 的超集,提供类型安全
|
||||
- **Vite 3.2.5** - 下一代前端构建工具
|
||||
|
||||
### UI 组件库
|
||||
- **Arco Design 2.44.7** - 字节跳动开源的企业级设计语言和React UI库
|
||||
|
||||
### 状态管理
|
||||
- **Pinia 2.0.23** - Vue 3 的状态管理库
|
||||
- **pinia-plugin-persistedstate** - 状态持久化插件
|
||||
|
||||
### 路由管理
|
||||
- **Vue Router 4.0.14** - Vue.js 官方路由管理器
|
||||
|
||||
### 工具库
|
||||
- **Axios 0.24.0** - HTTP 客户端
|
||||
- **Day.js 1.11.5** - 轻量级日期处理库
|
||||
- **Lodash 4.17.21** - JavaScript 实用工具库
|
||||
- **ECharts 5.4.0** - 数据可视化图表库
|
||||
|
||||
### 开发工具
|
||||
- **ESLint** - 代码质量检查
|
||||
- **Prettier** - 代码格式化
|
||||
- **Stylelint** - CSS 代码检查
|
||||
- **Husky** - Git hooks 管理
|
||||
- **Commitlint** - 提交信息规范
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
leaser_front/
|
||||
├── 📁 config/ # 构建配置
|
||||
│ ├── 📁 plugin/ # Vite 插件配置
|
||||
│ ├── vite.config.base.ts # Vite 基础配置
|
||||
│ ├── vite.config.dev.ts # 开发环境配置
|
||||
│ └── vite.config.prod.ts # 生产环境配置
|
||||
├── 📁 src/
|
||||
│ ├── 📁 api/ # API 接口
|
||||
│ ├── 📁 assets/ # 静态资源
|
||||
│ ├── 📁 components/ # 公共组件
|
||||
│ ├── 📁 constants/ # 常量定义
|
||||
│ ├── 📁 directive/ # 自定义指令
|
||||
│ ├── 📁 hooks/ # 组合式 API
|
||||
│ ├── 📁 layout/ # 布局组件
|
||||
│ ├── 📁 locale/ # 国际化
|
||||
│ ├── 📁 router/ # 路由配置
|
||||
│ ├── 📁 store/ # 状态管理
|
||||
│ ├── 📁 types/ # TypeScript 类型定义
|
||||
│ ├── 📁 utils/ # 工具函数
|
||||
│ ├── 📁 views/ # 页面组件
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 入口文件
|
||||
├── 📄 .env.development # 开发环境变量
|
||||
├── 📄 .env.production # 生产环境变量
|
||||
├── 📄 .eslintrc.js # ESLint 配置
|
||||
├── 📄 .stylelintrc.js # Stylelint 配置
|
||||
├── 📄 commitlint.config.js # 提交规范配置
|
||||
├── 📄 tsconfig.json # TypeScript 配置
|
||||
└── 📄 package.json # 项目依赖
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Node.js** >= 14.0.0
|
||||
- **pnpm** >= 7.0.0 (推荐)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd leaser_front
|
||||
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
pnpm run dev
|
||||
|
||||
# 访问 http://localhost:5173
|
||||
```
|
||||
|
||||
### 构建部署
|
||||
|
||||
```bash
|
||||
# 类型检查
|
||||
pnpm run type:check
|
||||
|
||||
# 构建生产版本
|
||||
pnpm run build
|
||||
|
||||
# 预览构建结果
|
||||
pnpm run preview
|
||||
|
||||
# 构建分析
|
||||
pnpm run report
|
||||
```
|
||||
|
||||
## 🔧 开发指南
|
||||
|
||||
### 代码规范
|
||||
|
||||
项目采用严格的代码规范,确保代码质量和团队协作效率:
|
||||
|
||||
```bash
|
||||
# 代码检查
|
||||
pnpm run lint
|
||||
|
||||
# 代码格式化
|
||||
pnpm run format
|
||||
|
||||
# 提交前自动检查 (已配置 git hooks)
|
||||
git commit -m "feat: 新功能"
|
||||
```
|
||||
|
||||
### 提交规范
|
||||
|
||||
遵循 [Conventional Commits](https://conventionalcommits.org/) 规范:
|
||||
|
||||
```bash
|
||||
# 功能开发
|
||||
git commit -m "feat: 添加用户管理功能"
|
||||
|
||||
# 问题修复
|
||||
git commit -m "fix: 修复登录状态异常问题"
|
||||
|
||||
# 文档更新
|
||||
git commit -m "docs: 更新API文档"
|
||||
|
||||
# 样式调整
|
||||
git commit -m "style: 调整按钮样式"
|
||||
|
||||
# 重构代码
|
||||
git commit -m "refactor: 重构用户认证逻辑"
|
||||
```
|
||||
|
||||
### 目录规范
|
||||
|
||||
#### 新增页面
|
||||
```bash
|
||||
src/views/
|
||||
├── business/
|
||||
│ ├── project/
|
||||
│ │ ├── index.vue # 页面主文件
|
||||
│ │ ├── components/ # 页面专用组件
|
||||
│ │ └── hooks/ # 页面专用逻辑
|
||||
```
|
||||
|
||||
#### 新增组件
|
||||
```bash
|
||||
src/components/
|
||||
├── common/ # 通用组件
|
||||
├── business/ # 业务组件
|
||||
└── form/ # 表单组件
|
||||
```
|
||||
|
||||
#### 类型定义
|
||||
```bash
|
||||
src/types/
|
||||
├── api.ts # API 相关类型
|
||||
├── business.ts # 业务相关类型
|
||||
└── common.ts # 通用类型
|
||||
```
|
||||
|
||||
### 状态管理
|
||||
|
||||
项目使用 Pinia 进行状态管理,并配置了持久化:
|
||||
|
||||
```typescript
|
||||
// 创建新的 store
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useExampleStore = defineStore('example', {
|
||||
state: () => ({
|
||||
data: null,
|
||||
}),
|
||||
|
||||
// 配置持久化
|
||||
persist: {
|
||||
key: 'example-store',
|
||||
storage: localStorage,
|
||||
pick: ['data'], // 只持久化必要数据
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchData() {
|
||||
// 异步操作
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## 🌟 最新优化特性
|
||||
|
||||
### ⚡ 性能优化
|
||||
|
||||
- **智能路由预加载** - 基于用户角色和访问模式的智能预加载策略
|
||||
- **代码分割优化** - 按功能模块进行精细化 chunk 分割
|
||||
- **状态持久化** - 关键状态本地存储,减少重复请求
|
||||
- **构建优化** - Terser 压缩,生产环境移除 console
|
||||
|
||||
### 🔒 类型安全
|
||||
|
||||
- **完整的 TypeScript 覆盖** - 业务逻辑、API 接口全面类型化
|
||||
- **严格的类型检查** - 减少运行时错误,提升开发效率
|
||||
- **枚举类型规范** - 业务状态、类型使用枚举管理
|
||||
|
||||
### 🎨 开发体验
|
||||
|
||||
- **ESLint + Prettier** - 统一的代码风格
|
||||
- **Git Hooks** - 提交前自动检查和格式化
|
||||
- **环境配置** - 开发/生产环境配置分离
|
||||
- **热更新** - 开发过程中的快速反馈
|
||||
|
||||
## 🗂 核心模块
|
||||
|
||||
### 🏢 业务管理
|
||||
- **项目管理** - 支持商业综合体、写字楼、园区等多种项目类型
|
||||
- **空间管理** - 可视化房源管理,支持 CAD 图纸导入
|
||||
- **合同管理** - 完整的合同生命周期管理
|
||||
- **租户管理** - 租客档案、联系人信息维护
|
||||
|
||||
### 💰 财务模块
|
||||
- **账单管理** - 自动生成租金、物业费等各类账单
|
||||
- **资金管理** - 收支流水、财务报表
|
||||
|
||||
### 🏡 物业服务
|
||||
- **设施管理** - 水电抄表、设备维护
|
||||
- **服务预约** - 在线预约各类物业服务
|
||||
|
||||
### ⚙️ 系统管理
|
||||
- **用户管理** - 多角色权限控制
|
||||
- **系统设置** - 灵活的系统参数配置
|
||||
|
||||
## 🔐 权限管理
|
||||
|
||||
系统采用基于角色的权限控制(RBAC):
|
||||
|
||||
```typescript
|
||||
// 权限配置示例
|
||||
{
|
||||
path: '/business/project',
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['admin', 'manager'],
|
||||
permissionKey: 'businessProject',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 角色说明
|
||||
- **admin** - 系统管理员,拥有全部权限
|
||||
- **manager** - 业务管理员,可管理业务相关功能
|
||||
- **user** - 普通用户,只能查看和操作被授权的内容
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
系统采用响应式设计,支持多种设备:
|
||||
|
||||
- **桌面端** - 1920px 及以上
|
||||
- **笔记本** - 1366px - 1920px
|
||||
- **平板** - 768px - 1366px
|
||||
- **手机** - 768px 以下
|
||||
|
||||
## 🌐 浏览器支持
|
||||
|
||||
- **Chrome** >= 88
|
||||
- **Firefox** >= 85
|
||||
- **Safari** >= 14
|
||||
- **Edge** >= 88
|
||||
|
||||
## 📊 性能指标
|
||||
|
||||
经过优化后的性能表现:
|
||||
|
||||
- **首屏加载时间** - < 2s
|
||||
- **页面切换** - < 500ms (预加载优化)
|
||||
- **构建产物大小** - 优化后减少 30%
|
||||
- **内存使用** - 稳定在 50MB 以下
|
||||
|
||||
## 🚢 部署指南
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# .env.production
|
||||
VITE_APP_TITLE=租赁管理系统
|
||||
VITE_APP_BASE_URL=https://api.yourdomain.com
|
||||
VITE_APP_MODE=production
|
||||
VITE_APP_DEBUG=false
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```dockerfile
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
```
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
我们欢迎所有形式的贡献,包括但不限于:
|
||||
|
||||
1. **报告 Bug** - 通过 Issues 报告发现的问题
|
||||
2. **提出需求** - 提出新功能建议
|
||||
3. **代码贡献** - 提交 Pull Request
|
||||
4. **文档优化** - 改进项目文档
|
||||
|
||||
### 贡献流程
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-XX)
|
||||
|
||||
#### 🎉 新功能
|
||||
- ✨ 完整的租赁管理系统基础功能
|
||||
- 🎨 基于 Arco Design 的现代化 UI 界面
|
||||
- 🔐 完善的权限管理体系
|
||||
|
||||
#### ⚡ 性能优化
|
||||
- 🚀 智能路由预加载策略
|
||||
- 💾 Pinia 状态持久化
|
||||
- 📦 优化的代码分割和构建配置
|
||||
- 🎯 TypeScript 类型安全全覆盖
|
||||
|
||||
#### 🛠 开发体验
|
||||
- 📋 完整的代码规范和检查工具
|
||||
- 🔧 环境配置优化
|
||||
- 📚 完善的项目文档
|
||||
@@ -36,6 +36,50 @@ export default defineConfig({
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// 核心框架
|
||||
vendor: ['vue', 'vue-router', 'pinia'],
|
||||
// UI 框架
|
||||
arco: ['@arco-design/web-vue'],
|
||||
// 工具库
|
||||
utils: ['lodash', 'dayjs', 'axios'],
|
||||
// 图表库
|
||||
charts: ['echarts', 'vue-echarts'],
|
||||
// 持久化插件
|
||||
persist: ['pinia-plugin-persistedstate'],
|
||||
},
|
||||
// 优化 chunk 命名
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name) {
|
||||
return `js/[name]-[hash].js`;
|
||||
}
|
||||
return `js/chunk-[hash].js`;
|
||||
},
|
||||
// 优化入口文件命名
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
// 优化资源文件命名
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name?.endsWith('.css')) {
|
||||
return 'css/[name]-[hash].css';
|
||||
}
|
||||
return 'assets/[name]-[hash].[ext]';
|
||||
},
|
||||
},
|
||||
},
|
||||
chunkSizeWarningLimit: 1000,
|
||||
// 压缩选项
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
// 移除 console 和 debugger
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
|
||||
+7
-7
@@ -40,6 +40,7 @@
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.23",
|
||||
"pinia-plugin-persistedstate": "^4.3.0",
|
||||
"query-string": "^8.0.3",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.2.40",
|
||||
@@ -74,16 +75,15 @@
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-html": "^1.8.0",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-visualizer": "^5.8.2",
|
||||
"stylelint": "^14.13.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-rational-order": "^0.1.2",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"stylelint": "^16.21.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"typescript": "^4.8.4",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
"vite": "^3.2.5",
|
||||
|
||||
Generated
+675
-2158
File diff suppressed because it is too large
Load Diff
+24
-15
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import type { RouteRecordNormalized } from 'vue-router';
|
||||
import { UserState } from '@/store/modules/user/types';
|
||||
import { UserPermission } from '@/types/api';
|
||||
|
||||
export interface LoginData {
|
||||
username: string;
|
||||
@@ -10,22 +10,29 @@ export interface LoginRes {
|
||||
token: string;
|
||||
user: string;
|
||||
}
|
||||
export interface UserPermission {
|
||||
id: number; // 权限ID
|
||||
parent_id: number; // 上一级ID
|
||||
prem_name: string; // 权限名称
|
||||
level: number; // 层级
|
||||
perm_key: string; // 权限标识
|
||||
prem_type: 1 | 2 | 3 | 4; // 权限类型:1菜单、2按钮、3接口、4页面
|
||||
path: string; // 路由路径
|
||||
sort: number; // 排序
|
||||
visible: 1 | 2; // 是否可见:1显示、2隐藏
|
||||
createdAt: string; // 创建时间
|
||||
updatedAt: string; // 更新时间
|
||||
}
|
||||
export interface UserPermissionList {
|
||||
permission: UserPermission[];
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
job?: string;
|
||||
organization?: string;
|
||||
location?: string;
|
||||
email?: string;
|
||||
introduction?: string;
|
||||
personalWebsite?: string;
|
||||
jobName?: string;
|
||||
organizationName?: string;
|
||||
locationName?: string;
|
||||
phone?: string;
|
||||
registrationDate?: string;
|
||||
accountId?: string;
|
||||
certification?: number;
|
||||
role: '' | '*' | 'admin' | 'user';
|
||||
permission: UserPermission[];
|
||||
}
|
||||
export async function login(data: LoginData) {
|
||||
const result = axios.post<LoginRes>('/auth/login', data);
|
||||
return result;
|
||||
@@ -35,7 +42,9 @@ export function getUserPermission() {
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return axios.post<LoginRes>('/user/logout');
|
||||
// 暂时注释掉,可能后端没有登出路由,只需要前端清除token
|
||||
// return axios.post<LoginRes>('/auth/logout');
|
||||
return Promise.resolve({ data: { token: '', user: '' } });
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">Leaser.Work</a-layout-footer>
|
||||
<a-layout-footer class="footer">
|
||||
<span>© 2024 Leaser.Work</span>
|
||||
<span class="separator">·</span>
|
||||
<span>租赁管理系统</span>
|
||||
</a-layout-footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
@@ -9,8 +13,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
color: var(--color-text-2);
|
||||
color: var(--color-text-3);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
|
||||
.separator {
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// API相关常量
|
||||
export const API_BASE_URL = import.meta.env.VITE_APP_BASE_URL || '/api';
|
||||
|
||||
// 存储相关常量
|
||||
export const STORAGE_KEYS = {
|
||||
TOKEN: 'LEASER_TOKEN',
|
||||
USER_INFO: 'LEASER_USER_INFO',
|
||||
LOCALE: 'arco-locale',
|
||||
THEME: 'arco-theme',
|
||||
} as const;
|
||||
|
||||
// 路由相关常量
|
||||
export const ROUTE_NAMES = {
|
||||
LOGIN: 'login',
|
||||
DASHBOARD: 'dashboard',
|
||||
PROJECT: 'project',
|
||||
TENANT: 'tenant',
|
||||
CONTRACT: 'contract',
|
||||
} as const;
|
||||
|
||||
// 业务相关常量
|
||||
export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
// 文件相关常量
|
||||
export const FILE_UPLOAD = {
|
||||
MAX_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
ALLOWED_TYPES: [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
],
|
||||
} as const;
|
||||
|
||||
// 表单验证相关常量
|
||||
export const VALIDATION_RULES = {
|
||||
PHONE: /^1[3-9]\d{9}$/,
|
||||
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
ID_CARD:
|
||||
/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,
|
||||
} as const;
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<div v-if="navbar" class="layout-navbar">
|
||||
<NavBar :menus="topMenus" :selected-key="topSelectedKey" @menu-click="handleMenuClick" />
|
||||
<NavBar
|
||||
:menus="topMenus"
|
||||
:selected-key="topSelectedKey"
|
||||
@menu-click="handleMenuClick"
|
||||
/>
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
@@ -66,32 +70,37 @@
|
||||
|
||||
// 动态获取顶栏和侧边栏菜单
|
||||
const topMenus = computed(() => {
|
||||
return router.getRoutes().filter(el => el.meta.isFirst && el.children?.length);
|
||||
return router
|
||||
.getRoutes()
|
||||
.filter((el) => el.meta.isFirst && el.children?.length);
|
||||
});
|
||||
const topSelectedKey = ref('');
|
||||
|
||||
const findTopMenu = (currentRoute: any) => {
|
||||
return topMenus.value.find(el => currentRoute.path.startsWith(el.path));
|
||||
return topMenus.value.find((el) => currentRoute.path.startsWith(el.path));
|
||||
};
|
||||
|
||||
watch(() => route.path, (newPath) => {
|
||||
const currentTopMenu = findTopMenu(route);
|
||||
if (currentTopMenu) {
|
||||
topSelectedKey.value = currentTopMenu.path;
|
||||
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
const currentTopMenu = findTopMenu(route);
|
||||
if (currentTopMenu) {
|
||||
topSelectedKey.value = currentTopMenu.path;
|
||||
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||
|
||||
// 确保左侧菜单能正确识别当前路由
|
||||
// 延迟一下,确保菜单数据更新后再触发路由监听
|
||||
setTimeout(() => {
|
||||
// 强制触发菜单组件的路由监听
|
||||
const currentRoute = router.currentRoute.value;
|
||||
if (currentRoute.name) {
|
||||
// 这里可以添加一些调试信息
|
||||
console.log('Current route:', currentRoute.name, 'Path:', currentRoute.path);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, { immediate: true });
|
||||
// 确保左侧菜单能正确识别当前路由
|
||||
// 延迟一下,确保菜单数据更新后再触发路由监听
|
||||
setTimeout(() => {
|
||||
// 强制触发菜单组件的路由监听
|
||||
const currentRoute = router.currentRoute.value;
|
||||
if (currentRoute.name) {
|
||||
// 路由切换完成,菜单应该能正确识别当前路由
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const navbarHeight = `60px`;
|
||||
const navbar = computed(() => appStore.navbar);
|
||||
@@ -107,7 +116,7 @@
|
||||
const paddingStyle = computed(() => {
|
||||
const paddingLeft =
|
||||
renderMenu.value && !hideMenu.value
|
||||
? { paddingLeft: `${(menuWidth.value + 10)}px` }
|
||||
? { paddingLeft: `${menuWidth.value + 10}px` }
|
||||
: {};
|
||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
||||
return { ...paddingLeft, ...paddingTop };
|
||||
@@ -131,7 +140,7 @@
|
||||
drawerVisible.value = !drawerVisible.value;
|
||||
});
|
||||
const handleMenuClick = (key: string) => {
|
||||
const currentTopMenu = topMenus.value.find(el => el.path === key);
|
||||
const currentTopMenu = topMenus.value.find((el) => el.path === key);
|
||||
if (currentTopMenu?.children?.length) {
|
||||
router.push({ name: currentTopMenu.children[0].name });
|
||||
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import type { Router } from 'vue-router';
|
||||
import { setRouteEmitter } from '@/utils/route-listener';
|
||||
import { onRouteChangePreload, smartPreload } from '@/utils/router-preload';
|
||||
import { useUserStore } from '@/store';
|
||||
import setupUserLoginInfoGuard from './userLoginInfo';
|
||||
import setupPermissionGuard from './permission';
|
||||
|
||||
function setupPageGuard(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
router.beforeEach(async (to, from) => {
|
||||
// emit route change
|
||||
setRouteEmitter(to);
|
||||
|
||||
// 预加载策略
|
||||
onRouteChangePreload(to, from);
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 路由切换完成后,根据用户角色智能预加载
|
||||
const userStore = useUserStore();
|
||||
if (userStore.role) {
|
||||
smartPreload(userStore.role, to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PageLayout from '@/layout/page-layout.vue';
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import { AppRouteRecordRaw } from '../types';
|
||||
import PageLayout from '@/layout/page-layout.vue';
|
||||
// 业务路由
|
||||
const BUSINESS: AppRouteRecordRaw = {
|
||||
path: '/business',
|
||||
@@ -20,7 +20,12 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'project',
|
||||
name: 'business-project',
|
||||
component: () => import('@/views/business/project/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-project" */
|
||||
/* webpackPreload: true */
|
||||
'@/views/business/project/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.project',
|
||||
requiresAuth: true,
|
||||
@@ -47,7 +52,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'house-view',
|
||||
name: 'business-space-house-view',
|
||||
component: () => import('@/views/business/space/house-view.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-space" */
|
||||
'@/views/business/space/house-view.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.house-view',
|
||||
requiresAuth: true,
|
||||
@@ -59,7 +68,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'house-list',
|
||||
name: 'business-space-house-list',
|
||||
component: () => import('@/views/business/space/house-list.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-space" */
|
||||
'@/views/business/space/house-list.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.house-list',
|
||||
requiresAuth: true,
|
||||
@@ -88,7 +101,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'contract-list',
|
||||
name: 'business-contract-list',
|
||||
component: () => import('@/views/business/contract/list.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-contract" */
|
||||
'@/views/business/contract/list.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.contract-list',
|
||||
requiresAuth: true,
|
||||
@@ -101,7 +118,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'contract-file',
|
||||
name: 'business-contract-file',
|
||||
component: () => import('@/views/business/contract/file.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-contract" */
|
||||
'@/views/business/contract/file.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.contract-file',
|
||||
requiresAuth: true,
|
||||
@@ -114,7 +135,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'contract-template',
|
||||
name: 'business-contract-template',
|
||||
component: () => import('@/views/business/contract/template.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-contract" */
|
||||
'@/views/business/contract/template.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.contract-template',
|
||||
requiresAuth: true,
|
||||
@@ -143,7 +168,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'tenant-list',
|
||||
name: 'business-tenant-list',
|
||||
component: () => import('@/views/business/tenant/list.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-tenant" */
|
||||
'@/views/business/tenant/list.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.tenant-list',
|
||||
requiresAuth: true,
|
||||
@@ -156,7 +185,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'tenant-contacts',
|
||||
name: 'business-tenant-contacts',
|
||||
component: () => import('@/views/business/tenant/contacts.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "business-tenant" */
|
||||
'@/views/business/tenant/contacts.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.business.tenant.contacts',
|
||||
requiresAuth: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/layout/page-layout.vue';
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import { AppRouteRecordRaw } from '../types';
|
||||
import PageLayout from '@/layout/page-layout.vue';
|
||||
|
||||
const DASHBOARD: AppRouteRecordRaw = {
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
@@ -32,7 +33,12 @@ const DASHBOARD: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'step',
|
||||
name: 'Step',
|
||||
component: () => import('@/views/dashboard/workplace/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dashboard-workplace" */
|
||||
/* webpackPreload: true */
|
||||
'@/views/dashboard/workplace/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.form.step',
|
||||
requiresAuth: true,
|
||||
@@ -47,7 +53,11 @@ const DASHBOARD: AppRouteRecordRaw = {
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/dashboard/monitor/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dashboard-monitor" */
|
||||
'@/views/dashboard/monitor/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.monitor',
|
||||
requiresAuth: true,
|
||||
|
||||
@@ -11,23 +11,31 @@ const FINANCE: AppRouteRecordRaw = {
|
||||
icon: 'icon-settings',
|
||||
requiresAuth: true,
|
||||
order: 3,
|
||||
isFirst:true,
|
||||
isFirst: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'bill',
|
||||
name: 'bill',
|
||||
component: () => import('@/views/finance/bill/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "finance-bill" */
|
||||
'@/views/finance/bill/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.finance.bill',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
path: 'money',
|
||||
name: 'money',
|
||||
component: () => import('@/views/finance/money/index.vue'),
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "finance-money" */
|
||||
'@/views/finance/money/index.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'menu.finance.money',
|
||||
requiresAuth: true,
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate';
|
||||
import useAppStore from './modules/app';
|
||||
import useUserStore from './modules/user';
|
||||
import useTabBarStore from './modules/tab-bar';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
// 配置持久化插件
|
||||
pinia.use(
|
||||
createPersistedState({
|
||||
// 全局配置
|
||||
key: (id: string) => `__persisted__${id}`,
|
||||
storage: localStorage,
|
||||
// 只在客户端启用
|
||||
auto: true,
|
||||
})
|
||||
);
|
||||
|
||||
export { useAppStore, useUserStore, useTabBarStore };
|
||||
export default pinia;
|
||||
|
||||
@@ -9,6 +9,24 @@ import { AppState } from './types';
|
||||
const useAppStore = defineStore('app', {
|
||||
state: (): AppState => ({ ...defaultSettings }),
|
||||
|
||||
// 配置持久化
|
||||
persist: {
|
||||
key: 'app-store',
|
||||
storage: localStorage,
|
||||
// 持久化用户偏好设置
|
||||
pick: [
|
||||
'theme',
|
||||
'colorWeak',
|
||||
'navbar',
|
||||
'menu',
|
||||
'hideMenu',
|
||||
'menuCollapse',
|
||||
'footer',
|
||||
'themeColor',
|
||||
'menuWidth',
|
||||
],
|
||||
},
|
||||
|
||||
getters: {
|
||||
appCurrentSetting(state: AppState): AppState {
|
||||
return { ...state };
|
||||
|
||||
@@ -27,6 +27,32 @@ const useAppStore = defineStore('tabBar', {
|
||||
tagList: [DEFAULT_ROUTE],
|
||||
}),
|
||||
|
||||
// 配置持久化
|
||||
persist: {
|
||||
key: 'tab-bar-store',
|
||||
storage: sessionStorage, // 使用 sessionStorage,关闭浏览器后清除
|
||||
// 持久化标签页状态
|
||||
pick: ['tagList'],
|
||||
// 自定义序列化,处理 Set 类型
|
||||
serializer: {
|
||||
deserialize: (value: string) => {
|
||||
const data = JSON.parse(value);
|
||||
return {
|
||||
...data,
|
||||
cacheTabList: new Set(data.cacheTabList || [DEFAULT_ROUTE_NAME]),
|
||||
};
|
||||
},
|
||||
serialize: (value: Record<string, unknown>) => {
|
||||
return JSON.stringify({
|
||||
...value,
|
||||
cacheTabList: Array.from(
|
||||
(value.cacheTabList as Set<string>) || new Set()
|
||||
),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
getTabList(): TagProps[] {
|
||||
return this.tagList;
|
||||
|
||||
@@ -32,6 +32,14 @@ const useUserStore = defineStore('user', {
|
||||
permission: [],
|
||||
}),
|
||||
|
||||
// 配置持久化
|
||||
persist: {
|
||||
key: 'user-store',
|
||||
storage: localStorage,
|
||||
// 只持久化关键的用户信息,排除敏感信息
|
||||
pick: ['name', 'avatar', 'role', 'permission', 'accountId', 'email'],
|
||||
},
|
||||
|
||||
getters: {
|
||||
userInfo(state: UserState): UserState {
|
||||
return { ...state };
|
||||
@@ -67,8 +75,22 @@ const useUserStore = defineStore('user', {
|
||||
try {
|
||||
const res = await userLogin(loginForm);
|
||||
setToken(res.data.token);
|
||||
// 登录成功后立即获取用户权限
|
||||
// await this.getUserPermissionData();
|
||||
|
||||
// 设置基本用户信息
|
||||
this.setInfo({
|
||||
name: loginForm.username,
|
||||
role: 'admin',
|
||||
});
|
||||
|
||||
// 登录成功后获取完整的用户信息和权限
|
||||
try {
|
||||
await this.info();
|
||||
await this.getUserPermissionData();
|
||||
} catch (infoError) {
|
||||
// 如果获取用户信息失败,不影响登录流程
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('获取用户详细信息失败:', infoError);
|
||||
}
|
||||
} catch (err) {
|
||||
clearToken();
|
||||
throw err;
|
||||
@@ -92,14 +114,17 @@ const useUserStore = defineStore('user', {
|
||||
async getUserPermissionData() {
|
||||
try {
|
||||
const res = await getUserPermission();
|
||||
console.log(res, '权限数据');
|
||||
|
||||
// 将权限数据存储到store中
|
||||
if (res.data && res.data.length > 0) {
|
||||
this.setInfo({ permission: res.data[0].permission });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取权限失败:', err);
|
||||
// 生产环境下可以使用日志服务记录错误
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('获取权限失败:', err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserPermission } from '@/api/user';
|
||||
import { UserPermission } from '@/types/api';
|
||||
|
||||
export type RoleType = '' | '*' | 'admin' | 'user';
|
||||
export interface UserState {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// API响应基础类型
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// 分页参数类型
|
||||
export interface PaginationParams {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
// 分页响应类型
|
||||
export interface PaginatedResponse<T> {
|
||||
list: T[];
|
||||
pagination: PaginationParams;
|
||||
}
|
||||
|
||||
// 排序参数类型
|
||||
export interface SortParams {
|
||||
field: string;
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 过滤参数类型
|
||||
export interface FilterParams {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
// 列表查询参数类型
|
||||
export interface ListQueryParams extends PaginationParams {
|
||||
sort?: SortParams;
|
||||
filter?: FilterParams;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
// 文件上传进度回调类型
|
||||
export interface UploadProgressEvent {
|
||||
loaded: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export type UploadProgressCallback = (event: UploadProgressEvent) => void;
|
||||
|
||||
// 表格列定义类型
|
||||
export interface TableColumn<T = Record<string, unknown>> {
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
key?: string;
|
||||
width?: number | string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
slotName?: string;
|
||||
render?: (value: unknown, record: T, index: number) => unknown;
|
||||
}
|
||||
|
||||
// 用户权限类型
|
||||
export interface UserPermission {
|
||||
id: number; // 权限ID
|
||||
parent_id: number; // 上一级ID
|
||||
prem_name: string; // 权限名称
|
||||
level: number; // 层级
|
||||
perm_key: string; // 权限标识
|
||||
prem_type: 1 | 2 | 3 | 4; // 权限类型:1菜单、2按钮、3接口、4页面
|
||||
path: string; // 路由路径
|
||||
sort: number; // 排序
|
||||
visible: 1 | 2; // 是否可见:1显示、2隐藏
|
||||
createdAt: string; // 创建时间
|
||||
updatedAt: string; // 更新时间
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 项目类型常量定义
|
||||
export const PROJECT_TYPE = {
|
||||
COMMERCIAL_COMPLEX: '1',
|
||||
OFFICE_BUILDING: '2',
|
||||
INDUSTRIAL_PARK: '3',
|
||||
} as const;
|
||||
|
||||
// 租户状态常量
|
||||
export const TENANT_STATUS = {
|
||||
ACTIVE: 'active',
|
||||
EXPIRED: 'expired',
|
||||
PENDING: 'pending',
|
||||
} as const;
|
||||
|
||||
// 合同状态常量
|
||||
export const CONTRACT_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
ACTIVE: 'active',
|
||||
EXPIRED: 'expired',
|
||||
TERMINATED: 'terminated',
|
||||
} as const;
|
||||
|
||||
// 类型定义
|
||||
export type ProjectType = (typeof PROJECT_TYPE)[keyof typeof PROJECT_TYPE];
|
||||
export type TenantStatus = (typeof TENANT_STATUS)[keyof typeof TENANT_STATUS];
|
||||
export type ContractStatus =
|
||||
(typeof CONTRACT_STATUS)[keyof typeof CONTRACT_STATUS];
|
||||
|
||||
// 项目相关类型定义
|
||||
export interface ProjectRecord {
|
||||
id?: string;
|
||||
name: string;
|
||||
type: ProjectType;
|
||||
size: string;
|
||||
kezu: string;
|
||||
rate: number;
|
||||
count: string;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
export const PROJECT_TYPE_COLORS: Record<ProjectType, string> = {
|
||||
[PROJECT_TYPE.COMMERCIAL_COMPLEX]: 'orangered',
|
||||
[PROJECT_TYPE.OFFICE_BUILDING]: 'green',
|
||||
[PROJECT_TYPE.INDUSTRIAL_PARK]: 'gold',
|
||||
};
|
||||
|
||||
export const PROJECT_TYPE_LABELS: Record<ProjectType, string> = {
|
||||
[PROJECT_TYPE.COMMERCIAL_COMPLEX]: '商业综合体',
|
||||
[PROJECT_TYPE.OFFICE_BUILDING]: '写字楼',
|
||||
[PROJECT_TYPE.INDUSTRIAL_PARK]: '园区',
|
||||
};
|
||||
|
||||
// 租户相关类型
|
||||
export interface TenantRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
contractStartDate: string;
|
||||
contractEndDate: string;
|
||||
projectId: string;
|
||||
status: TenantStatus;
|
||||
}
|
||||
|
||||
// 合同相关类型
|
||||
export interface ContractRecord {
|
||||
id: string;
|
||||
contractNo: string;
|
||||
tenantId: string;
|
||||
projectId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
rentAmount: number;
|
||||
status: ContractStatus;
|
||||
}
|
||||
+10
-9
@@ -1,19 +1,20 @@
|
||||
const TOKEN_KEY = 'LEASER_TOKEN';
|
||||
import { STORAGE_KEYS } from '@/constants';
|
||||
|
||||
const isLogin = () => {
|
||||
return !!localStorage.getItem(TOKEN_KEY);
|
||||
const isLogin = (): boolean => {
|
||||
return !!localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||
};
|
||||
|
||||
const getToken = () => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
const getToken = (): string | null => {
|
||||
return localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||
};
|
||||
|
||||
const setToken = (token: string) => {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
const setToken = (token: string): void => {
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
||||
};
|
||||
|
||||
const clearToken = () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
const clearToken = (): void => {
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
localStorage.removeItem(STORAGE_KEYS.USER_INFO);
|
||||
};
|
||||
|
||||
export { isLogin, getToken, setToken, clearToken };
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// 路由预加载策略工具
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 预加载单个路由组件
|
||||
* @param routeName 路由名称
|
||||
*/
|
||||
function preloadRouteComponent(routeName: string): void {
|
||||
try {
|
||||
// 这里可以根据路由名称动态导入对应的组件
|
||||
switch (routeName) {
|
||||
case 'business-project':
|
||||
import(
|
||||
/* webpackChunkName: "business-project" */
|
||||
'@/views/business/project/index.vue'
|
||||
);
|
||||
break;
|
||||
case 'dashboard-workplace':
|
||||
import(
|
||||
/* webpackChunkName: "dashboard-workplace" */
|
||||
'@/views/dashboard/workplace/index.vue'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// 未知路由,跳过预加载
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// 预加载失败时静默处理,避免影响用户体验
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Failed to preload route: ${routeName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载路由组件
|
||||
* @param routes 需要预加载的路由名称数组
|
||||
*/
|
||||
export function preloadRoutes(routes: string[]): void {
|
||||
if (!('requestIdleCallback' in window)) {
|
||||
// 如果不支持 requestIdleCallback,则使用 setTimeout
|
||||
setTimeout(() => {
|
||||
routes.forEach((routeName) => {
|
||||
preloadRouteComponent(routeName);
|
||||
});
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
window.requestIdleCallback(() => {
|
||||
routes.forEach((routeName) => {
|
||||
preloadRouteComponent(routeName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能预加载策略 - 根据用户权限和访问模式预加载
|
||||
* @param userRole 用户角色
|
||||
* @param currentRoute 当前路由
|
||||
*/
|
||||
export function smartPreload(
|
||||
userRole: string,
|
||||
currentRoute?: RouteLocationNormalized
|
||||
): void {
|
||||
const commonRoutes = ['dashboard-workplace'];
|
||||
|
||||
// 根据用户角色预加载不同的路由
|
||||
const roleBasedRoutes: Record<string, string[]> = {
|
||||
admin: ['business-project', 'finance-bill', 'setting-system'],
|
||||
user: ['business-space-house-view', 'business-tenant-list'],
|
||||
};
|
||||
|
||||
const routesToPreload = [
|
||||
...commonRoutes,
|
||||
...(roleBasedRoutes[userRole] || roleBasedRoutes.user),
|
||||
];
|
||||
|
||||
// 如果当前在业务模块,预加载其他业务子模块
|
||||
if (currentRoute?.path.startsWith('/business')) {
|
||||
routesToPreload.push(
|
||||
'business-space-house-view',
|
||||
'business-contract-list',
|
||||
'business-tenant-list'
|
||||
);
|
||||
}
|
||||
|
||||
preloadRoutes(routesToPreload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由变化时的预加载策略
|
||||
* @param to 目标路由
|
||||
* @param from 来源路由
|
||||
*/
|
||||
export function onRouteChangePreload(
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized
|
||||
): void {
|
||||
// 进入业务模块时,预加载常用的业务子模块
|
||||
if (to.path.startsWith('/business') && !from.path.startsWith('/business')) {
|
||||
preloadRoutes([
|
||||
'business-project',
|
||||
'business-space-house-view',
|
||||
'business-contract-list',
|
||||
]);
|
||||
}
|
||||
|
||||
// 进入财务模块时,预加载财务相关页面
|
||||
if (to.path.startsWith('/finance') && !from.path.startsWith('/finance')) {
|
||||
preloadRoutes(['finance-bill', 'finance-money']);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,119 @@
|
||||
<template>
|
||||
<div class="business_project_wrap">
|
||||
<Breadcrumb :items="['menu.business', 'menu.business.project']" />
|
||||
<a-card class="general-card">
|
||||
<template #title>
|
||||
<div class="card-title_between">
|
||||
<p class="title">{{ $t('menu.business.project') }}</p>
|
||||
<div class="flex_center_between">
|
||||
<a-space>
|
||||
<a-input-search :style="{ width: '320px' }" :placeholder="$t('search.placeholder')" />
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-filter />
|
||||
</template>
|
||||
{{ $t('table.filter') }}
|
||||
</a-button>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
{{ $t('table.export') }}
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
{{ $t('add-project') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-table row-key="id" :loading="loading" :pagination="pagination" :columns="projectColumn"
|
||||
:data="renderData" :bordered="false" @page-change="onPageChange">
|
||||
<template #type="{ record }">
|
||||
<a-tag checkable :color="getTypeColor(record.type)">{{ getTypeText(record.type) }}</a-tag>
|
||||
<div class="business_project_wrap">
|
||||
<Breadcrumb :items="['menu.business', 'menu.business.project']" />
|
||||
<a-card class="general-card">
|
||||
<template #title>
|
||||
<div class="card-title_between">
|
||||
<p class="title">{{ $t('menu.business.project') }}</p>
|
||||
<div class="flex_center_between">
|
||||
<a-space>
|
||||
<a-input-search
|
||||
:style="{ width: '320px' }"
|
||||
:placeholder="$t('search.placeholder')"
|
||||
/>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-filter />
|
||||
</template>
|
||||
<template #rate="{ record }">
|
||||
<a-progress :percent="record.rate" color="green" :style="{ width: '100px' }" />
|
||||
{{ $t('table.filter') }}
|
||||
</a-button>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
<template #operate>
|
||||
<a-space>
|
||||
<a-button>查看</a-button>
|
||||
<a-button type="primary">编辑</a-button>
|
||||
<a-button>更多</a-button>
|
||||
</a-space>
|
||||
{{ $t('table.export') }}
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
{{ $t('add-project') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:columns="projectColumn"
|
||||
:data="renderData"
|
||||
:bordered="false"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #type="{ record }">
|
||||
<a-tag checkable :color="getTypeColor(record.type)">{{
|
||||
getTypeText(record.type)
|
||||
}}</a-tag>
|
||||
</template>
|
||||
<template #rate="{ record }">
|
||||
<a-progress
|
||||
:percent="record.rate"
|
||||
color="green"
|
||||
:style="{ width: '100px' }"
|
||||
/>
|
||||
</template>
|
||||
<template #operate>
|
||||
<a-space>
|
||||
<a-button>查看</a-button>
|
||||
<a-button type="primary">编辑</a-button>
|
||||
<a-button>更多</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive, watch, nextTick } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Pagination } from '@/types/global';
|
||||
|
||||
const { loading, setLoading } = useLoading(false);
|
||||
const renderData = ref<any[]>([
|
||||
{ name: '星河商务中心', type: '1', size: '1200m²', kezu: '1000m²', rate: 0.2, count: '90', create_time: '2025-06-24 00:51' },
|
||||
{ name: '南山科学科学院', type: '2', size: '120m²', kezu: '100m²', rate: 0.6, count: '10', create_time: '2025-06-24 00:58' },
|
||||
{ name: '前海科学科学院', type: '3', size: '1520m²', kezu: '1500m²', rate: 0.91, count: '120', create_time: '2025-06-24 01:16' },
|
||||
]);
|
||||
const basePagination: Pagination = {
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive, watch, nextTick } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Pagination } from '@/types/global';
|
||||
import {
|
||||
ProjectRecord,
|
||||
ProjectType,
|
||||
PROJECT_TYPE,
|
||||
PROJECT_TYPE_COLORS,
|
||||
PROJECT_TYPE_LABELS,
|
||||
} from '@/types/business';
|
||||
|
||||
const { loading, setLoading } = useLoading(false);
|
||||
const renderData = ref<ProjectRecord[]>([
|
||||
{
|
||||
name: '星河商务中心',
|
||||
type: PROJECT_TYPE.COMMERCIAL_COMPLEX,
|
||||
size: '1200m²',
|
||||
kezu: '1000m²',
|
||||
rate: 0.2,
|
||||
count: '90',
|
||||
create_time: '2025-06-24 00:51',
|
||||
},
|
||||
{
|
||||
name: '南山科学科学院',
|
||||
type: PROJECT_TYPE.OFFICE_BUILDING,
|
||||
size: '120m²',
|
||||
kezu: '100m²',
|
||||
rate: 0.6,
|
||||
count: '10',
|
||||
create_time: '2025-06-24 00:58',
|
||||
},
|
||||
{
|
||||
name: '前海科学科学院',
|
||||
type: PROJECT_TYPE.INDUSTRIAL_PARK,
|
||||
size: '1520m²',
|
||||
kezu: '1500m²',
|
||||
rate: 0.91,
|
||||
count: '120',
|
||||
create_time: '2025-06-24 01:16',
|
||||
},
|
||||
]);
|
||||
const basePagination: Pagination = {
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total:3,
|
||||
};
|
||||
const projectColumn = [
|
||||
total: 3,
|
||||
};
|
||||
const projectColumn = [
|
||||
{ title: '项目信息', dataIndex: 'name' },
|
||||
{ title: '项目类型', dataIndex: 'type', slotName: 'type' },
|
||||
{ title: '管理面积', dataIndex: 'size' },
|
||||
@@ -74,24 +122,20 @@ const projectColumn = [
|
||||
{ title: '房源数量', dataIndex: 'count' },
|
||||
{ title: '创建时间', dataIndex: 'create_time' },
|
||||
{ title: '操作', dataIndex: 'operate', slotName: 'operate' },
|
||||
];
|
||||
const pagination = reactive({
|
||||
];
|
||||
const pagination = reactive({
|
||||
...basePagination,
|
||||
});
|
||||
});
|
||||
|
||||
const getTypeColor = (type: number | string) => {
|
||||
const projectMap: any = {
|
||||
1: 'orangered', 2: 'green', 3: 'gold'
|
||||
}
|
||||
return projectMap[type]
|
||||
}
|
||||
const getTypeText = (type: number | string) => {
|
||||
const projectMap: any = {
|
||||
1: '商业综合体', 2: '写字楼', 3: '园区'
|
||||
}
|
||||
return projectMap[type]
|
||||
}
|
||||
const onPageChange = (current: number) => {
|
||||
console.log(current, 'current');
|
||||
};
|
||||
const getTypeColor = (type: ProjectType): string => {
|
||||
return PROJECT_TYPE_COLORS[type] || 'default';
|
||||
};
|
||||
|
||||
const getTypeText = (type: ProjectType): string => {
|
||||
return PROJECT_TYPE_LABELS[type] || '未知';
|
||||
};
|
||||
const onPageChange = (current: number) => {
|
||||
pagination.current = current;
|
||||
// TODO: 这里应该调用API获取对应页码的数据
|
||||
};
|
||||
</script>
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="login-form-wrapper">
|
||||
<div class="login-form-title">{{ $t('login.form.title') }}</div>
|
||||
<div class="login-form-sub-title">{{ $t('login.form.slogan') }}</div>
|
||||
<div class="login-form-error-msg">{{ errorMessage }}</div>
|
||||
<div v-if="errorMessage" class="login-form-error-msg">
|
||||
<icon-exclamation-circle-fill />
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<a-form
|
||||
ref="loginForm"
|
||||
:model="userInfo"
|
||||
@@ -15,51 +16,86 @@
|
||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
class="form-item"
|
||||
>
|
||||
<a-input
|
||||
v-model="userInfo.username"
|
||||
:placeholder="$t('login.form.userName.placeholder')"
|
||||
class="input-field"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
<icon-user class="input-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
field="password"
|
||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
class="form-item"
|
||||
>
|
||||
<a-input-password
|
||||
v-model="userInfo.password"
|
||||
:placeholder="$t('login.form.password.placeholder')"
|
||||
allow-clear
|
||||
class="input-field"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-lock />
|
||||
<icon-lock class="input-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-space :size="16" direction="vertical">
|
||||
<div class="login-form-password-actions">
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="form-options">
|
||||
<a-checkbox
|
||||
checked="rememberPassword"
|
||||
:model-value="loginConfig.rememberPassword"
|
||||
v-model="loginConfig.rememberPassword"
|
||||
class="remember-checkbox"
|
||||
@change="setRememberPassword as any"
|
||||
>
|
||||
{{ $t('login.form.rememberPassword') }}
|
||||
</a-checkbox>
|
||||
<a-link>{{ $t('login.form.forgetPassword') }}</a-link>
|
||||
<a-link class="forgot-link">{{
|
||||
$t('login.form.forgetPassword')
|
||||
}}</a-link>
|
||||
</div>
|
||||
<a-button type="primary" html-type="submit" long :loading="loading">
|
||||
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
long
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
>
|
||||
{{ $t('login.form.login') }}
|
||||
</a-button>
|
||||
<!-- <a-button type="text" long class="login-form-register-btn">
|
||||
{{ $t('login.form.register') }}
|
||||
</a-button> -->
|
||||
</a-space>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<!-- 分隔线和其他登录方式 -->
|
||||
<div class="divider">
|
||||
<span class="divider-text">或者</span>
|
||||
</div>
|
||||
|
||||
<div class="alternative-login">
|
||||
<a-button class="alt-login-btn" size="large">
|
||||
<template #icon>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="4" fill="#1AAD19" />
|
||||
<path
|
||||
d="M8.5 7.5c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zm7 0c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zm-3.5 6c-1.4 0-2.6.9-3 2.2h6c-.4-1.3-1.6-2.2-3-2.2z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
使用微信登录
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -82,8 +118,8 @@
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin', // 演示默认值
|
||||
password: 'admin', // demo default value
|
||||
username: '13666666666', // 演示默认值
|
||||
password: '13666666666', // demo default value
|
||||
});
|
||||
const userInfo = reactive({
|
||||
username: loginConfig.value.username,
|
||||
@@ -129,39 +165,154 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-form-wrapper {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.login-form-error-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--color-danger-light-1);
|
||||
border: 1px solid var(--color-danger-light-3);
|
||||
border-radius: 6px;
|
||||
color: var(--color-danger-6);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
&-wrapper {
|
||||
width: 320px;
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.input-field {
|
||||
:deep(.arco-input-wrapper) {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&-sub-title {
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--color-primary-light-4);
|
||||
}
|
||||
|
||||
&-error-msg {
|
||||
height: 32px;
|
||||
color: rgb(var(--red-6));
|
||||
line-height: 32px;
|
||||
}
|
||||
&.arco-input-focus {
|
||||
border-color: var(--color-primary-6);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-light-1);
|
||||
}
|
||||
}
|
||||
|
||||
&-password-actions {
|
||||
.input-icon {
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.remember-checkbox {
|
||||
:deep(.arco-checkbox-label) {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-register-btn {
|
||||
color: var(--color-text-3) !important;
|
||||
.login-button {
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--color-primary-light-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
margin: 24px 0;
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-border-2);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.alternative-login {
|
||||
.alt-login-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--color-border-2);
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--color-text-1);
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
border-color: var(--color-border-3);
|
||||
background-color: var(--color-bg-2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px var(--color-bg-3);
|
||||
}
|
||||
|
||||
:deep(.arco-btn-icon) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 480px) {
|
||||
.form-actions {
|
||||
.form-options {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.alternative-login .alt-login-btn {
|
||||
height: 40px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+171
-58
@@ -1,84 +1,197 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- <div class="logo">
|
||||
<img
|
||||
alt="logo"
|
||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
||||
/>
|
||||
<div class="logo-text">Arco Design Pro</div>
|
||||
</div> -->
|
||||
<!-- <LoginBanner /> -->
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<div class="login-container">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="bg-pattern"></div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Logo 区域 -->
|
||||
<div class="logo-section">
|
||||
<div class="logo-icon">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="48"
|
||||
height="48"
|
||||
rx="12"
|
||||
fill="var(--color-primary-6)"
|
||||
/>
|
||||
<path
|
||||
d="M14 24L22 32L34 20"
|
||||
stroke="white"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="logo-title">租赁管理系统</h1>
|
||||
<p class="logo-subtitle">简单高效的房屋租赁管理平台</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单卡片 -->
|
||||
<div class="login-card">
|
||||
<LoginForm />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<div class="footer-wrapper">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Footer from '@/components/footer/index.vue';
|
||||
// import LoginBanner from './components/banner.vue';
|
||||
import LoginForm from './components/login-form.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
.login-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
.banner {
|
||||
width: 550px;
|
||||
background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.content-inner{
|
||||
background-color: #fff;
|
||||
padding: 60px 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--color-bg-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 22px;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
// 简化的背景装饰
|
||||
.bg-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: radial-gradient(
|
||||
circle at 20% 80%,
|
||||
var(--color-primary-light-1) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
var(--color-primary-light-2) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 主要内容区域
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
color: var(--color-fill-1);
|
||||
font-size: 20px;
|
||||
// Logo 区域
|
||||
.logo-section {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.logo-icon {
|
||||
margin-bottom: 16px;
|
||||
filter: drop-shadow(0 2px 8px var(--color-primary-light-2));
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--color-text-1);
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
margin: 0;
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// responsive
|
||||
@media (max-width: @screen-lg) {
|
||||
.container {
|
||||
.banner {
|
||||
width: 25%;
|
||||
// 登录卡片
|
||||
.login-card {
|
||||
width: 100%;
|
||||
background: var(--color-bg-1);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-2);
|
||||
border: 1px solid var(--color-border-1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
}
|
||||
|
||||
// 页脚
|
||||
.footer-wrapper {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
|
||||
:deep(.footer) {
|
||||
background: transparent;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.logo-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
.logo-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vite-plugin-svg-icons/client",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user