feat: 优化多个前端页面
- 扩展 Account 账户页面功能 - 优化 Script 脚本页面 - 完善 Settings 设置页面 - 增强 Chat 聊天页面 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,484 @@
|
||||
<script setup lang="ts">
|
||||
// Account 页面 - 占位
|
||||
import { ref, computed } from 'vue'
|
||||
import './database/database.css'
|
||||
|
||||
// 菜单类型
|
||||
type MenuKey = 'users' | 'roles' | 'permissions'
|
||||
|
||||
// 当前选中的菜单
|
||||
const activeMenu = ref<MenuKey>('users')
|
||||
|
||||
// 菜单列表
|
||||
const menuItems = [
|
||||
{ key: 'users', label: 'Users', icon: 'fa-users' },
|
||||
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
|
||||
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
|
||||
]
|
||||
|
||||
// 用户数据
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
role: string
|
||||
status: 'active' | 'inactive'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const users = ref<User[]>([
|
||||
{ id: 1, name: 'Alex Smith', email: 'alex@example.com', role: 'Admin', status: 'active', createdAt: '2025-01-15' },
|
||||
{ id: 2, name: 'John Doe', email: 'john@example.com', role: 'Developer', status: 'active', createdAt: '2025-02-20' },
|
||||
{ id: 3, name: 'Jane Wilson', email: 'jane@example.com', role: 'Viewer', status: 'active', createdAt: '2025-03-05' },
|
||||
{ id: 4, name: 'Mike Brown', email: 'mike@example.com', role: 'Developer', status: 'inactive', createdAt: '2025-03-10' },
|
||||
{ id: 5, name: 'Sarah Davis', email: 'sarah@example.com', role: 'Admin', status: 'active', createdAt: '2025-03-15' },
|
||||
])
|
||||
|
||||
// 角色数据
|
||||
interface Role {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
userCount: number
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
const roles = ref<Role[]>([
|
||||
{ id: 1, name: 'Admin', description: 'Full access to all features', userCount: 2, permissions: ['all'] },
|
||||
{ id: 2, name: 'Developer', description: 'Access to development tools', userCount: 2, permissions: ['read', 'write', 'execute'] },
|
||||
{ id: 3, name: 'Viewer', description: 'Read-only access', userCount: 1, permissions: ['read'] },
|
||||
{ id: 4, name: 'Manager', description: 'Manage users and resources', userCount: 0, permissions: ['read', 'write', 'manage'] },
|
||||
])
|
||||
|
||||
// 权限数据
|
||||
interface Permission {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
}
|
||||
|
||||
const permissions = ref<Permission[]>([
|
||||
{ id: 'read', name: 'Read', description: 'View resources', category: 'General' },
|
||||
{ id: 'write', name: 'Write', description: 'Create and update resources', category: 'General' },
|
||||
{ id: 'delete', name: 'Delete', description: 'Remove resources', category: 'General' },
|
||||
{ id: 'execute', name: 'Execute', description: 'Run scripts and tools', category: 'Tools' },
|
||||
{ id: 'manage', name: 'Manage', description: 'Manage users and settings', category: 'Admin' },
|
||||
{ id: 'all', name: 'All Access', description: 'Full system access', category: 'Admin' },
|
||||
])
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const filterRole = ref('')
|
||||
|
||||
// 编辑状态
|
||||
const isEditingUser = ref(false)
|
||||
const editingUser = ref<User | null>(null)
|
||||
const isEditingRole = ref(false)
|
||||
const editingRole = ref<Role | null>(null)
|
||||
const isCreatingRole = ref(false)
|
||||
const newRole = ref({ name: '', description: '' })
|
||||
|
||||
// 过滤后的用户
|
||||
const filteredUsers = computed(() => {
|
||||
return users.value.filter(user => {
|
||||
const matchSearch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchRole = filterRole.value === '' || user.role === filterRole.value
|
||||
return matchSearch && matchRole
|
||||
})
|
||||
})
|
||||
|
||||
// 获取用户角色选项
|
||||
const roleOptions = computed(() => {
|
||||
return [...new Set(users.value.map(u => u.role))]
|
||||
})
|
||||
|
||||
// 编辑用户
|
||||
const openEditUser = (user: User) => {
|
||||
editingUser.value = { ...user }
|
||||
isEditingUser.value = true
|
||||
}
|
||||
|
||||
const saveUser = () => {
|
||||
if (editingUser.value) {
|
||||
const index = users.value.findIndex(u => u.id === editingUser.value!.id)
|
||||
if (index !== -1) {
|
||||
users.value[index] = { ...editingUser.value }
|
||||
}
|
||||
}
|
||||
isEditingUser.value = false
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = (id: number) => {
|
||||
users.value = users.value.filter(u => u.id !== id)
|
||||
}
|
||||
|
||||
// 切换用户状态
|
||||
const toggleUserStatus = (user: User) => {
|
||||
user.status = user.status === 'active' ? 'inactive' : 'active'
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
const openEditRole = (role: Role) => {
|
||||
editingRole.value = { ...role }
|
||||
isEditingRole.value = true
|
||||
}
|
||||
|
||||
const saveRole = () => {
|
||||
if (editingRole.value) {
|
||||
const index = roles.value.findIndex(r => r.id === editingRole.value!.id)
|
||||
if (index !== -1) {
|
||||
roles.value[index] = { ...editingRole.value }
|
||||
}
|
||||
}
|
||||
isEditingRole.value = false
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
const deleteRole = (id: number) => {
|
||||
roles.value = roles.value.filter(r => r.id !== id)
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
const openCreateRole = () => {
|
||||
newRole.value = { name: '', description: '' }
|
||||
isCreatingRole.value = true
|
||||
}
|
||||
|
||||
const saveNewRole = () => {
|
||||
const newId = Math.max(...roles.value.map(r => r.id)) + 1
|
||||
roles.value.push({
|
||||
id: newId,
|
||||
name: newRole.value.name || 'New Role',
|
||||
description: newRole.value.description,
|
||||
userCount: 0,
|
||||
permissions: [],
|
||||
})
|
||||
isCreatingRole.value = false
|
||||
}
|
||||
|
||||
// 状态样式
|
||||
const statusClass = (status: string) => {
|
||||
return status === 'active' ? 'bg-primary-success' : 'bg-gray-500'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<i class="fa-solid fa-user text-gray-400"></i>
|
||||
<i class="fa-solid fa-user-shield text-orange-500"></i>
|
||||
<span class="font-medium">Account</span>
|
||||
</div>
|
||||
<div class="text-gray-400">
|
||||
Account management coming soon...
|
||||
|
||||
<div class="flex gap-6">
|
||||
<!-- 左侧菜单 -->
|
||||
<nav class="w-48 flex-shrink-0">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
@click="activeMenu = item.key as MenuKey"
|
||||
class="px-4 py-3 rounded-lg cursor-pointer transition-colors flex items-center gap-3"
|
||||
:class="activeMenu === item.key
|
||||
? 'bg-orange-500/10 text-orange-400'
|
||||
: 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
>
|
||||
<i :class="['fa-solid', item.icon]"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex-1">
|
||||
<!-- Users -->
|
||||
<div v-if="activeMenu === 'users'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Users</h2>
|
||||
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search users..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterRole" placeholder="All Roles" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option label="All Roles" value="" />
|
||||
<el-option v-for="role in roleOptions" :key="role" :label="role" :value="role" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Role</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in filteredUsers" :key="user.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-orange-500 to-red-500 flex items-center justify-center text-white text-sm font-medium">
|
||||
{{ user.name.charAt(0) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ user.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ user.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ user.role }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusClass(user.status)"></span>
|
||||
<span class="capitalize text-sm">{{ user.status }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ user.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="toggleUserStatus(user)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
:title="user.status === 'active' ? 'Deactivate' : 'Activate'"
|
||||
>
|
||||
<i :class="['fa-solid', user.status === 'active' ? 'fa-ban' : 'fa-check', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openEditUser(user)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteUser(user.id)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="filteredUsers.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-users text-4xl mb-3"></i>
|
||||
<p>No users found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles -->
|
||||
<div v-if="activeMenu === 'roles'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Roles</h2>
|
||||
<button @click="openCreateRole" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add Role
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div v-for="role in roles" :key="role.id" class="bg-dark-700 rounded-xl p-5 border border-dark-600">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg">{{ role.name }}</h3>
|
||||
<p class="text-sm text-gray-400">{{ role.description }}</p>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button @click="openEditRole(role)" class="p-2 rounded-lg hover:bg-dark-600 transition-colors">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button @click="deleteRole(role.id)" class="p-2 rounded-lg hover:bg-dark-600 transition-colors">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span v-for="perm in role.permissions.slice(0, 3)" :key="perm" class="px-2 py-0.5 bg-dark-600 rounded text-xs text-gray-400">
|
||||
{{ perm }}
|
||||
</span>
|
||||
<span v-if="role.permissions.length > 3" class="px-2 py-0.5 bg-dark-600 rounded text-xs text-gray-400">
|
||||
+{{ role.permissions.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-400">{{ role.userCount }} users</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions -->
|
||||
<div v-if="activeMenu === 'permissions'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Permissions</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Permission</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Description</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="perm in permissions" :key="perm.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||
<td class="px-5 py-4">
|
||||
<span class="font-medium">{{ perm.name }}</span>
|
||||
<span class="text-gray-500 ml-2">({{ perm.id }})</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400">{{ perm.description }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="px-2 py-1 bg-dark-500 rounded text-sm">{{ perm.category }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditingUser" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit User</h3>
|
||||
<button @click="isEditingUser = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Name</label>
|
||||
<input
|
||||
v-model="editingUser!.name"
|
||||
type="text"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Email</label>
|
||||
<input
|
||||
v-model="editingUser!.email"
|
||||
type="email"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Role</label>
|
||||
<el-select v-model="editingUser!.role" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="role in roleOptions" :key="role" :label="role" :value="role" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Status</label>
|
||||
<el-select v-model="editingUser!.status" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option label="Active" value="active" />
|
||||
<el-option label="Inactive" value="inactive" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="isEditingUser = false"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveUser"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 创建角色弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreatingRole" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Create Role</h3>
|
||||
<button @click="isCreatingRole = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Role Name</label>
|
||||
<input
|
||||
v-model="newRole.name"
|
||||
type="text"
|
||||
placeholder="Enter role name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="newRole.description"
|
||||
rows="3"
|
||||
placeholder="Describe this role..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="isCreatingRole = false"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewRole"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Create Role
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +19,14 @@ interface Agent {
|
||||
status: 'online' | 'offline'
|
||||
}
|
||||
|
||||
interface ChatSession {
|
||||
id: number
|
||||
title: string
|
||||
agentId: number
|
||||
lastMessage: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
// AI 助手配置
|
||||
const chatAgents = ref<Agent[]>([
|
||||
{ id: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'online' },
|
||||
@@ -32,13 +40,22 @@ const chatAgents = ref<Agent[]>([
|
||||
|
||||
// 当前选中的助手
|
||||
const selectedAgent = ref<Agent | null>(chatAgents.value[0])
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// 聊天消息
|
||||
const messages = ref<ChatMessage[]>([
|
||||
{ id: 1, role: 'assistant', content: '你好!我是 Claude,你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
|
||||
])
|
||||
|
||||
// 模拟历史对话列表
|
||||
const chatSessions = ref<ChatSession[]>([
|
||||
{ id: 1, title: '关于 Python 学习的讨论', agentId: 1, lastMessage: '谢谢你!', timestamp: new Date(Date.now() - 3600000) },
|
||||
{ id: 2, title: '代码调试帮助', agentId: 1, lastMessage: '让我看看这个问题...', timestamp: new Date(Date.now() - 7200000) },
|
||||
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
|
||||
])
|
||||
|
||||
// 侧边栏展开/收起状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// 输入内容
|
||||
const inputMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
@@ -118,6 +135,17 @@ const selectAgent = (agent: Agent) => {
|
||||
]
|
||||
}
|
||||
|
||||
// 选择历史对话
|
||||
const selectSession = (session: ChatSession) => {
|
||||
const agent = chatAgents.value.find(a => a.id === session.agentId)
|
||||
if (agent) {
|
||||
selectedAgent.value = agent
|
||||
}
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `已加载会话:${session.title}`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
|
||||
// 新建聊天
|
||||
const newChat = () => {
|
||||
messages.value = [
|
||||
@@ -130,6 +158,19 @@ const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 格式化相对时间
|
||||
const formatRelativeTime = (date: Date) => {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
|
||||
if (hours < 1) return '刚刚'
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
if (days < 7) return `${days}天前`
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 回车发送
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
@@ -145,18 +186,15 @@ const autoResize = (e: Event) => {
|
||||
target.style.height = Math.min(target.scrollHeight, 160) + 'px'
|
||||
}
|
||||
|
||||
// 切换侧边栏
|
||||
// 折叠侧边栏
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 350)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@@ -164,27 +202,27 @@ const toggleSidebar = () => {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
transform: translateY(16px) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.message-enter {
|
||||
animation: messageSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
animation: messageSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
@@ -195,53 +233,53 @@ const toggleSidebar = () => {
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
|
||||
50% { box-shadow: 0 0 20px 4px rgba(249, 115, 22, 0.2); }
|
||||
}
|
||||
|
||||
.agent-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex bg-[#0a0a0f]">
|
||||
<div class="h-screen flex bg-[#09090b]">
|
||||
<!-- 主聊天区域 -->
|
||||
<div class="flex-1 flex flex-col bg-[#0a0a0f]">
|
||||
<div class="flex-1 flex flex-col bg-[#09090b]">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="h-14 px-6 flex items-center justify-between border-b border-white/5 bg-[#0d0d12]/50 backdrop-blur-sm">
|
||||
<div class="h-16 px-4 flex items-center justify-between border-b border-white/[0.06] bg-[#0c0c0f]/80 backdrop-blur-xl">
|
||||
<!-- 左侧:当前AI信息 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-if="selectedAgent" class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg shadow-lg"
|
||||
:style="{ backgroundColor: selectedAgent.accentColor + '20', color: selectedAgent.accentColor }"
|
||||
class="w-9 h-9 rounded-xl flex items-center justify-center text-lg shadow-lg"
|
||||
:style="{ backgroundColor: selectedAgent.accentColor + '15', color: selectedAgent.accentColor }"
|
||||
>
|
||||
{{ selectedAgent.avatar }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-white">{{ selectedAgent?.name || 'Chat' }}</div>
|
||||
<div class="text-sm font-semibold text-white tracking-wide">{{ selectedAgent?.name || 'Chat' }}</div>
|
||||
<div class="text-[11px] flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span class="text-white/40">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右上角操作 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 展开侧边栏按钮 -->
|
||||
<!-- 中间:空白 -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- 右侧:折叠按钮 -->
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
v-if="sidebarCollapsed"
|
||||
@click="toggleSidebar"
|
||||
class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors"
|
||||
title="Show AI assistants"
|
||||
class="p-2.5 rounded-xl hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"
|
||||
:title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-[18px] h-[18px] transition-transform duration-300" :class="sidebarCollapsed ? '' : 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -249,20 +287,20 @@ const toggleSidebar = () => {
|
||||
</div>
|
||||
|
||||
<!-- 消息区域 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
||||
<div class="px-6">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-enter flex gap-4"
|
||||
class="message-enter flex items-start mb-4"
|
||||
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 shadow-lg"
|
||||
:class="message.role === 'user' ? 'bg-gradient-to-br from-emerald-500 to-teal-600' : ''"
|
||||
class="w-9 h-9 rounded-full flex-shrink-0 flex items-center justify-center mx-3 mt-1"
|
||||
:class="message.role === 'user' ? 'bg-gradient-to-br from-orange-500 to-amber-600' : ''"
|
||||
:style="message.role === 'assistant' && selectedAgent ? {
|
||||
backgroundColor: selectedAgent.accentColor + '20',
|
||||
backgroundColor: selectedAgent.accentColor + '25',
|
||||
color: selectedAgent.accentColor
|
||||
} : {}"
|
||||
>
|
||||
@@ -270,28 +308,24 @@ const toggleSidebar = () => {
|
||||
<span v-else class="text-lg">{{ selectedAgent?.avatar || '🧠' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div
|
||||
class="max-w-[75%] rounded-2xl px-4 py-3"
|
||||
:class="message.role === 'user' ? 'bg-[#1e1e28] text-white' : 'bg-transparent'"
|
||||
>
|
||||
<div class="text-sm leading-relaxed whitespace-pre-wrap text-white/90">{{ message.content }}
|
||||
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-violet-400 cursor-blink align-middle"></span>
|
||||
<!-- 气泡和时间戳容器 -->
|
||||
<div :class="message.role === 'user' ? 'mr-3 ml-auto' : 'ml-3'">
|
||||
<!-- 消息气泡 -->
|
||||
<div
|
||||
class="px-4 py-2.5 rounded-xl text-[14px] leading-6"
|
||||
:class="message.role === 'user'
|
||||
? 'bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-tr-sm'
|
||||
: 'bg-[#2a2a35] text-white/90 rounded-tl-sm max-w-[80%]'"
|
||||
>
|
||||
{{ message.content }}
|
||||
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-orange-300 cursor-blink align-middle"></span>
|
||||
</div>
|
||||
|
||||
<!-- 消息底部 -->
|
||||
<div class="flex items-center justify-end mt-2 gap-3">
|
||||
<span class="text-[10px] text-white/25">{{ formatTime(message.timestamp) }}</span>
|
||||
<button
|
||||
v-if="message.role === 'assistant' && !message.isStreaming"
|
||||
@click="copyMessage(message.content)"
|
||||
class="text-white/25 hover:text-violet-400 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 时间戳 -->
|
||||
<div
|
||||
class="text-[11px] text-white/30 mt-1"
|
||||
:class="message.role === 'user' ? 'text-right' : ''"
|
||||
>
|
||||
{{ formatTime(message.timestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,11 +333,11 @@ const toggleSidebar = () => {
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="p-4 border-t border-white/5 bg-[#0d0d12]/50">
|
||||
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="relative bg-[#12121a] rounded-2xl border border-white/8 focus-within:border-violet-500/40 focus-within:shadow-lg focus-within:shadow-violet-500/10 transition-all duration-300">
|
||||
<div class="relative bg-[#12121a] rounded-2xl border border-white/[0.08] focus-within:border-orange-500/40 focus-within:shadow-[0_0_30px_rgba(249,115,22,0.08)] transition-all duration-300">
|
||||
<!-- 附件按钮 -->
|
||||
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors p-1">
|
||||
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/25 hover:text-orange-400 transition-colors p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
@@ -314,117 +348,111 @@ const toggleSidebar = () => {
|
||||
v-model="inputMessage"
|
||||
@keydown="handleKeydown"
|
||||
@input="autoResize"
|
||||
placeholder="Send a message..."
|
||||
placeholder="发送消息..."
|
||||
rows="1"
|
||||
class="w-full bg-transparent text-white placeholder-white/30 py-3.5 pl-12 pr-24 resize-none focus:outline-none text-sm"
|
||||
class="w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
|
||||
></textarea>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!inputMessage.trim() || isLoading"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-violet-500 hover:bg-violet-400 disabled:bg-white/8 disabled:text-white/20 text-white transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/25"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 rounded-lg flex items-center justify-center transition-all duration-200 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:class="inputMessage.trim() && !isLoading
|
||||
? 'bg-orange-500 hover:bg-orange-400 shadow-lg shadow-orange-500/30 active:scale-90'
|
||||
: 'bg-white/10'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
<svg v-if="!isLoading" class="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 text-white animate-spin" 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="text-center mt-2.5">
|
||||
<span class="text-[10px] text-white/20">AI can make mistakes. Please verify important information.</span>
|
||||
<div class="text-center mt-3">
|
||||
<span class="text-[10px] text-white/20 tracking-wide">AI 可能会产生错误信息,请核实重要内容</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 - 可折叠 -->
|
||||
<transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-4"
|
||||
enter-to-class="opacity-100 translate-x-0"
|
||||
leave-active-class="transition-all duration-250 ease-in"
|
||||
leave-from-class="opacity-100 translate-x-0"
|
||||
leave-to-class="opacity-0 translate-x-4"
|
||||
<!-- 右侧边栏:AI Hub -->
|
||||
<div
|
||||
class="flex-shrink-0 border-l border-white/[0.06] bg-[#0c0c0f] transition-all duration-300 ease-in-out overflow-hidden"
|
||||
:class="sidebarCollapsed ? 'w-0 opacity-0' : 'w-72 opacity-100'"
|
||||
>
|
||||
<div v-show="!sidebarCollapsed" class="w-72 bg-[#0d0d12] border-l border-white/5 flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-4 border-b border-white/5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
|
||||
<span class="text-white text-lg">🤖</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-white tracking-tight">AI Hub</span>
|
||||
</div>
|
||||
<div class="w-72 h-full flex flex-col">
|
||||
<!-- 侧边栏头部 -->
|
||||
<div class="p-4 border-b border-white/[0.06]">
|
||||
<div class="flex items-center gap-2 text-white font-semibold">
|
||||
<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
||||
</svg>
|
||||
<span>AI Hub</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建对话按钮 -->
|
||||
<div class="p-3">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full flex items-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手选择 -->
|
||||
<div class="px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">选择 AI 助手</div>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white/60 transition-colors"
|
||||
title="Hide sidebar"
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="selectAgent(agent)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-200"
|
||||
:class="selectedAgent?.id === agent.id
|
||||
? 'bg-orange-500/15 text-orange-400'
|
||||
: 'text-white/60 hover:bg-white/5 hover:text-white'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<span class="text-base">{{ agent.avatar }}</span>
|
||||
<span class="text-sm truncate">{{ agent.name }}</span>
|
||||
<span
|
||||
v-if="agent.status === 'online'"
|
||||
class="w-1.5 h-1.5 rounded-full bg-emerald-400 ml-auto"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建聊天按钮 -->
|
||||
<div class="p-4">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full py-2.5 px-4 bg-[#1a1a24] hover:bg-[#22222e] border border-white/8 hover:border-violet-500/30 rounded-xl text-white/90 text-sm flex items-center justify-center gap-2 transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/10 group"
|
||||
>
|
||||
<svg class="w-4 h-4 text-violet-400 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="font-medium">New Chat</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 py-2">
|
||||
<div class="text-[11px] font-medium text-white/30 uppercase tracking-wider px-3 mb-3">AI Assistants</div>
|
||||
<!-- 历史对话列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">最近对话</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="selectAgent(agent)"
|
||||
class="group px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-200"
|
||||
:class="selectedAgent?.id === agent.id
|
||||
? 'bg-gradient-to-r ' + agent.gradient + ' border-l-2'
|
||||
: 'hover:bg-white/[0.03] border-l-2 border-transparent'"
|
||||
:style="selectedAgent?.id === agent.id ? `border-left-color: ${agent.accentColor}` : ''"
|
||||
<button
|
||||
v-for="session in chatSessions"
|
||||
:key="session.id"
|
||||
@click="selectSession(session)"
|
||||
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg transition-transform duration-200 group-hover:scale-110"
|
||||
:class="selectedAgent?.id === agent.id ? 'shadow-lg' : ''"
|
||||
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
||||
>
|
||||
{{ agent.avatar }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-white/90 truncate">{{ agent.name }}</div>
|
||||
<div class="text-[11px] text-white/40 truncate">{{ agent.description }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-white/30 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ session.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ formatRelativeTime(session.timestamp) }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部设置 -->
|
||||
<div class="p-4 border-t border-white/5">
|
||||
<button class="w-full py-2.5 rounded-xl bg-white/[0.02] hover:bg-white/[0.05] text-white/50 hover:text-white/80 text-sm flex items-center justify-center gap-2 transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,327 @@
|
||||
<script setup lang="ts">
|
||||
// Script 页面 - 占位
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface Script {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
status: 'running' | 'stopped'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 模拟脚本数据
|
||||
const scripts = ref<Script[]>([
|
||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', status: 'stopped', createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', status: 'running', createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', status: 'stopped', createdAt: '2025-04-12' },
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const isCreating = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const editingScript = ref<Script | null>(null)
|
||||
|
||||
const newScriptForm = ref({
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 过滤后的脚本
|
||||
const filteredScripts = computed(() => {
|
||||
return scripts.value.filter(script => {
|
||||
const matchSearch = script.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
script.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || script.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
})
|
||||
|
||||
// 状态样式
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running': return 'bg-primary-success'
|
||||
case 'stopped': return 'bg-gray-500'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const toggleStatus = (script: Script) => {
|
||||
script.status = script.status === 'running' ? 'stopped' : 'running'
|
||||
}
|
||||
|
||||
// 删除脚本
|
||||
const deleteScript = (id: number) => {
|
||||
scripts.value = scripts.value.filter(s => s.id !== id)
|
||||
}
|
||||
|
||||
// 编辑脚本
|
||||
const openEdit = (script: Script) => {
|
||||
editingScript.value = { ...script }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (editingScript.value) {
|
||||
const index = scripts.value.findIndex(s => s.id === editingScript.value!.id)
|
||||
if (index !== -1) {
|
||||
scripts.value[index] = { ...editingScript.value }
|
||||
}
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false
|
||||
editingScript.value = null
|
||||
}
|
||||
|
||||
// 新建脚本
|
||||
const openCreate = () => {
|
||||
newScriptForm.value = {
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
const saveNewScript = () => {
|
||||
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
|
||||
scripts.value.push({
|
||||
id: newId,
|
||||
name: newScriptForm.value.name || 'Untitled Script',
|
||||
type: newScriptForm.value.type,
|
||||
description: newScriptForm.value.description,
|
||||
status: 'stopped',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
isCreating.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<i class="fa-solid fa-code text-gray-400"></i>
|
||||
<span class="font-medium">Script</span>
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-code text-orange-500"></i>
|
||||
<span class="font-medium">Scripts</span>
|
||||
</div>
|
||||
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Script
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-gray-400">
|
||||
Script management coming soon...
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search scripts..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option label="All Status" value="all" />
|
||||
<el-option label="Running" value="running" />
|
||||
<el-option label="Stopped" value="stopped" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 脚本列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Script Name</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Type</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="script in filteredScripts" :key="script.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||||
<td class="px-5 py-4">
|
||||
<div class="font-medium">{{ script.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ script.description }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ script.type }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusClass(script.status)"></span>
|
||||
<span class="capitalize text-sm">{{ script.status }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ script.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="toggleStatus(script)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
:title="script.status === 'running' ? 'Stop' : 'Start'"
|
||||
>
|
||||
<i :class="['fa-solid', script.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="openEdit(script)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteScript(script.id)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredScripts.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-code text-4xl mb-3"></i>
|
||||
<p>No scripts found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建脚本弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreating" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">New Script</h3>
|
||||
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Script Name</label>
|
||||
<input
|
||||
v-model="newScriptForm.name"
|
||||
type="text"
|
||||
placeholder="Enter script name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="newScriptForm.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option label="Python" value="Python" />
|
||||
<el-option label="Shell" value="Shell" />
|
||||
<el-option label="JavaScript" value="JavaScript" />
|
||||
<el-option label="Go" value="Go" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="newScriptForm.description"
|
||||
rows="3"
|
||||
placeholder="Describe your script..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeCreate"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewScript"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Create Script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 编辑脚本弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Script</h3>
|
||||
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Script Name</label>
|
||||
<input
|
||||
v-model="editingScript!.name"
|
||||
type="text"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="editingScript!.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option label="Python" value="Python" />
|
||||
<el-option label="Shell" value="Shell" />
|
||||
<el-option label="JavaScript" value="JavaScript" />
|
||||
<el-option label="Go" value="Go" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="editingScript!.description"
|
||||
rows="3"
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveEdit"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
import FormDialog from '@/components/FormDialog.vue'
|
||||
@@ -47,6 +47,7 @@ const menuItems = [
|
||||
{ key: 'members', label: 'Members', icon: 'fa-users' },
|
||||
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
|
||||
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
|
||||
{ key: 'logs', label: 'Logs', icon: 'fa-file-lines' },
|
||||
]
|
||||
|
||||
// General 设置表单
|
||||
@@ -82,13 +83,92 @@ const saveChanges = () => {
|
||||
const showChangePassword = () => {
|
||||
ElMessage.info('Password change dialog would open here')
|
||||
}
|
||||
|
||||
// ========== Logs 功能 ==========
|
||||
interface Log {
|
||||
id: number
|
||||
level: 'info' | 'warning' | 'error' | 'debug'
|
||||
source: string
|
||||
message: string
|
||||
timestamp: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
const logs = ref<Log[]>([
|
||||
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
|
||||
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
|
||||
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
|
||||
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
|
||||
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
|
||||
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
|
||||
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
|
||||
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
|
||||
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
|
||||
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
|
||||
])
|
||||
|
||||
const logSearchQuery = ref('')
|
||||
const logFilterLevel = ref('')
|
||||
const logFilterSource = ref('')
|
||||
|
||||
const logLevelOptions = [
|
||||
{ value: '', label: 'All Levels' },
|
||||
{ value: 'info', label: 'Info' },
|
||||
{ value: 'warning', label: 'Warning' },
|
||||
{ value: 'error', label: 'Error' },
|
||||
{ value: 'debug', label: 'Debug' },
|
||||
]
|
||||
|
||||
const logSourceOptions = computed(() => {
|
||||
const sources = [...new Set(logs.value.map(l => l.source))]
|
||||
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
|
||||
})
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
return logs.value.filter(log => {
|
||||
const matchSearch = logSearchQuery.value === '' ||
|
||||
log.message.toLowerCase().includes(logSearchQuery.value.toLowerCase()) ||
|
||||
log.source.toLowerCase().includes(logSearchQuery.value.toLowerCase())
|
||||
const matchLevel = logFilterLevel.value === '' || log.level === logFilterLevel.value
|
||||
const matchSource = logFilterSource.value === '' || log.source === logFilterSource.value
|
||||
return matchSearch && matchLevel && matchSource
|
||||
})
|
||||
})
|
||||
|
||||
const logLevelClass = (level: string) => {
|
||||
switch (level) {
|
||||
case 'info': return 'bg-blue-500/20 text-blue-400'
|
||||
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
|
||||
case 'error': return 'bg-red-500/20 text-red-400'
|
||||
case 'debug': return 'bg-gray-500/20 text-gray-400'
|
||||
default: return 'bg-gray-500/20 text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const selectedLog = ref<Log | null>(null)
|
||||
const showLogDetail = ref(false)
|
||||
|
||||
const viewLogDetail = (log: Log) => {
|
||||
selectedLog.value = log
|
||||
showLogDetail.value = true
|
||||
}
|
||||
|
||||
const closeLogDetail = () => {
|
||||
showLogDetail.value = false
|
||||
selectedLog.value = null
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
logs.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<i class="fa-solid fa-gear text-orange-500"></i>
|
||||
<span class="font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-container">
|
||||
@@ -432,6 +512,141 @@ const showChangePassword = () => {
|
||||
</template>
|
||||
</FormDialog>
|
||||
</div>
|
||||
|
||||
<!-- Logs 设置 -->
|
||||
<div v-if="activeMenu === 'logs'" class="settings-section">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="section-title">Logs</h2>
|
||||
<p class="section-desc">View system logs</p>
|
||||
</div>
|
||||
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="logSearchQuery"
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="logFilterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logLevelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="logFilterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logSourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Level</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||
<td class="px-5 py-4">
|
||||
<span :class="['px-2 py-1 rounded text-xs font-medium', logLevelClass(log.level)]">
|
||||
{{ log.level.toUpperCase() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
|
||||
<td class="px-5 py-4 text-gray-300 max-w-md">
|
||||
<div class="truncate">{{ log.message }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
@click="viewLogDetail(log)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="View Details"
|
||||
>
|
||||
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
|
||||
<p>No logs found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeLogDetail">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Log Details</h3>
|
||||
<button @click="closeLogDetail" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog" class="p-5 space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span :class="['px-3 py-1 rounded text-sm font-medium', logLevelClass(selectedLog.level)]">
|
||||
{{ selectedLog.level.toUpperCase() }}
|
||||
</span>
|
||||
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
|
||||
<div class="text-white">{{ selectedLog.source }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
|
||||
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
|
||||
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
|
||||
{{ selectedLog.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
|
||||
<div class="text-gray-500">#{{ selectedLog.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeLogDetail"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user