mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-23 23:07:01 +00:00
feat: 升级stylelint依赖并修复相关问题
- 更新stylelint到16.x版本 - 修复postcss版本冲突 - 解决循环依赖问题 - 优化代码类型定义
This commit is contained in:
parent
d1c2d332ab
commit
4d66f8baa7
30 changed files with 2190 additions and 2424 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ module.exports = {
|
||||||
'@typescript-eslint/ban-ts-comment': 0,
|
'@typescript-eslint/ban-ts-comment': 0,
|
||||||
'@typescript-eslint/no-unused-vars': 1,
|
'@typescript-eslint/no-unused-vars': 1,
|
||||||
'@typescript-eslint/no-empty-function': 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': [
|
'import/extensions': [
|
||||||
2,
|
2,
|
||||||
'ignorePackages',
|
'ignorePackages',
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,3 +8,7 @@ node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Cursor rules
|
||||||
|
.cursor/
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
|
||||||
'stylelint-config-standard',
|
|
||||||
'stylelint-config-rational-order',
|
|
||||||
'stylelint-config-prettier',
|
|
||||||
'stylelint-config-recommended-vue',
|
|
||||||
],
|
|
||||||
defaultSeverity: 'warning',
|
defaultSeverity: 'warning',
|
||||||
plugins: ['stylelint-order'],
|
plugins: ['stylelint-order'],
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
||||||
396
README.md
Normal file
396
README.md
Normal file
|
|
@ -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: {
|
define: {
|
||||||
'process.env': {},
|
'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: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
less: {
|
less: {
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -40,6 +40,7 @@
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.23",
|
"pinia": "^2.0.23",
|
||||||
|
"pinia-plugin-persistedstate": "^4.3.0",
|
||||||
"query-string": "^8.0.3",
|
"query-string": "^8.0.3",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"vue": "^3.2.40",
|
"vue": "^3.2.40",
|
||||||
|
|
@ -74,16 +75,15 @@
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-html": "^1.8.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"rollup": "^3.9.1",
|
"rollup": "^3.9.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.2",
|
"rollup-plugin-visualizer": "^5.8.2",
|
||||||
"stylelint": "^14.13.0",
|
"stylelint": "^16.21.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-recommended-vue": "^1.5.0",
|
||||||
"stylelint-config-rational-order": "^0.1.2",
|
"stylelint-config-standard": "^36.0.1",
|
||||||
"stylelint-config-recommended-vue": "^1.4.0",
|
"stylelint-order": "^6.0.4",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
|
||||||
"stylelint-order": "^5.0.0",
|
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"unplugin-vue-components": "^0.24.1",
|
"unplugin-vue-components": "^0.24.1",
|
||||||
"vite": "^3.2.5",
|
"vite": "^3.2.5",
|
||||||
|
|
|
||||||
2833
pnpm-lock.yaml
generated
2833
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { RouteRecordNormalized } from 'vue-router';
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
import { UserState } from '@/store/modules/user/types';
|
import { UserPermission } from '@/types/api';
|
||||||
|
|
||||||
export interface LoginData {
|
export interface LoginData {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
@ -10,22 +10,29 @@ export interface LoginRes {
|
||||||
token: string;
|
token: string;
|
||||||
user: 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 {
|
export interface UserPermissionList {
|
||||||
permission: UserPermission[];
|
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) {
|
export async function login(data: LoginData) {
|
||||||
const result = axios.post<LoginRes>('/auth/login', data);
|
const result = axios.post<LoginRes>('/auth/login', data);
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -35,7 +42,9 @@ export function getUserPermission() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout() {
|
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() {
|
export function getUserInfo() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
@ -9,8 +13,15 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-3);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
44
src/constants/index.ts
Normal file
44
src/constants/index.ts
Normal file
|
|
@ -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>
|
<template>
|
||||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||||
<div v-if="navbar" class="layout-navbar">
|
<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>
|
</div>
|
||||||
<a-layout>
|
<a-layout>
|
||||||
<a-layout>
|
<a-layout>
|
||||||
|
|
@ -66,15 +70,19 @@
|
||||||
|
|
||||||
// 动态获取顶栏和侧边栏菜单
|
// 动态获取顶栏和侧边栏菜单
|
||||||
const topMenus = computed(() => {
|
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 topSelectedKey = ref('');
|
||||||
|
|
||||||
const findTopMenu = (currentRoute: any) => {
|
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) => {
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
const currentTopMenu = findTopMenu(route);
|
const currentTopMenu = findTopMenu(route);
|
||||||
if (currentTopMenu) {
|
if (currentTopMenu) {
|
||||||
topSelectedKey.value = currentTopMenu.path;
|
topSelectedKey.value = currentTopMenu.path;
|
||||||
|
|
@ -86,12 +94,13 @@
|
||||||
// 强制触发菜单组件的路由监听
|
// 强制触发菜单组件的路由监听
|
||||||
const currentRoute = router.currentRoute.value;
|
const currentRoute = router.currentRoute.value;
|
||||||
if (currentRoute.name) {
|
if (currentRoute.name) {
|
||||||
// 这里可以添加一些调试信息
|
// 路由切换完成,菜单应该能正确识别当前路由
|
||||||
console.log('Current route:', currentRoute.name, 'Path:', currentRoute.path);
|
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
const navbarHeight = `60px`;
|
const navbarHeight = `60px`;
|
||||||
const navbar = computed(() => appStore.navbar);
|
const navbar = computed(() => appStore.navbar);
|
||||||
|
|
@ -107,7 +116,7 @@
|
||||||
const paddingStyle = computed(() => {
|
const paddingStyle = computed(() => {
|
||||||
const paddingLeft =
|
const paddingLeft =
|
||||||
renderMenu.value && !hideMenu.value
|
renderMenu.value && !hideMenu.value
|
||||||
? { paddingLeft: `${(menuWidth.value + 10)}px` }
|
? { paddingLeft: `${menuWidth.value + 10}px` }
|
||||||
: {};
|
: {};
|
||||||
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
const paddingTop = navbar.value ? { paddingTop: navbarHeight } : {};
|
||||||
return { ...paddingLeft, ...paddingTop };
|
return { ...paddingLeft, ...paddingTop };
|
||||||
|
|
@ -131,7 +140,7 @@
|
||||||
drawerVisible.value = !drawerVisible.value;
|
drawerVisible.value = !drawerVisible.value;
|
||||||
});
|
});
|
||||||
const handleMenuClick = (key: string) => {
|
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) {
|
if (currentTopMenu?.children?.length) {
|
||||||
router.push({ name: currentTopMenu.children[0].name });
|
router.push({ name: currentTopMenu.children[0].name });
|
||||||
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
appStore.updateSettings({ serverMenu: currentTopMenu.children });
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
import { setRouteEmitter } from '@/utils/route-listener';
|
import { setRouteEmitter } from '@/utils/route-listener';
|
||||||
|
import { onRouteChangePreload, smartPreload } from '@/utils/router-preload';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
import setupUserLoginInfoGuard from './userLoginInfo';
|
import setupUserLoginInfoGuard from './userLoginInfo';
|
||||||
import setupPermissionGuard from './permission';
|
import setupPermissionGuard from './permission';
|
||||||
|
|
||||||
function setupPageGuard(router: Router) {
|
function setupPageGuard(router: Router) {
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to, from) => {
|
||||||
// emit route change
|
// emit route change
|
||||||
setRouteEmitter(to);
|
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 { DEFAULT_LAYOUT } from '../base';
|
||||||
import { AppRouteRecordRaw } from '../types';
|
import { AppRouteRecordRaw } from '../types';
|
||||||
import PageLayout from '@/layout/page-layout.vue';
|
|
||||||
// 业务路由
|
// 业务路由
|
||||||
const BUSINESS: AppRouteRecordRaw = {
|
const BUSINESS: AppRouteRecordRaw = {
|
||||||
path: '/business',
|
path: '/business',
|
||||||
|
|
@ -20,7 +20,12 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'project',
|
path: 'project',
|
||||||
name: 'business-project',
|
name: 'business-project',
|
||||||
component: () => import('@/views/business/project/index.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-project" */
|
||||||
|
/* webpackPreload: true */
|
||||||
|
'@/views/business/project/index.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.project',
|
locale: 'menu.business.project',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -47,7 +52,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'house-view',
|
path: 'house-view',
|
||||||
name: 'business-space-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: {
|
meta: {
|
||||||
locale: 'menu.business.house-view',
|
locale: 'menu.business.house-view',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -59,7 +68,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'house-list',
|
path: 'house-list',
|
||||||
name: 'business-space-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: {
|
meta: {
|
||||||
locale: 'menu.business.house-list',
|
locale: 'menu.business.house-list',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -88,7 +101,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'contract-list',
|
path: 'contract-list',
|
||||||
name: 'business-contract-list',
|
name: 'business-contract-list',
|
||||||
component: () => import('@/views/business/contract/list.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-contract" */
|
||||||
|
'@/views/business/contract/list.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.contract-list',
|
locale: 'menu.business.contract-list',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -101,7 +118,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'contract-file',
|
path: 'contract-file',
|
||||||
name: 'business-contract-file',
|
name: 'business-contract-file',
|
||||||
component: () => import('@/views/business/contract/file.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-contract" */
|
||||||
|
'@/views/business/contract/file.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.contract-file',
|
locale: 'menu.business.contract-file',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -114,7 +135,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'contract-template',
|
path: 'contract-template',
|
||||||
name: 'business-contract-template',
|
name: 'business-contract-template',
|
||||||
component: () => import('@/views/business/contract/template.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-contract" */
|
||||||
|
'@/views/business/contract/template.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.contract-template',
|
locale: 'menu.business.contract-template',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -143,7 +168,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'tenant-list',
|
path: 'tenant-list',
|
||||||
name: 'business-tenant-list',
|
name: 'business-tenant-list',
|
||||||
component: () => import('@/views/business/tenant/list.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-tenant" */
|
||||||
|
'@/views/business/tenant/list.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.tenant-list',
|
locale: 'menu.business.tenant-list',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -156,7 +185,11 @@ const BUSINESS: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'tenant-contacts',
|
path: 'tenant-contacts',
|
||||||
name: 'business-tenant-contacts',
|
name: 'business-tenant-contacts',
|
||||||
component: () => import('@/views/business/tenant/contacts.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "business-tenant" */
|
||||||
|
'@/views/business/tenant/contacts.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.business.tenant.contacts',
|
locale: 'menu.business.tenant.contacts',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import PageLayout from '@/layout/page-layout.vue';
|
||||||
import { DEFAULT_LAYOUT } from '../base';
|
import { DEFAULT_LAYOUT } from '../base';
|
||||||
import { AppRouteRecordRaw } from '../types';
|
import { AppRouteRecordRaw } from '../types';
|
||||||
import PageLayout from '@/layout/page-layout.vue';
|
|
||||||
const DASHBOARD: AppRouteRecordRaw = {
|
const DASHBOARD: AppRouteRecordRaw = {
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
|
|
@ -32,7 +33,12 @@ const DASHBOARD: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'step',
|
path: 'step',
|
||||||
name: 'Step',
|
name: 'Step',
|
||||||
component: () => import('@/views/dashboard/workplace/index.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "dashboard-workplace" */
|
||||||
|
/* webpackPreload: true */
|
||||||
|
'@/views/dashboard/workplace/index.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.form.step',
|
locale: 'menu.form.step',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -47,7 +53,11 @@ const DASHBOARD: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'monitor',
|
path: 'monitor',
|
||||||
name: 'Monitor',
|
name: 'Monitor',
|
||||||
component: () => import('@/views/dashboard/monitor/index.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "dashboard-monitor" */
|
||||||
|
'@/views/dashboard/monitor/index.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.dashboard.monitor',
|
locale: 'menu.dashboard.monitor',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ const FINANCE: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'bill',
|
path: 'bill',
|
||||||
name: 'bill',
|
name: 'bill',
|
||||||
component: () => import('@/views/finance/bill/index.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "finance-bill" */
|
||||||
|
'@/views/finance/bill/index.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.finance.bill',
|
locale: 'menu.finance.bill',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
@ -27,7 +31,11 @@ const FINANCE: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'money',
|
path: 'money',
|
||||||
name: 'money',
|
name: 'money',
|
||||||
component: () => import('@/views/finance/money/index.vue'),
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "finance-money" */
|
||||||
|
'@/views/finance/money/index.vue'
|
||||||
|
),
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.finance.money',
|
locale: 'menu.finance.money',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import { createPersistedState } from 'pinia-plugin-persistedstate';
|
||||||
import useAppStore from './modules/app';
|
import useAppStore from './modules/app';
|
||||||
import useUserStore from './modules/user';
|
import useUserStore from './modules/user';
|
||||||
import useTabBarStore from './modules/tab-bar';
|
import useTabBarStore from './modules/tab-bar';
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
// 配置持久化插件
|
||||||
|
pinia.use(
|
||||||
|
createPersistedState({
|
||||||
|
// 全局配置
|
||||||
|
key: (id: string) => `__persisted__${id}`,
|
||||||
|
storage: localStorage,
|
||||||
|
// 只在客户端启用
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export { useAppStore, useUserStore, useTabBarStore };
|
export { useAppStore, useUserStore, useTabBarStore };
|
||||||
export default pinia;
|
export default pinia;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,24 @@ import { AppState } from './types';
|
||||||
const useAppStore = defineStore('app', {
|
const useAppStore = defineStore('app', {
|
||||||
state: (): AppState => ({ ...defaultSettings }),
|
state: (): AppState => ({ ...defaultSettings }),
|
||||||
|
|
||||||
|
// 配置持久化
|
||||||
|
persist: {
|
||||||
|
key: 'app-store',
|
||||||
|
storage: localStorage,
|
||||||
|
// 持久化用户偏好设置
|
||||||
|
pick: [
|
||||||
|
'theme',
|
||||||
|
'colorWeak',
|
||||||
|
'navbar',
|
||||||
|
'menu',
|
||||||
|
'hideMenu',
|
||||||
|
'menuCollapse',
|
||||||
|
'footer',
|
||||||
|
'themeColor',
|
||||||
|
'menuWidth',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
appCurrentSetting(state: AppState): AppState {
|
appCurrentSetting(state: AppState): AppState {
|
||||||
return { ...state };
|
return { ...state };
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,32 @@ const useAppStore = defineStore('tabBar', {
|
||||||
tagList: [DEFAULT_ROUTE],
|
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: {
|
getters: {
|
||||||
getTabList(): TagProps[] {
|
getTabList(): TagProps[] {
|
||||||
return this.tagList;
|
return this.tagList;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@ const useUserStore = defineStore('user', {
|
||||||
permission: [],
|
permission: [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 配置持久化
|
||||||
|
persist: {
|
||||||
|
key: 'user-store',
|
||||||
|
storage: localStorage,
|
||||||
|
// 只持久化关键的用户信息,排除敏感信息
|
||||||
|
pick: ['name', 'avatar', 'role', 'permission', 'accountId', 'email'],
|
||||||
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
userInfo(state: UserState): UserState {
|
userInfo(state: UserState): UserState {
|
||||||
return { ...state };
|
return { ...state };
|
||||||
|
|
@ -67,8 +75,22 @@ const useUserStore = defineStore('user', {
|
||||||
try {
|
try {
|
||||||
const res = await userLogin(loginForm);
|
const res = await userLogin(loginForm);
|
||||||
setToken(res.data.token);
|
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) {
|
} catch (err) {
|
||||||
clearToken();
|
clearToken();
|
||||||
throw err;
|
throw err;
|
||||||
|
|
@ -92,14 +114,17 @@ const useUserStore = defineStore('user', {
|
||||||
async getUserPermissionData() {
|
async getUserPermissionData() {
|
||||||
try {
|
try {
|
||||||
const res = await getUserPermission();
|
const res = await getUserPermission();
|
||||||
console.log(res, '权限数据');
|
|
||||||
|
|
||||||
// 将权限数据存储到store中
|
// 将权限数据存储到store中
|
||||||
if (res.data && res.data.length > 0) {
|
if (res.data && res.data.length > 0) {
|
||||||
this.setInfo({ permission: res.data[0].permission });
|
this.setInfo({ permission: res.data[0].permission });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// 生产环境下可以使用日志服务记录错误
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('获取权限失败:', err);
|
console.error('获取权限失败:', err);
|
||||||
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { UserPermission } from '@/api/user';
|
import { UserPermission } from '@/types/api';
|
||||||
|
|
||||||
export type RoleType = '' | '*' | 'admin' | 'user';
|
export type RoleType = '' | '*' | 'admin' | 'user';
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
|
|
|
||||||
75
src/types/api.ts
Normal file
75
src/types/api.ts
Normal file
|
|
@ -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; // 更新时间
|
||||||
|
}
|
||||||
75
src/types/business.ts
Normal file
75
src/types/business.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
const TOKEN_KEY = 'LEASER_TOKEN';
|
import { STORAGE_KEYS } from '@/constants';
|
||||||
|
|
||||||
const isLogin = () => {
|
const isLogin = (): boolean => {
|
||||||
return !!localStorage.getItem(TOKEN_KEY);
|
return !!localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getToken = () => {
|
const getToken = (): string | null => {
|
||||||
return localStorage.getItem(TOKEN_KEY);
|
return localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setToken = (token: string) => {
|
const setToken = (token: string): void => {
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearToken = () => {
|
const clearToken = (): void => {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.USER_INFO);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { isLogin, getToken, setToken, clearToken };
|
export { isLogin, getToken, setToken, clearToken };
|
||||||
|
|
|
||||||
113
src/utils/router-preload.ts
Normal file
113
src/utils/router-preload.ts
Normal file
|
|
@ -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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
<p class="title">{{ $t('menu.business.project') }}</p>
|
<p class="title">{{ $t('menu.business.project') }}</p>
|
||||||
<div class="flex_center_between">
|
<div class="flex_center_between">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-input-search :style="{ width: '320px' }" :placeholder="$t('search.placeholder')" />
|
<a-input-search
|
||||||
|
:style="{ width: '320px' }"
|
||||||
|
:placeholder="$t('search.placeholder')"
|
||||||
|
/>
|
||||||
<a-button>
|
<a-button>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-filter />
|
<icon-filter />
|
||||||
|
|
@ -30,13 +33,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<a-table row-key="id" :loading="loading" :pagination="pagination" :columns="projectColumn"
|
<a-table
|
||||||
:data="renderData" :bordered="false" @page-change="onPageChange">
|
row-key="id"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
:columns="projectColumn"
|
||||||
|
:data="renderData"
|
||||||
|
:bordered="false"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
>
|
||||||
<template #type="{ record }">
|
<template #type="{ record }">
|
||||||
<a-tag checkable :color="getTypeColor(record.type)">{{ getTypeText(record.type) }}</a-tag>
|
<a-tag checkable :color="getTypeColor(record.type)">{{
|
||||||
|
getTypeText(record.type)
|
||||||
|
}}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #rate="{ record }">
|
<template #rate="{ record }">
|
||||||
<a-progress :percent="record.rate" color="green" :style="{ width: '100px' }" />
|
<a-progress
|
||||||
|
:percent="record.rate"
|
||||||
|
color="green"
|
||||||
|
:style="{ width: '100px' }"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #operate>
|
<template #operate>
|
||||||
<a-space>
|
<a-space>
|
||||||
|
|
@ -49,16 +65,48 @@
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, reactive, watch, nextTick } from 'vue';
|
import { computed, ref, reactive, watch, nextTick } from 'vue';
|
||||||
import useLoading from '@/hooks/loading';
|
import useLoading from '@/hooks/loading';
|
||||||
import { Pagination } from '@/types/global';
|
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 { loading, setLoading } = useLoading(false);
|
||||||
const renderData = ref<any[]>([
|
const renderData = ref<ProjectRecord[]>([
|
||||||
{ 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: '星河商务中心',
|
||||||
{ name: '前海科学科学院', type: '3', size: '1520m²', kezu: '1500m²', rate: 0.91, count: '120', create_time: '2025-06-24 01:16' },
|
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 = {
|
const basePagination: Pagination = {
|
||||||
current: 1,
|
current: 1,
|
||||||
|
|
@ -79,19 +127,15 @@ const pagination = reactive({
|
||||||
...basePagination,
|
...basePagination,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTypeColor = (type: number | string) => {
|
const getTypeColor = (type: ProjectType): string => {
|
||||||
const projectMap: any = {
|
return PROJECT_TYPE_COLORS[type] || 'default';
|
||||||
1: 'orangered', 2: 'green', 3: 'gold'
|
};
|
||||||
}
|
|
||||||
return projectMap[type]
|
const getTypeText = (type: ProjectType): string => {
|
||||||
}
|
return PROJECT_TYPE_LABELS[type] || '未知';
|
||||||
const getTypeText = (type: number | string) => {
|
};
|
||||||
const projectMap: any = {
|
|
||||||
1: '商业综合体', 2: '写字楼', 3: '园区'
|
|
||||||
}
|
|
||||||
return projectMap[type]
|
|
||||||
}
|
|
||||||
const onPageChange = (current: number) => {
|
const onPageChange = (current: number) => {
|
||||||
console.log(current, 'current');
|
pagination.current = current;
|
||||||
|
// TODO: 这里应该调用API获取对应页码的数据
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="login-form-wrapper">
|
<div class="login-form-wrapper">
|
||||||
<div class="login-form-title">{{ $t('login.form.title') }}</div>
|
<div v-if="errorMessage" class="login-form-error-msg">
|
||||||
<div class="login-form-sub-title">{{ $t('login.form.slogan') }}</div>
|
<icon-exclamation-circle-fill />
|
||||||
<div class="login-form-error-msg">{{ errorMessage }}</div>
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
<a-form
|
<a-form
|
||||||
ref="loginForm"
|
ref="loginForm"
|
||||||
:model="userInfo"
|
:model="userInfo"
|
||||||
|
|
@ -15,51 +16,86 @@
|
||||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||||
:validate-trigger="['change', 'blur']"
|
:validate-trigger="['change', 'blur']"
|
||||||
hide-label
|
hide-label
|
||||||
|
class="form-item"
|
||||||
>
|
>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="userInfo.username"
|
v-model="userInfo.username"
|
||||||
:placeholder="$t('login.form.userName.placeholder')"
|
:placeholder="$t('login.form.userName.placeholder')"
|
||||||
|
class="input-field"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<icon-user />
|
<icon-user class="input-icon" />
|
||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item
|
<a-form-item
|
||||||
field="password"
|
field="password"
|
||||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||||
:validate-trigger="['change', 'blur']"
|
:validate-trigger="['change', 'blur']"
|
||||||
hide-label
|
hide-label
|
||||||
|
class="form-item"
|
||||||
>
|
>
|
||||||
<a-input-password
|
<a-input-password
|
||||||
v-model="userInfo.password"
|
v-model="userInfo.password"
|
||||||
:placeholder="$t('login.form.password.placeholder')"
|
:placeholder="$t('login.form.password.placeholder')"
|
||||||
allow-clear
|
allow-clear
|
||||||
|
class="input-field"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<icon-lock />
|
<icon-lock class="input-icon" />
|
||||||
</template>
|
</template>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
</a-form-item>
|
</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
|
<a-checkbox
|
||||||
checked="rememberPassword"
|
v-model="loginConfig.rememberPassword"
|
||||||
:model-value="loginConfig.rememberPassword"
|
class="remember-checkbox"
|
||||||
@change="setRememberPassword as any"
|
@change="setRememberPassword as any"
|
||||||
>
|
>
|
||||||
{{ $t('login.form.rememberPassword') }}
|
{{ $t('login.form.rememberPassword') }}
|
||||||
</a-checkbox>
|
</a-checkbox>
|
||||||
<a-link>{{ $t('login.form.forgetPassword') }}</a-link>
|
<a-link class="forgot-link">{{
|
||||||
|
$t('login.form.forgetPassword')
|
||||||
|
}}</a-link>
|
||||||
</div>
|
</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') }}
|
{{ $t('login.form.login') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<!-- <a-button type="text" long class="login-form-register-btn">
|
</div>
|
||||||
{{ $t('login.form.register') }}
|
|
||||||
</a-button> -->
|
|
||||||
</a-space>
|
|
||||||
</a-form>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -82,8 +118,8 @@
|
||||||
|
|
||||||
const loginConfig = useStorage('login-config', {
|
const loginConfig = useStorage('login-config', {
|
||||||
rememberPassword: true,
|
rememberPassword: true,
|
||||||
username: 'admin', // 演示默认值
|
username: '13666666666', // 演示默认值
|
||||||
password: 'admin', // demo default value
|
password: '13666666666', // demo default value
|
||||||
});
|
});
|
||||||
const userInfo = reactive({
|
const userInfo = reactive({
|
||||||
username: loginConfig.value.username,
|
username: loginConfig.value.username,
|
||||||
|
|
@ -129,39 +165,154 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<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 {
|
.login-form {
|
||||||
&-wrapper {
|
.form-item {
|
||||||
width: 320px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-title {
|
.input-field {
|
||||||
color: var(--color-text-1);
|
:deep(.arco-input-wrapper) {
|
||||||
font-weight: 500;
|
border-radius: 6px;
|
||||||
font-size: 24px;
|
transition: all 0.2s ease;
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
&:hover {
|
||||||
|
border-color: var(--color-primary-light-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-sub-title {
|
&.arco-input-focus {
|
||||||
|
border-color: var(--color-primary-6);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-primary-light-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
}
|
||||||
text-align: center;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error-msg {
|
.form-actions {
|
||||||
height: 32px;
|
.form-options {
|
||||||
color: rgb(var(--red-6));
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-password-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.remember-checkbox {
|
||||||
|
:deep(.arco-checkbox-label) {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-register-btn {
|
.forgot-link {
|
||||||
color: var(--color-text-3) !important;
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,197 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="login-container">
|
||||||
<!-- <div class="logo">
|
<!-- 背景装饰 -->
|
||||||
<img
|
<div class="bg-pattern"></div>
|
||||||
alt="logo"
|
|
||||||
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
|
<!-- 主要内容区域 -->
|
||||||
|
<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)"
|
||||||
/>
|
/>
|
||||||
<div class="logo-text">Arco Design Pro</div>
|
<path
|
||||||
</div> -->
|
d="M14 24L22 32L34 20"
|
||||||
<!-- <LoginBanner /> -->
|
stroke="white"
|
||||||
<div class="content">
|
stroke-width="3"
|
||||||
<div class="content-inner">
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="logo-title">租赁管理系统</h1>
|
||||||
|
<p class="logo-subtitle">简单高效的房屋租赁管理平台</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单卡片 -->
|
||||||
|
<div class="login-card">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<div class="footer-wrapper">
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import Footer from '@/components/footer/index.vue';
|
import Footer from '@/components/footer/index.vue';
|
||||||
// import LoginBanner from './components/banner.vue';
|
|
||||||
import LoginForm from './components/login-form.vue';
|
import LoginForm from './components/login-form.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.container {
|
.login-container {
|
||||||
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;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.content-inner{
|
min-height: 100vh;
|
||||||
background-color: #fff;
|
padding: 20px;
|
||||||
padding: 60px 40px;
|
background-color: var(--color-bg-2);
|
||||||
border-radius: 10px;
|
overflow: hidden;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
// 简化的背景装饰
|
||||||
|
.bg-pattern {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
top: 0;
|
||||||
bottom: 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%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
.logo-title {
|
||||||
position: fixed;
|
margin: 0 0 8px 0;
|
||||||
top: 24px;
|
color: var(--color-text-1);
|
||||||
left: 22px;
|
font-size: 28px;
|
||||||
z-index: 1;
|
font-weight: 600;
|
||||||
display: inline-flex;
|
letter-spacing: -0.5px;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
&-text {
|
.logo-subtitle {
|
||||||
margin-right: 4px;
|
margin: 0;
|
||||||
margin-left: 4px;
|
color: var(--color-text-3);
|
||||||
color: var(--color-fill-1);
|
font-size: 16px;
|
||||||
font-size: 20px;
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
// 登录卡片
|
||||||
// responsive
|
.login-card {
|
||||||
@media (max-width: @screen-lg) {
|
width: 100%;
|
||||||
.container {
|
background: var(--color-bg-1);
|
||||||
.banner {
|
border-radius: 12px;
|
||||||
width: 25%;
|
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,
|
"skipLibCheck": true,
|
||||||
"types": [
|
"types": [
|
||||||
"vite/client",
|
"vite/client",
|
||||||
"vite-plugin-svg-icons/client",
|
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue