feat: 优化多个前端页面

- 扩展 Account 账户页面功能
- 优化 Script 脚本页面
- 完善 Settings 设置页面
- 增强 Chat 聊天页面

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 17:38:57 +08:00
parent c590aa21d0
commit d3100e8219
4 changed files with 1184 additions and 160 deletions

View File

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

View File

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

View File

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

View File

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