批量更新,增加用户管理

This commit is contained in:
李志强 2025-11-01 17:57:34 +08:00
parent 186dcb86d9
commit bfba8d7ad6
28 changed files with 2307 additions and 2377 deletions

0
43.133.71.191 Normal file
View File

View File

@ -17,7 +17,6 @@
"@wangeditor/table-module": "^1.1.4",
"axios": "^1.11.0",
"chart.js": "^4.5.1",
"element-plus": "^2.10.7",
"marked": "^16.4.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",

View File

@ -1,11 +1,3 @@
// 主题色变量SCSS变量 - 默认为亮色主题
$primary-color: #3498db;
$secondary-color: #2ecc71;
$accent-color: #ffcc00;
$background-color: #f8f9fa;
$text-color: #333;
$border-radius: 4px;
// 通用字体
$font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
@ -20,165 +12,6 @@ $box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 过渡
$transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
// ==========================================
// 主题系统 - 亮色和暗色主题
// 使用 data-theme="light" data-theme="dark" 来切换主题
// ==========================================
// 默认亮色主题
:root[data-theme="light"],
:root:not([data-theme]) {
// 主题标识
--theme-mode: light;
// 主要颜色
--primary-color: #1890ff;
--primary-hover: #40a9ff;
--primary-active: #096dd9;
--secondary-color: #2ecc71;
--secondary-hover: #27ae60;
--secondary-active: #229954;
--accent-color: #ffcc00;
--accent-hover: #e6b800;
// 背景颜色
--background-color: #ffffff;
--background-secondary: #f8f9fa;
--background-tertiary: #f0f2f5;
--background-hover: #e9ecef;
// 文本颜色
--text-color: #212529;
--text-secondary: #6c757d;
--text-tertiary: #adb5bd;
--text-inverse: #ffffff;
// 边框颜色
--border-color: #dee2e6;
--border-color-hover: #ced4da;
--border-color-active: #adb5bd;
// 状态颜色
--success-color: #28a745;
--success-bg: #d4edda;
--warning-color: #ffc107;
--warning-bg: #fff3cd;
--error-color: #dc3545;
--error-bg: #f8d7da;
--info-color: #17a2b8;
--info-bg: #d1ecf1;
// 组件颜色
--card-bg: #ffffff;
--card-bg1: #1890ff;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--sidebar-bg: #f8f9fa;
--sidebar-hover: #e9ecef;
--header-bg: #ffffff;
// Hero 渐变亮色主题 - 现代蓝色渐变
--hero-gradient-start: #667eea;
--hero-gradient-end: #764ba2;
// 阴影
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.12);
// 圆角
--border-radius: 4px;
--border-radius-sm: 2px;
--border-radius-lg: 8px;
--border-radius-xl: 12px;
--border-radius-full: 9999px;
// 过渡
--transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
--transition-fast: all 0.15s ease-in-out;
--transition-slow: all 0.5s ease-in-out;
// 字体
--font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
// 暗色主题
:root[data-theme="dark"] {
// 主题标识
--theme-mode: dark;
// 主要颜色
--primary-color: #409EFF;
--primary-hover: #66b1ff;
--primary-active: #337ecc;
--secondary-color: #58d68d;
--secondary-hover: #2ecc71;
--secondary-active: #27ae60;
--accent-color: #f7dc6f;
--accent-hover: #f4d03f;
// 背景颜色
--background-color: #1a1a1a;
--background-secondary: #2d2d2d;
--background-tertiary: #3d3d3d;
--background-hover: #4a4a4a;
// 文本颜色
--text-color: #e9ecef;
--text-secondary: #adb5bd;
--text-tertiary: #6c757d;
--text-inverse: #212529;
// 边框颜色
--border-color: #404040;
--border-color-hover: #4d4d4d;
--border-color-active: #5a5a5a;
// 状态颜色
--success-color: #58d68d;
--success-bg: #1e3c24;
--warning-color: #f7dc6f;
--warning-bg: #4d3e1a;
--error-color: #ec7063;
--error-bg: #3c2626;
--info-color: #5dade2;
--info-bg: #1e2a33;
// 组件颜色
--card-bg: #2d2d2d;
--card-bg1: #2d2d2d;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
--sidebar-bg: #1f1f1f;
--sidebar-hover: #3d3d3d;
--header-bg: #2d2d2d;
// Hero 渐变暗色主题 - 深紫色渐变
--hero-gradient-start: #4a5568;
--hero-gradient-end: #2d3748;
// 阴影
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.6);
// 圆角
--border-radius: 4px;
--border-radius-sm: 2px;
--border-radius-lg: 8px;
--border-radius-xl: 12px;
--border-radius-full: 9999px;
// 过渡
--transition-base: all 0.3s cubic-bezier(.25,.8,.25,1);
--transition-fast: all 0.15s ease-in-out;
--transition-slow: all 0.5s ease-in-out;
// 字体
--font-family-base: 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
body {
font-family: var(--font-family-base);
color: var(--text-color);
@ -291,251 +124,3 @@ a {
html {
scroll-behavior: smooth;
}
// ==========================================
// Element Plus 主题覆盖 - 支持主题切换
// ==========================================
// 亮色主题下的 Element Plus 组件
:root[data-theme="light"],
:root:not([data-theme]) {
--el-bg-color: #ffffff;
--el-bg-color-page: #f2f3f5;
--el-text-color-primary: #303133;
--el-text-color-regular: #606266;
--el-text-color-secondary: #909399;
--el-text-color-placeholder: #c0c4cc;
--el-text-color-disabled: #c0c4cc;
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f6fc;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
// 组件背景
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;
--el-fill-color-darker: #e6e8eb;
--el-fill-color-blank: #ffffff;
// 组件颜色
--el-color-primary: #1890ff;
--el-color-success: #2ecc71;
--el-color-warning: #ffcc00;
--el-color-danger: #dc3545;
--el-color-error: #dc3545;
--el-color-info: #909399;
// 阴影
--el-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
--el-box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
--el-box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
--el-box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
// 覆盖 Element Plus 表格样式亮色主题
--el-table-bg-color: #ffffff;
--el-table-header-bg-color: #fafafa;
--el-table-header-text-color: #909399;
--el-table-row-hover-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
--el-table-border: 1px solid #ebeef5;
// 对话框
--el-overlay-color-light: rgba(0, 0, 0, 0.5);
--el-overlay-color-lighter: rgba(0, 0, 0, 0.3);
--el-mask-color: rgba(0, 0, 0, 0.5);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.2);
}
// 暗色主题下的 Element Plus 组件
:root[data-theme="dark"] {
--el-bg-color: #2d2d2d;
--el-bg-color-page: #1a1a1a;
--el-text-color-primary: #e9ecef;
--el-text-color-regular: #adb5bd;
--el-text-color-secondary: #6c757d;
--el-text-color-placeholder: #495057;
--el-text-color-disabled: #495057;
--el-border-color: #404040;
--el-border-color-light: #404040;
--el-border-color-lighter: #3d3d3d;
--el-border-color-extra-light: #3d3d3d;
--el-border-color-dark: #4a4a4a;
--el-border-color-darker: #4a4a4a;
// 组件背景
--el-fill-color: #3d3d3d;
--el-fill-color-light: #3d3d3d;
--el-fill-color-lighter: #3d3d3d;
--el-fill-color-extra-light: #3d3d3d;
--el-fill-color-dark: #4a4a4a;
--el-fill-color-darker: #4a4a4a;
--el-fill-color-blank: #2d2d2d;
// 组件颜色
--el-color-primary: #409EFF;
--el-color-success: #58d68d;
--el-color-warning: #f7dc6f;
--el-color-danger: #ec7063;
--el-color-error: #ec7063;
--el-color-info: #adb5bd;
// 阴影暗色主题使用更深的阴影
--el-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
--el-box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.5);
--el-box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.4);
--el-box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.6);
// 覆盖 Element Plus 表格样式
--el-table-bg-color: var(--card-bg);
--el-table-header-bg-color: var(--header-bg);
--el-table-header-text-color: var(--text-color);
--el-table-row-hover-bg-color: var(--background-hover);
--el-table-border-color: var(--border-color);
--el-table-border: 1px solid var(--border-color);
// 对话框
--el-overlay-color-light: rgba(0, 0, 0, 0.7);
--el-overlay-color-lighter: rgba(0, 0, 0, 0.5);
--el-mask-color: rgba(0, 0, 0, 0.8);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
}
// ==========================================
// Element Plus 组件强制样式覆盖
// ==========================================
// 亮色和暗色主题统一样式
.el-table {
background-color: var(--card-bg);
color: var(--text-color);
transition: var(--transition-base);
th.el-table__cell {
background-color: var(--header-bg);
color: var(--text-color);
border-bottom-color: var(--border-color);
}
td.el-table__cell {
background-color: var(--card-bg);
color: var(--text-color);
border-bottom-color: var(--border-color);
}
.el-table__row:hover {
background-color: var(--background-hover) !important;
}
.el-table__row.current-row {
background-color: var(--background-hover) !important;
}
.el-table__border {
border-color: var(--border-color);
}
}
.el-dialog {
background-color: var(--card-bg);
color: var(--text-color);
transition: var(--transition-base);
.el-dialog__header {
border-bottom: 1px solid var(--border-color);
.el-dialog__title {
color: var(--text-color);
}
}
.el-dialog__body {
color: var(--text-color);
}
.el-dialog__footer {
border-top: 1px solid var(--border-color);
}
}
.el-descriptions {
.el-descriptions__label {
color: var(--text-secondary);
}
.el-descriptions__content {
color: var(--text-color);
}
.el-descriptions__border {
border-color: var(--border-color);
}
}
// 表单
.el-form {
.el-form-item__label {
color: var(--text-color);
}
.el-input__inner,
.el-textarea__inner {
background-color: var(--background-secondary);
color: var(--text-color);
border-color: var(--border-color);
&::placeholder {
color: var(--text-tertiary);
}
}
}
// 按钮
.el-button {
background-color: var(--background-secondary);
color: var(--text-color);
border-color: var(--border-color);
transition: var(--transition-fast);
&:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
&.el-button--primary {
background-color: var(--primary-color);
color: var(--text-inverse);
border-color: var(--primary-color);
&:hover {
background-color: var(--primary-hover);
}
}
}
// 标签页
.el-tabs {
.el-tabs__header {
background-color: var(--card-bg);
}
.el-tabs__item {
color: var(--text-secondary);
&.is-active {
color: var(--primary-color);
}
&:hover {
color: var(--primary-color);
}
}
.el-tabs__nav-wrap::after {
background-color: var(--border-color);
}
}

