Add 'frontend/' from commit '94f710850fa74ad3aaca5b65318f2fec9e1a2cdf'
git-subtree-dir: frontend git-subtree-mainline:3f114b2cc3git-subtree-split:94f710850f
This commit is contained in:
12
frontend/src/App.vue
Normal file
12
frontend/src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
30
frontend/src/api/authService.ts
Normal file
30
frontend/src/api/authService.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import apiClient from './axiosInstance';
|
||||
import type { TokenResponse } from './types';
|
||||
|
||||
const API_BASE_URL = '/auth';
|
||||
|
||||
export const authService = {
|
||||
async login(username: string, password: string): Promise<TokenResponse> {
|
||||
try {
|
||||
const response = await apiClient.post(`${API_BASE_URL}/login`, { username, password });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.data?.detail?.[0]?.msg) {
|
||||
throw new Error(error.response.data.detail[0].msg);
|
||||
}
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
},
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||
try {
|
||||
const response = await apiClient.post(`${API_BASE_URL}/refresh`, { refresh_token: refreshToken });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.data?.detail?.[0]?.msg) {
|
||||
throw new Error(error.response.data.detail[0].msg);
|
||||
}
|
||||
throw new Error('Token refresh failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
70
frontend/src/api/axiosInstance.ts
Normal file
70
frontend/src/api/axiosInstance.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import axios from 'axios';
|
||||
import { userStore } from '../store/userStore';
|
||||
import router from '../router';
|
||||
|
||||
// 创建axios实例
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
apiClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
const store = userStore();
|
||||
|
||||
// 只有已登录状态才添加Authorization头
|
||||
if (store.isLoggedIn) {
|
||||
const accessToken = store.accessToken;
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const store = userStore(); // 获取用户状态
|
||||
|
||||
// 如果未登录,直接返回错误
|
||||
if (!store.isLoggedIn) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const originalRequest = error.config;
|
||||
|
||||
// 如果401错误且不是刷新token的请求
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
// 尝试刷新 token
|
||||
await store.refreshTokenMethod();
|
||||
|
||||
// 更新请求头并重试原始请求
|
||||
originalRequest.headers.Authorization = `Bearer ${store.accessToken}`;
|
||||
return apiClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
// 刷新 token 失败,清除登录状态并跳转登录页
|
||||
store.logout();
|
||||
router.push({ name: 'login' });
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
// 非 401 错误或已重试过,直接返回错误
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
41
frontend/src/api/types.ts
Normal file
41
frontend/src/api/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface UserResponse {
|
||||
id: number;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UserCreate {
|
||||
username: string;
|
||||
password: string;
|
||||
role?: UserRole;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UserUpdate {
|
||||
username?: string;
|
||||
role?: UserRole;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
access_token_exp: number;
|
||||
refresh_token_exp: number;
|
||||
}
|
||||
|
||||
export interface HTTPValidationError {
|
||||
detail: ValidationError[];
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
loc: (string | number)[];
|
||||
msg: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type UserRole = 'system_admin' | 'admin' | 'user';
|
||||
32
frontend/src/api/userService.ts
Normal file
32
frontend/src/api/userService.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import apiClient from './axiosInstance';
|
||||
import type { UserResponse, UserCreate, UserUpdate } from './types';
|
||||
|
||||
const API_BASE_URL = '/users';
|
||||
|
||||
export const userService = {
|
||||
async getUsers(page = 1, limit = 100, role?: string): Promise<UserResponse[]> {
|
||||
const response = await apiClient.get(API_BASE_URL, {
|
||||
params: { page, limit, role }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createUser(userData: UserCreate): Promise<UserResponse> {
|
||||
const response = await apiClient.post(API_BASE_URL, userData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateUser(userId: number, userData: UserUpdate): Promise<UserResponse> {
|
||||
const response = await apiClient.put(`${API_BASE_URL}/${userId}`, userData);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteUser(userId: number): Promise<void> {
|
||||
await apiClient.delete(`${API_BASE_URL}/${userId}`);
|
||||
},
|
||||
|
||||
async getUser(userId: number): Promise<UserResponse> {
|
||||
const response = await apiClient.get(`${API_BASE_URL}/${userId}`);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
118
frontend/src/components/EditUserDialog.vue
Normal file
118
frontend/src/components/EditUserDialog.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="props.mode === 'create' ? '创建用户' : '修改用户信息'">
|
||||
<el-form :model="editForm">
|
||||
<el-form-item v-if="props.mode === 'edit'" label="用户ID">
|
||||
<el-input v-model="editForm.id" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="editForm.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="editForm.role">
|
||||
<el-option label="系统管理员" value="system_admin" />
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通用户" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="props.mode === 'create'" label="密码">
|
||||
<el-input v-model="editForm.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="editForm.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { UserResponse, UserCreate } from '@/api/types'
|
||||
import { userService } from '@/api/userService'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'create' | 'edit',
|
||||
userId?: number
|
||||
}>()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const options = ref<{ userId?: number, mode?: 'create' | 'edit', onConfirm?: () => void }>({})
|
||||
const editForm = ref<UserResponse & { password?: string }>({
|
||||
id: 0,
|
||||
username: '',
|
||||
role: 'user',
|
||||
description: '',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const open = async (opts: { userId?: number, mode?: 'create' | 'edit', onConfirm?: () => void }) => {
|
||||
options.value = opts
|
||||
if (options.value.mode === 'edit' && options.value.userId) {
|
||||
const user = await userService.getUser(options.value.userId)
|
||||
editForm.value = { ...user }
|
||||
} else {
|
||||
editForm.value = {
|
||||
id: 0,
|
||||
username: '',
|
||||
role: 'user',
|
||||
description: '',
|
||||
created_at: '',
|
||||
updated_at: ''
|
||||
}
|
||||
}
|
||||
if (options.value.onConfirm) {
|
||||
emit('confirm', options.value.onConfirm)
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
if (props.mode === 'create') {
|
||||
// 调用创建用户API
|
||||
const { id, created_at, updated_at, ...createData } = editForm.value
|
||||
if (!createData.password) {
|
||||
throw new Error('密码不能为空')
|
||||
}
|
||||
await userService.createUser({
|
||||
username: createData.username,
|
||||
password: createData.password,
|
||||
role: createData.role,
|
||||
description: createData.description
|
||||
} as UserCreate)
|
||||
} else {
|
||||
// 调用更新用户API
|
||||
const { id, created_at, updated_at, ...updateData } = editForm.value
|
||||
await userService.updateUser(id, updateData)
|
||||
}
|
||||
emit('confirm', editForm.value)
|
||||
if (options.value.onConfirm) {
|
||||
options.value.onConfirm()
|
||||
}
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: props.mode === 'create' ? '用户创建成功' : '用户信息更新成功'
|
||||
})
|
||||
dialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: props.mode === 'create' ? '用户创建失败' : '用户信息更新失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
handleConfirm
|
||||
})
|
||||
</script>
|
||||
74
frontend/src/components/UserTable.vue
Normal file
74
frontend/src/components/UserTable.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="100" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="role" label="角色" />
|
||||
<el-table-column prop="description" label="描述" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ new Date(scope.row.created_at).toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ new Date(scope.row.updated_at).toLocaleString() }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" v-if="isAdmin">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleEdit(scope.row)">修改</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<EditUserDialog ref="editDialogRef" mode="edit" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { userService } from '../api/userService';
|
||||
import { userStore } from '../store/userStore';
|
||||
import type { UserResponse } from '../api/types';
|
||||
import EditUserDialog from './EditUserDialog.vue';
|
||||
|
||||
const store = userStore();
|
||||
const users = ref<UserResponse[]>([]);
|
||||
const isAdmin = store.isAdmin;
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const data = await userService.getUsers();
|
||||
console.log('获取用户列表成功:', data);
|
||||
users.value = data;
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const editDialogRef = ref<InstanceType<typeof EditUserDialog>>();
|
||||
|
||||
const handleEdit = (user: UserResponse) => {
|
||||
editDialogRef.value?.open({
|
||||
userId: user.id,
|
||||
mode: 'edit',
|
||||
onConfirm: () => fetchUsers()
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (user: UserResponse) => {
|
||||
try {
|
||||
await userService.deleteUser(user.id);
|
||||
await fetchUsers();
|
||||
} catch (error) {
|
||||
console.error('删除用户失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
fetchUsers
|
||||
});
|
||||
</script>
|
||||
46
frontend/src/main.ts
Normal file
46
frontend/src/main.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import apiClient from './api/axiosInstance'
|
||||
import { userStore } from './store/userStore'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 配置路由和状态管理
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(ElementPlus)
|
||||
|
||||
// 初始化userStore
|
||||
const store = userStore()
|
||||
|
||||
// 使用插件进行持久化,监听 store 变化并保存到本地存储
|
||||
store.$subscribe((_, state) => {
|
||||
localStorage.setItem('authStore', JSON.stringify(state));
|
||||
});
|
||||
|
||||
// 监听 action 执行,登录和刷新令牌后保存状态到本地存储
|
||||
store.$onAction(({ name, store, after }) => {
|
||||
if (name === 'login' || name === 'refreshTokenMethod') {
|
||||
after(() => {
|
||||
localStorage.setItem('authStore', JSON.stringify(store.$state));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时从本地存储恢复状态
|
||||
const persistedState = JSON.parse(localStorage.getItem('authStore') || '{}');
|
||||
if (persistedState.accessToken && persistedState.refreshTokenToken && persistedState.role && persistedState.username && persistedState.userId !== null) {
|
||||
store.$patch(persistedState);
|
||||
}
|
||||
|
||||
// 设置axios拦截器
|
||||
apiClient
|
||||
|
||||
app.mount('#app')
|
||||
44
frontend/src/router/index.ts
Normal file
44
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { userStore } from '@/store/userStore'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/LoginPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/manage',
|
||||
name: 'manage',
|
||||
component: () => import('@/views/ManagePage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/manage'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
//const userStore = useUserStore()
|
||||
|
||||
// 如果路由需要认证但用户未登录
|
||||
if (to.meta.requiresAuth && !userStore().isLoggedIn) {
|
||||
next({ name: 'login' })
|
||||
}
|
||||
// 如果用户已登录但访问登录页
|
||||
else if (to.name === 'login' && userStore().isLoggedIn) {
|
||||
next({ name: 'manage' })
|
||||
}
|
||||
// 其他情况正常导航
|
||||
else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
67
frontend/src/store/userStore.ts
Normal file
67
frontend/src/store/userStore.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { authService } from '../api/authService';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
// 定义 store,包含用户认证相关状态和方法
|
||||
export const userStore = defineStore('user', () => {
|
||||
const accessToken = ref('');
|
||||
const refreshToken = ref('');
|
||||
const role = ref('');
|
||||
const username = ref('');
|
||||
const id = ref<number | null>(null);
|
||||
|
||||
// 计算属性,用于快速判断用户是否登录
|
||||
const isLoggedIn = computed(() => !!accessToken.value);
|
||||
const isAdmin = computed(() => ['system_admin', 'admin'].includes(role.value));
|
||||
|
||||
// 设置访问和刷新令牌的方法
|
||||
function setTokens(tokens: { access_token: string; refresh_token: string }) {
|
||||
accessToken.value = tokens.access_token;
|
||||
refreshToken.value = tokens.refresh_token;
|
||||
}
|
||||
|
||||
// 设置用户角色的方法
|
||||
function setRole(userRole: string) {
|
||||
role.value = userRole;
|
||||
}
|
||||
|
||||
// 用户登录方法,调用 authService.login 获取令牌并设置状态
|
||||
async function login(usernameParam: string, password: string) {
|
||||
try {
|
||||
const { access_token, refresh_token } = await authService.login(usernameParam, password);
|
||||
const decoded = jwtDecode<{ sub: string; role: string; username: string; id: string }>(access_token);
|
||||
setTokens({ access_token, refresh_token });
|
||||
setRole(decoded.role);
|
||||
username.value = decoded.username;
|
||||
id.value = parseInt(decoded.id);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
const err = new Error('登录失败');
|
||||
err.name = 'LoginError';
|
||||
err.cause = error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登出方法,清空所有状态
|
||||
async function logout() {
|
||||
accessToken.value = '';
|
||||
refreshToken.value = '';
|
||||
role.value = '';
|
||||
username.value = '';
|
||||
id.value = null;
|
||||
}
|
||||
|
||||
// 刷新访问令牌的方法,调用 authService.refreshToken 获取新令牌并设置状态
|
||||
async function refreshTokenMethod() {
|
||||
try {
|
||||
const { access_token, refresh_token } = await authService.refreshToken(refreshToken.value);
|
||||
setTokens({ access_token, refresh_token });
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return { accessToken, refreshTokenToken: refreshToken, role, username, userId: id, isLoggedIn, isAdmin, login, logout, refreshTokenMethod };
|
||||
});
|
||||
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
87
frontend/src/views/LoginPage.vue
Normal file
87
frontend/src/views/LoginPage.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h2 class="login-title">用户登录</h2>
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { userStore } from '@/store/userStore'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface LoginForm {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const store = userStore()
|
||||
|
||||
const form = ref<LoginForm>({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const { username, password } = form.value
|
||||
if (!username || !password) {
|
||||
throw new Error('用户名和密码不能为空')
|
||||
}
|
||||
await store.login(username, password)
|
||||
router.push({ name: 'manage' })
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
margin: 100px auto;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 50px 20px;
|
||||
padding: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
frontend/src/views/ManagePage.vue
Normal file
122
frontend/src/views/ManagePage.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="manage-page">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
class="nav-bar"
|
||||
>
|
||||
<el-menu-item index="1" class="nav-title">欢迎</el-menu-item>
|
||||
<el-menu-item index="2" class="user-info">
|
||||
<div class="user-detail">
|
||||
<span class="username">{{ userStore.username }}</span>
|
||||
<span class="role">({{ userStore.role }})</span>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="3">
|
||||
<el-button
|
||||
type="danger"
|
||||
class="logout-btn"
|
||||
@click="handleLogout"
|
||||
>
|
||||
登出
|
||||
</el-button>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<el-card class="page-container">
|
||||
<el-text class="system-title" type="primary" size="large" tag="h1">
|
||||
用户管理系统
|
||||
</el-text>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="create-btn"
|
||||
@click="handleCreateUser"
|
||||
v-if="userStore.isAdmin"
|
||||
>
|
||||
创建用户
|
||||
</el-button>
|
||||
<UserTable/>
|
||||
<EditUserDialog ref="editUserDialog" mode="create" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { userStore as store } from '@/store/userStore'
|
||||
import UserTable from '@/components/UserTable.vue'
|
||||
import EditUserDialog from '@/components/EditUserDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = store()
|
||||
|
||||
const editUserDialog = ref<InstanceType<typeof EditUserDialog>>()
|
||||
const userTableRef = ref<InstanceType<typeof UserTable>>()
|
||||
|
||||
const handleLogout = () => {
|
||||
userStore.logout()
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
|
||||
const handleCreateUser = () => {
|
||||
editUserDialog.value?.open({
|
||||
mode: 'create',
|
||||
onConfirm: () => {
|
||||
userTableRef.value?.fetchUsers()
|
||||
console.log('用户创建成功,刷新用户列表')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
userTableRef.value?.fetchUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manage-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-detail {
|
||||
margin-top: 4px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.role {
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.system-title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user