批量更新,增加用户管理
This commit is contained in:
Generated
+912
-7
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -14,7 +14,7 @@
|
||||
"axios": "^1.13.1",
|
||||
"chart": "^0.1.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"element-plus": "^2.11.5",
|
||||
"element-plus": "^2.11.7",
|
||||
"less": "^4.4.2",
|
||||
"marked": "^16.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
@@ -24,6 +24,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.9.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.7"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+35
-9
@@ -1,18 +1,35 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo(userId) {
|
||||
|
||||
//获取所有用户信息
|
||||
export function getAllUsers() {
|
||||
return request({
|
||||
url: `/users/${userId}`,
|
||||
url: '/api/allUsers',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export function updateUserInfo(userId, data) {
|
||||
// 获取用户信息
|
||||
export function getUserInfo(userId) {
|
||||
return request({
|
||||
url: `/users/${userId}`,
|
||||
method: 'put',
|
||||
url: `/api/user/${userId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/api/addUser',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
export function editUser(userId, data) {
|
||||
return request({
|
||||
url: `/api/editUser/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -20,7 +37,16 @@ export function updateUserInfo(userId, data) {
|
||||
// 删除用户
|
||||
export function deleteUser(userId) {
|
||||
return request({
|
||||
url: `/users/${userId}`,
|
||||
url: `/api/deleteUser/${userId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export function changePassword(userId, data) {
|
||||
return request({
|
||||
url: `/api/changePassword/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
+73
-1078
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,28 @@
|
||||
<template>
|
||||
<el-aside :width="width" class="common-aside">
|
||||
<el-menu
|
||||
<div v-if="loading" class="loading-spinner"> <!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff;"></i>
|
||||
</div>
|
||||
<el-menu v-else
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:background-color="asideBgColor"
|
||||
:text-color="asideTextColor"
|
||||
active-text-color="#73ffed"
|
||||
:active-text-color="activeColor"
|
||||
active-background-color="activeBgColor"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="handleMenuSelect"
|
||||
:default-active="route.path"
|
||||
>
|
||||
<h3 v-if="!isCollapse">管理后台</h3>
|
||||
<h3 v-else>管理</h3>
|
||||
<!-- 固定的仪表盘菜单项 -->
|
||||
<el-menu-item index="/dashboard">
|
||||
<i :class="['icons', 'fa', 'fa-solid', 'fa-gauge']"></i>
|
||||
<template #title>
|
||||
<span>仪表盘</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<template v-for="item in sortedMenuList" :key="item.path">
|
||||
<el-menu-item
|
||||
v-if="!item.children || item.children.length === 0"
|
||||
@@ -47,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, defineEmits } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAllDataStore } from '@/stores';
|
||||
import { getAllMenus } from '@/api/menu';
|
||||
@@ -57,13 +68,15 @@ const emit = defineEmits(['menu-click']);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const asideBgColor = ref('#0081ff');
|
||||
const asideBgColor = ref('#0074e9');
|
||||
const asideTextColor = ref('#ffffff');
|
||||
const activeColor = ref ('#fff');
|
||||
const activeBgColor = ref ('#0074e9');
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = () => {
|
||||
@@ -154,15 +167,13 @@ const transformMenuData = (menus) => {
|
||||
|
||||
// 先排序根菜单
|
||||
sortMenus(rootMenus);
|
||||
|
||||
// console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order })));
|
||||
|
||||
|
||||
return rootMenus;
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 优先从 localStorage 读取
|
||||
const cachedMenus = localStorage.getItem('menuData');
|
||||
@@ -171,7 +182,6 @@ const fetchMenus = async () => {
|
||||
if (cachedMenus) {
|
||||
try {
|
||||
menuData = JSON.parse(cachedMenus);
|
||||
// console.log('从缓存读取菜单数据');
|
||||
} catch (e) {
|
||||
console.warn('缓存菜单数据解析失败:', e);
|
||||
}
|
||||
@@ -194,14 +204,12 @@ const fetchMenus = async () => {
|
||||
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
// console.log('转换后的菜单数据:', transformedMenus);
|
||||
// console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id })));
|
||||
list.value = transformedMenus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
list.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -255,8 +263,23 @@ const sortedMenuList = computed(() => {
|
||||
return sorted;
|
||||
});
|
||||
|
||||
// 固定的仪表盘菜单项
|
||||
const dashboardMenuItem = {
|
||||
path: '/dashboard',
|
||||
label: '仪表盘',
|
||||
icon: 'fa-solid fa-gauge',
|
||||
route: '/dashboard',
|
||||
order: 0
|
||||
};
|
||||
|
||||
// 菜单点击事件:emit 通知父组件
|
||||
const handleMenuSelect = (index) => {
|
||||
// 如果是仪表盘路径,直接使用固定的仪表盘菜单项
|
||||
if (index === '/dashboard') {
|
||||
emit('menu-click', dashboardMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.route) {
|
||||
emit('menu-click', menuItem);
|
||||
@@ -281,7 +304,7 @@ const findMenuItemByPath = (menus, path) => {
|
||||
<style scoped lang="less">
|
||||
.common-aside {
|
||||
height: 100%;
|
||||
background-color: var(--aside-bg-color, #0081ff);
|
||||
background-color: var(--aside-bg-color, #0074e9);
|
||||
transition: width 0.3s, background-color 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -298,48 +321,15 @@ const findMenuItemByPath = (menus, path) => {
|
||||
&:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
&.is-active {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
transition: width 0.3s;
|
||||
|
||||
h3 {
|
||||
padding: 11px 0;
|
||||
line-height: 36px;
|
||||
color: var(--aside-text-color, #fff);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// 折叠状态下的样式
|
||||
.el-menu.el-menu--collapse {
|
||||
width: 64px;
|
||||
|
||||
.el-menu-item, .el-sub-menu {
|
||||
span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 非折叠状态
|
||||
.el-menu:not(.el-menu--collapse) {
|
||||
width: 180px;
|
||||
h3{
|
||||
line-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
</el-button>
|
||||
<el-breadcrumb separator="/" class="bread">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="r-content">
|
||||
@@ -38,43 +45,72 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore } from "@/stores";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { User, SwitchButton, Sunny, Moon } from '@element-plus/icons-vue';
|
||||
import { getTheme, toggleTheme as toggleThemeUtil, initTheme } from "@/utils/theme";
|
||||
import { getAllMenus } from '@/api/menu';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
interface Menu {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
parentId: number;
|
||||
}
|
||||
|
||||
interface Breadcrumb {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const menuList = ref<Menu[]>([]);
|
||||
|
||||
async function loadMenu() {
|
||||
try {
|
||||
const res = await getAllMenus();
|
||||
menuList.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load menu', error);
|
||||
menuList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMenu);
|
||||
|
||||
// 根据菜单列表和当前路径计算出的面包屑导航
|
||||
const breadcrumbs = computed(() => {
|
||||
let chain: Breadcrumb[] = [];
|
||||
let currentPath = route.path || '/';
|
||||
if (currentPath === '/' || currentPath === '') {
|
||||
return [{ label: '仪表盘', path: '/' }];
|
||||
}
|
||||
let current = menuList.value.find(m => m.path === currentPath);
|
||||
if (!current) {
|
||||
const candidates = menuList.value.filter(m => currentPath.startsWith(m.path));
|
||||
current = candidates.sort((a, b) => b.path.length - a.path.length)[0];
|
||||
}
|
||||
if (!current) return [];
|
||||
chain.push({ label: current.name || 'Unknown', path: current.path });
|
||||
let parentId = current.parentId;
|
||||
while (parentId > 0) {
|
||||
let parent = menuList.value.find(m => m.id === parentId);
|
||||
if (parent && !chain.some(c => c.label === parent.name)) {
|
||||
chain.push({ label: parent.name || 'Unknown', path: parent.path });
|
||||
parentId = parent.parentId;
|
||||
} else break;
|
||||
}
|
||||
chain = chain.reverse();
|
||||
return chain;
|
||||
});
|
||||
|
||||
const store = useAllDataStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 主题相关
|
||||
const currentTheme = ref(getTheme());
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return currentTheme.value === 'dark' ? Sunny : Moon;
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
const newTheme = toggleThemeUtil();
|
||||
currentTheme.value = newTheme;
|
||||
};
|
||||
|
||||
// 监听主题变化(支持多组件同步)
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
currentTheme.value = getTheme();
|
||||
|
||||
// 监听其他组件的主题变化
|
||||
window.addEventListener('theme-change', (event) => {
|
||||
currentTheme.value = event.detail.theme;
|
||||
});
|
||||
});
|
||||
|
||||
const getImageUrl = (user) => {
|
||||
return new URL(`/src/assets/images/default_avatar.png`, import.meta.url).href;
|
||||
};
|
||||
@@ -94,6 +130,11 @@ const handleCommand = (command) => {
|
||||
//清除租户数据
|
||||
localStorage.removeItem('tenant');
|
||||
sessionStorage.removeItem('tenant');
|
||||
//清除菜单数据
|
||||
localStorage.removeItem('menus');
|
||||
sessionStorage.removeItem('menus');
|
||||
localStorage.removeItem('menuData');
|
||||
sessionStorage.removeItem('menuData');
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
@@ -107,7 +148,7 @@ const handleCommand = (command) => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 40px;
|
||||
background-color: var(--header-bg-color, #0081ff);
|
||||
background-color: var(--header-bg-color, #0074e9);
|
||||
color: var(--header-text-color, #fff);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
@@ -141,20 +182,6 @@ const handleCommand = (command) => {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.theme-toggle-btn {
|
||||
margin-right: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--header-text-color, #fff);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from '@/App.vue'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import '@/assets/less/index.less'
|
||||
import '@/assets/css/all.min.css'
|
||||
import router from './router'
|
||||
import { loadAndAddDynamicRoutes } from './router'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { initTheme } from './utils/theme'
|
||||
// import { initTheme } from './utils/theme'
|
||||
// 导入全局组件
|
||||
import WangEditor from '@/views/components/WangEditor.vue';
|
||||
|
||||
@@ -24,7 +24,7 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
// 初始化主题(必须在挂载前执行)
|
||||
initTheme()
|
||||
// initTheme()
|
||||
|
||||
// 初始化时检查认证状态
|
||||
const authStore = useAuthStore()
|
||||
|
||||
@@ -7,7 +7,8 @@ const defaultUser = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
avatar: ''
|
||||
avatar: '',
|
||||
tenant_id: null
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
@@ -60,6 +61,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
Object.assign(user, defaultUser)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('tenant_id')
|
||||
sessionStorage.removeItem('tenant_id')
|
||||
}
|
||||
|
||||
// 检查认证状态
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* 主题管理工具
|
||||
*/
|
||||
|
||||
const THEME_KEY = 'app-theme';
|
||||
const THEME_LIGHT = 'light';
|
||||
const THEME_DARK = 'dark';
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
* @returns {string} 'light' | 'dark'
|
||||
*/
|
||||
export function getTheme() {
|
||||
// 优先从 localStorage 读取
|
||||
const savedTheme = localStorage.getItem(THEME_KEY);
|
||||
if (savedTheme === THEME_LIGHT || savedTheme === THEME_DARK) {
|
||||
return savedTheme;
|
||||
}
|
||||
|
||||
// 如果 localStorage 中没有,检查系统偏好
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return THEME_DARK;
|
||||
}
|
||||
|
||||
// 默认返回亮色主题
|
||||
return THEME_LIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param {string} theme - 'light' | 'dark'
|
||||
*/
|
||||
export function setTheme(theme) {
|
||||
if (theme !== THEME_LIGHT && theme !== THEME_DARK) {
|
||||
console.warn(`Invalid theme: ${theme}, using default: ${THEME_LIGHT}`);
|
||||
theme = THEME_LIGHT;
|
||||
}
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem(THEME_KEY, theme);
|
||||
|
||||
// 应用到 HTML 元素
|
||||
const html = document.documentElement;
|
||||
if (theme === THEME_DARK) {
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
html.removeAttribute('data-theme');
|
||||
}
|
||||
|
||||
// 触发主题变更事件
|
||||
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme } }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
* @returns {string} 新的主题
|
||||
*/
|
||||
export function toggleTheme() {
|
||||
const currentTheme = getTheme();
|
||||
const newTheme = currentTheme === THEME_LIGHT ? THEME_DARK : THEME_LIGHT;
|
||||
setTheme(newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
*/
|
||||
export function initTheme() {
|
||||
const theme = getTheme();
|
||||
setTheme(theme);
|
||||
}
|
||||
|
||||
@@ -207,4 +207,8 @@ function closeAllTabs() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu){
|
||||
border-right: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
label-width="80px"
|
||||
size="default"
|
||||
>
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="标题:" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="标题:" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="分类:" prop="category">
|
||||
<el-select
|
||||
@@ -65,13 +67,11 @@
|
||||
<el-input v-model="formData.author" placeholder="请输入作者" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20"> <!-- Second row for share -->
|
||||
<el-col :span="6">
|
||||
<el-form-item label="是否共享" prop="share">
|
||||
<el-form-item label="权限:" prop="share">
|
||||
<el-radio-group v-model="formData.share">
|
||||
<el-radio :label="0">个人</el-radio>
|
||||
<el-radio :label="1">共享</el-radio>
|
||||
<el-radio-button :label="0">个人</el-radio-button>
|
||||
<el-radio-button :label="1">共享</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -1013,10 +1013,6 @@ onMounted(() => {
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-color-primary);
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+238
-103
@@ -1,88 +1,92 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { login } from "@/api/login";
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const tenant = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const passwordVisible = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const tenant = ref("默认租户");
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const passwordVisible = ref(false);
|
||||
const rememberMe = ref(false);
|
||||
const loading = ref(false);
|
||||
const errorMsg = ref("");
|
||||
|
||||
// 自动填充记住的用户名、租户
|
||||
onMounted(() => {
|
||||
const savedUser = localStorage.getItem('loginUsername')
|
||||
const savedTenant = localStorage.getItem('tenant')
|
||||
const savedRemember = localStorage.getItem('loginRememberMe')
|
||||
if (savedRemember === 'true') {
|
||||
tenant.value = savedTenant || ''
|
||||
username.value = savedUser || ''
|
||||
rememberMe.value = true
|
||||
const savedUser = localStorage.getItem("loginUsername");
|
||||
const savedTenant = localStorage.getItem("tenant");
|
||||
const savedRemember = localStorage.getItem("loginRememberMe");
|
||||
|
||||
if (savedRemember === "true" && savedTenant) {
|
||||
// 只有勾选“记住我”且有缓存租户时,才用缓存值
|
||||
tenant.value = savedTenant;
|
||||
username.value = savedUser;
|
||||
rememberMe.value = true;
|
||||
} else {
|
||||
// 没有缓存时,显示默认值(这里手动赋值确保生效)
|
||||
tenant.value = "默认租户";
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 写入租户数据到缓存工具函数
|
||||
function cacheTenant(tenantValue) {
|
||||
localStorage.setItem('tenant', tenantValue)
|
||||
sessionStorage.setItem('tenant', tenantValue)
|
||||
localStorage.setItem("tenant", tenantValue);
|
||||
sessionStorage.setItem("tenant", tenantValue);
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
errorMsg.value = ''
|
||||
errorMsg.value = "";
|
||||
if (!tenant.value || !username.value || !password.value) {
|
||||
errorMsg.value = '请输入租户名称、用户名和密码'
|
||||
return
|
||||
errorMsg.value = "请输入租户名称、用户名和密码";
|
||||
return;
|
||||
}
|
||||
// 验证租户名称不能为空
|
||||
const tenantName = tenant.value.trim()
|
||||
const tenantName = tenant.value.trim();
|
||||
if (!tenantName) {
|
||||
errorMsg.value = '租户名称不能为空'
|
||||
return
|
||||
errorMsg.value = "租户名称不能为空";
|
||||
return;
|
||||
}
|
||||
// 记住我本地存储
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem('loginUsername', username.value)
|
||||
localStorage.setItem('loginRememberMe', 'true')
|
||||
localStorage.setItem('tenant', tenantName)
|
||||
localStorage.setItem("loginUsername", username.value);
|
||||
localStorage.setItem("loginRememberMe", "true");
|
||||
localStorage.setItem("tenant", tenantName); // 仅勾选时存租户
|
||||
} else {
|
||||
localStorage.removeItem('loginUsername')
|
||||
localStorage.setItem('loginRememberMe', 'false')
|
||||
// 仍要写tenant到localStorage(以便系统获取租户上下文)
|
||||
localStorage.setItem('tenant', tenantName)
|
||||
localStorage.removeItem("loginUsername");
|
||||
localStorage.setItem("loginRememberMe", "false");
|
||||
localStorage.removeItem("tenant"); // 不勾选时清除租户缓存
|
||||
}
|
||||
// 写入租户到缓存(无论记住与否都要写,便于系统获取租户上下文)
|
||||
cacheTenant(tenantName)
|
||||
loading.value = true
|
||||
cacheTenant(tenantName);
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await login(username.value, password.value, tenantName)
|
||||
const res = await login(username.value, password.value, tenantName);
|
||||
if (res && res.code === 0 && res.data) {
|
||||
authStore.setLoginInfo(res.data)
|
||||
// 登录如果有返回租户信息,可以用res.data.tenant,但这里优先使用当前登录租户输入
|
||||
cacheTenant(tenantName)
|
||||
router.push({ path: '/dashboard' })
|
||||
authStore.setLoginInfo(res.data);
|
||||
// 登录如果有返回租户信息
|
||||
cacheTenant(tenantName);
|
||||
router.push({ path: "/dashboard" });
|
||||
} else {
|
||||
errorMsg.value = res.message || '登录失败'
|
||||
errorMsg.value = res.message || "登录失败";
|
||||
}
|
||||
} catch (err) {
|
||||
errorMsg.value = err?.response?.data?.message || err?.message || '登录失败,请重试'
|
||||
errorMsg.value =
|
||||
err?.response?.data?.message || err?.message || "登录失败,请重试";
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转注册、忘记密码页面
|
||||
const goRegister = () => {
|
||||
router.push({ path: '/register' })
|
||||
}
|
||||
router.push({ path: "/register" });
|
||||
};
|
||||
const goForget = () => {
|
||||
router.push({ path: '/forget' })
|
||||
}
|
||||
router.push({ path: "/forget" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -91,25 +95,48 @@ const goForget = () => {
|
||||
<div class="login-side">
|
||||
<div class="brand">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect width="48" height="48" rx="18" fill="#eef8fc"/>
|
||||
<circle cx="24" cy="24" r="14" fill="#52a8ff" opacity="0.15"/>
|
||||
<circle cx="24" cy="24" r="9" fill="#3b7ddd" opacity="0.12"/>
|
||||
<text x="24" y="30" text-anchor="middle" fill="#2d5fa7" font-size="16" font-family="Arial" font-weight="bold">Mete</text>
|
||||
<rect width="48" height="48" rx="18" fill="#eef8fc" />
|
||||
<circle cx="24" cy="24" r="14" fill="#52a8ff" opacity="0.15" />
|
||||
<circle cx="24" cy="24" r="9" fill="#3b7ddd" opacity="0.12" />
|
||||
<text
|
||||
x="24"
|
||||
y="30"
|
||||
text-anchor="middle"
|
||||
fill="#2d5fa7"
|
||||
font-size="16"
|
||||
font-family="Arial"
|
||||
font-weight="bold"
|
||||
>
|
||||
Mete
|
||||
</text>
|
||||
</svg>
|
||||
<span class="brand-title">后台管理系统</span>
|
||||
</div>
|
||||
<div class="illus">
|
||||
<svg viewBox="0 0 300 160" style="max-width:100%;" fill="none">
|
||||
<svg viewBox="0 0 300 160" style="max-width: 100%" fill="none">
|
||||
<ellipse cx="150" cy="140" rx="120" ry="16" fill="#edf4fd" />
|
||||
<rect x="57" y="58" width="60" height="40" rx="12" fill="#64b6f7"/>
|
||||
<rect x="125" y="46" width="110" height="64" rx="14" fill="#389bf7" opacity="0.11" />
|
||||
<rect x="136" y="60" width="60" height="41" rx="10" fill="#b8e1ff"/>
|
||||
<rect x="57" y="58" width="60" height="40" rx="12" fill="#64b6f7" />
|
||||
<rect
|
||||
x="125"
|
||||
y="46"
|
||||
width="110"
|
||||
height="64"
|
||||
rx="14"
|
||||
fill="#389bf7"
|
||||
opacity="0.11"
|
||||
/>
|
||||
<rect
|
||||
x="136"
|
||||
y="60"
|
||||
width="60"
|
||||
height="41"
|
||||
rx="10"
|
||||
fill="#b8e1ff"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright">
|
||||
© 2024 Mete 管理系统
|
||||
</div>
|
||||
<div class="copyright">© 2024 Mete 管理系统</div>
|
||||
</div>
|
||||
<div class="login-panel">
|
||||
<h2 class="login-title">欢迎登录</h2>
|
||||
@@ -118,9 +145,24 @@ const goForget = () => {
|
||||
<span class="input-icon">
|
||||
<!-- 租户图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<rect x="2.2" y="4.2" width="15.6" height="11.6" rx="2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<rect x="6" y="8" width="8" height="4" rx="1" fill="#b9dbff"/>
|
||||
<rect x="6.7" y="8.7" width="6.6" height="2.6" rx="0.7" fill="#fff"/>
|
||||
<rect
|
||||
x="2.2"
|
||||
y="4.2"
|
||||
width="15.6"
|
||||
height="11.6"
|
||||
rx="2"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1.4"
|
||||
/>
|
||||
<rect x="6" y="8" width="8" height="4" rx="1" fill="#b9dbff" />
|
||||
<rect
|
||||
x="6.7"
|
||||
y="8.7"
|
||||
width="6.6"
|
||||
height="2.6"
|
||||
rx="0.7"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
@@ -135,8 +177,21 @@ const goForget = () => {
|
||||
<span class="input-icon">
|
||||
<!-- 用户图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="7" r="3.2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<ellipse cx="10" cy="14.1" rx="5.5" ry="3.3" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="7"
|
||||
r="3.2"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1.4"
|
||||
/>
|
||||
<ellipse
|
||||
cx="10"
|
||||
cy="14.1"
|
||||
rx="5.5"
|
||||
ry="3.3"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1.4"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
@@ -151,9 +206,31 @@ const goForget = () => {
|
||||
<span class="input-icon">
|
||||
<!-- 密码图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<rect x="3" y="8" width="14" height="7" rx="2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<circle cx="10" cy="11.5" r="1.5" stroke="#4da1ff" stroke-width="1.2"/>
|
||||
<rect x="7" y="5" width="6" height="3" rx="1.5" stroke="#4da1ff" stroke-width="1"/>
|
||||
<rect
|
||||
x="3"
|
||||
y="8"
|
||||
width="14"
|
||||
height="7"
|
||||
rx="2"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1.4"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="11.5"
|
||||
r="1.5"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
<rect
|
||||
x="7"
|
||||
y="5"
|
||||
width="6"
|
||||
height="3"
|
||||
rx="1.5"
|
||||
stroke="#4da1ff"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
@@ -163,35 +240,66 @@ const goForget = () => {
|
||||
autocomplete="current-password"
|
||||
class="input input-with-icon"
|
||||
/>
|
||||
<span class="visible-btn" @click="passwordVisible = !passwordVisible" :title="passwordVisible ? '隐藏密码' : '显示密码'">
|
||||
<svg v-if="passwordVisible" width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<span
|
||||
class="visible-btn"
|
||||
@click="passwordVisible = !passwordVisible"
|
||||
:title="passwordVisible ? '隐藏密码' : '显示密码'"
|
||||
>
|
||||
<svg
|
||||
v-if="passwordVisible"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<!-- 可见(eye)图标 -->
|
||||
<path d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z" stroke="#7bb7fa" stroke-width="1.4" fill="#eef5ff"/>
|
||||
<circle cx="10" cy="10" r="2.5" stroke="#3794f7" stroke-width="1.4" fill="#fff"/>
|
||||
<path
|
||||
d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z"
|
||||
stroke="#7bb7fa"
|
||||
stroke-width="1.4"
|
||||
fill="#eef5ff"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="2.5"
|
||||
stroke="#3794f7"
|
||||
stroke-width="1.4"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<!-- 不可见(eye-off)图标 -->
|
||||
<path d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z" stroke="#b7c7db" stroke-width="1.3" fill="#f2f6fd"/>
|
||||
<path d="M5 15L15 5" stroke="#b7c7db" stroke-width="1.2"/>
|
||||
<path
|
||||
d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z"
|
||||
stroke="#b7c7db"
|
||||
stroke-width="1.3"
|
||||
fill="#f2f6fd"
|
||||
/>
|
||||
<path d="M5 15L15 5" stroke="#b7c7db" stroke-width="1.2" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remember-me-row">
|
||||
<label class="remember-me-label">
|
||||
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="rememberMe"
|
||||
class="remember-me-checkbox"
|
||||
/>
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<div class="action-links">
|
||||
<a class="register-link" @click.prevent="goRegister">注册账号</a>
|
||||
<span class="divider">|</span>
|
||||
<a class="forget-link" @click.prevent="goForget">忘记密码?</a>
|
||||
</div>
|
||||
<div class="action-links">
|
||||
<a class="register-link" @click.prevent="goRegister">注册账号</a>
|
||||
<span class="divider">|</span>
|
||||
<a class="forget-link" @click.prevent="goForget">忘记密码?</a>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
</transition>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
{{ loading ? "登录中..." : "登 录" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,9 +323,10 @@ const goForget = () => {
|
||||
.login-card {
|
||||
display: flex;
|
||||
min-width: 770px;
|
||||
background: rgba(255,255,255, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 8px 36px 0 rgba(73,150,255,0.14), 0 1.5px 4px 0 rgba(30,42,79,0.05);
|
||||
box-shadow: 0 8px 36px 0 rgba(73, 150, 255, 0.14),
|
||||
0 1.5px 4px 0 rgba(30, 42, 79, 0.05);
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -228,7 +337,7 @@ const goForget = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 44px 16px 32px 16px;
|
||||
box-shadow: 4px 0 32px 0 rgba(189,231,255,0.13) inset;
|
||||
box-shadow: 4px 0 32px 0 rgba(189, 231, 255, 0.13) inset;
|
||||
position: relative;
|
||||
}
|
||||
.brand {
|
||||
@@ -248,7 +357,7 @@ const goForget = () => {
|
||||
.illus {
|
||||
margin-top: 30px;
|
||||
user-select: none;
|
||||
opacity: .95;
|
||||
opacity: 0.95;
|
||||
}
|
||||
/* 版权信息样式 */
|
||||
.copyright {
|
||||
@@ -385,7 +494,7 @@ const goForget = () => {
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 2px 12px 0 rgba(81,173,255,0.13);
|
||||
box-shadow: 0 2px 12px 0 rgba(81, 173, 255, 0.13);
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 15px;
|
||||
@@ -407,16 +516,28 @@ const goForget = () => {
|
||||
padding: 7px 4px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 14.5px;
|
||||
letter-spacing: .3px;
|
||||
animation: shake .28s;
|
||||
letter-spacing: 0.3px;
|
||||
animation: shake 0.28s;
|
||||
}
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0);}
|
||||
20% { transform: translateX(-6px);}
|
||||
40% { transform: translateX(6px);}
|
||||
60% { transform: translateX(-2px);}
|
||||
80% { transform: translateX(2px);}
|
||||
100% { transform: translateX(0);}
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
20% {
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
40% {
|
||||
transform: translateX(6px);
|
||||
}
|
||||
60% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
80% {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 注册、忘记密码链接 */
|
||||
@@ -452,10 +573,12 @@ const goForget = () => {
|
||||
}
|
||||
|
||||
/* 渐隐提示 */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .24s;
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.24s;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -483,9 +606,21 @@ const goForget = () => {
|
||||
background: radial-gradient(circle at 55% 60%, #f3e7ff99 0%, #daf3ff10 100%);
|
||||
}
|
||||
@media (max-width: 940px) {
|
||||
.login-card { min-width: 330px; flex-direction: column; }
|
||||
.login-side { width: 100%; min-width: 0; border-radius: 0 0 18px 18px;}
|
||||
.login-panel { padding: 30px 22px 34px 22px; min-width: 0; }
|
||||
.copyright { padding-top: 13px; }
|
||||
.login-card {
|
||||
min-width: 330px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.login-side {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border-radius: 0 0 18px 18px;
|
||||
}
|
||||
.login-panel {
|
||||
padding: 30px 22px 34px 22px;
|
||||
min-width: 0;
|
||||
}
|
||||
.copyright {
|
||||
padding-top: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>菜单管理</h2>
|
||||
<el-button type="primary" @click="handleAddMenu = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -261,7 +261,7 @@ const fetchMenus = async () => {
|
||||
const result = await getAllMenus();
|
||||
if (result.success) {
|
||||
// getAllMenus返回的data里每一项key是小写下划线式(见后端),需要转为Pascal命名
|
||||
const data = result.data.map((item: any) => ({
|
||||
let data = result.data.map((item: any) => ({
|
||||
Id: item.id,
|
||||
Name: item.name,
|
||||
Path: item.path,
|
||||
@@ -278,6 +278,9 @@ const fetchMenus = async () => {
|
||||
UpdateTime: "",
|
||||
}));
|
||||
|
||||
// 按照参数Order的大小从小到大排序
|
||||
data = data.sort((a, b) => a.Order - b.Order);
|
||||
|
||||
const tree = buildMenuTree(data);
|
||||
menuTree.value = tree;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
@@ -219,22 +220,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-management-module {
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
padding: 28px 22px 14px 18px;
|
||||
min-height: 500px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
font-size: 1.18rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser = true">
|
||||
<el-button type="warning" @click="testElMessage">测试ElMessage</el-button>
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
@@ -12,121 +13,469 @@
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
align="center"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column prop="role" label="角色" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.status === 'active' ? 'success' : 'danger'"
|
||||
>
|
||||
{{ scope.row.status === "active" ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="lastLogin"
|
||||
label="最后登录"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
align="center"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column prop="role" label="角色" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === "active" ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="lastLoginTime"
|
||||
label="最后登录"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dialog for add/edit -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="currentForm">
|
||||
<el-form-item label="用户名" v-show="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" v-if="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" v-if="dialogTitle === '添加用户'">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="旧密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogTitle === '修改密码' && passwordError">
|
||||
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" v-if="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select v-model="form.role">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通用户" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select v-model="form.status">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllUsers,
|
||||
addUser,
|
||||
editUser,
|
||||
deleteUser,
|
||||
getUserInfo,
|
||||
changePassword,
|
||||
} from "@/api/user";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
role: string;
|
||||
status: string;
|
||||
lastLogin: string;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<User[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: "admin",
|
||||
email: "admin@example.com",
|
||||
role: "admin",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-20 10:30:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "user1",
|
||||
email: "user1@example.com",
|
||||
role: "user",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-19 15:45:00",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "user2",
|
||||
email: "user2@example.com",
|
||||
role: "user",
|
||||
status: "inactive",
|
||||
lastLogin: "2025-01-18 09:20:00",
|
||||
},
|
||||
]);
|
||||
const users = ref<any[]>([]);
|
||||
|
||||
//校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password) {
|
||||
return "请输入密码";
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return "密码长度不能小于6位";
|
||||
}
|
||||
if (password.length > 16) {
|
||||
return "密码长度不能大于16位";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 校验确认密码,需传递新密码和确认密码
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
if (!confirmPassword) {
|
||||
return "请再次输入密码";
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
return "两次输入的密码不一致";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await getAllUsers();
|
||||
// 兼容接口返回的数据结构
|
||||
let userList: any[] = [];
|
||||
if (Array.isArray(res)) {
|
||||
userList = res;
|
||||
} else if (res?.data && Array.isArray(res.data)) {
|
||||
userList = res.data;
|
||||
} else if (res?.data?.data && Array.isArray(res.data.data)) {
|
||||
userList = res.data.data;
|
||||
} else if (res?.data) {
|
||||
userList = res.data;
|
||||
}
|
||||
// 映射接口字段到表格所需结构
|
||||
users.value = userList.map((item: any) => ({
|
||||
id: item.id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
email: item.email,
|
||||
role: item.role || (item.username === "admin" ? "admin" : "user"),
|
||||
status: item.status || "active",
|
||||
lastLoginTime: item.lastLoginTime
|
||||
? new Date(item.lastLoginTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "",
|
||||
}));
|
||||
total.value = users.value.length;
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers();
|
||||
});
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
page.value = p;
|
||||
// 分页仅前端做演示
|
||||
};
|
||||
|
||||
// 为添加 / 编辑对话框而添加
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref("");
|
||||
const isEdit = ref(false);
|
||||
const form = ref<any>({
|
||||
id: 0,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: "user",
|
||||
status: "active",
|
||||
});
|
||||
const passwordForm = ref<any>({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const passwordError = ref("");
|
||||
|
||||
// 根据对话框类型返回对应的表单数据
|
||||
const currentForm = computed(() => {
|
||||
return dialogTitle.value === '修改密码' ? passwordForm.value : form.value;
|
||||
});
|
||||
|
||||
const handleAddUser = () => {
|
||||
console.log("添加用户");
|
||||
dialogTitle.value = "添加用户";
|
||||
isEdit.value = false;
|
||||
|
||||
// 从本地缓存读取tenant_id
|
||||
let tenantId = null;
|
||||
const cachedUser = localStorage.getItem("userInfo");
|
||||
if (cachedUser) {
|
||||
try {
|
||||
const userInfo = JSON.parse(cachedUser);
|
||||
tenantId = userInfo.tenant_id || null;
|
||||
} catch (e) {
|
||||
tenantId = null;
|
||||
}
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: 0,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: "user",
|
||||
status: "active",
|
||||
tenant_id: tenantId,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
console.log("编辑用户:", user);
|
||||
const handleEdit = async (user: User) => {
|
||||
dialogTitle.value = "编辑用户";
|
||||
isEdit.value = true;
|
||||
try {
|
||||
const res = await getUserInfo(user.id);
|
||||
const data = res.data || res;
|
||||
form.value = {
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
nickname: data.nickname,
|
||||
password: "",
|
||||
email: data.email,
|
||||
role: data.role || "user",
|
||||
status: data.status || "active",
|
||||
};
|
||||
} catch (e) {
|
||||
ElMessage.error("加载用户失败");
|
||||
return;
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = (user: User) => {
|
||||
console.log("删除用户:", user);
|
||||
const submitForm = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (dialogTitle.value === "修改密码") {
|
||||
// 校验旧密码
|
||||
if (!passwordForm.value.oldPassword) {
|
||||
passwordError.value = "请输入旧密码";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验密码格式
|
||||
const passwordCheck = validatePassword(passwordForm.value.newPassword);
|
||||
if (passwordCheck !== true) {
|
||||
passwordError.value = passwordCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验确认密码
|
||||
const confirmCheck = validateConfirmPassword(
|
||||
passwordForm.value.newPassword,
|
||||
passwordForm.value.confirmPassword
|
||||
);
|
||||
if (confirmCheck !== true) {
|
||||
passwordError.value = confirmCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
|
||||
// 检查返回结果
|
||||
if (res.code === 0) {
|
||||
// 先显示成功消息
|
||||
ElMessage.success({
|
||||
message: "密码修改成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
// 延迟关闭弹窗,确保消息已经渲染
|
||||
setTimeout(() => {
|
||||
dialogVisible.value = false;
|
||||
// 重置密码表单
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
}, 100);
|
||||
} else {
|
||||
passwordError.value = res.message || "密码修改失败";
|
||||
}
|
||||
} else if (isEdit.value) {
|
||||
await editUser(form.value.id, form.value);
|
||||
ElMessage.success({
|
||||
message: "更新成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
dialogVisible.value = false;
|
||||
fetchUsers();
|
||||
} else {
|
||||
await addUser(form.value);
|
||||
ElMessage.success({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
dialogVisible.value = false;
|
||||
fetchUsers();
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 显示具体的错误信息
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
if (dialogTitle.value === "修改密码") {
|
||||
passwordError.value = errorMsg;
|
||||
} else {
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 测试ElMessage功能
|
||||
const testElMessage = () => {
|
||||
console.log("测试 ElMessage");
|
||||
console.log("ElMessage 类型:", typeof ElMessage);
|
||||
console.log("ElMessage 对象:", ElMessage);
|
||||
console.log("ElMessage.success:", ElMessage.success);
|
||||
console.log("ElMessage.error:", ElMessage.error);
|
||||
|
||||
try {
|
||||
ElMessage("这是普通消息");
|
||||
setTimeout(() => {
|
||||
ElMessage.success("这是成功消息");
|
||||
}, 500);
|
||||
setTimeout(() => {
|
||||
ElMessage.error("这是错误消息");
|
||||
}, 1000);
|
||||
setTimeout(() => {
|
||||
ElMessage.warning("这是警告消息");
|
||||
}, 1500);
|
||||
setTimeout(() => {
|
||||
ElMessage.info("这是信息消息");
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error("ElMessage 调用失败:", e);
|
||||
alert("ElMessage 调用失败: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
//修改密码
|
||||
const handleChangePassword = async (user: User) => {
|
||||
dialogTitle.value = "修改密码";
|
||||
isEdit.value = true;
|
||||
passwordError.value = "";
|
||||
form.value = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -141,4 +490,8 @@ const handleDelete = (user: User) => {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user