View File

@ -368,14 +368,6 @@ onMounted(() => {
}
}
.sidebar-menu :deep(.el-menu-item.is-active) {
background-color: var(--background-hover) !important;
color: var(--primary-color) !important;
i {
color: var(--primary-color) !important;
}
}
.user-nickname {
font-size: 14px;
font-weight: bold;

View File

@ -156,14 +156,6 @@ const handleSubMenuClick = (item: SubMenuItem) => {
}
}
&.is-active {
background-color: var(--background-hover) !important;
color: var(--primary-color) !important;
i {
color: var(--primary-color) !important;
}
}
}
}

View File

@ -32,7 +32,7 @@ export default {
this.detectDevice();
this.initAuth();
this.initRouteGuard();
this.initTheme();
// this.initTheme();
},
initAuth() {

919
pc/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -1,5 +1,4 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
</script>
<template>

View File

@ -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,
});
}

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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;

View File

@ -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()

View File

@ -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')
}
// 检查认证状态

View File

@ -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);
}

View File

@ -207,4 +207,8 @@ function closeAllTabs() {
}
}
}
:deep(.el-menu){
border-right: none !important;
}
</style>

View File

@ -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>

View File

@ -1013,10 +1013,6 @@ onMounted(() => {
border-color: var(--border-color);
color: var(--text-color-primary);
&.is-active {
background-color: var(--primary-color);
color: white;
}
}
}

