first commit
This commit is contained in:
15
frontend/src/App.vue
Normal file
15
frontend/src/App.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div id="app" class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 根组件逻辑
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
BIN
frontend/src/assets/ChatGPT.png
Normal file
BIN
frontend/src/assets/ChatGPT.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/src/assets/claude.png
Normal file
BIN
frontend/src/assets/claude.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
frontend/src/assets/grok.png
Normal file
BIN
frontend/src/assets/grok.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
35
frontend/src/components/icons/index.ts
Normal file
35
frontend/src/components/icons/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 用户图标
|
||||
export const UserIcon = {
|
||||
template: `
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
// 钥匙图标
|
||||
export const KeyIcon = {
|
||||
template: `
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4l-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
// 盾牌图标
|
||||
export const ShieldIcon = {
|
||||
template: `
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
||||
// 图表图标
|
||||
export const ChartIcon = {
|
||||
template: `
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
62
frontend/src/config/websites.ts
Normal file
62
frontend/src/config/websites.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// 网站URL配置
|
||||
// 支持通过Docker环境变量配置
|
||||
|
||||
export interface WebsiteConfig {
|
||||
name: string
|
||||
url: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// 从环境变量获取URL,如果没有配置则使用默认值
|
||||
const getWebsiteUrls = () => {
|
||||
// 检查是否在浏览器环境中
|
||||
if (typeof window !== 'undefined') {
|
||||
// 在客户端,使用Vite定义的全局变量
|
||||
return {
|
||||
claude: (window as any).__CLAUDE_URL__ || 'https://chat.micar9.com:8443',
|
||||
chatgpt: (window as any).__CHATGPT_URL__ || 'https://chat.openai.com',
|
||||
grok: (window as any).__GROK_URL__ || 'https://grok-mirror.micar9.com:8443'
|
||||
}
|
||||
}
|
||||
|
||||
// 在服务端,使用环境变量
|
||||
return {
|
||||
claude: process.env.CLAUDE_TARGET_URL || 'https://chat.micar9.com:8443',
|
||||
chatgpt: process.env.CHATGPT_TARGET_URL || 'https://chat.openai.com',
|
||||
grok: process.env.GROK_TARGET_URL || 'https://grok-mirror.micar9.com:8443'
|
||||
}
|
||||
}
|
||||
|
||||
// 网站配置
|
||||
export const websiteConfigs: Record<string, WebsiteConfig> = {
|
||||
claude: {
|
||||
name: 'Claude',
|
||||
url: getWebsiteUrls().claude,
|
||||
icon: '/src/assets/claude.png'
|
||||
},
|
||||
chatgpt: {
|
||||
name: 'ChatGPT',
|
||||
url: getWebsiteUrls().chatgpt,
|
||||
icon: '/src/assets/ChatGPT.png'
|
||||
},
|
||||
grok: {
|
||||
name: 'Grok',
|
||||
url: getWebsiteUrls().grok,
|
||||
icon: '/src/assets/grok.png'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网站URL的函数
|
||||
export const getWebsiteUrl = (site: string): string => {
|
||||
return websiteConfigs[site]?.url || ''
|
||||
}
|
||||
|
||||
// 获取网站配置的函数
|
||||
export const getWebsiteConfig = (site: string): WebsiteConfig | null => {
|
||||
return websiteConfigs[site] || null
|
||||
}
|
||||
|
||||
// 获取所有网站配置
|
||||
export const getAllWebsiteConfigs = (): WebsiteConfig[] => {
|
||||
return Object.values(websiteConfigs)
|
||||
}
|
||||
11
frontend/src/env.d.ts
vendored
Normal file
11
frontend/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string
|
||||
readonly VITE_APP_TITLE: string
|
||||
readonly VITE_APP_VERSION: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
44
frontend/src/main.ts
Normal file
44
frontend/src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Toast from 'vue-toastification'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { useAdminStore } from './stores/admin'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 配置 Pinia 状态管理
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// 初始化认证状态
|
||||
const authStore = useAuthStore()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
authStore.initAuth()
|
||||
adminStore.initAuth()
|
||||
|
||||
// 配置路由
|
||||
app.use(router)
|
||||
|
||||
// 配置 Toast 通知
|
||||
app.use(Toast, {
|
||||
position: 'top-right',
|
||||
timeout: 5000,
|
||||
closeOnClick: true,
|
||||
pauseOnFocusLoss: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
draggablePercent: 0.6,
|
||||
showCloseButtonOnHover: false,
|
||||
hideProgressBar: false,
|
||||
closeButton: 'button',
|
||||
icon: true,
|
||||
rtl: false,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
105
frontend/src/router/index.ts
Normal file
105
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { adminAuth } from '@/utils/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: { title: '首页' }
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
name: 'AdminLogin',
|
||||
component: () => import('@/views/AdminLogin.vue'),
|
||||
meta: { title: '管理员登录' }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { title: '仪表板', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: () => import('@/views/Admin.vue'),
|
||||
meta: { title: '管理后台', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/users',
|
||||
name: 'AdminUsers',
|
||||
component: () => import('@/views/AdminUsers.vue'),
|
||||
meta: { title: '用户管理', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/accounts',
|
||||
name: 'AdminAccounts',
|
||||
component: () => import('@/views/AdminAccounts.vue'),
|
||||
meta: { title: '账号管理', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/permissions',
|
||||
name: 'AdminPermissions',
|
||||
component: () => import('@/views/AdminPermissions.vue'),
|
||||
meta: { title: '权限管理', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/monitor',
|
||||
name: 'AdminMonitor',
|
||||
component: () => import('@/views/AdminMonitor.vue'),
|
||||
meta: { title: '系统监控', requiresAdminAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'Test',
|
||||
component: () => import('@/views/Test.vue'),
|
||||
meta: { title: 'API测试' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue'),
|
||||
meta: { title: '页面未找到' }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to: any, from: any, next: any) => {
|
||||
// 设置页面标题
|
||||
document.title = `${to.meta.title} - Pandora`
|
||||
|
||||
// 获取认证状态
|
||||
const authStore = useAuthStore()
|
||||
const adminStore = useAdminStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 检查是否需要用户认证
|
||||
if (to.meta.requiresAuth) {
|
||||
if (!authStore.isLoggedIn) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要管理员认证
|
||||
if (to.meta.requiresAdminAuth) {
|
||||
if (!adminAuth.isLoggedIn()) {
|
||||
next('/admin/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
248
frontend/src/stores/admin.ts
Normal file
248
frontend/src/stores/admin.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { adminAPI } from '@/utils/api'
|
||||
import { adminAuth } from '@/utils/auth'
|
||||
import type { User, PaginatedResponse } from '@/types'
|
||||
|
||||
export const useAdminStore = defineStore('admin', () => {
|
||||
// 状态
|
||||
const admin = ref<User | null>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const users = ref<User[]>([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
totalPages: 0
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => !!token.value && !!admin.value)
|
||||
|
||||
// 初始化认证状态
|
||||
const initAuth = () => {
|
||||
const storedToken = adminAuth.getToken()
|
||||
const storedAdmin = adminAuth.getAdminInfo()
|
||||
|
||||
if (storedToken && storedAdmin) {
|
||||
token.value = storedToken
|
||||
admin.value = storedAdmin
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员登录
|
||||
const login = async (data: { username: string; password: string }) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.login(data)
|
||||
|
||||
// 保存认证信息
|
||||
token.value = response.token
|
||||
admin.value = response.admin
|
||||
adminAuth.setLogin(response.token, response.admin)
|
||||
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '登录失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员登出
|
||||
const logout = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 清除本地状态
|
||||
token.value = null
|
||||
admin.value = null
|
||||
users.value = []
|
||||
adminAuth.logout()
|
||||
} catch (err) {
|
||||
console.warn('Admin logout failed:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const loadUsers = async (params?: { page?: number; limit?: number; search?: string }) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.getUsers(params)
|
||||
users.value = response.users
|
||||
pagination.value = response.pagination
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '获取用户列表失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
const createUser = async (data: {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
}) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.createUser(data)
|
||||
// 重新加载用户列表
|
||||
await loadUsers()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '创建用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
const updateUser = async (userId: string, data: any) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.updateUser(userId, data)
|
||||
// 重新加载用户列表
|
||||
await loadUsers()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '更新用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户账号权限
|
||||
const updateUserAccounts = async (userId: string, accountIds: string[]) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.updateUserAccounts(userId, accountIds)
|
||||
// 重新加载用户列表
|
||||
await loadUsers()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '更新用户账号权限失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (userId: string) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.deleteUser(userId)
|
||||
// 重新加载用户列表
|
||||
await loadUsers()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '删除用户失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const getStats = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.getStats()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '获取统计数据失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最近活动
|
||||
const getRecentActivities = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.getRecentActivities()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '获取最近活动失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账号列表
|
||||
const getAccounts = async (params?: { page?: number; limit?: number; search?: string; status?: string }) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await adminAPI.getAccounts(params)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '获取账号列表失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
admin,
|
||||
token,
|
||||
loading,
|
||||
error,
|
||||
users,
|
||||
pagination,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
|
||||
// 方法
|
||||
initAuth,
|
||||
login,
|
||||
logout,
|
||||
loadUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserAccounts,
|
||||
deleteUser,
|
||||
getStats,
|
||||
getRecentActivities,
|
||||
getAccounts,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
247
frontend/src/stores/auth.ts
Normal file
247
frontend/src/stores/auth.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { authAPI } from '@/utils/api'
|
||||
import { userAuth } from '@/utils/auth'
|
||||
import type { User } from '@/types'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref<User | null>(null)
|
||||
const token = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
||||
|
||||
// 初始化认证状态
|
||||
const initAuth = () => {
|
||||
const storedToken = userAuth.getToken()
|
||||
const storedUser = userAuth.getUserInfo()
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
token.value = storedToken
|
||||
user.value = storedUser
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 用户注册
|
||||
const register = async (data: {
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.register(data)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '注册失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
const login = async (data: {
|
||||
username: string
|
||||
password: string
|
||||
}) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.login(data)
|
||||
|
||||
// 保存认证信息
|
||||
token.value = response.token
|
||||
user.value = response.user
|
||||
userAuth.setLogin(response.token, response.user)
|
||||
|
||||
return response
|
||||
} catch (err: any) {
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 邮箱验证
|
||||
const verifyEmail = async (token: string) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.verifyEmail(token)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '邮箱验证失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重新发送验证邮件
|
||||
const resendVerification = async (email: string) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.resendVerification(email)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '发送验证邮件失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码
|
||||
const forgotPassword = async (email: string) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.forgotPassword(email)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '发送重置邮件失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const resetPassword = async (data: {
|
||||
token: string
|
||||
password: string
|
||||
}) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.resetPassword(data)
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '重置密码失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置TOTP
|
||||
const setupTOTP = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.setupTOTP()
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '设置二步验证失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 验证TOTP
|
||||
const verifyTOTP = async (totpToken: string) => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.verifyTOTP(totpToken)
|
||||
|
||||
// 更新用户信息
|
||||
if (user.value && token.value) {
|
||||
user.value.totpEnabled = true
|
||||
userAuth.setLogin(token.value, user.value)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '验证失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getProfile = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await authAPI.getProfile()
|
||||
user.value = response.user
|
||||
if (token.value) {
|
||||
userAuth.setLogin(token.value, response.user)
|
||||
}
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.message || '获取用户信息失败'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登出
|
||||
const logout = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
await authAPI.logout()
|
||||
} catch (err: any) {
|
||||
console.error('登出API调用失败:', err)
|
||||
} finally {
|
||||
// 清除本地状态
|
||||
token.value = null
|
||||
user.value = null
|
||||
userAuth.logout()
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清除错误
|
||||
const clearError = () => {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
|
||||
// 方法
|
||||
initAuth,
|
||||
register,
|
||||
login,
|
||||
verifyEmail,
|
||||
resendVerification,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
setupTOTP,
|
||||
verifyTOTP,
|
||||
getProfile,
|
||||
logout,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
39
frontend/src/style.css
Normal file
39
frontend/src/style.css
Normal file
@@ -0,0 +1,39 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply btn border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-gray-800 shadow rounded-lg;
|
||||
}
|
||||
}
|
||||
119
frontend/src/types/index.ts
Normal file
119
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// 用户相关类型
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: string
|
||||
isActive: boolean
|
||||
emailVerified?: boolean
|
||||
totpEnabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 网站账号相关类型
|
||||
export interface WebsiteAccount {
|
||||
id: string
|
||||
website: string
|
||||
username: string
|
||||
token?: string
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
assignedUsers?: User[]
|
||||
}
|
||||
|
||||
// 账号分配相关类型
|
||||
export interface AccountAssignment {
|
||||
id: string
|
||||
userId: string
|
||||
accountId: string
|
||||
assignedAt: string
|
||||
expiresAt?: string
|
||||
isActive: boolean
|
||||
account: WebsiteAccount
|
||||
user: User
|
||||
}
|
||||
|
||||
// 会话相关类型
|
||||
export interface Session {
|
||||
id: string
|
||||
userId: string
|
||||
token: string
|
||||
expiresAt: string
|
||||
createdAt: string
|
||||
user: User
|
||||
}
|
||||
|
||||
// 审计日志相关类型
|
||||
export interface AuditLog {
|
||||
id: string
|
||||
userId: string
|
||||
action: string
|
||||
resource: string
|
||||
resourceId?: string
|
||||
details?: any
|
||||
ipAddress?: string
|
||||
userAgent?: string
|
||||
createdAt: string
|
||||
user: User
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: T
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 分页响应类型
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[]
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
// 认证相关类型
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: User
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
message: string
|
||||
user: User
|
||||
}
|
||||
|
||||
// 表单验证类型
|
||||
export interface ValidationError {
|
||||
field: string
|
||||
message: string
|
||||
}
|
||||
|
||||
// 主题类型
|
||||
export type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
// 通知类型
|
||||
export interface Notification {
|
||||
id: string
|
||||
type: 'success' | 'error' | 'warning' | 'info'
|
||||
title: string
|
||||
message: string
|
||||
duration?: number
|
||||
createdAt: string
|
||||
}
|
||||
13
frontend/src/types/vue.d.ts
vendored
Normal file
13
frontend/src/types/vue.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string
|
||||
requiresAuth?: boolean
|
||||
requiresAdmin?: boolean
|
||||
}
|
||||
}
|
||||
294
frontend/src/utils/api.ts
Normal file
294
frontend/src/utils/api.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { userAuth, adminAuth } from './auth'
|
||||
|
||||
// API基础配置
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
|
||||
// 创建axios实例
|
||||
const api: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器 - 添加认证token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 优先使用管理员token,如果没有则使用用户token
|
||||
let token = adminAuth.getToken()
|
||||
if (!token) {
|
||||
token = userAuth.getToken()
|
||||
}
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
} else {
|
||||
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器 - 处理认证错误
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token过期或无效,清除所有认证状态
|
||||
userAuth.logout()
|
||||
adminAuth.logout()
|
||||
|
||||
// 自动重定向到登录页面(如果不是已经在登录页面)
|
||||
if (window.location.pathname !== '/' && window.location.pathname !== '/login') {
|
||||
window.location.href = '/'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 用户认证API
|
||||
export const authAPI = {
|
||||
// 用户注册
|
||||
async register(data: {
|
||||
username: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}) {
|
||||
// 只发送后端需要的字段
|
||||
const requestData = {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
confirmPassword: data.confirmPassword
|
||||
}
|
||||
|
||||
const response = await api.post('/auth/register', requestData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 用户登录
|
||||
async login(data: {
|
||||
username: string
|
||||
password: string
|
||||
}) {
|
||||
try {
|
||||
const response = await api.post('/auth/login', data)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 邮箱验证
|
||||
async verifyEmail(token: string) {
|
||||
const response = await api.post('/auth/verify-email', { token })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 重新发送验证邮件
|
||||
async resendVerification(email: string) {
|
||||
const response = await api.post('/auth/resend-verification', { email })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 忘记密码
|
||||
async forgotPassword(email: string) {
|
||||
const response = await api.post('/auth/forgot-password', { email })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 重置密码
|
||||
async resetPassword(data: {
|
||||
token: string
|
||||
password: string
|
||||
}) {
|
||||
const response = await api.post('/auth/reset-password', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 设置TOTP
|
||||
async setupTOTP() {
|
||||
const response = await api.post('/auth/setup-totp')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 验证TOTP
|
||||
async verifyTOTP(token: string) {
|
||||
const response = await api.post('/auth/verify-totp', { token })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
async getProfile() {
|
||||
const response = await api.get('/auth/me')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
async updateProfile(data: {
|
||||
username?: string
|
||||
email?: string
|
||||
currentPassword?: string
|
||||
newPassword?: string
|
||||
}) {
|
||||
const response = await api.put('/auth/profile', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 用户登出
|
||||
async logout() {
|
||||
const response = await api.post('/auth/logout')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取TOTP二维码
|
||||
async getTOTPQRCode() {
|
||||
const response = await api.get('/auth/totp/qr-code')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 启用TOTP
|
||||
async enableTOTP(token: string) {
|
||||
const response = await api.post('/auth/totp/enable', { token })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 禁用TOTP
|
||||
async disableTOTP(password: string) {
|
||||
const response = await api.post('/auth/totp/disable', { password })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
// 账号管理API
|
||||
export const accountAPI = {
|
||||
// 获取用户可用账号
|
||||
async getUserAccounts() {
|
||||
const response = await api.get('/accounts/user/assigned')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取账号详情
|
||||
async getAccountDetails(accountId: string) {
|
||||
const response = await api.get(`/accounts/${accountId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 登录到网站
|
||||
async loginToWebsite(accountId: string, userId: string) {
|
||||
const response = await api.post(`/accounts/${accountId}/login`, { userId })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员API
|
||||
export const adminAPI = {
|
||||
// 管理员登录
|
||||
async login(data: { username: string; password: string }) {
|
||||
const response = await api.post('/admin/login', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取用户列表
|
||||
async getUsers(params?: { page?: number; limit?: number; search?: string }) {
|
||||
const response = await api.get('/admin/users', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
async createUser(data: {
|
||||
username: string
|
||||
password: string
|
||||
role: string
|
||||
}) {
|
||||
const response = await api.post('/admin/users', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
async updateUser(userId: string, data: any) {
|
||||
const response = await api.put(`/admin/users/${userId}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 更新用户账号权限
|
||||
async updateUserAccounts(userId: string, accountIds: string[]) {
|
||||
const response = await api.put(`/admin/users/${userId}/accounts`, { accountIds })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
async deleteUser(userId: string) {
|
||||
const response = await api.delete(`/admin/users/${userId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取统计数据
|
||||
async getStats() {
|
||||
console.log('发送获取统计数据请求...')
|
||||
const response = await api.get('/admin/stats')
|
||||
console.log('统计数据API响应:', response.data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取最近活动
|
||||
async getRecentActivities() {
|
||||
console.log('发送获取最近活动请求...')
|
||||
const response = await api.get('/admin/activities')
|
||||
console.log('最近活动API响应:', response.data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取账号列表
|
||||
async getAccounts(params?: { page?: number; limit?: number; search?: string; status?: string }) {
|
||||
const response = await api.get('/admin/accounts', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 创建账号
|
||||
async createAccount(data: {
|
||||
website: string
|
||||
username: string
|
||||
token?: string
|
||||
isActive: boolean
|
||||
}) {
|
||||
const requestData = {
|
||||
website: data.website,
|
||||
accountName: data.username,
|
||||
token: data.token,
|
||||
isActive: data.isActive
|
||||
}
|
||||
const response = await api.post('/admin/accounts', requestData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 更新账号
|
||||
async updateAccount(accountId: string, data: any) {
|
||||
const response = await api.put(`/admin/accounts/${accountId}`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 删除账号
|
||||
async deleteAccount(accountId: string) {
|
||||
const response = await api.delete(`/admin/accounts/${accountId}`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
// 路径API
|
||||
export const pathAPI = {
|
||||
// 获取路径
|
||||
async getPaths() {
|
||||
const response = await api.get('/paths')
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
96
frontend/src/utils/auth.ts
Normal file
96
frontend/src/utils/auth.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// 认证工具函数
|
||||
|
||||
// 用户认证相关
|
||||
export const userAuth = {
|
||||
// 检查用户是否已登录
|
||||
isLoggedIn(): boolean {
|
||||
return !!localStorage.getItem('userToken')
|
||||
},
|
||||
|
||||
// 获取用户token
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem('userToken')
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo(): any {
|
||||
const userInfo = localStorage.getItem('userInfo')
|
||||
return userInfo ? JSON.parse(userInfo) : null
|
||||
},
|
||||
|
||||
// 设置用户登录状态
|
||||
setLogin(token: string, userInfo: any): void {
|
||||
localStorage.setItem('userToken', token)
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo))
|
||||
},
|
||||
|
||||
// 清除用户登录状态
|
||||
logout(): void {
|
||||
localStorage.removeItem('userToken')
|
||||
localStorage.removeItem('userInfo')
|
||||
}
|
||||
}
|
||||
|
||||
// 管理员认证相关
|
||||
export const adminAuth = {
|
||||
// 检查管理员是否已登录
|
||||
isLoggedIn(): boolean {
|
||||
return !!localStorage.getItem('adminToken')
|
||||
},
|
||||
|
||||
// 获取管理员token
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem('adminToken')
|
||||
},
|
||||
|
||||
// 获取管理员信息
|
||||
getAdminInfo(): any {
|
||||
try {
|
||||
const adminInfo = localStorage.getItem('adminUser')
|
||||
return adminInfo ? JSON.parse(adminInfo) : null
|
||||
} catch (error) {
|
||||
console.error('解析管理员信息失败:', error)
|
||||
// 清除可能损坏的数据
|
||||
localStorage.removeItem('adminUser')
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 设置管理员登录状态
|
||||
setLogin(token: string, adminInfo: any): void {
|
||||
try {
|
||||
if (!token || !adminInfo) {
|
||||
console.error('设置管理员登录状态失败:token 或 adminInfo 为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!adminInfo.username) {
|
||||
console.error('设置管理员登录状态失败:adminInfo 缺少 username 字段')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('adminToken', token)
|
||||
localStorage.setItem('adminUser', JSON.stringify(adminInfo))
|
||||
console.log('管理员登录状态设置成功:', { token, adminInfo })
|
||||
} catch (error) {
|
||||
console.error('设置管理员登录状态失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 清除管理员登录状态
|
||||
logout(): void {
|
||||
localStorage.removeItem('adminToken')
|
||||
localStorage.removeItem('adminUser')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证管理员凭据
|
||||
export const validateAdminCredentials = (username: string, password: string): boolean => {
|
||||
// 这里可以扩展为从API验证
|
||||
const validCredentials = {
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
}
|
||||
|
||||
return username === validCredentials.username && password === validCredentials.password
|
||||
}
|
||||
472
frontend/src/views/Admin.vue
Normal file
472
frontend/src/views/Admin.vue
Normal file
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
Pandora 管理后台
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">
|
||||
管理员:{{ adminUser?.username || '未知用户' }}
|
||||
</span>
|
||||
<button class="btn-outline" @click="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
系统管理
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
管理用户、权限和系统配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 管理菜单 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div
|
||||
v-for="menu in adminMenus"
|
||||
:key="menu.id"
|
||||
class="card p-6 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
@click="navigateToMenu(menu)"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path v-if="menu.icon === 'UserIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
<path v-else-if="menu.icon === 'KeyIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
<path v-else-if="menu.icon === 'ShieldIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
<path v-else-if="menu.icon === 'ChartIcon'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ menu.name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ menu.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统统计 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">总用户数</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.totalUsers }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">总账号数</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.totalAccounts }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">今日访问</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.todayVisits }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-100 dark:bg-red-900 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">系统告警</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ stats.alerts }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="mb-8">
|
||||
<!-- 快速操作卡片 -->
|
||||
<div class="card">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
快速操作
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<button
|
||||
@click="router.push('/admin/users')"
|
||||
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">添加用户</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">创建新用户账号</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="router.push('/admin/accounts')"
|
||||
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">添加账号</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">管理网站账号</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="router.push('/admin/permissions')"
|
||||
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">权限设置</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">配置访问权限</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="router.push('/admin/monitor')"
|
||||
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg class="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">系统监控</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">查看运行状态</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="card">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
最近活动
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div v-if="recentActivities.length === 0" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">暂无活动</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
系统运行正常,暂无异常活动
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
class="flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
:class="{
|
||||
'border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/20':
|
||||
activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED'
|
||||
}"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center"
|
||||
:class="{
|
||||
'bg-red-100 dark:bg-red-900': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
|
||||
'bg-gray-100 dark:bg-gray-800': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
:class="{
|
||||
'text-red-600 dark:text-red-400': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
|
||||
'text-gray-600 dark:text-gray-400': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
|
||||
}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
v-if="activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED'"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-1">
|
||||
<p
|
||||
class="text-sm"
|
||||
:class="{
|
||||
'text-red-900 dark:text-red-100': activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED',
|
||||
'text-gray-900 dark:text-white': !(activity.action === 'USER_LOGIN_FAILED' || activity.action === 'ADMIN_LOGIN_FAILED')
|
||||
}"
|
||||
>
|
||||
{{ activity.description }}
|
||||
</p>
|
||||
<div class="flex items-center space-x-4 mt-1">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatTime(activity.time) }}
|
||||
</p>
|
||||
<p v-if="activity.ipAddress" class="text-xs text-blue-600 dark:text-blue-400">
|
||||
IP: {{ activity.ipAddress }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { adminAuth } from '@/utils/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 管理菜单
|
||||
const adminMenus = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '用户管理',
|
||||
description: '管理用户账号和权限',
|
||||
icon: 'UserIcon',
|
||||
route: '/admin/users'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '账号管理',
|
||||
description: '管理网站账号和token',
|
||||
icon: 'KeyIcon',
|
||||
route: '/admin/accounts'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '权限管理',
|
||||
description: '配置用户访问权限',
|
||||
icon: 'ShieldIcon',
|
||||
route: '/admin/permissions'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '系统监控',
|
||||
description: '查看系统运行状态',
|
||||
icon: 'ChartIcon',
|
||||
route: '/admin/monitor'
|
||||
}
|
||||
])
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
totalUsers: 0,
|
||||
totalAccounts: 0,
|
||||
todayVisits: 0,
|
||||
alerts: 0
|
||||
})
|
||||
|
||||
// 最近活动
|
||||
const recentActivities = ref([])
|
||||
|
||||
// 导航到菜单
|
||||
const navigateToMenu = (menu: any) => {
|
||||
router.push(menu.route)
|
||||
}
|
||||
|
||||
// 管理员用户信息
|
||||
const adminUser = ref<any>(null)
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
adminAuth.logout()
|
||||
router.push('/admin/login')
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) { // 1分钟内
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) { // 1小时内
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) { // 1天内
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
console.log('开始加载统计数据...')
|
||||
const response = await adminStore.getStats()
|
||||
console.log('统计数据响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
stats.value = response.data
|
||||
console.log('统计数据加载成功:', stats.value)
|
||||
} else {
|
||||
console.error('统计数据格式错误:', response)
|
||||
toast.error('统计数据格式错误')
|
||||
// 设置默认值
|
||||
stats.value = {
|
||||
totalUsers: 0,
|
||||
totalAccounts: 0,
|
||||
todayVisits: 0,
|
||||
alerts: 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
toast.error('加载统计数据失败')
|
||||
// 设置默认值
|
||||
stats.value = {
|
||||
totalUsers: 0,
|
||||
totalAccounts: 0,
|
||||
todayVisits: 0,
|
||||
alerts: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近活动
|
||||
const loadRecentActivities = async () => {
|
||||
try {
|
||||
console.log('开始加载最近活动...')
|
||||
const response = await adminStore.getRecentActivities()
|
||||
console.log('最近活动响应:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
recentActivities.value = response.data.activities || []
|
||||
console.log('最近活动加载成功:', recentActivities.value)
|
||||
} else {
|
||||
console.error('最近活动数据格式错误:', response)
|
||||
toast.error('最近活动数据格式错误')
|
||||
recentActivities.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载最近活动失败:', error)
|
||||
toast.error('加载最近活动失败')
|
||||
recentActivities.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(async () => {
|
||||
|
||||
// 检查管理员登录状态
|
||||
if (!adminAuth.isLoggedIn()) {
|
||||
router.push('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管理员用户信息
|
||||
const adminInfo = adminAuth.getAdminInfo()
|
||||
|
||||
if (!adminInfo) {
|
||||
// 如果没有管理员信息,清除登录状态并跳转到登录页
|
||||
adminAuth.logout()
|
||||
router.push('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
adminUser.value = adminInfo
|
||||
|
||||
// 加载数据
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadRecentActivities()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
422
frontend/src/views/AdminAccounts.vue
Normal file
422
frontend/src/views/AdminAccounts.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<button @click="$router.back()" class="mr-4">
|
||||
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
账号管理
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 页面标题和操作 -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
网站账号
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
管理系统中的所有网站账号和token
|
||||
</p>
|
||||
</div>
|
||||
<button @click="showCreateModal = true" class="btn-primary">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
添加账号
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索网站类型或用户名..."
|
||||
class="input w-full"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="statusFilter" class="input" @change="handleSearch">
|
||||
<option value="">所有状态</option>
|
||||
<option value="active">活跃</option>
|
||||
<option value="inactive">非活跃</option>
|
||||
</select>
|
||||
<button @click="handleSearch" class="btn-outline">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 账号列表 -->
|
||||
<div class="card">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
账号信息
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
分配用户
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
创建时间
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="account in accounts" :key="account.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-lg bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<!-- Claude 图标 -->
|
||||
<img v-if="account.website === 'claude'" :src="claudeIcon" alt="Claude" class="w-6 h-6">
|
||||
<!-- ChatGPT 图标 -->
|
||||
<img v-else-if="account.website === 'chatgpt'" :src="chatgptIcon" alt="ChatGPT" class="w-6 h-6">
|
||||
<!-- Grok 图标 -->
|
||||
<img v-else-if="account.website === 'grok'" :src="grokIcon" alt="Grok" class="w-6 h-6">
|
||||
<!-- 默认占位符 -->
|
||||
<div v-else class="w-6 h-6 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ getWebsiteTypeName(account.website) }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
用户名: {{ account.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="account.isActive ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'">
|
||||
{{ account.isActive ? '活跃' : '非活跃' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ account.assignedUsers?.length || 0 }} 个用户
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ formatDate(account.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button @click="editAccount(account)" class="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300">
|
||||
编辑
|
||||
</button>
|
||||
<button @click="deleteAccount(account)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 创建账号模态框 -->
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">添加网站账号</h3>
|
||||
<form @submit.prevent="handleCreateAccount">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">网站类型</label>
|
||||
<select v-model="createForm.website" required class="input mt-1 w-full">
|
||||
<option value="">请选择网站类型</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="chatgpt">ChatGPT</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||
<input v-model="createForm.username" type="text" required class="input mt-1 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Token</label>
|
||||
<textarea v-model="createForm.token" rows="3" class="input mt-1 w-full" placeholder="输入API token或访问凭证"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input v-model="createForm.isActive" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">账号活跃</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" @click="showCreateModal = false" class="btn-outline">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="loading" class="btn-primary">
|
||||
{{ loading ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑账号模态框 -->
|
||||
<div v-if="showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">编辑网站账号</h3>
|
||||
<form @submit.prevent="handleUpdateAccount">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">网站类型</label>
|
||||
<select v-model="editForm.website" required class="input mt-1 w-full">
|
||||
<option value="">请选择网站类型</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="chatgpt">ChatGPT</option>
|
||||
<option value="grok">Grok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||
<input v-model="editForm.username" type="text" required class="input mt-1 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Token</label>
|
||||
<textarea v-model="editForm.token" rows="3" class="input mt-1 w-full" placeholder="输入API token或访问凭证"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input v-model="editForm.isActive" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded" />
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">账号活跃</span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- 显示当前分配用户信息 -->
|
||||
<div class="bg-gray-50 dark:bg-gray-700 p-3 rounded-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
当前分配用户: <span class="font-medium text-gray-900 dark:text-white">{{ selectedAccount?.assignedUsers?.length || 0 }} 个</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
创建时间: {{ selectedAccount ? formatDate(selectedAccount.createdAt) : '' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" @click="closeEditModal" class="btn-outline">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="loading" class="btn-primary">
|
||||
{{ loading ? '更新中...' : '更新' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { adminAPI } from '@/utils/api'
|
||||
import type { WebsiteAccount } from '@/types'
|
||||
import claudeIcon from '@/assets/claude.png'
|
||||
import chatgptIcon from '@/assets/ChatGPT.png'
|
||||
import grokIcon from '@/assets/grok.png'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
// 账号列表
|
||||
const accounts = ref<WebsiteAccount[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
// 创建账号模态框
|
||||
const showCreateModal = ref(false)
|
||||
const createForm = reactive({
|
||||
website: '',
|
||||
username: '',
|
||||
token: '',
|
||||
isActive: true
|
||||
})
|
||||
|
||||
// 编辑账号模态框
|
||||
const showEditModal = ref(false)
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
website: '',
|
||||
username: '',
|
||||
token: '',
|
||||
isActive: true
|
||||
})
|
||||
|
||||
const selectedAccount = ref<WebsiteAccount | null>(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取网站类型名称
|
||||
const getWebsiteTypeName = (website: string) => {
|
||||
switch (website) {
|
||||
case 'claude':
|
||||
return 'Claude'
|
||||
case 'chatgpt':
|
||||
return 'ChatGPT'
|
||||
case 'grok':
|
||||
return 'Grok'
|
||||
default:
|
||||
return website
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
loadAccounts()
|
||||
}
|
||||
|
||||
// 加载账号列表
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await adminAPI.getAccounts({
|
||||
search: searchQuery.value,
|
||||
status: statusFilter.value
|
||||
})
|
||||
accounts.value = response.accounts || []
|
||||
} catch (error) {
|
||||
console.error('加载账号列表失败:', error)
|
||||
toast.error('加载账号列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建账号
|
||||
const handleCreateAccount = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await adminAPI.createAccount(createForm)
|
||||
toast.success('账号创建成功')
|
||||
showCreateModal.value = false
|
||||
|
||||
// 重置表单
|
||||
createForm.website = ''
|
||||
createForm.username = ''
|
||||
createForm.token = ''
|
||||
createForm.isActive = true
|
||||
|
||||
// 重新加载列表
|
||||
await loadAccounts()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '创建账号失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭编辑模态框
|
||||
const closeEditModal = () => {
|
||||
showEditModal.value = false
|
||||
selectedAccount.value = null
|
||||
// 重置编辑表单
|
||||
editForm.id = ''
|
||||
editForm.website = ''
|
||||
editForm.username = ''
|
||||
editForm.token = ''
|
||||
editForm.isActive = true
|
||||
}
|
||||
|
||||
// 编辑账号
|
||||
const editAccount = (account: WebsiteAccount) => {
|
||||
// 设置选中的账号(用于显示用户信息)
|
||||
selectedAccount.value = account
|
||||
|
||||
// 填充编辑表单
|
||||
editForm.id = account.id
|
||||
editForm.website = account.website
|
||||
editForm.username = account.username
|
||||
editForm.token = account.token || ''
|
||||
editForm.isActive = account.isActive
|
||||
|
||||
// 显示编辑模态框
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 更新账号
|
||||
const handleUpdateAccount = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 准备更新数据
|
||||
const updateData = {
|
||||
website: editForm.website,
|
||||
accountName: editForm.username,
|
||||
token: editForm.token,
|
||||
isActive: editForm.isActive
|
||||
}
|
||||
|
||||
await adminAPI.updateAccount(editForm.id, updateData)
|
||||
toast.success('账号更新成功')
|
||||
closeEditModal()
|
||||
|
||||
// 重新加载列表
|
||||
await loadAccounts()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '更新账号失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除账号
|
||||
const deleteAccount = async (account: WebsiteAccount) => {
|
||||
if (!confirm(`确定要删除账号 ${account.website} 吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await adminAPI.deleteAccount(account.id)
|
||||
toast.success('账号删除成功')
|
||||
await loadAccounts()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '删除账号失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
})
|
||||
</script>
|
||||
208
frontend/src/views/AdminLogin.vue
Normal file
208
frontend/src/views/AdminLogin.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-red-50 to-red-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- 标题 -->
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-16 w-16 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
管理员登录
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
仅限管理员账号访问
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form class="mt-8 space-y-6" @submit.prevent="handleLogin">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
管理员用户名
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
required
|
||||
class="input mt-1"
|
||||
placeholder="请输入管理员用户名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
管理员密码
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="input mt-1"
|
||||
placeholder="请输入管理员密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 二步验证 -->
|
||||
<div v-if="showTwoFactor" class="space-y-4">
|
||||
<div>
|
||||
<label for="verificationCode" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
验证码
|
||||
</label>
|
||||
<input
|
||||
id="verificationCode"
|
||||
v-model="form.verificationCode"
|
||||
type="text"
|
||||
required
|
||||
class="input mt-1"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
v-model="form.rememberMe"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-red-600 focus:ring-red-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="remember-me" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<a href="#" class="font-medium text-red-600 hover:text-red-500">
|
||||
忘记密码?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMessage" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="loading" class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
登录中...
|
||||
</span>
|
||||
<span v-else>管理员登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 返回首页 -->
|
||||
<div class="text-center">
|
||||
<router-link
|
||||
to="/"
|
||||
class="text-sm text-red-600 hover:text-red-500"
|
||||
>
|
||||
返回首页
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 安全提示 -->
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
此页面仅限管理员访问,请确保您有相应的权限。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { adminAuth } from '@/utils/auth'
|
||||
import { adminAPI } from '@/utils/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
verificationCode: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const showTwoFactor = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
console.log('开始管理员登录:', { username: form.username })
|
||||
|
||||
// 调用后端管理员登录 API
|
||||
const response = await adminAPI.login({
|
||||
username: form.username,
|
||||
password: form.password
|
||||
})
|
||||
|
||||
console.log('管理员登录响应:', response)
|
||||
|
||||
if (response.success) {
|
||||
// 登录成功,设置管理员状态
|
||||
adminAuth.setLogin(response.token, response.admin)
|
||||
|
||||
toast.success('管理员登录成功')
|
||||
|
||||
// 跳转到管理后台
|
||||
router.push('/admin')
|
||||
} else {
|
||||
errorMessage.value = response.message || '登录失败'
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('管理员登录失败:', error)
|
||||
errorMessage.value = error.response?.data?.message || '登录失败,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
38
frontend/src/views/AdminMonitor.vue
Normal file
38
frontend/src/views/AdminMonitor.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<button @click="$router.back()" class="mr-4">
|
||||
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
系统监控
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">系统监控</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
查看系统运行状态功能正在开发中...
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 系统监控页面 - 开发中
|
||||
</script>
|
||||
388
frontend/src/views/AdminPermissions.vue
Normal file
388
frontend/src/views/AdminPermissions.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<button @click="$router.back()" class="mr-4">
|
||||
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
权限管理
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
权限管理
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
管理用户访问权限和账号配置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 用户账号权限管理 -->
|
||||
<div class="space-y-6">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="card">
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索用户名或邮箱..."
|
||||
class="input w-full"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="accountFilter" class="input" @change="handleSearch">
|
||||
<option value="">所有账号</option>
|
||||
<option v-for="account in availableAccounts" :key="account.id" :value="account.id">
|
||||
{{ account.username }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="handleSearch" class="btn-outline">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户权限列表 -->
|
||||
<div class="card">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
用户信息
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
账号权限
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="user in filteredUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-primary-600 dark:text-primary-400">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex space-x-2">
|
||||
<span
|
||||
v-for="account in user.accounts || []"
|
||||
:key="account"
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
||||
>
|
||||
{{ getAccountName(account) }}
|
||||
</span>
|
||||
<span v-if="!user.accounts || user.accounts.length === 0" class="text-sm text-gray-500 dark:text-gray-400">
|
||||
无访问权限
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
@click="editUserAccounts(user)"
|
||||
class="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
编辑权限
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统权限设置 -->
|
||||
<div class="space-y-6 mt-8">
|
||||
<div class="card">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
系统权限设置
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-6">
|
||||
<!-- 注册设置 -->
|
||||
<div>
|
||||
<h4 class="text-md font-medium text-gray-900 dark:text-white mb-4">注册设置</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">允许用户注册</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">是否允许新用户注册账号</p>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSystemSetting('allowRegistration')"
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
:class="systemSettings.allowRegistration ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-700'"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="systemSettings.allowRegistration ? 'translate-x-5' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安全设置 -->
|
||||
<div>
|
||||
<h4 class="text-md font-medium text-gray-900 dark:text-white mb-4">安全设置</h4>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">二步验证</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">是否启用TOTP二步验证</p>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSystemSetting('enableTOTP')"
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
:class="systemSettings.enableTOTP ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-700'"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="systemSettings.enableTOTP ? 'translate-x-5' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">会话超时</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">用户会话超时时间(小时)</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="systemSettings.sessionTimeout"
|
||||
type="number"
|
||||
min="1"
|
||||
max="72"
|
||||
class="input w-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 编辑用户账号权限模态框 -->
|
||||
<div v-if="showAccountModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
编辑 {{ selectedUser?.username }} 的账号权限
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="account in availableAccounts" :key="account.id" class="flex items-center">
|
||||
<input
|
||||
:id="`account-${account.id}`"
|
||||
type="checkbox"
|
||||
:checked="selectedUserAccounts.includes(account.id)"
|
||||
@change="toggleUserAccount(account.id)"
|
||||
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label :for="`account-${account.id}`" class="ml-3 text-sm text-gray-900 dark:text-white">
|
||||
{{ account.username }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button @click="showAccountModal = false" class="btn-outline">
|
||||
取消
|
||||
</button>
|
||||
<button @click="saveUserAccounts" class="btn-primary">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const accountFilter = ref('')
|
||||
|
||||
// 用户列表
|
||||
const users = ref([])
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = users.value
|
||||
|
||||
if (searchQuery.value) {
|
||||
filtered = filtered.filter((user: any) =>
|
||||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
if (accountFilter.value) {
|
||||
filtered = filtered.filter((user: any) =>
|
||||
user.accounts?.includes(accountFilter.value)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 可用账号
|
||||
const availableAccounts = ref([])
|
||||
|
||||
// 加载可用账号
|
||||
const loadAvailableAccounts = async () => {
|
||||
try {
|
||||
const response = await adminStore.getAccounts()
|
||||
availableAccounts.value = response.accounts || []
|
||||
} catch (error) {
|
||||
console.error('加载可用账号失败:', error)
|
||||
// 如果加载失败,使用默认数据
|
||||
availableAccounts.value = [
|
||||
{ id: 'account1', username: '账号1', description: '第一个登录账号' },
|
||||
{ id: 'account2', username: '账号2', description: '第二个登录账号' },
|
||||
{ id: 'account3', username: '账号3', description: '第三个登录账号' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 系统设置
|
||||
const systemSettings = ref({
|
||||
allowRegistration: true,
|
||||
enableTOTP: false,
|
||||
sessionTimeout: 24
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const showAccountModal = ref(false)
|
||||
const selectedUser = ref(null)
|
||||
const selectedUserAccounts = ref([])
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 这里可以添加实际的搜索逻辑
|
||||
console.log('搜索:', searchQuery.value, '账号筛选:', accountFilter.value)
|
||||
}
|
||||
|
||||
// 根据账户ID获取账户名称
|
||||
const getAccountName = (accountId: string) => {
|
||||
const account = availableAccounts.value.find(acc => acc.id === accountId)
|
||||
return account ? account.username : accountId
|
||||
}
|
||||
|
||||
// 编辑用户账号权限
|
||||
const editUserAccounts = (user: any) => {
|
||||
selectedUser.value = { ...user }
|
||||
selectedUserAccounts.value = [...(user.accounts || [])]
|
||||
showAccountModal.value = true
|
||||
}
|
||||
|
||||
// 切换用户账号权限
|
||||
const toggleUserAccount = (accountId: string) => {
|
||||
const index = selectedUserAccounts.value.indexOf(accountId)
|
||||
if (index > -1) {
|
||||
selectedUserAccounts.value.splice(index, 1)
|
||||
} else {
|
||||
selectedUserAccounts.value.push(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户账号权限
|
||||
const saveUserAccounts = async () => {
|
||||
try {
|
||||
// 调用API保存用户账号权限到后端
|
||||
await adminStore.updateUserAccounts(selectedUser.value.id, selectedUserAccounts.value)
|
||||
console.log('保存用户账号权限:', selectedUser.value?.id, selectedUserAccounts.value)
|
||||
|
||||
// 直接更新本地用户数据
|
||||
if (selectedUser.value) {
|
||||
const userIndex = users.value.findIndex((user: any) => user.id === selectedUser.value.id)
|
||||
console.log('找到用户索引:', userIndex, '用户ID:', selectedUser.value.id)
|
||||
if (userIndex !== -1) {
|
||||
// 使用Vue的响应式更新机制
|
||||
users.value[userIndex] = {
|
||||
...users.value[userIndex],
|
||||
accounts: [...selectedUserAccounts.value]
|
||||
}
|
||||
console.log('更新后的用户数据:', users.value[userIndex])
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('权限保存成功')
|
||||
showAccountModal.value = false
|
||||
} catch (error) {
|
||||
console.error('保存权限失败:', error)
|
||||
toast.error('保存权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 切换系统设置
|
||||
const toggleSystemSetting = (setting: string) => {
|
||||
systemSettings.value[setting] = !systemSettings.value[setting]
|
||||
console.log('切换系统设置:', setting, systemSettings.value[setting])
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const response = await adminStore.loadUsers()
|
||||
// 确保用户数据包含accounts字段,如果没有则初始化为空数组
|
||||
users.value = (response.users || []).map((user: any) => ({
|
||||
...user,
|
||||
accounts: user.accounts || []
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error)
|
||||
toast.error('加载用户列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(async () => {
|
||||
await loadUsers()
|
||||
await loadAvailableAccounts()
|
||||
})
|
||||
</script>
|
||||
400
frontend/src/views/AdminUsers.vue
Normal file
400
frontend/src/views/AdminUsers.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<button @click="$router.back()" class="mr-4">
|
||||
<svg class="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
用户管理
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 页面标题和操作 -->
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
用户列表
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
管理系统中的所有用户账号
|
||||
</p>
|
||||
</div>
|
||||
<button @click="showCreateModal = true" class="btn-primary">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
添加用户
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="card mb-6">
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索用户名或邮箱..."
|
||||
class="input w-full"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select v-model="roleFilter" class="input" @change="handleSearch">
|
||||
<option value="">所有角色</option>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
<button @click="handleSearch" class="btn-outline">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="card">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
用户信息
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
角色
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
创建时间
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="user in adminStore.users" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-primary-600 dark:text-primary-400">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ user.firstName || user.lastName ? `${user.firstName || ''} ${user.lastName || ''}`.trim() : '未设置姓名' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="user.role === 'admin' ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'">
|
||||
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
:class="user.isActive ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'">
|
||||
{{ user.isActive ? '激活' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ formatDate(user.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div class="flex space-x-2">
|
||||
<button @click="editUser(user)" class="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300">
|
||||
编辑
|
||||
</button>
|
||||
<button @click="deleteUser(user)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="adminStore.pagination.totalPages > 1" class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
显示第 {{ (adminStore.pagination.page - 1) * adminStore.pagination.limit + 1 }} 到
|
||||
{{ Math.min(adminStore.pagination.page * adminStore.pagination.limit, adminStore.pagination.total) }} 条,
|
||||
共 {{ adminStore.pagination.total }} 条记录
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="changePage(adminStore.pagination.page - 1)"
|
||||
:disabled="adminStore.pagination.page <= 1"
|
||||
class="btn-outline px-3 py-1"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': adminStore.pagination.page <= 1 }"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
@click="changePage(adminStore.pagination.page + 1)"
|
||||
:disabled="adminStore.pagination.page >= adminStore.pagination.totalPages"
|
||||
class="btn-outline px-3 py-1"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': adminStore.pagination.page >= adminStore.pagination.totalPages }"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 创建用户模态框 -->
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">创建新用户</h3>
|
||||
<form @submit.prevent="handleCreateUser">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||
<input v-model="createForm.username" type="text" required class="input mt-1 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">密码</label>
|
||||
<input v-model="createForm.password" type="password" required class="input mt-1 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">角色</label>
|
||||
<select v-model="createForm.role" class="input mt-1 w-full">
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" @click="showCreateModal = false" class="btn-outline">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="adminStore.loading" class="btn-primary">
|
||||
{{ adminStore.loading ? '创建中...' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户模态框 -->
|
||||
<div v-if="showEditModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">编辑用户</h3>
|
||||
<form @submit.prevent="handleEditUser">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">用户名</label>
|
||||
<input v-model="editForm.username" type="text" required class="input mt-1 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">角色</label>
|
||||
<select v-model="editForm.role" class="input mt-1 w-full">
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">状态</label>
|
||||
<select v-model="editForm.isActive" class="input mt-1 w-full">
|
||||
<option :value="true">激活</option>
|
||||
<option :value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">新密码(留空则不修改)</label>
|
||||
<input v-model="editForm.password" type="password" class="input mt-1 w-full" autocomplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" @click="showEditModal = false" class="btn-outline">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" :disabled="adminStore.loading" class="btn-primary">
|
||||
{{ adminStore.loading ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const roleFilter = ref('')
|
||||
|
||||
// 创建用户模态框
|
||||
const showCreateModal = ref(false)
|
||||
const createForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
// 编辑用户模态框
|
||||
const showEditModal = ref(false)
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
username: '',
|
||||
role: 'user',
|
||||
isActive: true,
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
adminStore.pagination.page = 1 // 重置到第一页
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const params: any = {
|
||||
page: adminStore.pagination.page,
|
||||
limit: adminStore.pagination.limit
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
params.search = searchQuery.value
|
||||
}
|
||||
|
||||
if (roleFilter.value) {
|
||||
params.role = roleFilter.value
|
||||
}
|
||||
|
||||
await adminStore.loadUsers(params)
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const changePage = async (page: number) => {
|
||||
adminStore.pagination.page = page
|
||||
await loadUsers()
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
const handleCreateUser = async () => {
|
||||
try {
|
||||
await adminStore.createUser(createForm)
|
||||
toast.success('用户创建成功')
|
||||
showCreateModal.value = false
|
||||
|
||||
// 重置表单
|
||||
createForm.username = ''
|
||||
createForm.password = ''
|
||||
createForm.role = 'user'
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '创建用户失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const editUser = (user: User) => {
|
||||
// 填充编辑表单
|
||||
editForm.id = user.id
|
||||
editForm.username = user.username
|
||||
editForm.role = user.role
|
||||
editForm.isActive = user.isActive
|
||||
editForm.password = ''
|
||||
|
||||
// 显示编辑模态框
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 处理编辑用户
|
||||
const handleEditUser = async () => {
|
||||
try {
|
||||
const updateData: any = {
|
||||
username: editForm.username,
|
||||
role: editForm.role,
|
||||
isActive: editForm.isActive
|
||||
}
|
||||
if (editForm.password) {
|
||||
console.log('正在更新密码:', editForm.password)
|
||||
updateData.password = editForm.password
|
||||
}
|
||||
updateData.loginAttempts = 0
|
||||
console.log('发送更新数据:', updateData)
|
||||
await adminStore.updateUser(editForm.id, updateData)
|
||||
toast.success('用户更新成功')
|
||||
showEditModal.value = false
|
||||
|
||||
// 重置表单
|
||||
editForm.id = ''
|
||||
editForm.username = ''
|
||||
editForm.role = 'user'
|
||||
editForm.isActive = true
|
||||
editForm.password = ''
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '更新用户失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = async (user: User) => {
|
||||
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await adminStore.deleteUser(user.id)
|
||||
toast.success('用户删除成功')
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || '删除用户失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
243
frontend/src/views/Dashboard.vue
Normal file
243
frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
AI Route
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ authStore.user?.username }}
|
||||
</span>
|
||||
<div class="w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-primary-600 dark:text-primary-400">
|
||||
{{ authStore.user?.username?.charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="handleLogout" class="btn-outline" :disabled="authStore.loading">
|
||||
{{ authStore.loading ? '退出中...' : '退出' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- 欢迎信息 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
欢迎回来,{{ authStore.user?.username }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 账号列表 -->
|
||||
<div class="card">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
我的账号
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div v-if="accounts.length === 0" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">暂无账号</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
您还没有被分配任何网站账号
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="account in accounts"
|
||||
:key="account.id"
|
||||
class="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-lg flex items-center justify-center">
|
||||
<!-- Claude 图标 -->
|
||||
<img v-if="account.website === 'claude'" :src="claudeIcon" alt="Claude" class="w-6 h-6">
|
||||
<!-- ChatGPT 图标 -->
|
||||
<img v-else-if="account.website === 'chatgpt'" :src="chatgptIcon" alt="ChatGPT" class="w-6 h-6">
|
||||
<!-- Grok 图标 -->
|
||||
<img v-else-if="account.website === 'grok'" :src="grokIcon" alt="Grok" class="w-6 h-6">
|
||||
<!-- 默认占位符 -->
|
||||
<div v-else class="w-6 h-6 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ account.username }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="handleAccessAccount(account)" class="btn-primary">
|
||||
访问
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { accountAPI } from '@/utils/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import type { WebsiteAccount } from '@/types'
|
||||
import claudeIcon from '@/assets/claude.png'
|
||||
import chatgptIcon from '@/assets/ChatGPT.png'
|
||||
import grokIcon from '@/assets/grok.png'
|
||||
import { getWebsiteUrl, getWebsiteConfig } from '@/config/websites'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 账号列表
|
||||
const accounts = ref<WebsiteAccount[]>([])
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authStore.logout()
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
// 即使API调用失败也清除本地状态并跳转
|
||||
authStore.logout()
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网站URL
|
||||
const getLocalWebsiteUrl = (site: string) => {
|
||||
return getWebsiteUrl(site)
|
||||
}
|
||||
|
||||
// 访问账号
|
||||
const handleAccessAccount = async (account: WebsiteAccount) => {
|
||||
try {
|
||||
// 显示加载状态
|
||||
toast.info('正在准备登录...')
|
||||
|
||||
// 调用网站登录API
|
||||
const response = await accountAPI.loginToWebsite(account.id, authStore.user?.id || '')
|
||||
|
||||
if (response.success && response.loginUrl) {
|
||||
// 打开登录URL
|
||||
const jichuurl = getLocalWebsiteUrl(account.website)
|
||||
window.open(jichuurl + response.loginUrl, '_blank')
|
||||
const config = getWebsiteConfig(account.website)
|
||||
toast.success(`正在跳转到 ${config?.name || account.website}...`)
|
||||
} else {
|
||||
toast.error('获取登录链接失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('访问账号失败:', error)
|
||||
|
||||
// 根据不同的错误状态码显示不同的错误信息
|
||||
if (error.response?.status === 403) {
|
||||
const errorMessage = error.response?.data?.error || '您没有权限访问该账号'
|
||||
toast.error(errorMessage)
|
||||
|
||||
// 如果是权限问题,建议用户联系管理员
|
||||
if (errorMessage.includes('权限') || errorMessage.includes('permission')) {
|
||||
console.log('权限错误详情:', {
|
||||
userId: authStore.user?.id,
|
||||
accountId: account.id,
|
||||
error: error.response?.data
|
||||
})
|
||||
}
|
||||
} else if (error.response?.status === 404) {
|
||||
toast.error('账号不存在或已被删除')
|
||||
} else if (error.response?.status === 400) {
|
||||
toast.error(error.response?.data?.error || '请求参数错误')
|
||||
} else if (error.response?.status === 500) {
|
||||
toast.error('服务器内部错误,请稍后重试')
|
||||
} else if (error.code === 'NETWORK_ERROR') {
|
||||
toast.error('网络连接失败,请检查网络连接')
|
||||
} else {
|
||||
toast.error(error.response?.data?.error || '访问账号失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户账号
|
||||
const loadUserAccounts = async () => {
|
||||
try {
|
||||
|
||||
const response = await accountAPI.getUserAccounts()
|
||||
accounts.value = response.accounts || []
|
||||
} catch (error: any) {
|
||||
console.error('加载用户账号失败:', error)
|
||||
console.error('错误详情:', error.response?.data)
|
||||
|
||||
// 如果是认证错误,不显示错误消息(因为响应拦截器会处理重定向)
|
||||
if (error.response?.status === 401) {
|
||||
console.log('认证错误,响应拦截器将处理重定向')
|
||||
return
|
||||
}
|
||||
|
||||
toast.error('加载账号列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 初始化认证状态
|
||||
authStore.initAuth()
|
||||
|
||||
// 确保用户已登录
|
||||
if (!authStore.isLoggedIn) {
|
||||
toast.error('请先登录')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token是否有效(通过尝试获取用户信息)
|
||||
try {
|
||||
await authStore.getProfile()
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 401) {
|
||||
// Token无效,清除认证状态并重定向
|
||||
authStore.logout()
|
||||
toast.error('登录已过期,请重新登录')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户账号
|
||||
await loadUserAccounts()
|
||||
} catch (error) {
|
||||
console.error('Dashboard初始化失败:', error)
|
||||
|
||||
// 如果是认证错误,重定向到登录页面
|
||||
if (error.response?.status === 401) {
|
||||
authStore.logout()
|
||||
toast.error('登录已过期,请重新登录')
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
toast.error('加载数据失败,请刷新页面重试')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
401
frontend/src/views/Home.vue
Normal file
401
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 relative overflow-hidden">
|
||||
<!-- Claude风格背景装饰 -->
|
||||
<div class="absolute inset-0">
|
||||
<!-- 动态背景粒子效果 -->
|
||||
<div class="absolute top-20 left-20 w-2 h-2 bg-blue-400 rounded-full opacity-60 animate-pulse"></div>
|
||||
<div class="absolute top-40 right-32 w-1 h-1 bg-purple-400 rounded-full opacity-40 animate-ping"></div>
|
||||
<div class="absolute bottom-32 left-1/4 w-1.5 h-1.5 bg-cyan-400 rounded-full opacity-50 animate-bounce"></div>
|
||||
<div class="absolute top-1/2 right-1/4 w-1 h-1 bg-green-400 rounded-full opacity-30 animate-pulse"></div>
|
||||
<div class="absolute bottom-20 right-20 w-2 h-2 bg-yellow-400 rounded-full opacity-40 animate-ping"></div>
|
||||
|
||||
<!-- 渐变光晕效果 -->
|
||||
<div class="absolute top-0 left-1/4 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl"></div>
|
||||
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="relative z-10 flex items-center justify-center min-h-screen px-4">
|
||||
<div class="w-full max-w-md">
|
||||
<!-- 标题 -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-white mb-2">
|
||||
AI Route
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 模式切换 -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="bg-white/10 backdrop-blur-lg rounded-lg p-1 border border-white/20">
|
||||
<button
|
||||
@click="currentMode = 'login'"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-md text-sm font-medium transition-all',
|
||||
currentMode === 'login'
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
: 'text-slate-300 hover:text-white'
|
||||
]"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
@click="currentMode = 'register'"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-md text-sm font-medium transition-all',
|
||||
currentMode === 'register'
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
: 'text-slate-300 hover:text-white'
|
||||
]"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div v-if="currentMode === 'login'" class="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20 shadow-2xl">
|
||||
<form @submit.prevent="handleLogin" class="space-y-6">
|
||||
<!-- 用户名输入框 -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-200 mb-2">
|
||||
用户名
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-200 mb-2">
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 记住我 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
v-model="loginForm.rememberMe"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-white/20 rounded bg-white/10"
|
||||
/>
|
||||
<label for="remember-me" class="ml-2 block text-sm text-slate-200">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleForgotPassword"
|
||||
class="text-slate-300 hover:text-white transition-colors"
|
||||
>
|
||||
忘记密码?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="authStore.loading"
|
||||
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
<span v-if="authStore.loading" class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
登录中...
|
||||
</span>
|
||||
<span v-else>登录</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div v-if="currentMode === 'register'" class="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20 shadow-2xl">
|
||||
<form @submit.prevent="handleRegister" class="space-y-6">
|
||||
<!-- 用户名输入框 -->
|
||||
<div>
|
||||
<label for="regUsername" class="block text-sm font-medium text-slate-200 mb-2">
|
||||
用户名
|
||||
</label>
|
||||
<input
|
||||
id="regUsername"
|
||||
v-model="registerForm.username"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div>
|
||||
<label for="regPassword" class="block text-sm font-medium text-slate-200 mb-2">
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
id="regPassword"
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 确认密码输入框 -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-slate-200 mb-2">
|
||||
确认密码
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="authStore.loading"
|
||||
class="w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-slate-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
<span v-if="authStore.loading" class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
注册中...
|
||||
</span>
|
||||
<span v-else>注册</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="authStore.error" class="mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
||||
<p class="text-red-300 text-sm">{{ authStore.error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<div v-if="successMessage" class="mt-4 p-3 bg-green-500/20 border border-green-500/30 rounded-lg">
|
||||
<p class="text-green-300 text-sm">{{ successMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// 当前模式:login 或 register
|
||||
const currentMode = ref<'login' | 'register'>('login')
|
||||
|
||||
// 成功消息
|
||||
const successMessage = ref('')
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
// 注册表单数据
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 登录处理函数
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.username || !loginForm.password) {
|
||||
toast.error('请填写完整的登录信息')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await authStore.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
|
||||
// 如果选择记住我,保存登录信息到本地存储
|
||||
if (loginForm.rememberMe) {
|
||||
localStorage.setItem('rememberedUser', JSON.stringify({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
}))
|
||||
} else {
|
||||
localStorage.removeItem('rememberedUser')
|
||||
}
|
||||
|
||||
router.push('/dashboard')
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
// 根据不同的错误类型显示不同的错误信息
|
||||
if (error.response?.status === 401) {
|
||||
const errorMessage = error.response?.data?.error || '用户名或密码错误'
|
||||
const remainingAttempts = error.response?.data?.remainingAttempts
|
||||
|
||||
if (errorMessage.includes('锁定')) {
|
||||
toast.error(errorMessage)
|
||||
} else if (errorMessage.includes('禁用') || errorMessage.includes('inactive')) {
|
||||
toast.error('您的账户尚未激活,请联系管理员激活账户')
|
||||
} else {
|
||||
if (remainingAttempts !== undefined) {
|
||||
toast.error(`用户名或密码错误,还剩${remainingAttempts}次尝试机会`)
|
||||
} else {
|
||||
toast.error('用户名或密码错误,请检查后重试')
|
||||
}
|
||||
}
|
||||
} else if (error.response?.status === 400) {
|
||||
toast.error(error.response?.data?.message || '请求参数错误')
|
||||
} else if (error.response?.status === 500) {
|
||||
toast.error('服务器内部错误,请稍后重试')
|
||||
} else if (error.code === 'NETWORK_ERROR') {
|
||||
toast.error('网络连接失败,请检查网络连接')
|
||||
} else {
|
||||
toast.error(error.response?.data?.message || '登录失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册处理函数
|
||||
const handleRegister = async () => {
|
||||
if (!registerForm.username || !registerForm.password || !registerForm.confirmPassword) {
|
||||
toast.error('请填写完整的注册信息')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名格式
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(registerForm.username)) {
|
||||
toast.error('用户名只能包含字母、数字和下划线')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名长度
|
||||
if (registerForm.username.length < 3 || registerForm.username.length > 30) {
|
||||
toast.error('用户名长度必须在3-30个字符之间')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码长度
|
||||
if (registerForm.password.length < 8) {
|
||||
toast.error('密码长度至少8个字符')
|
||||
return
|
||||
}
|
||||
|
||||
if (registerForm.password !== registerForm.confirmPassword) {
|
||||
toast.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await authStore.register({
|
||||
username: registerForm.username,
|
||||
password: registerForm.password,
|
||||
confirmPassword: registerForm.confirmPassword
|
||||
})
|
||||
|
||||
successMessage.value = '注册成功!您的账户已创建,请等待管理员激活后即可登录。'
|
||||
toast.success('注册成功!请等待管理员激活您的账户。')
|
||||
|
||||
// 注册成功时清空注册表单
|
||||
registerForm.username = ''
|
||||
registerForm.password = ''
|
||||
registerForm.confirmPassword = ''
|
||||
|
||||
// 切换到登录模式
|
||||
currentMode.value = 'login'
|
||||
} catch (error: any) {
|
||||
console.error('注册错误详情:', error)
|
||||
console.error('错误响应:', error.response.data.error)
|
||||
|
||||
// 根据不同的错误类型显示不同的错误信息
|
||||
if (error.response?.status === 400) {
|
||||
const details = error.response?.data?.error
|
||||
if (details) {
|
||||
// 显示具体的验证错误信息
|
||||
const errorMessages = details
|
||||
toast.error(`注册失败: ${errorMessages}`)
|
||||
}
|
||||
} else if (error.response?.status === 409) {
|
||||
toast.error('用户名已存在,请使用其他信息')
|
||||
} else if (error.response?.status === 500) {
|
||||
toast.error('服务器内部错误,请稍后重试')
|
||||
} else if (error.code === 'NETWORK_ERROR') {
|
||||
toast.error('网络连接失败,请检查网络连接')
|
||||
} else {
|
||||
toast.error(error.response?.data?.message || '注册失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码处理函数
|
||||
const handleForgotPassword = () => {
|
||||
toast.info('请联系管理员')
|
||||
}
|
||||
|
||||
// 页面加载时检查是否有记住的登录信息
|
||||
const loadRememberedUser = () => {
|
||||
const remembered = localStorage.getItem('rememberedUser')
|
||||
if (remembered) {
|
||||
try {
|
||||
const userData = JSON.parse(remembered)
|
||||
loginForm.username = userData.username || ''
|
||||
loginForm.rememberMe = true
|
||||
} catch (error) {
|
||||
console.error('解析记住的用户信息失败:', error)
|
||||
localStorage.removeItem('rememberedUser')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面挂载时初始化
|
||||
onMounted(() => {
|
||||
loadRememberedUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义动画 */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
43
frontend/src/views/NotFound.vue
Normal file
43
frontend/src/views/NotFound.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full text-center">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-9xl font-bold text-primary-600 dark:text-primary-400">404</h1>
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
页面未找到
|
||||
</h2>
|
||||
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8">
|
||||
抱歉,您访问的页面不存在或已被移除。
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<router-link
|
||||
to="/"
|
||||
class="btn-primary w-full"
|
||||
>
|
||||
返回首页
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
@click="goBack"
|
||||
class="btn-outline w-full"
|
||||
>
|
||||
返回上页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
</script>
|
||||
212
frontend/src/views/Test.vue
Normal file
212
frontend/src/views/Test.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-8">
|
||||
API 集成测试
|
||||
</h1>
|
||||
|
||||
<!-- 认证状态 -->
|
||||
<div class="card mb-8">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
认证状态
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">登录状态</p>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ authStore.isLoggedIn ? '已登录' : '未登录' }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">用户信息</p>
|
||||
<p class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ authStore.user?.username || '无' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API 测试 -->
|
||||
<div class="card mb-8">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
API 测试
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- 获取路径 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testGetPaths"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ loading ? '测试中...' : '测试获取路径' }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
结果: {{ pathsResult }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 用户注册 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testRegister"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ loading ? '测试中...' : '测试用户注册' }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
结果: {{ registerResult }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 用户登录 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testLogin"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ loading ? '测试中...' : '测试用户登录' }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
结果: {{ loginResult }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 获取用户信息 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="testGetProfile"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ loading ? '测试中...' : '测试获取用户信息' }}
|
||||
</button>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
结果: {{ profileResult }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 响应数据 -->
|
||||
<div class="card">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
响应数据
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-sm overflow-auto max-h-96">{{ responseData }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { authAPI, pathAPI } from '@/utils/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const loading = ref(false)
|
||||
const pathsResult = ref('未测试')
|
||||
const registerResult = ref('未测试')
|
||||
const loginResult = ref('未测试')
|
||||
const profileResult = ref('未测试')
|
||||
const responseData = ref('')
|
||||
|
||||
// 测试获取路径
|
||||
const testGetPaths = async () => {
|
||||
loading.value = true
|
||||
pathsResult.value = '测试中...'
|
||||
|
||||
try {
|
||||
const response = await pathAPI.getPaths()
|
||||
pathsResult.value = '成功'
|
||||
responseData.value = JSON.stringify(response, null, 2)
|
||||
toast.success('获取路径成功')
|
||||
} catch (error: any) {
|
||||
pathsResult.value = '失败'
|
||||
responseData.value = JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
toast.error('获取路径失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户注册
|
||||
const testRegister = async () => {
|
||||
loading.value = true
|
||||
registerResult.value = '测试中...'
|
||||
|
||||
try {
|
||||
const response = await authAPI.register({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
confirmPassword: 'password123'
|
||||
})
|
||||
registerResult.value = '成功'
|
||||
responseData.value = JSON.stringify(response, null, 2)
|
||||
toast.success('注册测试成功')
|
||||
} catch (error: any) {
|
||||
registerResult.value = '失败'
|
||||
responseData.value = JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
toast.error('注册测试失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户登录
|
||||
const testLogin = async () => {
|
||||
loading.value = true
|
||||
loginResult.value = '测试中...'
|
||||
|
||||
try {
|
||||
const response = await authAPI.login({
|
||||
username: 'testuser',
|
||||
password: 'password123'
|
||||
})
|
||||
loginResult.value = '成功'
|
||||
responseData.value = JSON.stringify(response, null, 2)
|
||||
toast.success('登录测试成功')
|
||||
} catch (error: any) {
|
||||
loginResult.value = '失败'
|
||||
responseData.value = JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
toast.error('登录测试失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试获取用户信息
|
||||
const testGetProfile = async () => {
|
||||
loading.value = true
|
||||
profileResult.value = '测试中...'
|
||||
|
||||
try {
|
||||
const response = await authAPI.getProfile()
|
||||
profileResult.value = '成功'
|
||||
responseData.value = JSON.stringify(response, null, 2)
|
||||
toast.success('获取用户信息成功')
|
||||
} catch (error: any) {
|
||||
profileResult.value = '失败'
|
||||
responseData.value = JSON.stringify(error.response?.data || error.message, null, 2)
|
||||
toast.error('获取用户信息失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user