feat: 升级stylelint依赖并修复相关问题
- 更新stylelint到16.x版本 - 修复postcss版本冲突 - 解决循环依赖问题 - 优化代码类型定义
This commit is contained in:
+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 });
|
||||
|
||||
// 确保左侧菜单能正确识别当前路由
|
||||
// 延迟一下,确保菜单数据更新后再触发路由监听
|
||||
setTimeout(() => {
|
||||
// 强制触发菜单组件的路由监听
|
||||
const currentRoute = router.currentRoute.value;
|
||||
if (currentRoute.name) {
|
||||
// 这里可以添加一些调试信息
|
||||
console.log('Current route:', currentRoute.name, 'Path:', currentRoute.path);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, { immediate: true });
|
||||
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) {
|
||||
// 路由切换完成,菜单应该能正确识别当前路由
|
||||
}
|
||||
}, 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');
|
||||
};
|
||||
</script>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user