登录权限
This commit is contained in:
parent
53bd72b37e
commit
a51e8e8d66
File diff suppressed because it is too large
Load Diff
|
|
@ -9,14 +9,18 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"cache-loader": "^4.1.0",
|
||||
"element-ui": "^2.15.6",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1"
|
||||
"vue-loader": "^15.11.1",
|
||||
"vue-router": "^3.5.1",
|
||||
"webpack": "^4.47.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-eslint": "^5.0.9",
|
||||
"@vue/cli-plugin-router": "^5.0.9",
|
||||
"@vue/cli-service": "^5.0.9",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^7.14.0"
|
||||
|
|
|
|||
377
src/App.vue
377
src/App.vue
|
|
@ -1,209 +1,12 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<el-container>
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header height="60px" class="app-header">
|
||||
<div class="header-left">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<el-button
|
||||
v-if="isMobile"
|
||||
type="text"
|
||||
class="menu-toggle-btn"
|
||||
@click="drawerVisible = true"
|
||||
>
|
||||
<i class="el-icon-s-fold" style="font-size: 24px; color: white;"></i>
|
||||
</el-button>
|
||||
<h1 class="app-title">租房管理系统</h1>
|
||||
</div>
|
||||
<el-button type="primary" plain size="small" @click="logout">退出</el-button>
|
||||
</el-header>
|
||||
|
||||
<el-container>
|
||||
<!-- PC端侧边栏 -->
|
||||
<el-aside
|
||||
v-if="!isMobile"
|
||||
width="200px"
|
||||
class="app-aside"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleSelect"
|
||||
background-color="#f0f2f5"
|
||||
text-color="#333"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<el-menu-item index="dashboard">
|
||||
<i class="el-icon-s-home"></i>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="region-list">
|
||||
<i class="el-icon-location"></i>
|
||||
<span>区域管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="apartment-list">
|
||||
<i class="el-icon-office-building"></i>
|
||||
<span>公寓管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="room-list">
|
||||
<i class="el-icon-menu"></i>
|
||||
<span>房间管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rental-list">
|
||||
<i class="el-icon-key"></i>
|
||||
<span>租房管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rental-archive">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>租赁档案</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="water-archive">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>水费档案</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rent-statistics">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<span>租金统计</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="room-statistics">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<span>房间状态统计</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-main class="app-main">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 移动端侧边栏抽屉 -->
|
||||
<el-drawer
|
||||
:visible.sync="drawerVisible"
|
||||
:with-header="false"
|
||||
:size="drawerWidth"
|
||||
direction="ltr"
|
||||
class="mobile-drawer"
|
||||
>
|
||||
<div class="drawer-header">
|
||||
<h3>菜单</h3>
|
||||
<el-button type="text" @click="drawerVisible = false">
|
||||
<i class="el-icon-close" style="font-size: 20px;"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleMobileSelect"
|
||||
background-color="#fff"
|
||||
text-color="#333"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<el-menu-item index="dashboard">
|
||||
<i class="el-icon-s-home"></i>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="region-list">
|
||||
<i class="el-icon-location"></i>
|
||||
<span>区域管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="apartment-list">
|
||||
<i class="el-icon-office-building"></i>
|
||||
<span>公寓管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="room-list">
|
||||
<i class="el-icon-menu"></i>
|
||||
<span>房间管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rental-list">
|
||||
<i class="el-icon-key"></i>
|
||||
<span>租房管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rental-archive">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>租赁档案</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="water-archive">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>水费档案</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rent-statistics">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<span>租金统计</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="room-statistics">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
<span>房间状态统计</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
activeIndex: 'dashboard',
|
||||
isMobile: false,
|
||||
drawerVisible: false,
|
||||
drawerWidth: '70%'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkDevice()
|
||||
window.addEventListener('resize', this.checkDevice)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkDevice)
|
||||
},
|
||||
methods: {
|
||||
checkDevice() {
|
||||
const width = window.innerWidth
|
||||
this.isMobile = width <= 768
|
||||
// 根据屏幕宽度调整抽屉大小
|
||||
if (width <= 375) {
|
||||
this.drawerWidth = '80%'
|
||||
} else if (width <= 768) {
|
||||
this.drawerWidth = '70%'
|
||||
}
|
||||
},
|
||||
handleSelect(key, keyPath) {
|
||||
this.activeIndex = key
|
||||
this.navigateToRoute(key)
|
||||
},
|
||||
handleMobileSelect(key, keyPath) {
|
||||
this.activeIndex = key
|
||||
this.drawerVisible = false
|
||||
this.navigateToRoute(key)
|
||||
},
|
||||
navigateToRoute(key) {
|
||||
const routeMap = {
|
||||
'dashboard': '/',
|
||||
'region-list': '/region/list',
|
||||
'region-add': '/region/add',
|
||||
'apartment-list': '/apartment/list',
|
||||
'apartment-add': '/apartment/add',
|
||||
'room-list': '/room/list',
|
||||
'room-add': '/room/add',
|
||||
'rental-list': '/rental/list',
|
||||
'rental-add': '/rental/add',
|
||||
'rental-archive': '/rental/archive',
|
||||
'water-archive': '/water/archive',
|
||||
'rent-statistics': '/statistics/rent',
|
||||
'room-statistics': '/statistics/room'
|
||||
}
|
||||
if (routeMap[key] && this.$route.path !== routeMap[key]) {
|
||||
this.$router.push(routeMap[key])
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.$message.success('退出成功')
|
||||
}
|
||||
}
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -220,180 +23,4 @@ export default {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
.app-header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.menu-toggle-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.app-aside {
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* 主内容区样式 */
|
||||
.app-main {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 移动端抽屉样式 */
|
||||
.mobile-drawer .el-drawer__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.drawer-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.app-header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 表格横向滚动 */
|
||||
.el-table {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 表单元素适配 */
|
||||
.el-form-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding: 0 0 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 搜索表单适配 */
|
||||
.search-form .el-form-item {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-form .el-form-item__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-form .el-input,
|
||||
.search-form .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 卡片适配 */
|
||||
.el-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* 分页适配 */
|
||||
.el-pagination {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.el-pagination .el-pagination__total,
|
||||
.el-pagination .el-pagination__sizes,
|
||||
.el-pagination .el-pagination__jump {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 按钮组适配 */
|
||||
.el-button + .el-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* 对话框适配 */
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
margin-top: 10vh !important;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕手机适配 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.app-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
// API服务层,用于与后端进行交互
|
||||
import { get, post, put, del } from './request';
|
||||
|
||||
// 区域管理API
|
||||
export const regionApi = {
|
||||
getAll: () => get('/regions'),
|
||||
list: (params = {}) => get('/regions/list', params),
|
||||
getById: (id) => get(`/regions/${id}`),
|
||||
create: (data) => post('/regions', data),
|
||||
update: (id, data) => put(`/regions/${id}`, data),
|
||||
delete: (id) => del(`/regions/${id}`)
|
||||
};
|
||||
|
||||
// 公寓管理API
|
||||
export const apartmentApi = {
|
||||
getAll: (params = {}) => get('/apartments', params),
|
||||
|
|
@ -66,7 +56,8 @@ export const statisticsApi = {
|
|||
getRoomStatus: () => get('/statistics/room-status'),
|
||||
getRegionHouseStats: () => get('/statistics/region-house'),
|
||||
getRegionApartmentHouseStats: () => get('/statistics/region-apartment-house'),
|
||||
getDashboardStats: () => get('/statistics/dashboard')
|
||||
getDashboardStats: () => get('/statistics/dashboard'),
|
||||
getApartmentRoomStatusStats: () => get('/statistics/apartment-room-status')
|
||||
};
|
||||
|
||||
// 水费管理API
|
||||
|
|
@ -89,7 +80,6 @@ export const electricityBillApi = {
|
|||
};
|
||||
|
||||
export default {
|
||||
region: regionApi,
|
||||
apartment: apartmentApi,
|
||||
room: roomApi,
|
||||
tenant: tenantApi,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { post, get } from './request'
|
||||
|
||||
// 登录
|
||||
export function login(data) {
|
||||
return post('/auth/login', data)
|
||||
}
|
||||
|
||||
// 登出
|
||||
export function logout() {
|
||||
return post('/auth/logout')
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
export function getCurrentUser() {
|
||||
return get('/auth/user')
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function changePassword(data) {
|
||||
return post('/auth/change-password', data)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { get, post } from './request'
|
||||
|
||||
// 获取操作日志列表
|
||||
export function getOperationLogs(params) {
|
||||
return get('/logs/operation', params)
|
||||
}
|
||||
|
||||
// 获取登录日志列表
|
||||
export function getLoginLogs(params) {
|
||||
return get('/logs/login', params)
|
||||
}
|
||||
|
||||
// 清空操作日志
|
||||
export function clearOperationLogs(data) {
|
||||
return post('/logs/operation/clear', data)
|
||||
}
|
||||
|
||||
// 清空登录日志
|
||||
export function clearLoginLogs(data) {
|
||||
return post('/logs/login/clear', data)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { get, post, put, del } from './request'
|
||||
|
||||
// 获取菜单树
|
||||
export function getMenuTree(params) {
|
||||
return get('/menus/tree', params)
|
||||
}
|
||||
|
||||
// 获取菜单列表
|
||||
export function getMenuList(params) {
|
||||
return get('/menus', params)
|
||||
}
|
||||
|
||||
// 获取菜单详情
|
||||
export function getMenuById(id) {
|
||||
return get(`/menus/${id}`)
|
||||
}
|
||||
|
||||
// 创建菜单
|
||||
export function createMenu(data) {
|
||||
return post('/menus', data)
|
||||
}
|
||||
|
||||
// 更新菜单
|
||||
export function updateMenu(id, data) {
|
||||
return put(`/menus/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
export function deleteMenu(id) {
|
||||
return del(`/menus/${id}`)
|
||||
}
|
||||
|
||||
// 获取角色的菜单权限
|
||||
export function getRoleMenus(roleId) {
|
||||
return get(`/menus/role/${roleId}`)
|
||||
}
|
||||
|
||||
// 分配菜单权限给角色
|
||||
export function assignMenusToRole(roleId, data) {
|
||||
return post(`/menus/role/${roleId}/assign`, data)
|
||||
}
|
||||
|
||||
// 获取当前用户的菜单权限
|
||||
export function getUserMenus() {
|
||||
return get('/menus/user/menus')
|
||||
}
|
||||
|
|
@ -1,18 +1,64 @@
|
|||
// 统一请求处理
|
||||
import { getToken, isTokenExpired, clearAuth } from '@/utils/auth'
|
||||
import router from '@/router'
|
||||
|
||||
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || '/api';
|
||||
|
||||
// 不需要认证的接口列表
|
||||
const publicUrls = ['/auth/login']
|
||||
|
||||
// 检查是否为公开接口
|
||||
function isPublicUrl(url) {
|
||||
return publicUrls.some(publicUrl => url.includes(publicUrl))
|
||||
}
|
||||
|
||||
// 通用请求函数
|
||||
export async function request(url, options = {}) {
|
||||
try {
|
||||
const isPublic = isPublicUrl(url)
|
||||
|
||||
// 检查 Token 是否过期(公开接口跳过)
|
||||
if (!isPublic && isTokenExpired()) {
|
||||
clearAuth()
|
||||
// 避免重复导航
|
||||
if (router.currentRoute.path !== '/login') {
|
||||
router.push('/login')
|
||||
}
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
// 获取 Token 并添加到请求头
|
||||
const token = getToken()
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
headers
|
||||
});
|
||||
|
||||
// 处理 401 未授权
|
||||
if (response.status === 401) {
|
||||
const errorData = await response.json();
|
||||
// 检查是否是认证失败(如用户名密码错误)
|
||||
if (errorData.code === 401 && errorData.message) {
|
||||
throw new Error(errorData.message);
|
||||
}
|
||||
// 其他 401 情况(如 token 过期)
|
||||
clearAuth()
|
||||
// 避免重复导航
|
||||
if (router.currentRoute.path !== '/login') {
|
||||
router.push('/login')
|
||||
}
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { get, post, put, del } from './request'
|
||||
|
||||
// 获取角色列表
|
||||
export function getRoles(params) {
|
||||
return get('/roles', params)
|
||||
}
|
||||
|
||||
// 获取角色详情
|
||||
export function getRoleById(id) {
|
||||
return get(`/roles/${id}`)
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
export function createRole(data) {
|
||||
return post('/roles', data)
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
export function updateRole(id, data) {
|
||||
return put(`/roles/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
export function deleteRole(id) {
|
||||
return del(`/roles/${id}`)
|
||||
}
|
||||
|
||||
// 获取所有角色(用于下拉选择)
|
||||
export function getAllRoles() {
|
||||
return get('/roles/all/list')
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { get, post, put, del } from './request'
|
||||
|
||||
// 获取用户列表
|
||||
export function getUserList(params) {
|
||||
return get('/users', params)
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
export function getUserInfo() {
|
||||
return get('/users/info')
|
||||
}
|
||||
|
||||
// 更新个人资料
|
||||
export function updateUserProfile(data) {
|
||||
return put('/users/profile', data)
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function changePassword(data) {
|
||||
return post('/users/change-password', data)
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export function createUser(data) {
|
||||
return post('/users', data)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export function updateUser(id, data) {
|
||||
return put(`/users/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export function deleteUser(id) {
|
||||
return del(`/users/${id}`)
|
||||
}
|
||||
|
||||
// 重置用户密码
|
||||
export function resetUserPassword(id) {
|
||||
return post(`/users/${id}/reset-password`)
|
||||
}
|
||||
|
||||
// 获取用户列表(用于下拉选择)
|
||||
export function getAllUsers() {
|
||||
return get('/users/all/list')
|
||||
}
|
||||
|
||||
// 获取所有角色(用于下拉选择)
|
||||
export function getAllRoles() {
|
||||
return get('/roles/all/list')
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div class="auth-layout">
|
||||
<div class="auth-container">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">租房管理系统</h1>
|
||||
<p class="auth-subtitle">欢迎登录</p>
|
||||
</div>
|
||||
<div class="auth-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<p>© 2026 租房管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AuthLayout'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.auth-layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 30px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-container {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<el-container>
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header height="60px" class="app-header">
|
||||
<div class="header-left">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<el-button
|
||||
v-if="isMobile"
|
||||
type="text"
|
||||
class="menu-toggle-btn"
|
||||
@click="drawerVisible = true"
|
||||
>
|
||||
<i class="el-icon-s-fold" style="font-size: 24px; color: white;"></i>
|
||||
</el-button>
|
||||
<h1 class="app-title">租房管理系统</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleUserCommand">
|
||||
<span class="user-info">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
{{ username }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="password">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-container>
|
||||
<!-- PC端侧边栏 -->
|
||||
<el-aside
|
||||
v-if="!isMobile"
|
||||
width="200px"
|
||||
class="app-aside"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleSelect"
|
||||
background-color="#f0f2f5"
|
||||
text-color="#333"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<template v-for="menu in menuList">
|
||||
<!-- 有子菜单 -->
|
||||
<el-submenu v-if="menu.children && menu.children.length > 0" :key="menu.id" :index="menu.code">
|
||||
<template slot="title">
|
||||
<i :class="menu.icon || 'el-icon-menu'"></i>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.id"
|
||||
:index="child.code"
|
||||
>
|
||||
<i :class="child.icon || 'el-icon-document'"></i>
|
||||
<span>{{ child.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
<!-- 没有子菜单 -->
|
||||
<el-menu-item v-else :key="menu.id" :index="menu.code">
|
||||
<i :class="menu.icon || 'el-icon-document'"></i>
|
||||
<span>{{ menu.name }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-main class="app-main">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 移动端侧边栏抽屉 -->
|
||||
<el-drawer
|
||||
:visible.sync="drawerVisible"
|
||||
:with-header="false"
|
||||
:size="drawerWidth"
|
||||
direction="ltr"
|
||||
class="mobile-drawer"
|
||||
>
|
||||
<div class="drawer-header">
|
||||
<h3>菜单</h3>
|
||||
<el-button type="text" @click="drawerVisible = false">
|
||||
<i class="el-icon-close" style="font-size: 20px;"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleMobileSelect"
|
||||
background-color="#fff"
|
||||
text-color="#333"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<template v-for="menu in menuList">
|
||||
<!-- 有子菜单 -->
|
||||
<el-submenu v-if="menu.children && menu.children.length > 0" :key="menu.id" :index="menu.code">
|
||||
<template slot="title">
|
||||
<i :class="menu.icon || 'el-icon-menu'"></i>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in menu.children"
|
||||
:key="child.id"
|
||||
:index="child.code"
|
||||
>
|
||||
<i :class="child.icon || 'el-icon-document'"></i>
|
||||
<span>{{ child.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
<!-- 没有子菜单 -->
|
||||
<el-menu-item v-else :key="menu.id" :index="menu.code">
|
||||
<i :class="menu.icon || 'el-icon-document'"></i>
|
||||
<span>{{ menu.name }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo, getUserMenus, clearAuth } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'MainLayout',
|
||||
data() {
|
||||
return {
|
||||
activeIndex: '',
|
||||
isMobile: false,
|
||||
drawerVisible: false,
|
||||
drawerWidth: '70%',
|
||||
username: '',
|
||||
menuList: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkDevice()
|
||||
window.addEventListener('resize', this.checkDevice)
|
||||
this.loadUserInfo()
|
||||
this.loadMenus()
|
||||
this.setActiveMenu()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkDevice)
|
||||
},
|
||||
watch: {
|
||||
'$route': 'setActiveMenu'
|
||||
},
|
||||
methods: {
|
||||
checkDevice() {
|
||||
const width = window.innerWidth
|
||||
this.isMobile = width <= 768
|
||||
// 根据屏幕宽度调整抽屉大小
|
||||
if (width <= 375) {
|
||||
this.drawerWidth = '80%'
|
||||
} else if (width <= 768) {
|
||||
this.drawerWidth = '70%'
|
||||
}
|
||||
},
|
||||
loadUserInfo() {
|
||||
const userInfo = getUserInfo()
|
||||
if (userInfo) {
|
||||
this.username = userInfo.username || '用户'
|
||||
}
|
||||
},
|
||||
loadMenus() {
|
||||
// 从本地存储获取菜单,并过滤掉按钮类型的菜单
|
||||
const menus = getUserMenus()
|
||||
this.menuList = this.filterButtons(menus)
|
||||
},
|
||||
filterButtons(menus) {
|
||||
// 递归过滤掉按钮类型的菜单
|
||||
return menus.filter(menu => menu.type !== 'button').map(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
return {
|
||||
...menu,
|
||||
children: this.filterButtons(menu.children)
|
||||
}
|
||||
}
|
||||
return menu
|
||||
})
|
||||
},
|
||||
setActiveMenu() {
|
||||
// 根据当前路由设置激活的菜单
|
||||
const path = this.$route.path
|
||||
const menu = this.findMenuByPath(this.menuList, path)
|
||||
if (menu) {
|
||||
this.activeIndex = menu.code
|
||||
}
|
||||
},
|
||||
findMenuByPath(menus, path) {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = this.findMenuByPath(menu.children, path)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
handleSelect(key, keyPath) {
|
||||
this.activeIndex = key
|
||||
this.navigateToRoute(key)
|
||||
},
|
||||
handleMobileSelect(key, keyPath) {
|
||||
this.activeIndex = key
|
||||
this.drawerVisible = false
|
||||
this.navigateToRoute(key)
|
||||
},
|
||||
navigateToRoute(key) {
|
||||
// 在菜单列表中查找对应的菜单项
|
||||
const menu = this.findMenuByCode(this.menuList, key)
|
||||
if (menu && menu.path && this.$route.path !== menu.path) {
|
||||
this.$router.push(menu.path)
|
||||
}
|
||||
},
|
||||
findMenuByCode(menus, code) {
|
||||
for (const menu of menus) {
|
||||
if (menu.code === code) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = this.findMenuByCode(menu.children, code)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
handleUserCommand(command) {
|
||||
switch (command) {
|
||||
case 'profile':
|
||||
this.$router.push('/user/profile')
|
||||
break
|
||||
case 'password':
|
||||
this.$router.push('/user/profile?tab=password')
|
||||
break
|
||||
case 'logout':
|
||||
this.logout()
|
||||
break
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
clearAuth()
|
||||
this.$message.success('退出成功')
|
||||
this.$router.push('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 顶部导航栏样式 */
|
||||
.app-header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.user-info .el-icon-arrow-down {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
.el-dropdown-menu {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.menu-toggle-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.app-aside {
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 主内容区样式 */
|
||||
.app-main {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 抽屉头部样式 */
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.drawer-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,103 +1,147 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
// 登录路由 - 使用 AuthLayout
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/layouts/AuthLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/auth/Login.vue'),
|
||||
meta: { public: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 系统路由 - 使用 MainLayout
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/Dashboard.vue')
|
||||
component: () => import('@/layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue')
|
||||
},
|
||||
// 公寓管理
|
||||
{
|
||||
path: '/apartment/list',
|
||||
name: 'ApartmentList',
|
||||
component: () => import('@/views/apartment/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/apartment/add',
|
||||
name: 'ApartmentAdd',
|
||||
component: () => import('@/views/apartment/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/apartment/edit/:id',
|
||||
name: 'ApartmentEdit',
|
||||
component: () => import('@/views/apartment/Edit.vue')
|
||||
},
|
||||
// 房间管理
|
||||
{
|
||||
path: '/room/list',
|
||||
name: 'RoomList',
|
||||
component: () => import('@/views/room/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/room/add',
|
||||
name: 'RoomAdd',
|
||||
component: () => import('@/views/room/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/room/edit/:id',
|
||||
name: 'RoomEdit',
|
||||
component: () => import('@/views/room/Edit.vue')
|
||||
},
|
||||
// 租房管理
|
||||
{
|
||||
path: '/rental/list',
|
||||
name: 'RentalList',
|
||||
component: () => import('@/views/rental/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/add',
|
||||
name: 'RentalAdd',
|
||||
component: () => import('@/views/rental/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/edit/:id',
|
||||
name: 'RentalEdit',
|
||||
component: () => import('@/views/rental/Edit.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/detail/:id',
|
||||
name: 'RentalDetail',
|
||||
component: () => import('@/views/rental/Detail.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/archive',
|
||||
name: 'RentalArchive',
|
||||
component: () => import('@/views/rental/RentalArchive.vue')
|
||||
},
|
||||
{
|
||||
path: '/water/archive',
|
||||
name: 'WaterArchive',
|
||||
component: () => import('@/views/rental/WaterArchive.vue')
|
||||
},
|
||||
// 统计分析
|
||||
{
|
||||
path: '/statistics/rent',
|
||||
name: 'RentStatistics',
|
||||
component: () => import('@/views/statistics/Rent.vue')
|
||||
},
|
||||
{
|
||||
path: '/statistics/room',
|
||||
name: 'RoomStatistics',
|
||||
component: () => import('@/views/statistics/House.vue')
|
||||
},
|
||||
// 用户管理
|
||||
{
|
||||
path: '/user/list',
|
||||
name: 'UserList',
|
||||
component: () => import('@/views/user/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/user/profile',
|
||||
name: 'UserProfile',
|
||||
component: () => import('@/views/user/Profile.vue')
|
||||
},
|
||||
// 日志管理
|
||||
{
|
||||
path: '/log/operation',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/log/OperationLog.vue')
|
||||
},
|
||||
{
|
||||
path: '/log/login',
|
||||
name: 'LoginLog',
|
||||
component: () => import('@/views/log/LoginLog.vue')
|
||||
},
|
||||
// 角色管理
|
||||
{
|
||||
path: '/role/list',
|
||||
name: 'RoleList',
|
||||
component: () => import('@/views/role/List.vue')
|
||||
},
|
||||
// 菜单管理
|
||||
{
|
||||
path: '/menu/list',
|
||||
name: 'MenuList',
|
||||
component: () => import('@/views/menu/List.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
// 区域管理
|
||||
// 404 页面
|
||||
{
|
||||
path: '/region/list',
|
||||
name: 'RegionList',
|
||||
component: () => import('../views/region/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/region/add',
|
||||
name: 'RegionAdd',
|
||||
component: () => import('../views/region/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/region/edit/:id',
|
||||
name: 'RegionEdit',
|
||||
component: () => import('../views/region/Edit.vue')
|
||||
},
|
||||
// 公寓管理
|
||||
{
|
||||
path: '/apartment/list',
|
||||
name: 'ApartmentList',
|
||||
component: () => import('../views/apartment/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/apartment/add',
|
||||
name: 'ApartmentAdd',
|
||||
component: () => import('../views/apartment/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/apartment/edit/:id',
|
||||
name: 'ApartmentEdit',
|
||||
component: () => import('../views/apartment/Edit.vue')
|
||||
},
|
||||
// 房间管理
|
||||
{
|
||||
path: '/room/list',
|
||||
name: 'RoomList',
|
||||
component: () => import('../views/room/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/room/add',
|
||||
name: 'RoomAdd',
|
||||
component: () => import('../views/room/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/room/edit/:id',
|
||||
name: 'RoomEdit',
|
||||
component: () => import('../views/room/Edit.vue')
|
||||
},
|
||||
// 租房管理
|
||||
{
|
||||
path: '/rental/list',
|
||||
name: 'RentalList',
|
||||
component: () => import('../views/rental/List.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/add',
|
||||
name: 'RentalAdd',
|
||||
component: () => import('../views/rental/Add.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/edit/:id',
|
||||
name: 'RentalEdit',
|
||||
component: () => import('../views/rental/Edit.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/detail/:id',
|
||||
name: 'RentalDetail',
|
||||
component: () => import('../views/rental/Detail.vue')
|
||||
},
|
||||
{
|
||||
path: '/rental/archive',
|
||||
name: 'RentalArchive',
|
||||
component: () => import('../views/rental/RentalArchive.vue')
|
||||
},
|
||||
{
|
||||
path: '/water/archive',
|
||||
name: 'WaterArchive',
|
||||
component: () => import('../views/rental/WaterArchive.vue')
|
||||
},
|
||||
// 统计分析
|
||||
{
|
||||
path: '/statistics/rent',
|
||||
name: 'RentStatistics',
|
||||
component: () => import('../views/statistics/Rent.vue')
|
||||
},
|
||||
{
|
||||
path: '/statistics/room',
|
||||
name: 'RoomStatistics',
|
||||
component: () => import('../views/statistics/House.vue')
|
||||
path: '*',
|
||||
redirect: '/'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -107,4 +151,20 @@ const router = new VueRouter({
|
|||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 检查路由是否需要认证
|
||||
const isPublicRoute = to.matched.some(record => record.meta.public)
|
||||
|
||||
if (!isPublicRoute && !isAuthenticated()) {
|
||||
// 未登录且访问需要认证的页面,跳转到登录页
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && isAuthenticated()) {
|
||||
// 已登录但访问登录页,跳转到首页
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
// Token 过期时间(2小时 = 7200000毫秒)
|
||||
const TOKEN_EXPIRE_TIME = 2 * 60 * 60 * 1000
|
||||
|
||||
const TOKEN_KEY = 'rentease_token'
|
||||
const USER_INFO_KEY = 'rentease_user_info'
|
||||
const USER_MENUS_KEY = 'rentease_user_menus'
|
||||
const TOKEN_TIME_KEY = 'rentease_token_time'
|
||||
|
||||
// 设置登录信息
|
||||
export function setAuth(token, userInfo, menus) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo))
|
||||
localStorage.setItem(USER_MENUS_KEY, JSON.stringify(menus || []))
|
||||
localStorage.setItem(TOKEN_TIME_KEY, Date.now().toString())
|
||||
}
|
||||
|
||||
// 获取 Token
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
const userInfoStr = localStorage.getItem(USER_INFO_KEY)
|
||||
try {
|
||||
return userInfoStr ? JSON.parse(userInfoStr) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 Token 是否过期
|
||||
export function isTokenExpired() {
|
||||
const tokenTime = localStorage.getItem(TOKEN_TIME_KEY)
|
||||
if (!tokenTime) return true
|
||||
|
||||
const elapsed = Date.now() - parseInt(tokenTime)
|
||||
return elapsed > TOKEN_EXPIRE_TIME
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
export function isAuthenticated() {
|
||||
const token = getToken()
|
||||
if (!token) return false
|
||||
if (isTokenExpired()) {
|
||||
clearAuth()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 清除登录信息
|
||||
export function clearAuth() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_INFO_KEY)
|
||||
localStorage.removeItem(USER_MENUS_KEY)
|
||||
localStorage.removeItem(TOKEN_TIME_KEY)
|
||||
}
|
||||
|
||||
// 获取用户菜单
|
||||
export function getUserMenus() {
|
||||
const menusStr = localStorage.getItem(USER_MENUS_KEY)
|
||||
try {
|
||||
return menusStr ? JSON.parse(menusStr) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户菜单
|
||||
export function setUserMenus(menus) {
|
||||
localStorage.setItem(USER_MENUS_KEY, JSON.stringify(menus || []))
|
||||
}
|
||||
|
||||
// 获取剩余有效时间(毫秒)
|
||||
export function getTokenRemainingTime() {
|
||||
const tokenTime = localStorage.getItem(TOKEN_TIME_KEY)
|
||||
if (!tokenTime) return 0
|
||||
|
||||
const elapsed = Date.now() - parseInt(tokenTime)
|
||||
const remaining = TOKEN_EXPIRE_TIME - elapsed
|
||||
return remaining > 0 ? remaining : 0
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { getUserMenus } from './auth'
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限
|
||||
* @param {string} permission - 权限代码
|
||||
* @returns {boolean} - 是否有权限
|
||||
*/
|
||||
export function hasPermission(permission) {
|
||||
const menus = getUserMenus()
|
||||
return checkPermission(menus, permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归检查权限
|
||||
* @param {Array} menus - 菜单列表
|
||||
* @param {string} permission - 权限代码
|
||||
* @returns {boolean} - 是否有权限
|
||||
*/
|
||||
function checkPermission(menus, permission) {
|
||||
for (const menu of menus) {
|
||||
if (menu.code === permission) {
|
||||
return true
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
if (checkPermission(menu.children, permission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否为管理员
|
||||
* @returns {boolean} - 是否为管理员
|
||||
*/
|
||||
export function isAdmin() {
|
||||
const userInfo = localStorage.getItem('rentease_user_info')
|
||||
try {
|
||||
const user = userInfo ? JSON.parse(userInfo) : null
|
||||
return user && (user.isSuperAdmin === 1 || (user.role && user.role.code === 'admin'))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -2,21 +2,10 @@
|
|||
<div class="dashboard">
|
||||
<el-card class="welcome-card">
|
||||
<h2>欢迎使用租房管理系统</h2>
|
||||
<p>本系统提供区域管理、房源管理、租房管理和统计分析等功能</p>
|
||||
<p>本系统提供房源管理、租房管理和统计分析等功能</p>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :xs="12" :sm="12" :md="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<i class="el-icon-location stat-icon"></i>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ regionCount }}</div>
|
||||
<div class="stat-label">区域数量</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
|
|
@ -122,18 +111,17 @@
|
|||
<el-card class="stats-table-card">
|
||||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>区域公寓房间状态分布</span>
|
||||
<span>公寓房间状态分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="table-wrapper">
|
||||
<el-table
|
||||
:data="regionApartmentHouseStats"
|
||||
:data="apartmentHouseStats"
|
||||
style="width: 100%"
|
||||
show-summary
|
||||
:summary-method="getSummary"
|
||||
class="stats-table"
|
||||
>
|
||||
<el-table-column prop="region" label="区域" min-width="100"></el-table-column>
|
||||
<el-table-column prop="apartment" label="公寓" min-width="100"></el-table-column>
|
||||
<el-table-column prop="empty" label="空房" width="70"></el-table-column>
|
||||
<el-table-column prop="reserved" label="预订" width="70"></el-table-column>
|
||||
|
|
@ -150,13 +138,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { regionApi, apartmentApi, roomApi, tenantApi, contractApi, statisticsApi } from '../api/api'
|
||||
import { apartmentApi, roomApi, tenantApi, contractApi, statisticsApi } from '../api/api'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
data() {
|
||||
return {
|
||||
regionCount: 0,
|
||||
apartmentCount: 0,
|
||||
roomCount: 0,
|
||||
emptyRoomCount: 0,
|
||||
|
|
@ -166,7 +153,7 @@ export default {
|
|||
expiredRoomCount: 0,
|
||||
collectedRentAmount: 0,
|
||||
collectedWaterAmount: 0,
|
||||
regionApartmentHouseStats: []
|
||||
apartmentHouseStats: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -175,17 +162,13 @@ export default {
|
|||
methods: {
|
||||
async loadData() {
|
||||
try {
|
||||
// 并行加载所有数据
|
||||
const [dashboardStatsResponse, regionApartmentHouseStatsResponse] = await Promise.all([
|
||||
statisticsApi.getDashboardStats(),
|
||||
statisticsApi.getRegionApartmentHouseStats()
|
||||
])
|
||||
// 加载仪表盘统计数据
|
||||
const dashboardStatsResponse = await statisticsApi.getDashboardStats()
|
||||
|
||||
// 处理响应数据
|
||||
const dashboardStats = dashboardStatsResponse.data || dashboardStatsResponse
|
||||
|
||||
// 更新统计数据
|
||||
this.regionCount = dashboardStats.regionCount
|
||||
this.apartmentCount = dashboardStats.apartmentCount
|
||||
this.roomCount = dashboardStats.roomCount
|
||||
this.emptyRoomCount = dashboardStats.emptyRoomCount
|
||||
|
|
@ -195,7 +178,10 @@ export default {
|
|||
this.expiredRoomCount = dashboardStats.expiredRoomCount
|
||||
this.collectedRentAmount = dashboardStats.collectedRentAmount
|
||||
this.collectedWaterAmount = dashboardStats.collectedWaterAmount
|
||||
this.regionApartmentHouseStats = regionApartmentHouseStatsResponse
|
||||
|
||||
// 加载公寓房间状态分布数据
|
||||
const apartmentRoomStatusResponse = await statisticsApi.getApartmentRoomStatusStats()
|
||||
this.apartmentHouseStats = apartmentRoomStatusResponse.data || apartmentRoomStatusResponse
|
||||
} catch (error) {
|
||||
this.$message.error('加载数据失败')
|
||||
}
|
||||
|
|
@ -214,10 +200,6 @@ export default {
|
|||
sums[index] = '合计';
|
||||
return;
|
||||
}
|
||||
if (index === 1) {
|
||||
sums[index] = '';
|
||||
return;
|
||||
}
|
||||
const values = data.map(item => Number(item[column.property]) || 0);
|
||||
if (values.every(value => !isNaN(value))) {
|
||||
sums[index] = values.reduce((prev, curr) => {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<el-form :model="apartmentForm" :rules="rules" ref="apartmentForm" label-width="100px" class="form-content">
|
||||
<el-form-item label="区域" prop="regionId">
|
||||
<el-select v-model="apartmentForm.regionId" placeholder="请选择区域" style="width: 100%">
|
||||
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公寓名称" prop="name">
|
||||
<el-input v-model="apartmentForm.name" placeholder="请输入公寓名称"></el-input>
|
||||
</el-form-item>
|
||||
|
|
@ -29,22 +25,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { apartmentApi, regionApi } from '../../api/api'
|
||||
import { apartmentApi } from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'ApartmentAdd',
|
||||
data() {
|
||||
return {
|
||||
apartmentForm: {
|
||||
regionId: '',
|
||||
name: '',
|
||||
address: ''
|
||||
},
|
||||
regions: [],
|
||||
rules: {
|
||||
regionId: [
|
||||
{ required: true, message: '请选择区域', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入公寓名称', trigger: 'blur' }
|
||||
]
|
||||
|
|
@ -52,17 +43,9 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRegions()
|
||||
},
|
||||
methods: {
|
||||
async loadRegions() {
|
||||
try {
|
||||
const response = await regionApi.list()
|
||||
this.regions = response
|
||||
} catch (error) {
|
||||
this.$message.error('加载区域数据失败')
|
||||
}
|
||||
},
|
||||
|
||||
async submitForm() {
|
||||
this.$refs.apartmentForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<el-form :model="apartmentForm" :rules="rules" ref="apartmentForm" label-width="100px" class="form-content">
|
||||
<el-form-item label="区域" prop="regionId">
|
||||
<el-select v-model="apartmentForm.regionId" placeholder="请选择区域" style="width: 100%">
|
||||
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公寓名称" prop="name">
|
||||
<el-input v-model="apartmentForm.name" placeholder="请输入公寓名称"></el-input>
|
||||
</el-form-item>
|
||||
|
|
@ -29,7 +25,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { apartmentApi, regionApi } from '../../api/api'
|
||||
import { apartmentApi } from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'ApartmentEdit',
|
||||
|
|
@ -37,15 +33,10 @@ export default {
|
|||
return {
|
||||
apartmentForm: {
|
||||
id: '',
|
||||
regionId: '',
|
||||
name: '',
|
||||
address: ''
|
||||
},
|
||||
regions: [],
|
||||
rules: {
|
||||
regionId: [
|
||||
{ required: true, message: '请选择区域', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入公寓名称', trigger: 'blur' }
|
||||
]
|
||||
|
|
@ -53,18 +44,10 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRegions()
|
||||
this.loadApartmentData()
|
||||
},
|
||||
methods: {
|
||||
async loadRegions() {
|
||||
try {
|
||||
const response = await regionApi.list()
|
||||
this.regions = response
|
||||
} catch (error) {
|
||||
this.$message.error('加载区域数据失败')
|
||||
}
|
||||
},
|
||||
|
||||
async loadApartmentData() {
|
||||
try {
|
||||
const id = this.$route.params.id
|
||||
|
|
|
|||
|
|
@ -4,18 +4,13 @@
|
|||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>公寓管理</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加公寓</el-button>
|
||||
<el-button type="primary" size="small" @click="handleAdd" v-if="hasPermission('apartment:add')">添加公寓</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="区域">
|
||||
<el-select v-model="searchForm.regionId" placeholder="请选择区域" style="width: 100%">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option v-for="region in regions" :key="region.id" :label="region.name" :value="region.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公寓名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入公寓名称"></el-input>
|
||||
</el-form-item>
|
||||
|
|
@ -29,14 +24,13 @@
|
|||
<div class="table-wrapper hidden-xs-only">
|
||||
<el-table :data="apartments" style="width: 100%" v-loading="isLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="regionName" label="区域" min-width="100"></el-table-column>
|
||||
<el-table-column prop="name" label="公寓名称" min-width="120"></el-table-column>
|
||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)" v-if="hasPermission('apartment:edit')">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)" v-if="hasPermission('apartment:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -50,10 +44,7 @@
|
|||
<span class="mobile-card-id">ID: {{ item.id }}</span>
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">区域:</span>
|
||||
<span class="mobile-card-value">{{ item.regionName || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">地址:</span>
|
||||
<span class="mobile-card-value">{{ item.address || '-' }}</span>
|
||||
|
|
@ -64,8 +55,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(item.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(item.id)">删除</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleEdit(item.id)" v-if="hasPermission('apartment:edit')">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(item.id)" v-if="hasPermission('apartment:delete')">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -86,17 +77,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { apartmentApi, regionApi } from '../../api/api'
|
||||
import { apartmentApi } from '../../api/api'
|
||||
import { hasPermission } from '../../utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'ApartmentList',
|
||||
data() {
|
||||
return {
|
||||
regions: [],
|
||||
apartments: [],
|
||||
total: 0,
|
||||
searchForm: {
|
||||
regionId: '',
|
||||
name: ''
|
||||
},
|
||||
currentPage: 1,
|
||||
|
|
@ -119,19 +109,15 @@ export default {
|
|||
window.removeEventListener('resize', this.checkDevice)
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
checkDevice() {
|
||||
this.isMobile = window.innerWidth <= 768
|
||||
},
|
||||
async loadData() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
// 加载区域数据
|
||||
const regionsResponse = await regionApi.getAll()
|
||||
this.regions = regionsResponse
|
||||
|
||||
// 构建查询参数
|
||||
const params = {
|
||||
regionId: this.searchForm.regionId,
|
||||
name: this.searchForm.name,
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
|
|
@ -139,13 +125,7 @@ export default {
|
|||
|
||||
// 加载公寓数据
|
||||
const apartmentsResponse = await apartmentApi.getAll(params)
|
||||
this.apartments = apartmentsResponse.data.map(apartment => {
|
||||
const region = regionsResponse.find(r => r.id == apartment.regionId)
|
||||
return {
|
||||
...apartment,
|
||||
regionName: region ? region.name : ''
|
||||
}
|
||||
})
|
||||
this.apartments = apartmentsResponse.data
|
||||
this.total = apartmentsResponse.total
|
||||
} catch (error) {
|
||||
this.$message.error('加载数据失败')
|
||||
|
|
@ -182,7 +162,6 @@ export default {
|
|||
},
|
||||
resetSearch() {
|
||||
this.searchForm = {
|
||||
regionId: '',
|
||||
name: ''
|
||||
}
|
||||
this.currentPage = 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="login-page">
|
||||
<el-form
|
||||
ref="loginForm"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
@keyup.enter.native="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="el-icon-user"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="el-icon-lock"
|
||||
size="large"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-btn"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { login } from '@/api/auth'
|
||||
import { setAuth } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data() {
|
||||
return {
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loginRules: {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleLogin() {
|
||||
this.$refs.loginForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await login(this.loginForm)
|
||||
if (res.code === 200) {
|
||||
setAuth(res.data.token, res.data.userInfo, res.data.menus)
|
||||
this.$message.success('登录成功')
|
||||
this.$router.push('/')
|
||||
} else {
|
||||
this.$message.error(res.message || '登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '登录失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<div class="login-log-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>登录日志</span>
|
||||
<el-button
|
||||
style="float: right; margin-left: 10px;"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleClear"
|
||||
>
|
||||
清空日志
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录类型">
|
||||
<el-select v-model="searchForm.loginType" placeholder="请选择类型" clearable>
|
||||
<el-option label="登录" value="login" />
|
||||
<el-option label="登出" value="logout" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="fail" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchForm.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-table v-loading="loading" :data="logList" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="loginType" label="类型" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.loginType === 'login'" type="primary">登录</el-tag>
|
||||
<el-tag v-else type="info">登出</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip" label="IP地址" width="150" />
|
||||
<el-table-column prop="userAgent" label="浏览器信息" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 'success'" type="success">成功</el-tag>
|
||||
<el-tag v-else type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="信息" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="时间" width="180">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getLoginLogs, clearLoginLogs } from '@/api/log'
|
||||
|
||||
export default {
|
||||
name: 'LoginLog',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
logList: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
searchForm: {
|
||||
username: '',
|
||||
loginType: '',
|
||||
status: '',
|
||||
timeRange: []
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchLogList()
|
||||
},
|
||||
methods: {
|
||||
async fetchLogList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
username: this.searchForm.username,
|
||||
loginType: this.searchForm.loginType,
|
||||
status: this.searchForm.status
|
||||
}
|
||||
|
||||
if (this.searchForm.timeRange && this.searchForm.timeRange.length === 2) {
|
||||
params.startTime = this.searchForm.timeRange[0]
|
||||
params.endTime = this.searchForm.timeRange[1]
|
||||
}
|
||||
|
||||
const res = await getLoginLogs(params)
|
||||
if (res.code === 200) {
|
||||
this.logList = res.data.list
|
||||
this.total = res.data.total
|
||||
} else {
|
||||
this.$message.error(res.message || '获取日志失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '获取日志失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleReset() {
|
||||
this.searchForm = {
|
||||
username: '',
|
||||
loginType: '',
|
||||
status: '',
|
||||
timeRange: []
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleClear() {
|
||||
this.$confirm('确定要清空登录日志吗?此操作不可恢复!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const params = {}
|
||||
if (this.searchForm.timeRange && this.searchForm.timeRange.length === 2) {
|
||||
params.startTime = this.searchForm.timeRange[0]
|
||||
params.endTime = this.searchForm.timeRange[1]
|
||||
}
|
||||
const res = await clearLoginLogs(params)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('清空成功')
|
||||
this.fetchLogList()
|
||||
} else {
|
||||
this.$message.error(res.message || '清空失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '清空失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-log-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
<template>
|
||||
<div class="operation-log-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>操作日志</span>
|
||||
<el-button
|
||||
style="float: right; margin-left: 10px;"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleClear"
|
||||
>
|
||||
清空日志
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作模块">
|
||||
<el-select v-model="searchForm.module" placeholder="请选择模块" clearable>
|
||||
<el-option label="区域管理" value="区域管理" />
|
||||
<el-option label="公寓管理" value="公寓管理" />
|
||||
<el-option label="房间管理" value="房间管理" />
|
||||
<el-option label="租房管理" value="租房管理" />
|
||||
<el-option label="水费管理" value="水费管理" />
|
||||
<el-option label="电费管理" value="电费管理" />
|
||||
<el-option label="用户管理" value="用户管理" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="searchForm.action" placeholder="请选择类型" clearable>
|
||||
<el-option label="查询" value="查询" />
|
||||
<el-option label="新增" value="新增" />
|
||||
<el-option label="修改" value="修改" />
|
||||
<el-option label="删除" value="删除" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="fail" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchForm.timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-table v-loading="loading" :data="logList" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="操作用户" width="120" />
|
||||
<el-table-column prop="module" label="操作模块" width="120" />
|
||||
<el-table-column prop="action" label="操作类型" width="100" />
|
||||
<el-table-column prop="description" label="操作描述" show-overflow-tooltip />
|
||||
<el-table-column prop="method" label="请求方法" width="100" />
|
||||
<el-table-column prop="ip" label="IP地址" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 'success'" type="success">成功</el-tag>
|
||||
<el-tag v-else type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="耗时(ms)" width="100" />
|
||||
<el-table-column prop="createTime" label="操作时间" width="180">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOperationLogs, clearOperationLogs } from '@/api/log'
|
||||
|
||||
export default {
|
||||
name: 'OperationLog',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
logList: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
searchForm: {
|
||||
username: '',
|
||||
module: '',
|
||||
action: '',
|
||||
status: '',
|
||||
timeRange: []
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchLogList()
|
||||
},
|
||||
methods: {
|
||||
async fetchLogList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
username: this.searchForm.username,
|
||||
module: this.searchForm.module,
|
||||
action: this.searchForm.action,
|
||||
status: this.searchForm.status
|
||||
}
|
||||
|
||||
if (this.searchForm.timeRange && this.searchForm.timeRange.length === 2) {
|
||||
params.startTime = this.searchForm.timeRange[0]
|
||||
params.endTime = this.searchForm.timeRange[1]
|
||||
}
|
||||
|
||||
const res = await getOperationLogs(params)
|
||||
if (res.code === 200) {
|
||||
this.logList = res.data.list
|
||||
this.total = res.data.total
|
||||
} else {
|
||||
this.$message.error(res.message || '获取日志失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '获取日志失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleReset() {
|
||||
this.searchForm = {
|
||||
username: '',
|
||||
module: '',
|
||||
action: '',
|
||||
status: '',
|
||||
timeRange: []
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val
|
||||
this.fetchLogList()
|
||||
},
|
||||
handleClear() {
|
||||
this.$confirm('确定要清空操作日志吗?此操作不可恢复!', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const params = {}
|
||||
if (this.searchForm.timeRange && this.searchForm.timeRange.length === 2) {
|
||||
params.startTime = this.searchForm.timeRange[0]
|
||||
params.endTime = this.searchForm.timeRange[1]
|
||||
}
|
||||
const res = await clearOperationLogs(params)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('清空成功')
|
||||
this.fetchLogList()
|
||||
} else {
|
||||
this.$message.error(res.message || '清空失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '清空失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation-log-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
<template>
|
||||
<div class="menu-list-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>菜单管理</span>
|
||||
<el-button
|
||||
v-if="hasPermission('menu:add')"
|
||||
style="float: right;"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加菜单
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="菜单名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入菜单名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="searchForm.type" placeholder="请选择类型" clearable>
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="按钮" value="button" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="hasPermission('menu:search')" type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button v-if="hasPermission('menu:reset')" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 菜单树表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="menuTree"
|
||||
border
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
>
|
||||
<el-table-column prop="name" label="菜单名称" width="200" />
|
||||
<el-table-column prop="code" label="菜单编码" width="180" />
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.type === 'menu'" type="primary">菜单</el-tag>
|
||||
<el-tag v-else type="success">按钮</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="path" label="路由路径" width="200" />
|
||||
<el-table-column prop="component" label="组件路径" width="200" />
|
||||
<el-table-column prop="icon" label="图标" width="100">
|
||||
<template slot-scope="scope">
|
||||
<i v-if="scope.row.icon" :class="scope.row.icon" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="visible" label="显示" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.visible === 'show'" type="success">显示</el-tag>
|
||||
<el-tag v-else type="info">隐藏</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 'active'" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="hasPermission('menu:add-child')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleAddChild(scope.row)"
|
||||
>
|
||||
添加子菜单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('menu:edit')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('menu:delete')"
|
||||
type="text"
|
||||
size="small"
|
||||
style="color: #f56c6c;"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑菜单对话框 -->
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="menuForm"
|
||||
:model="menuForm"
|
||||
:rules="menuRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="上级菜单" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="menuForm.parentId"
|
||||
:data="menuOptions"
|
||||
:props="{ label: 'name', value: 'id' }"
|
||||
placeholder="请选择上级菜单(不选则为顶级菜单)"
|
||||
clearable
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="menuForm.name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单编码" prop="code">
|
||||
<el-input v-model="menuForm.code" placeholder="请输入菜单编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-radio-group v-model="menuForm.type">
|
||||
<el-radio label="menu">菜单</el-radio>
|
||||
<el-radio label="button">按钮</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="menuForm.type === 'menu'" label="路由路径" prop="path">
|
||||
<el-input v-model="menuForm.path" placeholder="请输入路由路径" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="menuForm.type === 'menu'" label="组件路径" prop="component">
|
||||
<el-input v-model="menuForm.component" placeholder="请输入组件路径" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input v-model="menuForm.icon" placeholder="请输入图标class">
|
||||
<template slot="prepend">
|
||||
<i v-if="menuForm.icon" :class="menuForm.icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="menuForm.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否显示" prop="visible">
|
||||
<el-radio-group v-model="menuForm.visible">
|
||||
<el-radio label="show">显示</el-radio>
|
||||
<el-radio label="hide">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="menuForm.status">
|
||||
<el-radio label="active">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMenuTree, getMenuList, createMenu, updateMenu, deleteMenu } from '@/api/menu'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'MenuList',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
menuTree: [],
|
||||
menuOptions: [],
|
||||
searchForm: {
|
||||
name: '',
|
||||
type: '',
|
||||
status: ''
|
||||
},
|
||||
dialogVisible: false,
|
||||
dialogTitle: '添加菜单',
|
||||
isEdit: false,
|
||||
submitLoading: false,
|
||||
menuForm: {
|
||||
id: null,
|
||||
parentId: null,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
visible: 'show',
|
||||
status: 'active'
|
||||
},
|
||||
menuRules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入菜单编码', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'change' }
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '请输入路由路径', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMenuTree()
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
async fetchMenuTree() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = { ...this.searchForm }
|
||||
const res = await getMenuTree(params)
|
||||
if (res.code === 200) {
|
||||
this.menuTree = res.data
|
||||
this.menuOptions = this.buildMenuOptions(res.data)
|
||||
} else {
|
||||
this.$message.error(res.message || '获取菜单列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '获取菜单列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.fetchMenuTree()
|
||||
},
|
||||
handleReset() {
|
||||
this.searchForm = {
|
||||
name: '',
|
||||
type: '',
|
||||
status: ''
|
||||
}
|
||||
this.fetchMenuTree()
|
||||
},
|
||||
handleAdd() {
|
||||
this.isEdit = false
|
||||
this.dialogTitle = '添加菜单'
|
||||
this.menuForm = {
|
||||
id: null,
|
||||
parentId: null,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
visible: 'show',
|
||||
status: 'active'
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menuForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleAddChild(row) {
|
||||
this.isEdit = false
|
||||
this.dialogTitle = '添加子菜单'
|
||||
this.menuForm = {
|
||||
id: null,
|
||||
parentId: row.id,
|
||||
name: '',
|
||||
code: '',
|
||||
type: 'menu',
|
||||
path: '',
|
||||
component: '',
|
||||
icon: '',
|
||||
sort: 0,
|
||||
visible: 'show',
|
||||
status: 'active'
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menuForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleEdit(row) {
|
||||
this.isEdit = true
|
||||
this.dialogTitle = '编辑菜单'
|
||||
this.menuForm = {
|
||||
id: row.id,
|
||||
parentId: row.parentId,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
type: row.type,
|
||||
path: row.path,
|
||||
component: row.component,
|
||||
icon: row.icon,
|
||||
sort: row.sort,
|
||||
visible: row.visible,
|
||||
status: row.status
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menuForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.menuForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.submitLoading = true
|
||||
try {
|
||||
let res
|
||||
if (this.isEdit) {
|
||||
res = await updateMenu(this.menuForm.id, this.menuForm)
|
||||
} else {
|
||||
res = await createMenu(this.menuForm)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
this.$message.success(this.isEdit ? '更新成功' : '添加成功')
|
||||
this.dialogVisible = false
|
||||
this.fetchMenuTree()
|
||||
} else {
|
||||
this.$message.error(res.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
} finally {
|
||||
this.submitLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm(`确定要删除菜单 "${row.name}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteMenu(row.id)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchMenuTree()
|
||||
} else {
|
||||
this.$message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
buildMenuOptions(menus, parentId = null) {
|
||||
return menus
|
||||
.filter(menu => menu.parentId === parentId)
|
||||
.map(menu => ({
|
||||
id: menu.id,
|
||||
name: menu.name,
|
||||
children: this.buildMenuOptions(menus, menu.id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-list-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
<template>
|
||||
<div class="region-add">
|
||||
<el-card>
|
||||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>添加区域</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="regionForm" :rules="rules" ref="regionForm" label-width="100px" class="form-content">
|
||||
<el-form-item label="区域名称" prop="name">
|
||||
<el-input v-model="regionForm.name" placeholder="请输入区域名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域描述" prop="description">
|
||||
<el-input type="textarea" v-model="regionForm.description" placeholder="请输入区域描述" rows="4"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { regionApi } from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'RegionAdd',
|
||||
data() {
|
||||
return {
|
||||
regionForm: {
|
||||
name: '',
|
||||
description: ''
|
||||
},
|
||||
rules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入区域名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submitForm() {
|
||||
this.$refs.regionForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
await regionApi.create(this.regionForm)
|
||||
this.$message.success('添加成功')
|
||||
this.$router.push('/region/list')
|
||||
} catch (error) {
|
||||
this.$message.error('添加失败')
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
resetForm() {
|
||||
this.$refs.regionForm.resetFields()
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push('/region/list')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-add {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.form-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.form-content .el-form-item__label {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-content .el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions .el-button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
<template>
|
||||
<div class="region-edit">
|
||||
<el-card>
|
||||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>编辑区域</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="regionForm" :rules="rules" ref="regionForm" label-width="100px" class="form-content">
|
||||
<el-form-item label="区域名称" prop="name">
|
||||
<el-input v-model="regionForm.name" placeholder="请输入区域名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域描述" prop="description">
|
||||
<el-input type="textarea" v-model="regionForm.description" placeholder="请输入区域描述" rows="4"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { regionApi } from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'RegionEdit',
|
||||
data() {
|
||||
return {
|
||||
regionForm: {
|
||||
id: '',
|
||||
name: '',
|
||||
description: ''
|
||||
},
|
||||
rules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入区域名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRegionData()
|
||||
},
|
||||
methods: {
|
||||
async loadRegionData() {
|
||||
try {
|
||||
const id = this.$route.params.id
|
||||
const region = await regionApi.getById(id)
|
||||
if (region) {
|
||||
this.regionForm = region
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('加载区域数据失败')
|
||||
}
|
||||
},
|
||||
async submitForm() {
|
||||
this.$refs.regionForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
await regionApi.update(this.regionForm.id, this.regionForm)
|
||||
this.$message.success('编辑成功')
|
||||
this.$router.push('/region/list')
|
||||
} catch (error) {
|
||||
this.$message.error('编辑失败')
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
resetForm() {
|
||||
this.loadRegionData()
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push('/region/list')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-edit {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.form-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.form-content .el-form-item__label {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-content .el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions .el-button {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
<template>
|
||||
<div class="region-list">
|
||||
<el-card>
|
||||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>区域列表</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加区域</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- PC端表格 -->
|
||||
<div class="table-wrapper hidden-xs-only">
|
||||
<el-table :data="regions" style="width: 100%" v-loading="isLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="name" label="区域名称" min-width="120"></el-table-column>
|
||||
<el-table-column prop="description" label="区域描述" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="mobile-list hidden-sm-and-up">
|
||||
<div v-for="item in regions" :key="item.id" class="mobile-card">
|
||||
<div class="mobile-card-header">
|
||||
<span class="mobile-card-title">{{ item.name }}</span>
|
||||
<span class="mobile-card-id">ID: {{ item.id }}</span>
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">描述:</span>
|
||||
<span class="mobile-card-value">{{ item.description || '-' }}</span>
|
||||
</div>
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">创建时间:</span>
|
||||
<span class="mobile-card-value">{{ item.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(item.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(item.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:layout="paginationLayout"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:current-page="currentPage"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { regionApi } from '../../api/api'
|
||||
|
||||
export default {
|
||||
name: 'RegionList',
|
||||
data() {
|
||||
return {
|
||||
regions: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
isLoading: false,
|
||||
isMobile: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
paginationLayout() {
|
||||
return this.isMobile ? 'prev, pager, next' : 'total, sizes, prev, pager, next, jumper'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkDevice()
|
||||
window.addEventListener('resize', this.checkDevice)
|
||||
this.loadRegions()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkDevice)
|
||||
},
|
||||
methods: {
|
||||
checkDevice() {
|
||||
this.isMobile = window.innerWidth <= 768
|
||||
},
|
||||
async loadRegions() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize
|
||||
}
|
||||
const response = await regionApi.getAll(params)
|
||||
this.regions = response.data || response
|
||||
this.total = response.total || 0
|
||||
} catch (error) {
|
||||
this.$message.error('加载区域数据失败')
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
handleAdd() {
|
||||
this.$router.push('/region/add')
|
||||
},
|
||||
handleEdit(id) {
|
||||
this.$router.push(`/region/edit/${id}`)
|
||||
},
|
||||
async handleDelete(id) {
|
||||
this.$confirm('确定要删除这个区域吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await regionApi.delete(id)
|
||||
this.$message.success('删除成功')
|
||||
this.loadRegions()
|
||||
} catch (error) {
|
||||
this.$message.error('删除失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
// 取消删除
|
||||
})
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val
|
||||
this.loadRegions()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.currentPage = 1
|
||||
this.loadRegions()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 移动端卡片列表样式 */
|
||||
.mobile-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mobile-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.mobile-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.mobile-card-id {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.mobile-card-body {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mobile-card-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-card-label {
|
||||
color: #909399;
|
||||
min-width: 70px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-card-value {
|
||||
color: #606266;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mobile-card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
/* 响应式显示控制 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.hidden-xs-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.hidden-sm-and-up {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -76,6 +76,7 @@
|
|||
|
||||
<script>
|
||||
import { rentalApi, roomApi, apartmentApi } from '../../api/api'
|
||||
import { getUserInfo } from '../../utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'RentalAdd',
|
||||
|
|
@ -124,6 +125,7 @@ export default {
|
|||
this.rentalForm.startDate = new Date()
|
||||
this.updateEndDate()
|
||||
this.loadData()
|
||||
this.setDefaultTenantName()
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
|
|
@ -197,6 +199,12 @@ export default {
|
|||
endDate.setMonth(endDate.getMonth() + this.leaseMonths)
|
||||
this.rentalForm.endDate = endDate
|
||||
}
|
||||
},
|
||||
setDefaultTenantName() {
|
||||
const userInfo = getUserInfo()
|
||||
if (userInfo && userInfo.nickname) {
|
||||
this.rentalForm.tenantName = userInfo.nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
<div class="card-header">
|
||||
<span>房屋详情</span>
|
||||
<div class="action-buttons">
|
||||
<el-button v-if="room.status === 'empty' || room.status === 'reserved'" type="primary" size="small" @click="handleRent">租房</el-button>
|
||||
<el-button v-if="room.status === 'empty'" type="warning" size="small" @click="handleReserve">预订</el-button>
|
||||
<el-button v-if="room.status === 'reserved'" type="warning" size="small" @click="handleCancelReserve">取消预订</el-button>
|
||||
<el-button v-if="room.status === 'rented'" type="warning" size="small" @click="handleCheckout">退房</el-button>
|
||||
<el-button v-if="!room.otherStatus || room.otherStatus === ''" type="info" size="small" @click="handleCleaning">打扫</el-button>
|
||||
<el-button v-if="!room.otherStatus || room.otherStatus === ''" type="danger" size="small" @click="handleMaintenance">维修</el-button>
|
||||
<el-button v-if="room.otherStatus === 'cleaning'" type="success" size="small" @click="handleComplete">打扫完成</el-button>
|
||||
<el-button v-if="room.otherStatus === 'maintenance'" type="success" size="small" @click="handleComplete">维修完成</el-button>
|
||||
<el-button v-if="(room.status === 'empty' || room.status === 'reserved') && hasPermission('rental:add')" type="primary" size="small" @click="handleRent">租房</el-button>
|
||||
<el-button v-if="room.status === 'empty' && hasPermission('room:update')" type="warning" size="small" @click="handleReserve">预订</el-button>
|
||||
<el-button v-if="room.status === 'reserved' && hasPermission('room:update')" type="warning" size="small" @click="handleCancelReserve">取消预订</el-button>
|
||||
<el-button v-if="room.status === 'rented' && hasPermission('rental:update')" type="warning" size="small" @click="handleCheckout">退房</el-button>
|
||||
<el-button v-if="(!room.otherStatus || room.otherStatus === '') && hasPermission('room:update')" type="info" size="small" @click="handleCleaning">打扫</el-button>
|
||||
<el-button v-if="(!room.otherStatus || room.otherStatus === '') && hasPermission('room:update')" type="danger" size="small" @click="handleMaintenance">维修</el-button>
|
||||
<el-button v-if="room.otherStatus === 'cleaning' && hasPermission('room:update')" type="success" size="small" @click="handleComplete">打扫完成</el-button>
|
||||
<el-button v-if="room.otherStatus === 'maintenance' && hasPermission('room:update')" type="success" size="small" @click="handleComplete">维修完成</el-button>
|
||||
<el-button type="primary" size="small" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,10 +66,10 @@
|
|||
<el-table-column prop="createTime" label="创建时间" min-width="140"></el-table-column>
|
||||
<el-table-column label="操作" min-width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="primary" size="mini" @click="handleEditRental(scope.row)">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenewRental(scope.row)">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDeleteRental(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
<el-button type="primary" size="mini" @click="handleEditRental(scope.row)" v-if="hasPermission('rental:edit')">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenewRental(scope.row)" v-if="hasPermission('rental:add')">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDeleteRental(scope.row.id)" v-if="hasPermission('rental:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
</el-tab-pane>
|
||||
<el-tab-pane label="水费记录" name="water">
|
||||
<div class="section-header">
|
||||
<el-button type="primary" size="small" @click="handleAddWaterBill">添加水费</el-button>
|
||||
<el-button type="primary" size="small" @click="handleAddWaterBill" v-if="hasPermission('water:add')">添加水费</el-button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="waterBills" style="width: 100%" class="detail-table">
|
||||
|
|
@ -111,9 +111,9 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="140" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="primary" size="mini" @click="handleEditWaterBill(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDeleteWaterBill(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
<el-button type="primary" size="mini" @click="handleEditWaterBill(scope.row)" v-if="hasPermission('water:edit')">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDeleteWaterBill(scope.row.id)" v-if="hasPermission('water:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
|
||||
<!-- 水费编辑对话框 -->
|
||||
<el-dialog title="编辑水费" :visible.sync="waterBillDialogVisible" width="500px">
|
||||
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm" label-width="90px">
|
||||
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm" label-width="120px">
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
<el-date-picker v-model="waterBillForm.startDate" type="date" placeholder="选择开始日期" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
|
|
@ -202,6 +202,8 @@
|
|||
|
||||
<script>
|
||||
import { roomApi, rentalApi, waterBillApi } from '../../api/api'
|
||||
import { hasPermission } from '../../utils/permission'
|
||||
import { getUserInfo } from '../../utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'RentalDetail',
|
||||
|
|
@ -291,6 +293,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
formatDate(date) {
|
||||
if (!date) return null
|
||||
const d = new Date(date)
|
||||
|
|
@ -455,7 +458,7 @@ export default {
|
|||
this.$message.error('房间信息加载失败,无法进行预订操作')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.$confirm('确定要预订这个房间吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
|
@ -477,7 +480,7 @@ export default {
|
|||
this.$message.error('房间信息加载失败,无法进行取消预订操作')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.$confirm('确定要取消预订吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
|
@ -499,7 +502,7 @@ export default {
|
|||
this.$message.error('房间信息加载失败,无法进行退房操作')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
this.$confirm('确定要退房吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
|
@ -604,7 +607,7 @@ export default {
|
|||
unitPrice: this.waterBillForm.unitPrice !== '' ? parseFloat(this.waterBillForm.unitPrice) : null,
|
||||
status: this.waterBillForm.status
|
||||
}
|
||||
|
||||
|
||||
if (this.waterBillForm.id) {
|
||||
await waterBillApi.update(this.waterBillForm.id, data)
|
||||
this.$message.success('水费记录更新成功')
|
||||
|
|
@ -674,7 +677,7 @@ export default {
|
|||
} else {
|
||||
await rentalApi.create(this.rentalForm)
|
||||
this.$message.success('租赁记录添加成功')
|
||||
|
||||
|
||||
if (this.renewingRentalId) {
|
||||
await rentalApi.update(this.renewingRentalId, { status: 'expired' })
|
||||
this.$message.success('原租赁记录状态已更新为已到期')
|
||||
|
|
@ -710,6 +713,10 @@ export default {
|
|||
},
|
||||
goBack() {
|
||||
this.$router.push({ path: '/rental/list', query: this.returnQuery })
|
||||
},
|
||||
getDefaultTenantName() {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo && userInfo.nickname ? userInfo.nickname : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -810,46 +817,46 @@ export default {
|
|||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.card-header span {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.room-info-section h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
.room-basic-info {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.info-item .label {
|
||||
width: auto;
|
||||
margin-bottom: 3px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
|
||||
.info-item .value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
|
|
|||
|
|
@ -56,11 +56,6 @@
|
|||
<div class="table-wrapper hidden-xs-only">
|
||||
<el-table :data="rentalList" style="width: 100%" v-loading="isLoading">
|
||||
<el-table-column prop="tenantName" label="租客" min-width="100"></el-table-column>
|
||||
<el-table-column label="区域" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.Room.Apartment.Region.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公寓" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.Room.Apartment.name }}
|
||||
|
|
@ -86,9 +81,9 @@
|
|||
<el-table-column prop="createTime" label="创建时间" min-width="150"></el-table-column>
|
||||
<el-table-column label="操作" min-width="200" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="primary" size="mini" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenew(scope.row)">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
<el-button type="primary" size="mini" @click="handleEdit(scope.row)" v-if="hasPermission('rental:edit')">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenew(scope.row)" v-if="hasPermission('rental:add')">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)" v-if="hasPermission('rental:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -104,10 +99,6 @@
|
|||
</el-tag>
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">区域:</span>
|
||||
<span class="mobile-card-value">{{ item.Room.Apartment.Region.name }}</span>
|
||||
</div>
|
||||
<div class="mobile-card-item">
|
||||
<span class="mobile-card-label">公寓:</span>
|
||||
<span class="mobile-card-value">{{ item.Room.Apartment.name }}</span>
|
||||
|
|
@ -134,9 +125,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<el-button type="primary" size="mini" @click="handleEdit(item)">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenew(item)">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(item.id)">删除</el-button>
|
||||
<el-button type="primary" size="mini" @click="handleEdit(item)" v-if="hasPermission('rental:edit')">编辑</el-button>
|
||||
<el-button type="success" size="mini" @click="handleRenew(item)" v-if="hasPermission('rental:add')">续租</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(item.id)" v-if="hasPermission('rental:delete')">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -196,6 +187,8 @@
|
|||
|
||||
<script>
|
||||
import { rentalApi, apartmentApi, roomApi } from '../../api/api'
|
||||
import { hasPermission } from '../../utils/permission'
|
||||
import { getUserInfo } from '../../utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'RentalArchive',
|
||||
|
|
@ -253,6 +246,7 @@ export default {
|
|||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
handleResize() {
|
||||
this.$forceUpdate()
|
||||
},
|
||||
|
|
@ -420,6 +414,10 @@ export default {
|
|||
this.$message.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
getDefaultTenantName() {
|
||||
const userInfo = getUserInfo()
|
||||
return userInfo && userInfo.nickname ? userInfo.nickname : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>水费档案</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加水费</el-button>
|
||||
<el-button type="primary" size="small" @click="handleAdd" v-if="hasPermission('water:add')">添加水费</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
|
||||
<!-- PC端表格 -->
|
||||
<div class="table-wrapper hidden-xs-only">
|
||||
<el-table :data="waterBillList" style="width: 100%" v-loading="isLoading">
|
||||
|
|
@ -73,13 +73,13 @@
|
|||
<el-table-column prop="createTime" label="创建时间" min-width="150"></el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="primary" size="mini" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
<el-button type="primary" size="mini" @click="handleEdit(scope.row)" v-if="hasPermission('water:edit')">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(scope.row.id)" v-if="hasPermission('water:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="mobile-list hidden-sm-and-up">
|
||||
<div v-for="item in waterBillList" :key="item.id" class="mobile-card">
|
||||
|
|
@ -112,12 +112,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<el-button type="primary" size="mini" @click="handleEdit(item)">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(item.id)">删除</el-button>
|
||||
<el-button type="primary" size="mini" @click="handleEdit(item)" v-if="hasPermission('water:edit')">编辑</el-button>
|
||||
<el-button type="danger" size="mini" @click="handleDelete(item.id)" v-if="hasPermission('water:delete')">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
@size-change="handleSizeChange"
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</el-card>
|
||||
|
||||
<el-dialog :title="waterBillForm.id ? '编辑水费' : '添加水费'" :visible.sync="waterBillDialogVisible" width="500px">
|
||||
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm" label-width="90px">
|
||||
<el-form :model="waterBillForm" :rules="waterBillRules" ref="waterBillForm" label-width="120px">
|
||||
<el-form-item label="房间" prop="roomId">
|
||||
<el-select v-model="waterBillForm.roomId" placeholder="请选择房间" style="width: 100%">
|
||||
<el-option v-for="room in allRooms" :key="room.id" :label="`${getApartmentName(room.id)} - ${room.roomNumber}`" :value="room.id"></el-option>
|
||||
|
|
@ -171,6 +171,7 @@
|
|||
|
||||
<script>
|
||||
import { waterBillApi, apartmentApi, roomApi } from '../../api/api'
|
||||
import { hasPermission } from '../../utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'WaterArchive',
|
||||
|
|
@ -234,6 +235,7 @@ export default {
|
|||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
formatDate(date) {
|
||||
if (!date) return null
|
||||
const d = new Date(date)
|
||||
|
|
@ -392,7 +394,7 @@ export default {
|
|||
unitPrice: this.waterBillForm.unitPrice !== '' ? parseFloat(this.waterBillForm.unitPrice) : null,
|
||||
status: this.waterBillForm.status
|
||||
}
|
||||
|
||||
|
||||
if (this.waterBillForm.id) {
|
||||
await waterBillApi.update(this.waterBillForm.id, data)
|
||||
this.$message.success('水费记录更新成功')
|
||||
|
|
@ -458,16 +460,16 @@ export default {
|
|||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.search-form .el-form-item__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.search-form .el-select,
|
||||
.search-form .el-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.pagination-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,400 @@
|
|||
<template>
|
||||
<div class="role-list-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>角色管理</span>
|
||||
<el-button
|
||||
v-if="hasPermission('role:add')"
|
||||
style="float: right;"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入角色名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="hasPermission('role:search')" type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button v-if="hasPermission('role:reset')" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<el-table v-loading="loading" :data="roleList" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="角色名称" />
|
||||
<el-table-column prop="code" label="角色编码" />
|
||||
<el-table-column prop="description" label="角色描述" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 'active'" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="hasPermission('role:edit')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('role:assign')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleAssignMenus(scope.row)"
|
||||
>
|
||||
配置权限
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('role:delete')"
|
||||
type="text"
|
||||
size="small"
|
||||
style="color: #f56c6c;"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑角色对话框 -->
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
>
|
||||
<el-form
|
||||
ref="roleForm"
|
||||
:model="roleForm"
|
||||
:rules="roleRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="roleForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色编码" prop="code">
|
||||
<el-input v-model="roleForm.code" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述" prop="description">
|
||||
<el-input v-model="roleForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="roleForm.status" style="width: 100%;">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 配置菜单权限对话框 -->
|
||||
<el-dialog
|
||||
title="配置菜单权限"
|
||||
:visible.sync="menuDialogVisible"
|
||||
width="500px"
|
||||
>
|
||||
<el-tree
|
||||
ref="menuTree"
|
||||
:data="menuTree"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
/>
|
||||
<div slot="footer">
|
||||
<el-button @click="menuDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="menuSubmitLoading" @click="handleMenuSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRoles, createRole, updateRole, deleteRole } from '@/api/role'
|
||||
import { getMenuTree, getRoleMenus, assignMenusToRole } from '@/api/menu'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'RoleList',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
roleList: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
searchForm: {
|
||||
name: '',
|
||||
status: ''
|
||||
},
|
||||
dialogVisible: false,
|
||||
dialogTitle: '添加角色',
|
||||
isEdit: false,
|
||||
submitLoading: false,
|
||||
roleForm: {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
},
|
||||
roleRules: {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入角色编码', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
description: [
|
||||
{ max: 200, message: '最多 200 个字符', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
},
|
||||
menuDialogVisible: false,
|
||||
menuSubmitLoading: false,
|
||||
menuTree: [],
|
||||
currentRoleId: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchRoleList()
|
||||
this.fetchMenuTree()
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
async fetchRoleList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
...this.searchForm
|
||||
}
|
||||
const res = await getRoles(params)
|
||||
if (res.code === 200) {
|
||||
this.roleList = res.data.list
|
||||
this.total = res.data.total
|
||||
} else {
|
||||
this.$message.error(res.message || '获取角色列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '获取角色列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1
|
||||
this.fetchRoleList()
|
||||
},
|
||||
handleReset() {
|
||||
this.searchForm = {
|
||||
name: '',
|
||||
status: ''
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.fetchRoleList()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.fetchRoleList()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val
|
||||
this.fetchRoleList()
|
||||
},
|
||||
handleAdd() {
|
||||
this.isEdit = false
|
||||
this.dialogTitle = '添加角色'
|
||||
this.roleForm = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.roleForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleEdit(row) {
|
||||
this.isEdit = true
|
||||
this.dialogTitle = '编辑角色'
|
||||
this.roleForm = {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
description: row.description || '',
|
||||
status: row.status
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.roleForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.roleForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.submitLoading = true
|
||||
try {
|
||||
let res
|
||||
if (this.isEdit) {
|
||||
res = await updateRole(this.roleForm.id, {
|
||||
name: this.roleForm.name,
|
||||
description: this.roleForm.description,
|
||||
status: this.roleForm.status
|
||||
})
|
||||
} else {
|
||||
res = await createRole(this.roleForm)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
this.$message.success(this.isEdit ? '更新成功' : '添加成功')
|
||||
this.dialogVisible = false
|
||||
this.fetchRoleList()
|
||||
} else {
|
||||
this.$message.error(res.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
} finally {
|
||||
this.submitLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm(`确定要删除角色 "${row.name}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteRole(row.id)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchRoleList()
|
||||
} else {
|
||||
this.$message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
async fetchMenuTree() {
|
||||
try {
|
||||
const res = await getMenuTree()
|
||||
if (res.code === 200) {
|
||||
this.menuTree = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单树失败:', error)
|
||||
}
|
||||
},
|
||||
async handleAssignMenus(row) {
|
||||
this.currentRoleId = row.id
|
||||
this.menuDialogVisible = true
|
||||
|
||||
try {
|
||||
const res = await getRoleMenus(row.id)
|
||||
if (res.code === 200) {
|
||||
const menuIds = res.data.map(menu => menu.id)
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menuTree.setCheckedKeys(menuIds)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('获取角色菜单权限失败')
|
||||
}
|
||||
},
|
||||
async handleMenuSubmit() {
|
||||
const checkedKeys = this.$refs.menuTree.getCheckedKeys()
|
||||
const halfCheckedKeys = this.$refs.menuTree.getHalfCheckedKeys()
|
||||
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys]
|
||||
|
||||
this.menuSubmitLoading = true
|
||||
try {
|
||||
const res = await assignMenusToRole(this.currentRoleId, {
|
||||
menuIds: allCheckedKeys
|
||||
})
|
||||
if (res.code === 200) {
|
||||
this.$message.success('配置成功')
|
||||
this.menuDialogVisible = false
|
||||
} else {
|
||||
this.$message.error(res.message || '配置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '配置失败')
|
||||
} finally {
|
||||
this.menuSubmitLoading = false
|
||||
}
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-list-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>房间管理</span>
|
||||
<el-button type="primary" size="small" @click="handleAdd">添加房间</el-button>
|
||||
<el-button type="primary" size="small" @click="handleAdd" v-if="hasPermission('room:add')">添加房间</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -80,8 +80,8 @@
|
|||
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleEdit(scope.row.id)" v-if="hasPermission('room:edit')">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row.id)" v-if="hasPermission('room:delete')">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -126,8 +126,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<el-button size="mini" type="primary" @click="handleEdit(item.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(item.id)">删除</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleEdit(item.id)" v-if="hasPermission('room:edit')">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(item.id)" v-if="hasPermission('room:delete')">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -149,6 +149,7 @@
|
|||
|
||||
<script>
|
||||
import { roomApi, apartmentApi } from '../../api/api'
|
||||
import { hasPermission } from '../../utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'RoomList',
|
||||
|
|
@ -185,6 +186,7 @@ export default {
|
|||
window.removeEventListener('resize', this.checkDevice)
|
||||
},
|
||||
methods: {
|
||||
hasPermission,
|
||||
checkDevice() {
|
||||
this.isMobile = window.innerWidth <= 768
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,398 @@
|
|||
<template>
|
||||
<div class="user-list-page">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>用户管理</span>
|
||||
<el-button
|
||||
v-if="hasPermission('user:add')"
|
||||
style="float: right;"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加用户
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="searchForm.roleId" placeholder="请选择角色" clearable>
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="hasPermission('user:search')" type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button v-if="hasPermission('user:reset')" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-table v-loading="loading" :data="userList" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="nickname" label="昵称" />
|
||||
<el-table-column label="角色">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="info">{{ scope.row.role && scope.row.role.name ? scope.row.role.name : '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 'active'" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="hasPermission('user:edit')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('user:reset-password')"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleResetPassword(scope.row)"
|
||||
>
|
||||
重置密码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasPermission('user:delete')"
|
||||
type="text"
|
||||
size="small"
|
||||
style="color: #f56c6c;"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加/编辑用户对话框 -->
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="userForm"
|
||||
:model="userForm"
|
||||
:rules="userRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="userForm.username" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickname">
|
||||
<el-input v-model="userForm.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!isEdit" label="密码" prop="password">
|
||||
<el-input v-model="userForm.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="roleId">
|
||||
<el-select v-model="userForm.roleId" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="userForm.status" style="width: 100%;">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserList, createUser, updateUser, deleteUser, resetUserPassword } from '@/api/user'
|
||||
import { getAllRoles } from '@/api/role'
|
||||
import { getUserInfo } from '@/utils/auth'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
|
||||
export default {
|
||||
name: 'UserList',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
userList: [],
|
||||
roleList: [],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
searchForm: {
|
||||
username: '',
|
||||
roleId: '',
|
||||
status: ''
|
||||
},
|
||||
dialogVisible: false,
|
||||
dialogTitle: '添加用户',
|
||||
isEdit: false,
|
||||
submitLoading: false,
|
||||
currentUserId: null,
|
||||
isAdmin: false,
|
||||
userForm: {
|
||||
id: null,
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
roleId: null,
|
||||
status: 'active'
|
||||
},
|
||||
userRules: {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
nickname: [
|
||||
{ max: 20, message: '最多 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
roleId: [
|
||||
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUserInfo()
|
||||
this.fetchRoleList()
|
||||
this.fetchUserList()
|
||||
},
|
||||
methods: {
|
||||
loadUserInfo() {
|
||||
const userInfo = getUserInfo()
|
||||
if (userInfo) {
|
||||
this.currentUserId = userInfo.id
|
||||
this.isAdmin = userInfo.role === 'admin' || (userInfo.role && userInfo.role.code === 'admin')
|
||||
}
|
||||
},
|
||||
hasPermission,
|
||||
|
||||
async fetchRoleList() {
|
||||
try {
|
||||
const res = await getAllRoles()
|
||||
if (res.code === 200) {
|
||||
this.roleList = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error)
|
||||
}
|
||||
},
|
||||
async fetchUserList() {
|
||||
this.loading = true
|
||||
try {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
...this.searchForm
|
||||
}
|
||||
const res = await getUserList(params)
|
||||
if (res.code === 200) {
|
||||
this.userList = res.data.list
|
||||
this.total = res.data.total
|
||||
} else {
|
||||
this.$message.error(res.message || '获取用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '获取用户列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1
|
||||
this.fetchUserList()
|
||||
},
|
||||
handleReset() {
|
||||
this.searchForm = {
|
||||
username: '',
|
||||
role: ''
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.fetchUserList()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.fetchUserList()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.currentPage = val
|
||||
this.fetchUserList()
|
||||
},
|
||||
handleAdd() {
|
||||
this.isEdit = false
|
||||
this.dialogTitle = '添加用户'
|
||||
this.userForm = {
|
||||
id: null,
|
||||
username: '',
|
||||
nickname: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleEdit(row) {
|
||||
this.isEdit = true
|
||||
this.dialogTitle = '编辑用户'
|
||||
this.userForm = {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
nickname: row.nickname || '',
|
||||
password: '',
|
||||
roleId: row.roleId,
|
||||
status: row.status
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userForm.clearValidate()
|
||||
})
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.userForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.submitLoading = true
|
||||
try {
|
||||
let res
|
||||
if (this.isEdit) {
|
||||
res = await updateUser(this.userForm.id, {
|
||||
nickname: this.userForm.nickname,
|
||||
roleId: this.userForm.roleId,
|
||||
status: this.userForm.status
|
||||
})
|
||||
} else {
|
||||
res = await createUser(this.userForm)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
this.$message.success(this.isEdit ? '更新成功' : '添加成功')
|
||||
this.dialogVisible = false
|
||||
this.fetchUserList()
|
||||
} else {
|
||||
this.$message.error(res.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || (this.isEdit ? '更新失败' : '添加失败'))
|
||||
} finally {
|
||||
this.submitLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
handleResetPassword(row) {
|
||||
this.$confirm(`确定要重置用户 "${row.username}" 的密码吗?重置后密码为:123456`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await resetUserPassword(row.id)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('密码重置成功,新密码为:123456')
|
||||
} else {
|
||||
this.$message.error(res.message || '密码重置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '密码重置失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm(`确定要删除用户 "${row.username}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteUser(row.id)
|
||||
if (res.code === 200) {
|
||||
this.$message.success('删除成功')
|
||||
this.fetchUserList()
|
||||
} else {
|
||||
this.$message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleString('zh-CN')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-list-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<div class="profile-container">
|
||||
<el-card shadow="hover">
|
||||
<template slot="header">
|
||||
<div class="card-header">
|
||||
<span>个人中心</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 基本资料 -->
|
||||
<el-tab-pane label="基本资料" name="basic">
|
||||
<el-form :model="userForm" :rules="rules" ref="userForm" label-width="120px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="userForm.username" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="昵称" prop="nickname">
|
||||
<el-input v-model="userForm.nickname" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色">
|
||||
<el-tag type="info">{{ userForm.role && userForm.role.name ? userForm.role.name : '-' }}</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-tag :type="userForm.status === 'active' ? 'success' : 'danger'">
|
||||
{{ userForm.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="创建时间">
|
||||
<el-input v-model="userForm.createdAt" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="updateProfile" :loading="loading">保存修改</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<el-tab-pane label="修改密码" name="password">
|
||||
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="120px">
|
||||
<el-form-item label="当前密码" prop="oldPassword">
|
||||
<el-input type="password" v-model="passwordForm.oldPassword" placeholder="请输入当前密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input type="password" v-model="passwordForm.newPassword" placeholder="请输入新密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input type="password" v-model="passwordForm.confirmPassword" placeholder="请确认新密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="updatePassword" :loading="passwordLoading">修改密码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo, updateUserProfile, changePassword } from '@/api/user'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'basic',
|
||||
loading: false,
|
||||
passwordLoading: false,
|
||||
userForm: {
|
||||
username: '',
|
||||
nickname: '',
|
||||
role: null,
|
||||
status: '',
|
||||
createdAt: ''
|
||||
},
|
||||
passwordForm: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
rules: {
|
||||
nickname: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
passwordRules: {
|
||||
oldPassword: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少 6 个字符', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value !== this.passwordForm.newPassword) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 检查URL参数,自动切换标签页
|
||||
const tab = this.$route.query.tab
|
||||
if (tab === 'password') {
|
||||
this.activeTab = 'password'
|
||||
}
|
||||
this.loadUserProfile()
|
||||
},
|
||||
methods: {
|
||||
async loadUserProfile() {
|
||||
try {
|
||||
const res = await getUserInfo()
|
||||
if (res.code === 200) {
|
||||
this.userForm = {
|
||||
...res.data,
|
||||
createdAt: res.data.createdAt ? new Date(res.data.createdAt).toLocaleString() : ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('获取用户信息失败')
|
||||
}
|
||||
},
|
||||
|
||||
async updateProfile() {
|
||||
this.$refs.userForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await updateUserProfile({
|
||||
nickname: this.userForm.nickname
|
||||
})
|
||||
if (res.code === 200) {
|
||||
this.$message.success('个人资料更新成功')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('更新失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async updatePassword() {
|
||||
this.$refs.passwordForm.validate(async (valid) => {
|
||||
if (valid) {
|
||||
this.passwordLoading = true
|
||||
try {
|
||||
const res = await changePassword({
|
||||
oldPassword: this.passwordForm.oldPassword,
|
||||
newPassword: this.passwordForm.newPassword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
this.$message.success('密码修改成功,请重新登录')
|
||||
setTimeout(() => {
|
||||
this.$router.push('/login')
|
||||
}, 1500)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('密码修改失败')
|
||||
} finally {
|
||||
this.passwordLoading = false
|
||||
this.passwordForm = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
module.exports = {
|
||||
devServer: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/api': '/api'
|
||||
}
|
||||
ws: true,
|
||||
secure: false,
|
||||
logLevel: 'debug'
|
||||
}
|
||||
}
|
||||
},
|
||||
lintOnSave: false
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue