修复bug

This commit is contained in:
2025-07-08 16:44:04 +08:00
parent aa2416c5d6
commit 1af79c4111
22 changed files with 400 additions and 1036 deletions

View File

@@ -11,11 +11,14 @@ RUN apk add --no-cache \
COPY package*.json ./
# 安装依赖
RUN npm ci
RUN npm install
# 复制源代码
COPY . .
# 构建项目
RUN npm run build
# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
@@ -30,6 +33,5 @@ EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000 || exit 1
# 启动命令 - 支持环境变量
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# 启动命令
CMD ["npm", "run", "start"]

View File

@@ -4,8 +4,9 @@
"description": "Pandora 前端应用",
"type": "module",
"scripts": {
"start": "vite preview --host 0.0.0.0 --port 3000",
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
@@ -13,17 +14,19 @@
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"@vueuse/core": "^10.4.1",
"vee-validate": "^4.10.5",
"vue-toastification": "^2.0.0-rc.5",
"axios": "^1.5.0",
"@headlessui/vue": "^1.7.16",
"@heroicons/vue": "^2.0.18",
"@vue/runtime-core": "^3.3.4",
"@vue/runtime-dom": "^3.3.4",
"@vueuse/core": "^10.4.1",
"axios": "^1.5.0",
"clsx": "^2.0.0",
"tailwind-merge": "^1.14.0"
"pinia": "^2.1.7",
"tailwind-merge": "^1.14.0",
"vee-validate": "^4.10.5",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@types/node": "^20.6.3",
@@ -38,13 +41,13 @@
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.0",
"typescript": "^5.8.3",
"vite": "^4.4.11",
"vitest": "^0.34.4",
"vue-tsc": "^1.8.15"
"vue-tsc": "^3.0.1"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
}
}
}

View File

@@ -11,11 +11,11 @@ export interface WebsiteConfig {
const getWebsiteUrls = () => {
// 检查是否在浏览器环境中
if (typeof window !== 'undefined') {
// 在客户端使用Vite定义的全局变量
// 在客户端使用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'
claude: import.meta.env.CLAUDE_TARGET_URL || 'https://chat.micar9.com:8443',
chatgpt: import.meta.env.CHATGPT_TARGET_URL || 'https://chat.openai.com',
grok: import.meta.env.GROK_TARGET_URL || 'https://grok-mirror.micar9.com:8443'
}
}

View File

@@ -1,9 +1,7 @@
import type { RouteRecordRaw, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
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[] = [
{
@@ -74,17 +72,18 @@ const router = createRouter({
})
// 路由守卫
router.beforeEach((to: any, from: any, next: any) => {
router.beforeEach(async (
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// 设置页面标题
document.title = `${to.meta.title} - Pandora`
// 获取认证状态
const authStore = useAuthStore()
const adminStore = useAdminStore()
const toast = useToast()
const title = to.meta.title as string
document.title = `${title} - Pandora`
// 检查是否需要用户认证
if (to.meta.requiresAuth) {
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
next('/')
return

View File

@@ -2,7 +2,7 @@ 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'
import type { User } from '@/types'
export const useAdminStore = defineStore('admin', () => {
// 状态

View File

@@ -138,45 +138,6 @@ export const useAuthStore = defineStore('auth', () => {
}
}
// 设置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
@@ -238,8 +199,6 @@ export const useAuthStore = defineStore('auth', () => {
resendVerification,
forgotPassword,
resetPassword,
setupTOTP,
verifyTOTP,
getProfile,
logout,
clearError

View File

@@ -2,12 +2,13 @@
export interface User {
id: string
username: string
email: string
role: string
isActive: boolean
emailVerified?: boolean
totpEnabled: boolean
createdAt: string
updatedAt: string
accounts?: string[]
}
// 网站账号相关类型
@@ -47,15 +48,10 @@ export interface Session {
// 审计日志相关类型
export interface AuditLog {
id: string
userId: string
action: string
resource: string
resourceId?: string
details?: any
ipAddress?: string
userAgent?: string
createdAt: string
user: User
description: string
time: string
ipAddress: string
}
// API响应类型
@@ -116,4 +112,37 @@ export interface Notification {
message: string
duration?: number
createdAt: string
}
export interface Account {
id: string
username: string
description: string
}
export interface SystemSettings {
allowRegistration: boolean
sessionTimeout: number
[key: string]: any
}
export interface AdminMenu {
id: string
name: string
description: string
icon: string
path: string
}
export interface AdminUser {
id: string
username: string
role: string
}
export interface SystemStats {
totalUsers: number
totalAccounts: number
todayVisits: number
alerts: number
}

View File

@@ -113,17 +113,6 @@ export const authAPI = {
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() {
@@ -147,26 +136,7 @@ export const authAPI = {
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 = {
// 获取用户可用账号

View File

@@ -301,70 +301,94 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAdminStore } from '@/stores/admin'
import type { AdminMenu, AdminUser, SystemStats } from '@/types'
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({
const adminUser = ref<AdminUser | null>(null)
const stats = ref<SystemStats>({
totalUsers: 0,
totalAccounts: 0,
todayVisits: 0,
alerts: 0
})
// 最近活动
const recentActivities = ref([])
const adminMenus: AdminMenu[] = [
{
id: 'users',
name: '用户管理',
description: '管理系统用户',
icon: 'UserIcon',
path: '/admin/users'
},
{
id: 'accounts',
name: '账号管理',
description: '管理网站账号',
icon: 'KeyIcon',
path: '/admin/accounts'
},
{
id: 'permissions',
name: '权限管理',
description: '配置访问权限',
icon: 'ShieldIcon',
path: '/admin/permissions'
},
{
id: 'monitor',
name: '系统监控',
description: '监控系统状态',
icon: 'ChartIcon',
path: '/admin/monitor'
}
]
// 导航到菜单
const navigateToMenu = (menu: any) => {
router.push(menu.route)
const navigateToMenu = (menu: AdminMenu) => {
router.push(menu.path)
}
// 管理员用户信息
const adminUser = ref<any>(null)
// 退出登录
const handleLogout = () => {
adminAuth.logout()
router.push('/admin/login')
const handleLogout = async () => {
try {
await adminStore.logout()
router.push('/admin/login')
} catch (error) {
console.error('退出登录失败:', error)
}
}
// 加载管理员信息
const loadAdminInfo = async () => {
try {
// 从adminAuth获取管理员信息
const adminInfo = adminAuth.getAdminInfo()
if (adminInfo) {
adminUser.value = adminInfo
}
} catch (error) {
console.error('加载管理员信息失败:', error)
}
}
// 加载系统统计数据
const loadStats = async () => {
try {
const response = await adminStore.getStats()
if (response && response.stats) {
stats.value = response.stats
}
} catch (error) {
console.error('加载系统统计失败:', error)
}
}
// 最近活动
const recentActivities = ref<any[]>([])
// 格式化时间
const formatTime = (time: string) => {
const date = new Date(time)
@@ -382,40 +406,6 @@ const formatTime = (time: string) => {
}
}
// 加载统计数据
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 {
@@ -423,50 +413,22 @@ const loadRecentActivities = async () => {
const response = await adminStore.getRecentActivities()
console.log('最近活动响应:', response)
if (response.success && response.data) {
recentActivities.value = response.data.activities || []
if (response && response.activities) {
recentActivities.value = response.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)
}
await loadAdminInfo()
await loadStats()
await loadRecentActivities()
})
</script>

View File

@@ -164,23 +164,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">是否启用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>
@@ -241,85 +224,62 @@
import { ref, computed, onMounted } from 'vue'
import { useAdminStore } from '@/stores/admin'
import { useToast } from 'vue-toastification'
import type { User, Account, SystemSettings } from '@/types'
const adminStore = useAdminStore()
const toast = useToast()
// 搜索和筛选
const users = ref<User[]>([])
const availableAccounts = ref<Account[]>([])
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,
const showAccountModal = ref(false)
const selectedUser = ref<User | null>(null)
const selectedUserAccounts = ref<string[]>([])
const systemSettings = ref<SystemSettings>({
allowRegistration: false,
sessionTimeout: 24
})
// 模态框状态
const showAccountModal = ref(false)
const selectedUser = ref(null)
const selectedUserAccounts = ref([])
// 过滤用户列表
const filteredUsers = computed(() => {
return users.value.filter(user => {
const matchesSearch = !searchQuery.value ||
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesAccount = !accountFilter.value ||
(user.accounts && user.accounts.includes(accountFilter.value))
return matchesSearch && matchesAccount
})
})
// 搜索处理
const handleSearch = () => {
// 这里可以添加实际的搜索逻辑
console.log('搜索:', searchQuery.value, '账号筛选:', accountFilter.value)
}
// 根据账户ID获取账户名称
const getAccountName = (accountId: string) => {
// 获取账号名称
const getAccountName = (accountId: string): 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 || [])]
const editUserAccounts = (user: User) => {
selectedUser.value = user
selectedUserAccounts.value = user.accounts || []
showAccountModal.value = true
}
// 切换系统设置
const toggleSystemSetting = (setting: keyof SystemSettings) => {
if (typeof systemSettings.value[setting] === 'boolean') {
systemSettings.value[setting] = !systemSettings.value[setting]
}
}
// 搜索处理
const handleSearch = () => {
// 实现搜索逻辑
}
// 切换用户账号权限
const toggleUserAccount = (accountId: string) => {
const index = selectedUserAccounts.value.indexOf(accountId)
@@ -333,22 +293,25 @@ const toggleUserAccount = (accountId: string) => {
// 保存用户账号权限
const saveUserAccounts = async () => {
try {
if (!selectedUser.value?.id) {
toast.error('用户ID不存在')
return
}
// 调用API保存用户账号权限到后端
await adminStore.updateUserAccounts(selectedUser.value.id, selectedUserAccounts.value)
console.log('保存用户账号权限:', 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])
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('权限保存成功')
@@ -359,12 +322,6 @@ const saveUserAccounts = async () => {
}
}
// 切换系统设置
const toggleSystemSetting = (setting: string) => {
systemSettings.value[setting] = !systemSettings.value[setting]
console.log('切换系统设置:', setting, systemSettings.value[setting])
}
// 加载用户列表
const loadUsers = async () => {
try {
@@ -380,6 +337,22 @@ const loadUsers = async () => {
}
}
// 加载可用账号
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: '第三个登录账号' }
]
}
}
// 组件挂载时加载数据
onMounted(async () => {
await loadUsers()

View File

@@ -103,9 +103,6 @@
<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>
@@ -269,7 +266,7 @@ const showCreateModal = ref(false)
const createForm = reactive({
username: '',
password: '',
role: 'user'
role: 'user' as string
})
// 编辑用户模态框
@@ -277,7 +274,7 @@ const showEditModal = ref(false)
const editForm = reactive({
id: '',
username: '',
role: 'user',
role: 'user' as string,
isActive: true,
password: ''
})
@@ -324,7 +321,11 @@ const changePage = async (page: number) => {
// 创建用户
const handleCreateUser = async () => {
try {
await adminStore.createUser(createForm)
await adminStore.createUser({
username: createForm.username,
password: createForm.password,
role: createForm.role
})
toast.success('用户创建成功')
showCreateModal.value = false

View File

@@ -226,7 +226,7 @@ onMounted(async () => {
// 加载用户账号
await loadUserAccounts()
} catch (error) {
} catch (error: any) {
console.error('Dashboard初始化失败:', error)
// 如果是认证错误,重定向到登录页面

View File

@@ -10,20 +10,12 @@ export default defineConfig({
'@': resolve(__dirname, 'src'),
},
},
define: {
// 定义环境变量,使其在客户端可用
VITE_API_URL: JSON.stringify(process.env.VITE_API_URL || 'http://localhost:3001'),
VITE_APP_NAME: JSON.stringify(process.env.VITE_APP_NAME || 'Pandora'),
VITE_CLAUDE_TARGET_URL: JSON.stringify(process.env.CLAUDE_TARGET_URL || 'https://claude.ai'),
VITE_CHATGPT_TARGET_URL: JSON.stringify(process.env.CHATGPT_TARGET_URL || 'https://chat.openai.com'),
VITE_GROK_TARGET_URL: JSON.stringify(process.env.GROK_TARGET_URL || 'https://grok.x.ai'),
},
server: {
port: 3000,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3001',
target: 'http://backend:3001',
changeOrigin: true,
secure: false,
},