View File

@ -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')
// tenantlocalStorage便
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>

View File

@ -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>

View File

@ -261,7 +261,7 @@ const fetchMenus = async () => {
const result = await getAllMenus();
if (result.success) {
// getAllMenusdatakey线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;

View File

@ -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;

View File

@ -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>

View File

@ -2,9 +2,10 @@ package controllers
import (
"encoding/json"
"fmt"
"server/models"
"time"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
@ -16,16 +17,16 @@ type AuthController struct {
beego.Controller
}
// Login 处理登录请求(支持租户模式,使用租户名称)
// Login 处理登录请求
func (c *AuthController) Login() {
var username, password, tenantName string
// 优先尝试从URL参数获取Apifox测试方式
// 优先尝试从URL参数获取
username = c.GetString("username")
password = c.GetString("password")
tenantName = c.GetString("tenant_name")
// 如果URL参数为空尝试从JSON请求体获取(前端方式)
// 如果URL参数为空尝试从JSON请求体获取
if username == "" || password == "" || tenantName == "" {
var loginData struct {
Username string `json:"username"`
@ -60,28 +61,17 @@ func (c *AuthController) Login() {
return
}
// 添加日志调试
fmt.Println("接收到的登录请求:")
fmt.Println("用户名:", username)
fmt.Println("租户名称:", tenantName)
// 验证用户(先验证租户,再验证租户下的用户)
fmt.Println("开始验证用户:", username, "租户:", tenantName)
user, err := models.ValidateUser(username, password, tenantName)
fmt.Println("验证结果:", err)
if user != nil {
fmt.Println("用户信息ID=", user.Id, "Username=", user.Username, "TenantId=", user.TenantId)
}
if err != nil {
// 登录失败
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": err.Error(),
"data": nil,
}
} else {
// 使用models包中的GenerateToken函数生成token包含租户ID
// 使用models包中的GenerateToken函数生成token
tokenString, err := models.GenerateToken(user.Id, user.Username, user.TenantId)
if err != nil {
@ -91,7 +81,11 @@ func (c *AuthController) Login() {
"data": nil,
}
} else {
// 登录成功
// 登录成功写当前时间到last_login_time并增加login_count
loginTime := time.Now()
o := orm.NewOrm()
_, _ = o.Raw("UPDATE yz_users SET last_login_time = ?, login_count = IFNULL(login_count,0)+1 WHERE id = ?", loginTime, user.Id).Exec()
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "登录成功",
@ -114,85 +108,6 @@ func (c *AuthController) Login() {
c.ServeJSON()
}
// ResetPassword 重置用户密码(支持租户模式)
func (c *AuthController) ResetPassword() {
// 获取请求参数
username := c.GetString("username")
superPassword := c.GetString("superPassword")
tenantId, _ := c.GetInt("tenant_id", 0)
// 如果URL参数中没有租户ID尝试从JSON请求体获取
if tenantId == 0 {
var resetData struct {
Username string `json:"username"`
SuperPassword string `json:"superPassword"`
TenantId int `json:"tenant_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &resetData); err == nil {
username = resetData.Username
superPassword = resetData.SuperPassword
tenantId = resetData.TenantId
}
}
if tenantId <= 0 {
c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"}
c.ServeJSON()
return
}
// 调用模型方法
err := models.ResetPassword(username, superPassword, tenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()}
} else {
c.Data["json"] = map[string]interface{}{"success": true, "message": "密码重置成功"}
}
c.ServeJSON()
}
// ChangePassword 修改用户密码(支持租户模式)
func (c *AuthController) ChangePassword() {
// 获取请求参数
username := c.GetString("username")
oldPassword := c.GetString("oldPassword")
newPassword := c.GetString("newPassword")
tenantId, _ := c.GetInt("tenant_id", 0)
// 如果URL参数中没有租户ID尝试从JSON请求体获取
if tenantId == 0 {
var changeData struct {
Username string `json:"username"`
OldPassword string `json:"oldPassword"`
NewPassword string `json:"newPassword"`
TenantId int `json:"tenant_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &changeData); err == nil {
username = changeData.Username
oldPassword = changeData.OldPassword
newPassword = changeData.NewPassword
tenantId = changeData.TenantId
}
}
if tenantId <= 0 {
c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"}
c.ServeJSON()
return
}
// 调用模型方法
err := models.ChangePassword(username, oldPassword, newPassword, tenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()}
} else {
c.Data["json"] = map[string]interface{}{"success": true, "message": "密码修改成功"}
}
c.ServeJSON()
}
// Logout 处理登出请求
func (c *AuthController) Logout() {
// 在实际应用中这里需要处理JWT或Session的清除
@ -202,297 +117,3 @@ func (c *AuthController) Logout() {
}
c.ServeJSON()
}
// FindAllUsers 获取所有用户(支持按租户过滤)
func (c *AuthController) FindAllUsers() {
// 从查询参数获取租户ID可选
tenantId, _ := c.GetInt("tenant_id", 0)
users := models.FindAllUsers(tenantId)
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取用户列表成功",
"data": users,
}
c.ServeJSON()
}
// GetUserByUsername 通过用户名查询用户信息(支持租户模式)
func (c *AuthController) GetUserByUsername() {
// 获取请求参数中的用户名和租户ID
username := c.GetString("username")
tenantId, _ := c.GetInt("tenant_id", 0)
if username == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户名不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if tenantId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "租户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法查询用户
user, err := models.GetUserByUsername(username, tenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "查询用户失败: " + err.Error(),
"data": nil,
}
} else if user == nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户不存在",
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "查询成功",
"data": map[string]interface{}{
"id": user.Id,
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"nickname": user.Nickname,
"tenant_id": user.TenantId,
},
}
}
c.ServeJSON()
}
// AddUser 添加新用户(支持租户模式)
func (c *AuthController) AddUser() {
// 定义接收用户数据的结构体与JSON请求体对应
var userData struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
TenantId int `json:"tenant_id"`
}
// 解析请求体JSON数据
err := json.Unmarshal(c.Ctx.Input.RequestBody, &userData)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "请求参数格式错误: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 校验必要参数
if userData.Username == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户名不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if userData.Password == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "密码不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if userData.TenantId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "租户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法添加用户(传递参数,接收新用户对象)
newUser, err := models.AddUser(
userData.Username,
userData.Password,
userData.Email,
userData.Nickname,
userData.Avatar,
userData.TenantId,
)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "添加用户失败: " + err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户添加成功",
"data": map[string]interface{}{
"id": newUser.Id,
"username": newUser.Username,
"email": newUser.Email,
"nickname": newUser.Nickname,
"avatar": newUser.Avatar,
"tenant_id": newUser.TenantId,
},
}
}
c.ServeJSON()
}
// UpdateUser 更新用户信息(支持租户模式)
func (c *AuthController) UpdateUser() {
// 定义接收更新数据的结构体
var updateData struct {
Id int `json:"id"` // 必须包含用户ID用于定位要更新的用户
Username string `json:"username"` // 可选更新字段
Email string `json:"email"` // 可选更新字段
Nickname string `json:"nickname"` // 可选更新字段
Avatar string `json:"avatar"` // 可选更新字段
TenantId int `json:"tenant_id"` // 必须包含租户ID用于验证用户归属
}
// 解析请求体JSON
err := json.Unmarshal(c.Ctx.Input.RequestBody, &updateData)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "请求参数格式错误: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 校验必要参数用户ID和租户ID不能为空
if updateData.Id == 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if updateData.TenantId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "租户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法更新用户
updatedUser, err := models.UpdateUser(
updateData.Id,
updateData.Username,
updateData.Email,
updateData.Nickname,
updateData.Avatar,
updateData.TenantId,
)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "更新用户失败: " + err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户更新成功",
"data": map[string]interface{}{
"id": updatedUser.Id,
"username": updatedUser.Username,
"email": updatedUser.Email,
"nickname": updatedUser.Nickname,
"avatar": updatedUser.Avatar,
"tenant_id": updatedUser.TenantId,
},
}
}
c.ServeJSON()
}
// DeleteUser 删除用户(支持租户模式)
func (c *AuthController) DeleteUser() {
// 获取要删除的用户ID和租户ID从URL参数或请求体中获取
userId, err := c.GetInt("id") // 从URL参数获取如 /user?id=1
tenantId, _ := c.GetInt("tenant_id", 0)
if err != nil || tenantId == 0 {
// 若URL参数获取失败尝试从JSON请求体获取
var deleteData struct {
Id int `json:"id"`
TenantId int `json:"tenant_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &deleteData); err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户ID或租户ID获取失败: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
userId = deleteData.Id
tenantId = deleteData.TenantId
}
// 校验用户ID和租户ID
if userId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "无效的用户ID",
"data": nil,
}
c.ServeJSON()
return
}
if tenantId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "租户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法删除用户
err = models.DeleteUser(userId, tenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "删除用户失败: " + err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户删除成功",
"data": nil,
}
}
c.ServeJSON()
}

363
server/controllers/user.go Normal file
View File

@ -0,0 +1,363 @@
package controllers
import (
"encoding/json"
"server/models"
"github.com/beego/beego/v2/server/web"
)
type UserController struct {
web.Controller
}
// GetAllUsers 获取所有用户
func (c *UserController) GetAllUsers() {
tenantId, _ := c.GetInt("tenant_id", 0)
users := models.GetAllUsers(tenantId)
userList := make([]map[string]interface{}, 0)
for _, user := range users {
userList = append(userList, map[string]interface{}{
"id": user.Id,
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"nickname": user.Nickname,
"tenant_id": user.TenantId,
"lastLoginTime": user.LastLoginTime,
})
}
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "获取用户列表成功",
"data": userList,
}
c.ServeJSON()
}
// ChangePassword 修改用户密码
func (c *UserController) ChangePassword() {
// 从URL获取用户ID
userId, err := c.GetInt(":id")
if err != nil || userId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户ID无效",
"data": nil,
}
c.ServeJSON()
return
}
// 解析请求参数
var changeData struct {
OldPassword string `json:"oldPassword"`
NewPassword string `json:"newPassword"`
}
err = json.Unmarshal(c.Ctx.Input.RequestBody, &changeData)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "请求参数格式错误: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 校验参数
if changeData.OldPassword == "" || changeData.NewPassword == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "旧密码和新密码不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 先获取用户信息
user, err := models.GetUserInfo(userId, "", 0)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型方法修改密码
err = models.ChangePassword(user.Username, changeData.OldPassword, changeData.NewPassword, user.TenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "密码修改成功",
"data": nil,
}
}
c.ServeJSON()
}
// GetUserInfo 通过ID查询用户信息
func (c *UserController) GetUserInfo() {
// 从URL获取用户ID
userId, err := c.GetInt(":id")
if err != nil || userId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户ID无效",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法根据ID查询
user, err := models.GetUserInfo(userId, "", 0)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "查询成功",
"data": map[string]interface{}{
"id": user.Id,
"username": user.Username,
"email": user.Email,
"avatar": user.Avatar,
"nickname": user.Nickname,
"tenant_id": user.TenantId,
},
}
c.ServeJSON()
}
// AddUser 添加新用户
func (c *UserController) AddUser() {
// 定义接收用户数据的结构体
var userData struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
TenantId int `json:"tenant_id"`
}
// 解析请求体JSON数据
err := json.Unmarshal(c.Ctx.Input.RequestBody, &userData)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "请求参数格式错误: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 校验必要参数
if userData.Username == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户名不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if userData.Password == "" {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "密码不能为空",
"data": nil,
}
c.ServeJSON()
return
}
if userData.TenantId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "租户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法添加用户(传递参数,接收新用户对象)
newUser, err := models.AddUser(
userData.Username,
userData.Password,
userData.Email,
userData.Nickname,
userData.Avatar,
userData.TenantId,
)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "添加用户失败: " + err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户添加成功",
"data": map[string]interface{}{
"id": newUser.Id,
"username": newUser.Username,
"email": newUser.Email,
"nickname": newUser.Nickname,
"avatar": newUser.Avatar,
"tenant_id": newUser.TenantId,
},
}
}
c.ServeJSON()
}
// EditUser 更新用户信息
func (c *UserController) EditUser() {
// 定义接收更新数据的结构体
var updateData struct {
Id int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}
// 解析请求体JSON
err := json.Unmarshal(c.Ctx.Input.RequestBody, &updateData)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "请求参数格式错误: " + err.Error(),
"data": nil,
}
c.ServeJSON()
return
}
// 校验必要参数
if updateData.Id == 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "用户ID不能为空",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法更新用户
_, err = models.EditUser(
updateData.Id,
updateData.Username,
updateData.Email,
updateData.Nickname,
updateData.Avatar,
)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "更新用户失败: " + err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户更新成功",
}
}
c.ServeJSON()
}
// DeleteUser 删除用户
func (c *UserController) DeleteUser() {
// 从URL获取用户ID
userId, err := c.GetInt(":id")
if err != nil || userId <= 0 {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "无效的用户ID",
"data": nil,
}
c.ServeJSON()
return
}
// 调用模型层方法删除用户
err = models.DeleteUser(userId)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 1,
"message": "删除用户失败: " + err.Error(),
"data": nil,
}
} else {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "用户删除成功",
"data": nil,
}
}
c.ServeJSON()
}
// ResetPassword 重置用户密码(支持租户模式)
func (c *UserController) ResetPassword() {
// 获取请求参数
username := c.GetString("username")
superPassword := c.GetString("superPassword")
tenantId, _ := c.GetInt("tenant_id", 0)
// 如果URL参数中没有租户ID尝试从JSON请求体获取
if tenantId == 0 {
var resetData struct {
Username string `json:"username"`
SuperPassword string `json:"superPassword"`
TenantId int `json:"tenant_id"`
}
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &resetData); err == nil {
username = resetData.Username
superPassword = resetData.SuperPassword
tenantId = resetData.TenantId
}
}
if tenantId <= 0 {
c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"}
c.ServeJSON()
return
}
// 调用模型方法
err := models.ResetPassword(username, superPassword, tenantId)
if err != nil {
c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()}
} else {
c.Data["json"] = map[string]interface{}{"success": true, "message": "密码重置成功"}
}
c.ServeJSON()
}

View File

@ -5,6 +5,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/beego/beego/v2/client/orm"
"golang.org/x/crypto/scrypt"
@ -13,16 +14,18 @@ import (
_ "github.com/go-sql-driver/mysql"
)
// User 用户模型增加Salt字段存储每个用户的唯一盐值
// User 用户模型
type User struct {
Id int `orm:"auto"`
TenantId int `orm:"column(tenant_id);default(0)" json:"tenant_id"` // 租户ID
Username string // 用户名不再全局唯一而是在租户内唯一tenant_id + username 的组合唯一)
Password string // 存储加密后的密码
Salt string // 存储该用户的唯一盐值
Email string
Avatar string
Nickname string // 昵称字段,与数据库表中的列名匹配
Id int `orm:"auto"`
TenantId int `orm:"column(tenant_id);default(0)" json:"tenant_id"`
Username string
Password string
Salt string
Email string
Avatar string
Nickname string
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
LastLoginTime *time.Time `orm:"column(last_login_time);null;type(datetime)" json:"last_login_time"`
}
// TableName 设置表名默认为yz_users
@ -67,14 +70,14 @@ func verifyPassword(password, salt, storedHash string) bool {
return hash == storedHash
}
// ResetPassword 重置用户密码(支持租户模式)
// ResetPassword 重置用户密码
func ResetPassword(username, superPassword string, tenantId int) error {
if superPassword != "Lzq920103" {
return fmt.Errorf("超级密码错误")
}
o := orm.NewOrm()
user, err := GetUserByUsername(username, tenantId)
user, err := GetUserInfo(0, username, tenantId)
if err != nil {
return fmt.Errorf("用户不存在: %v", err)
}
@ -102,9 +105,9 @@ func ResetPassword(username, superPassword string, tenantId int) error {
return nil
}
// ChangePassword 修改用户密码(支持租户模式)
// ChangePassword 修改用户密码
func ChangePassword(username, oldPassword, newPassword string, tenantId int) error {
user, err := GetUserByUsername(username, tenantId)
user, err := GetUserInfo(0, username, tenantId)
if err != nil {
return err
}
@ -124,8 +127,8 @@ func ChangePassword(username, oldPassword, newPassword string, tenantId int) err
return err
}
// FindAllUsers 获取所有用户(支持按租户过滤)
func FindAllUsers(tenantId int) []*User {
// GetAllUsers 获取所有用户
func GetAllUsers(tenantId int) []*User {
o := orm.NewOrm()
var users []*User
if tenantId > 0 {
@ -144,23 +147,36 @@ func FindAllUsers(tenantId int) []*User {
return users
}
// GetUserByUsername 根据用户名获取用户(支持租户隔离)
func GetUserByUsername(username string, tenantId int) (*User, error) {
// GetUserInfo 根据用户ID或用户名获取用户
func GetUserInfo(userId int, username string, tenantId int) (*User, error) {
o := orm.NewOrm()
user := &User{}
// 使用原生 SQL 查询考虑租户ID
err := o.Raw("SELECT * FROM yz_users WHERE username = ? AND tenant_id = ?", username, tenantId).QueryRow(user)
if err == orm.ErrNoRows {
return nil, errors.New("用户不存在")
}
if err != nil {
return nil, err
var err error
if userId > 0 {
// 按ID查询
user.Id = userId
err = o.Read(user)
if err == orm.ErrNoRows {
return nil, errors.New("用户不存在")
}
if err != nil {
return nil, err
}
} else {
// 按用户名和租户ID查询
err = o.Raw("SELECT * FROM yz_users WHERE username = ? AND tenant_id = ?", username, tenantId).QueryRow(user)
if err == orm.ErrNoRows {
return nil, errors.New("用户不存在")
}
if err != nil {
return nil, err
}
}
return user, nil
}
// ValidateUser 验证用户登录信息(支持租户模式,根据租户名称)
// 先验证租户是否存在且有效,再验证租户下的用户
// ValidateUser 验证用户登录信息
func ValidateUser(username, password string, tenantName string) (*User, error) {
o := orm.NewOrm()
@ -179,12 +195,6 @@ func ValidateUser(username, password string, tenantName string) (*User, error) {
return nil, fmt.Errorf("查询租户失败: %v", err)
}
// 检查租户是否被删除(软删除)
if tenant.DeleteTime != nil {
// delete_time 不为 NULL说明已被删除
return nil, errors.New("租户已被删除")
}
// 检查租户状态
if tenant.Status == "disabled" {
return nil, errors.New("租户已被禁用")
@ -197,7 +207,7 @@ func ValidateUser(username, password string, tenantName string) (*User, error) {
tenantId := tenant.Id
// 2. 获取租户下的用户
user, err := GetUserByUsername(username, tenantId)
user, err := GetUserInfo(0, username, tenantId)
if err != nil {
// 用户不存在或查询失败
return nil, err
@ -210,7 +220,7 @@ func ValidateUser(username, password string, tenantName string) (*User, error) {
return nil, errors.New("密码不正确")
}
// AddUser 向数据库添加新用户(模型层核心方法,支持租户模式)
// AddUser 向数据库添加新用户
func AddUser(username, password, email, nickname, avatar string, tenantId int) (*User, error) {
// 1. 验证租户是否存在且有效
o := orm.NewOrm()
@ -224,7 +234,7 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
}
// 2. 检查该租户下用户是否已存在(避免用户名重复,但不同租户可以有相同的用户名)
existingUser, err := GetUserByUsername(username, tenantId)
existingUser, err := GetUserInfo(0, username, tenantId)
if err == nil && existingUser != nil {
return nil, fmt.Errorf("该租户下用户名已存在")
}
@ -265,23 +275,23 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
return user, nil
}
// UpdateUser 更新用户信息(模型层方法,支持租户模式)
func UpdateUser(id int, username, email, nickname, avatar string, tenantId int) (*User, error) {
// 1. 根据ID和租户ID查询用户是否存在(确保只能更新自己租户下的用户)
// EditUser 更新用户信息
func EditUser(id int, username, email, nickname, avatar string) (*User, error) {
// 根据ID查询用户
o := orm.NewOrm()
user := &User{}
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user)
err := o.Raw("SELECT * FROM yz_users WHERE id = ?", id).QueryRow(user)
if err == orm.ErrNoRows {
return nil, fmt.Errorf("用户不存在或不属于该租户")
return nil, fmt.Errorf("用户不存在")
}
if err != nil {
return nil, fmt.Errorf("查询用户失败: %v", err)
}
// 2. 仅更新非空字段(避免覆盖原有值)
// 仅更新非空字段(避免覆盖原有值)
if username != "" {
// 若更新用户名,需检查同一租户下新用户名是否已被占用
existingUser, _ := GetUserByUsername(username, tenantId)
existingUser, _ := GetUserInfo(0, username, user.TenantId)
if existingUser != nil && existingUser.Id != id {
return nil, fmt.Errorf("该租户下用户名已被占用")
}
@ -297,7 +307,7 @@ func UpdateUser(id int, username, email, nickname, avatar string, tenantId int)
user.Avatar = avatar
}
// 3. 执行数据库更新
// 执行数据库更新
_, err = o.Update(user)
if err != nil {
return nil, fmt.Errorf("数据库更新失败: %v", err)
@ -306,23 +316,24 @@ func UpdateUser(id int, username, email, nickname, avatar string, tenantId int)
return user, nil
}
// DeleteUser 根据ID删除用户(模型层方法,支持租户模式)
func DeleteUser(id int, tenantId int) error {
// DeleteUser 根据ID进行软删除
func DeleteUser(id int) error {
o := orm.NewOrm()
// 先查询用户是否存在且属于指定租户
user := &User{}
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user)
err := o.Raw("SELECT * FROM yz_users WHERE id = ?", id).QueryRow(user)
if err == orm.ErrNoRows {
return fmt.Errorf("用户不存在或不属于该租户")
return fmt.Errorf("用户不存在")
}
if err != nil {
return fmt.Errorf("查询用户失败: %v", err)
}
// 执行删除操作
_, err = o.Delete(user)
// 设置删除时间为当前时间(软删除)
now := time.Now()
user.DeleteTime = &now
_, err = o.Update(user, "DeleteTime")
if err != nil {
return fmt.Errorf("数据库删除失败: %v", err)
return fmt.Errorf("设置删除时间失败: %v", err)
}
return nil
}

View File

@ -56,17 +56,17 @@ func init() {
beego.Router("/admin", &controllers.AdminController{})
//用户相关
beego.Router("/api/users", &controllers.AuthController{}, "get:FindAllUsers")
beego.Router("/api/users/:id", &controllers.AuthController{}, "get:GetUserByUsername")
beego.Router("/api/users", &controllers.AuthController{}, "post:AddUser")
beego.Router("/api/users/:id", &controllers.AuthController{}, "put:UpdateUser")
beego.Router("/api/users/:id", &controllers.AuthController{}, "delete:DeleteUser")
beego.Router("/api/allUsers", &controllers.UserController{}, "get:GetAllUsers")
beego.Router("/api/user/:id", &controllers.UserController{}, "get:GetUserInfo")
beego.Router("/api/addUser", &controllers.UserController{}, "post:AddUser")
beego.Router("/api/editUser/:id", &controllers.UserController{}, "post:EditUser")
beego.Router("/api/deleteUser/:id", &controllers.UserController{}, "delete:DeleteUser")
beego.Router("/api/changePassword/:id", &controllers.UserController{}, "post:ChangePassword")
beego.Router("/api/reset-password", &controllers.UserController{}, "post:ResetPassword")
// 认证路由
beego.Router("/api/login", &controllers.AuthController{}, "post:Login")
beego.Router("/api/logout", &controllers.AuthController{}, "post:Logout")
beego.Router("/api/reset-password", &controllers.AuthController{}, "post:ResetPassword")
beego.Router("/api/change-password", &controllers.AuthController{}, "post:ChangePassword")
// 手动配置菜单路由以匹配前台的 API 路径
beego.Router("/api/menu", &controllers.MenuController{}, "post:CreateMenu")
@ -114,4 +114,5 @@ func init() {
beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")
beego.Router("/api/program-infos/public", &controllers.ProgramInfoController{}, "get:GetProgramInfosPublic")
beego.Router("/api/files/public", &controllers.FileController{}, "get:GetFilesPublic")
}