refactor: 优化样式
This commit is contained in:
parent
fffd3f36d8
commit
9e9535c794
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 489 B |
|
|
@ -21,6 +21,8 @@ const sidebarCollapsed = ref(false)
|
|||
.main-content { flex: 1; margin-left: 260px; padding: 2rem; min-height: 100vh; transition: margin-left 0.3s; }
|
||||
.main-content.no-sidebar { margin-left: 0; }
|
||||
.sidebar-collapsed .main-content { margin-left: 70px; }
|
||||
@media (max-width: 1024px) { .main-content { margin-left: 70px; } }
|
||||
@media (max-width: 768px) { .main-content { margin-left: 0; } }
|
||||
@media (max-width: 768px) {
|
||||
.main-content { margin-left: 70px; }
|
||||
.sidebar-collapsed .main-content { margin-left: 0; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.5 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
|
|
@ -1,119 +1,125 @@
|
|||
<template>
|
||||
<aside class="sidebar" :class="{ collapsed }">
|
||||
<div class="sidebar-header">
|
||||
<div class="brand">
|
||||
<span class="brand-icon">✨</span>
|
||||
<span class="brand-text">Luxx</span>
|
||||
</div>
|
||||
<button @click="$emit('toggle')" class="toggle-btn">{{ collapsed ? '→' : '←' }}</button>
|
||||
<button v-if="collapsed" @click="$emit('toggle')" class="expand-btn" title="展开侧栏">
|
||||
✨
|
||||
</button>
|
||||
<template v-else>
|
||||
<div class="brand">
|
||||
<span class="brand-text">✨ Luxx</span>
|
||||
</div>
|
||||
<button @click="$emit('toggle')" class="toggle-btn" title="收起侧栏">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<router-link v-for="item in navItems" :key="item.path" :to="item.path" class="nav-item" active-class="active">
|
||||
<span class="nav-icon">{{ item.icon }}</span>
|
||||
<span class="nav-text">{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button @click="handleLogout" class="user-button">
|
||||
<div class="user-avatar">{{ user?.username?.charAt(0)?.toUpperCase() || 'U' }}</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ user?.username }}</span>
|
||||
<span class="user-role">{{ user?.role || '用户' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuth } from '../composables/useAuth.js'
|
||||
import { authAPI } from '../services/api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineProps({ collapsed: Boolean })
|
||||
defineEmits(['toggle'])
|
||||
|
||||
const router = useRouter()
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', icon: '🏠', label: '首页' },
|
||||
{ path: '/conversations', icon: '💬', label: '会话' },
|
||||
{ path: '/tools', icon: '🛠️', label: '工具' },
|
||||
{ path: '/settings', icon: '⚙️', label: '设置' },
|
||||
{ path: '/about', icon: 'ℹ️', label: '关于' }
|
||||
{ path: '/', label: '首页' },
|
||||
{ path: '/conversations', label: '会话' },
|
||||
{ path: '/tools', label: '工具' },
|
||||
{ path: '/settings', label: '设置' }
|
||||
]
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout(authAPI)
|
||||
router.push('/auth')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 260px; height: 100vh;
|
||||
background: var(--bg); border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
position: fixed; left: 0; top: 0; z-index: 100;
|
||||
transition: width 0.3s;
|
||||
width: var(--sidebar-width, 260px);
|
||||
height: 100vh;
|
||||
background: var(--bg);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.sidebar.collapsed { width: 70px; }
|
||||
.sidebar.collapsed { width: var(--sidebar-collapsed-width, 70px); }
|
||||
.sidebar-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 1.25rem; border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 60px;
|
||||
}
|
||||
.sidebar.collapsed .sidebar-header {
|
||||
justify-content: center;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.brand-icon { font-size: 1.75rem; }
|
||||
.brand-text {
|
||||
font-size: 1.5rem; font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.collapsed .brand-text, .collapsed .toggle-btn { display: none; }
|
||||
.toggle-btn {
|
||||
background: var(--code-bg); border: none; border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem; cursor: pointer;
|
||||
background: var(--code-bg);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.toggle-btn:hover { color: var(--accent); }
|
||||
.expand-btn {
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.expand-btn:hover { transform: scale(1.1); }
|
||||
.sidebar-nav { flex: 1; padding: 1rem 0.75rem; overflow-y: auto; }
|
||||
.nav-item {
|
||||
display: flex; align-items: center; gap: 0.875rem;
|
||||
padding: 0.875rem 1rem; border-radius: 10px;
|
||||
text-decoration: none; color: var(--text); margin-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-item:hover { background: var(--code-bg); color: var(--text-h); }
|
||||
.nav-item.active { background: var(--accent-bg); color: var(--accent); }
|
||||
.nav-icon { font-size: 1.25rem; width: 24px; text-align: center; }
|
||||
.nav-text { font-size: 0.95rem; font-weight: 500; white-space: nowrap; }
|
||||
.nav-text {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.collapsed .nav-text { display: none; }
|
||||
.collapsed .nav-item { justify-content: center; padding: 0.875rem; }
|
||||
.sidebar-footer { padding: 1rem; border-top: 1px solid var(--border); }
|
||||
.user-button {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
width: 100%; padding: 0.75rem; background: var(--code-bg);
|
||||
border: 1px solid var(--border); border-radius: 10px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.user-button:hover { border-color: var(--accent); }
|
||||
.user-avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, #8b5cf6 100%);
|
||||
color: white; display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; flex-shrink: 0;
|
||||
}
|
||||
.user-info { display: flex; flex-direction: column; overflow: hidden; }
|
||||
.user-name { font-weight: 600; color: var(--text-h); font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.user-role { font-size: 0.8rem; color: var(--text); }
|
||||
.collapsed .user-info { display: none; }
|
||||
.collapsed .user-button { justify-content: center; }
|
||||
.collapsed .nav-item { display: none; }
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar { width: 70px; }
|
||||
.brand-text, .toggle-btn, .nav-text, .user-info { display: none !important; }
|
||||
.nav-item { justify-content: center; padding: 0.875rem; }
|
||||
.user-button { justify-content: center; }
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { width: 0 !important; overflow: hidden; }
|
||||
.sidebar-header { display: none; }
|
||||
.sidebar-nav { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
<template>
|
||||
<button
|
||||
:class="['base-btn', `btn-${type}`, { 'btn-block': block, 'btn-loading': loading }]"
|
||||
:disabled="disabled || loading"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<span v-if="loading" class="spinner"></span>
|
||||
<slot v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: { type: String, default: 'primary' },
|
||||
disabled: Boolean,
|
||||
loading: Boolean,
|
||||
block: Boolean
|
||||
})
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.base-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-secondary { background: var(--bg); color: var(--text); border: 1px solid var(--border); }
|
||||
.btn-danger { background: #dc2626; color: white; }
|
||||
.btn-block { width: 100%; }
|
||||
.base-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.base-btn:not(:disabled):hover { transform: translateY(-2px); }
|
||||
.spinner {
|
||||
width: 20px; height: 20px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">{{ icon }}</div>
|
||||
<div class="empty-icon"></div>
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ description }}</p>
|
||||
<button v-if="actionText" @click="$emit('action')" class="btn-action">
|
||||
|
|
@ -11,10 +11,6 @@
|
|||
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: '📭'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
|
|
@ -45,7 +41,10 @@ defineEmits(['action'])
|
|||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: var(--accent);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +73,7 @@ defineEmits(['action'])
|
|||
}
|
||||
|
||||
.btn-action:hover {
|
||||
background: var(--accent-border);
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="error-message" :class="typeClass">
|
||||
<div class="error-icon">{{ icon }}</div>
|
||||
<div class="error-icon"></div>
|
||||
<div class="error-content">
|
||||
<h4 v-if="title">{{ title }}</h4>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
<button v-if="showRetry" @click="$emit('retry')" class="btn-retry">
|
||||
🔄 重试
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -37,14 +37,6 @@ const props = defineProps({
|
|||
defineEmits(['retry'])
|
||||
|
||||
const typeClass = computed(() => `type-${props.type}`)
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'warning': return '⚠️'
|
||||
case 'info': return 'ℹ️'
|
||||
default: return '❌'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -59,9 +51,9 @@ const icon = computed(() => {
|
|||
}
|
||||
|
||||
.type-error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
background: var(--accent-bg);
|
||||
border: 1px solid var(--accent-border);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.type-warning {
|
||||
|
|
@ -77,7 +69,11 @@ const icon = computed(() => {
|
|||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 3rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.error-content h4 {
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import viteLogo from '../assets/vite.svg'
|
||||
import heroImg from '../assets/hero.png'
|
||||
import vueLogo from '../assets/vue.svg'
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="center">
|
||||
<div class="hero">
|
||||
<img :src="heroImg" class="base" width="170" height="179" alt="" />
|
||||
<img :src="vueLogo" class="framework" alt="Vue logo" />
|
||||
<img :src="viteLogo" class="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
|
||||
</div>
|
||||
<button class="counter" @click="count++">Count is {{ count }}</button>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
|
||||
<section id="next-steps">
|
||||
<div id="docs">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#documentation-icon"></use>
|
||||
</svg>
|
||||
<h2>Documentation</h2>
|
||||
<p>Your questions, answered</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vite.dev/" target="_blank">
|
||||
<img class="logo" :src="viteLogo" alt="" />
|
||||
Explore Vite
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img class="button-icon" :src="vueLogo" alt="" />
|
||||
Learn more
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="social">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#social-icon"></use>
|
||||
</svg>
|
||||
<h2>Connect with us</h2>
|
||||
<p>Join the Vite community</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#github-icon"></use>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vite.dev/" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#discord-icon"></use>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/vite_js" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#x-icon"></use>
|
||||
</svg>
|
||||
X.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#bluesky-icon"></use>
|
||||
</svg>
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
</template>
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<div class="loading-spinner" :class="sizeClass">
|
||||
<div class="spinner"></div>
|
||||
<p v-if="text" class="loading-text">{{ text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => `size-${props.size}`)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-top-color: var(--accent);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.size-small .spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.size-medium .spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
.size-large .spinner {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-width: 5px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin: 1rem 0 0 0;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
// 导出 composables
|
||||
export * from './composables/useApi.js'
|
||||
export * from './composables/useFormatters.js'
|
||||
export * from './composables/useUtils.js'
|
||||
|
||||
// 导出组件
|
||||
export { default as LoadingSpinner } from './components/LoadingSpinner.vue'
|
||||
export { default as ErrorMessage } from './components/ErrorMessage.vue'
|
||||
export { default as EmptyState } from './components/EmptyState.vue'
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--accent: #2563eb;
|
||||
--accent-bg: rgba(37, 99, 235, 0.1);
|
||||
--accent-border: rgba(37, 99, 235, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
|
@ -37,9 +37,9 @@
|
|||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--accent: #60a5fa;
|
||||
--accent-bg: rgba(96, 165, 250, 0.15);
|
||||
--accent-border: rgba(96, 165, 250, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>关于此项目</h1>
|
||||
<p>这是 Luxx 项目的管理仪表板前端。</p>
|
||||
<p>技术栈:Vue 3 + Vue Router + Pinia + Vite</p>
|
||||
<router-link to="/">返回首页</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 可以在此添加组件逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,14 +7,14 @@
|
|||
<p>开始对话吧!</p>
|
||||
</div>
|
||||
<div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
|
||||
<div class="message-avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
|
||||
<div class="message-avatar">{{ msg.role === 'user' ? 'U' : 'A' }}</div>
|
||||
<div class="message-content">
|
||||
<div class="message-text">{{ msg.content }}</div>
|
||||
<div class="message-time">{{ formatTime(msg.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="streaming" class="message assistant streaming">
|
||||
<div class="message-avatar">🤖</div>
|
||||
<div class="message-avatar">A</div>
|
||||
<div class="message-content">
|
||||
<div class="message-text">{{ streamContent }}<span class="cursor">▋</span></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<h3>{{ c.title || '未命名会话' }}</h3>
|
||||
<p>{{ formatDate(c.created_at) }} • {{ c.model || '默认模型' }}</p>
|
||||
</div>
|
||||
<button @click.stop="deleteConv(c)" class="btn-delete">🗑️</button>
|
||||
<button @click.stop="deleteConv(c)" class="btn-delete">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -150,7 +150,8 @@ onMounted(fetchData)
|
|||
.card:hover { border-color: var(--accent); transform: translateY(-2px); }
|
||||
.card h3 { margin: 0 0 0.5rem; color: var(--text-h); }
|
||||
.card p { margin: 0; color: var(--text); font-size: 0.875rem; }
|
||||
.btn-delete { background: transparent; border: none; font-size: 1.2rem; cursor: pointer; padding: 0.5rem; }
|
||||
.btn-delete { background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); font-size: 0.875rem; cursor: pointer; padding: 0.25rem 0.75rem; border-radius: 6px; transition: all 0.2s; }
|
||||
.btn-delete:hover { background: var(--accent); color: white; }
|
||||
.pagination { display: flex; justify-content: center; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }
|
||||
.pagination button { padding: 0.5rem 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
|
||||
.pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
|
|
|||
|
|
@ -4,30 +4,19 @@
|
|||
<h1>欢迎使用 Luxx</h1>
|
||||
<p class="subtitle">智能会话管理与工具平台</p>
|
||||
<div class="hero-actions">
|
||||
<router-link to="/conversations" class="btn-primary">💬 开始会话</router-link>
|
||||
<router-link to="/tools" class="btn-secondary">🛠️ 查看工具</router-link>
|
||||
<router-link to="/conversations" class="btn-primary">开始会话</router-link>
|
||||
<router-link to="/tools" class="btn-secondary">查看工具</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">💬<div><div class="stat-value">{{ stats.conversations }}</div><div class="stat-label">会话总数</div></div></div>
|
||||
<div class="stat-card">🛠️<div><div class="stat-value">{{ stats.tools }}</div><div class="stat-label">可用工具</div></div></div>
|
||||
<div class="stat-card">📝<div><div class="stat-value">{{ stats.messages }}</div><div class="stat-label">消息总数</div></div></div>
|
||||
<div class="stat-card">🤖<div><div class="stat-value">{{ stats.models }}</div><div class="stat-label">支持模型</div></div></div>
|
||||
<div class="stat-card"><div><div class="stat-value">{{ stats.conversations }}</div><div class="stat-label">会话总数</div></div></div>
|
||||
<div class="stat-card"><div><div class="stat-value">{{ stats.tools }}</div><div class="stat-label">可用工具</div></div></div>
|
||||
<div class="stat-card"><div><div class="stat-value">{{ stats.messages }}</div><div class="stat-label">消息总数</div></div></div>
|
||||
<div class="stat-card"><div><div class="stat-value">{{ stats.models }}</div><div class="stat-label">支持模型</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<h2>核心功能</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card" v-for="f in features" :key="f.title">
|
||||
<div class="feature-icon">{{ f.icon }}</div>
|
||||
<h3>{{ f.title }}</h3>
|
||||
<p>{{ f.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">🚀 正在运行 <strong>Luxx</strong> 智能会话系统</div>
|
||||
<div class="footer-note">正在运行 <strong>Luxx</strong> 智能会话系统</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -36,12 +25,6 @@ import { ref, onMounted } from 'vue'
|
|||
import { conversationsAPI, toolsAPI } from '../services/api.js'
|
||||
|
||||
const stats = ref({ conversations: 0, tools: 0, messages: 0, models: 1 })
|
||||
const features = [
|
||||
{ icon: '💬', title: '智能会话', desc: '支持多模型对话,灵活配置参数' },
|
||||
{ icon: '🛠️', title: '工具生态', desc: '内置多种工具,扩展系统能力' },
|
||||
{ icon: '🔐', title: '安全认证', desc: 'JWT Token 授权,安全可靠' },
|
||||
{ icon: '📊', title: '数据分析', desc: '强大的数据处理能力' }
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
|
@ -61,14 +44,14 @@ onMounted(async () => {
|
|||
|
||||
<style scoped>
|
||||
.home { padding: 0; max-width: 1200px; margin: 0 auto; }
|
||||
.hero { background: linear-gradient(135deg, var(--accent) 0%, #8b5cf6 100%); border-radius: 20px; padding: 3rem 2rem; margin-bottom: 3rem; color: white; text-align: center; }
|
||||
.hero { background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%); border-radius: 20px; padding: 3rem 2rem; margin-bottom: 3rem; color: white; text-align: center; }
|
||||
.hero h1 { font-size: 3rem; margin: 0 0 1rem; color: white; }
|
||||
.subtitle { font-size: 1.3rem; opacity: 0.9; margin: 0 0 2rem; }
|
||||
.hero-actions { display: flex; justify-content: center; gap: 1rem; }
|
||||
.btn-primary { padding: 1rem 2rem; background: white; color: var(--accent); border-radius: 12px; text-decoration: none; font-weight: 500; }
|
||||
.btn-secondary { padding: 1rem 2rem; background: rgba(255,255,255,0.2); color: white; border: 2px solid rgba(255,255,255,0.3); border-radius: 12px; text-decoration: none; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-bottom: 3rem; }
|
||||
.stat-card { display: flex; align-items: center; gap: 1rem; padding: 1.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; font-size: 2rem; }
|
||||
.stat-card { display: flex; align-items: center; gap: 1rem; padding: 1.5rem; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; }
|
||||
.stat-value { font-size: 2.5rem; font-weight: bold; color: var(--text-h); }
|
||||
.stat-label { color: var(--text); font-size: 0.9rem; }
|
||||
.features { margin-bottom: 3rem; }
|
||||
|
|
@ -76,7 +59,6 @@ onMounted(async () => {
|
|||
.features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }
|
||||
.feature-card { background: var(--bg); border: 1px solid var(--border); border-radius: 16px; padding: 2rem; transition: all 0.3s; }
|
||||
.feature-card:hover { border-color: var(--accent); transform: translateY(-5px); }
|
||||
.feature-icon { font-size: 3rem; margin-bottom: 1rem; }
|
||||
.feature-card h3 { font-size: 1.3rem; margin: 0 0 0.75rem; color: var(--text-h); }
|
||||
.feature-card p { color: var(--text); margin: 0; }
|
||||
.footer-note { background: var(--code-bg); border-radius: 16px; padding: 2rem; text-align: center; color: var(--text); }
|
||||
|
|
|
|||
|
|
@ -1,10 +1,42 @@
|
|||
<template>
|
||||
<div class="settings">
|
||||
<h1>LLM Provider 设置</h1>
|
||||
<p class="subtitle">配置您的 AI 模型提供商</p>
|
||||
<h1>设置</h1>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="showModal = true" class="btn-primary">+ 添加 Provider</button>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>用户信息</h2>
|
||||
</div>
|
||||
<div v-if="loadingUser" class="loading-small"><div class="spinner-small"></div>加载中...</div>
|
||||
<div v-else class="user-info-card">
|
||||
<div class="info-item"><span class="label">用户名</span><span class="value">{{ userForm.username || '-' }}</span></div>
|
||||
<div class="info-item"><span class="label">邮箱</span><span class="value">{{ userForm.email || '-' }}</span></div>
|
||||
<div class="card-actions">
|
||||
<button @click="openUserModal" class="btn-primary">编辑</button>
|
||||
<button @click="handleLogout" class="btn-danger">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息模态框 -->
|
||||
<div v-if="showUserModal" class="modal-overlay" @click.self="closeUserModal">
|
||||
<div class="modal">
|
||||
<h2>编辑用户信息</h2>
|
||||
<div class="form-group"><label>用户名</label><input v-model="userFormEdit.username" placeholder="输入用户名" /></div>
|
||||
<div class="form-group"><label>邮箱</label><input v-model="userFormEdit.email" type="email" placeholder="输入邮箱" /></div>
|
||||
<div class="form-group"><label>新密码 <span class="optional">(留空不修改)</span></label><input v-model="userFormEdit.password" type="password" placeholder="输入新密码" /></div>
|
||||
<div v-if="userFormError" class="error">{{ userFormError }}</div>
|
||||
<div class="modal-actions">
|
||||
<button @click="closeUserModal" class="btn-secondary">取消</button>
|
||||
<button @click="updateUser" :disabled="savingUser" class="btn-primary">{{ savingUser ? '保存中...' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>LLM Provider</h2>
|
||||
<button @click="showModal = true" class="btn-primary">+ 添加</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading"><div class="spinner"></div>加载中...</div>
|
||||
|
|
@ -16,7 +48,7 @@
|
|||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h3>{{ p.name }}</h3>
|
||||
<span v-if="p.is_default" class="badge default">⭐ 默认</span>
|
||||
<span v-if="p.is_default" class="badge default">默认</span>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" :checked="p.enabled" @change="toggleEnabled(p)" />
|
||||
|
|
@ -25,48 +57,56 @@
|
|||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<div class="info-row"><span class="label">类型:</span> {{ p.provider_type }}</div>
|
||||
<div class="info-row"><span class="label">API:</span> {{ p.base_url }}</div>
|
||||
<div class="info-row"><span class="label">模型:</span> {{ p.default_model }}</div>
|
||||
<div class="info-row"><span class="label">API:</span><span class="value">{{ p.base_url }}</span></div>
|
||||
<div class="info-row"><span class="label">模型:</span><span class="value">{{ p.default_model }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<button @click="testProvider(p)" :disabled="testing === p.id">🔗 测试</button>
|
||||
<button @click="editProvider(p)">✏️ 编辑</button>
|
||||
<button @click="deleteProvider(p)" class="btn-danger">🗑️ 删除</button>
|
||||
<button @click="editProvider(p)">编辑</button>
|
||||
<button @click="testProvider(p)" :disabled="testing === p.id">测试</button>
|
||||
<button @click="deleteProvider(p)" class="btn-danger">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果弹窗 -->
|
||||
<div v-if="testResult !== null" class="modal-overlay" @click.self="testResult = null">
|
||||
<div class="modal result-modal" :class="{ success: testResult.success, error: !testResult.success }">
|
||||
<div class="result-icon">
|
||||
<span v-if="testResult.success">✓</span>
|
||||
<span v-else>✗</span>
|
||||
</div>
|
||||
<h2>{{ testResult.success ? '连接成功' : '连接失败' }}</h2>
|
||||
|
||||
<pre v-if="testResult.json" class="result-json">{{ testResult.json }}</pre>
|
||||
<div v-else class="result-message">{{ testResult.message }}</div>
|
||||
<button @click="testResult = null" class="btn-primary">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal">
|
||||
<h2>{{ editing ? '编辑 Provider' : '添加 Provider' }}</h2>
|
||||
|
||||
<div class="form-group"><label>名称</label><input v-model="form.name" placeholder="如: 我的 DeepSeek" /></div>
|
||||
<div class="form-group">
|
||||
<label>类型</label>
|
||||
<select v-model="form.provider_type">
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="deepseek">DeepSeek</option>
|
||||
<option value="glm">智谱 GLM</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Base URL</label><input v-model="form.base_url" placeholder="https://api.deepseek.com/v1" /></div>
|
||||
<div class="form-group"><label>API Key</label><input v-model="form.api_key" type="password" placeholder="sk-..." /></div>
|
||||
<div class="form-group"><label>默认模型</label><input v-model="form.default_model" placeholder="gpt-4 / deepseek-chat" /></div>
|
||||
<div class="form-group"><label>API Key</label><input v-model="form.api_key" type="text" :placeholder="editing ? '留空则保持原密码' : 'sk-...'" /></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="radio-card" :class="{ active: form.is_default }">
|
||||
<input v-model="form.is_default" type="checkbox" />
|
||||
<div class="radio-content">
|
||||
<span class="radio-icon">⭐</span>
|
||||
<div class="radio-text">
|
||||
<span class="radio-title">设为默认 Provider</span>
|
||||
<span class="radio-desc">新会话将默认使用此 Provider</span>
|
||||
</div>
|
||||
<label>模型名称</label>
|
||||
<input v-model="form.default_model" placeholder="gpt-4 / deepseek-chat" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="switch-card" :class="{ active: form.is_default }">
|
||||
<div class="switch-content">
|
||||
<span class="switch-title">设为默认 Provider</span>
|
||||
<span class="switch-desc">新会话将默认使用此 Provider</span>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input v-model="form.is_default" type="checkbox" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -84,6 +124,67 @@
|
|||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { providersAPI } from '../services/api.js'
|
||||
import { useAuth } from '../composables/useAuth.js'
|
||||
import { authAPI } from '../services/api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (!confirm('确定要退出登录吗?')) return
|
||||
await logout(authAPI)
|
||||
router.push('/auth')
|
||||
}
|
||||
|
||||
const userForm = ref({ username: '', email: '', password: '' })
|
||||
const userFormEdit = ref({ username: '', email: '', password: '' })
|
||||
const showUserModal = ref(false)
|
||||
const savingUser = ref(false)
|
||||
const userFormError = ref('')
|
||||
const loadingUser = ref(false)
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
loadingUser.value = true
|
||||
try {
|
||||
const res = await authAPI.getMe()
|
||||
if (res.success && res.data) {
|
||||
userForm.value.username = res.data.username || ''
|
||||
userForm.value.email = res.data.email || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败:', e)
|
||||
} finally {
|
||||
loadingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openUserModal = () => {
|
||||
userFormEdit.value = { ...userForm.value, password: '' }
|
||||
userFormError.value = ''
|
||||
showUserModal.value = true
|
||||
}
|
||||
|
||||
const closeUserModal = () => {
|
||||
showUserModal.value = false
|
||||
userFormError.value = ''
|
||||
}
|
||||
|
||||
const updateUser = async () => {
|
||||
savingUser.value = true
|
||||
userFormError.value = ''
|
||||
try {
|
||||
// TODO: Call API to update user info
|
||||
alert('用户信息已保存')
|
||||
userForm.value.username = userFormEdit.value.username
|
||||
userForm.value.email = userFormEdit.value.email
|
||||
closeUserModal()
|
||||
} catch (e) {
|
||||
userFormError.value = e.message || '保存失败'
|
||||
} finally {
|
||||
savingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const providers = ref([])
|
||||
const loading = ref(true)
|
||||
|
|
@ -92,10 +193,11 @@ const showModal = ref(false)
|
|||
const editing = ref(null)
|
||||
const saving = ref(false)
|
||||
const testing = ref(null)
|
||||
const testResult = ref(null)
|
||||
const formError = ref('')
|
||||
|
||||
const form = ref({
|
||||
name: '', provider_type: 'openai', base_url: '', api_key: '', default_model: '', is_default: false
|
||||
name: '', base_url: '', api_key: '', default_model: '', is_default: false
|
||||
})
|
||||
|
||||
const fetchProviders = async () => {
|
||||
|
|
@ -112,21 +214,31 @@ const fetchProviders = async () => {
|
|||
const closeModal = () => {
|
||||
showModal.value = false
|
||||
editing.value = null
|
||||
form.value = { name: '', provider_type: 'openai', base_url: '', api_key: '', default_model: '', is_default: false }
|
||||
form.value = { name: '', base_url: '', api_key: '', default_model: '', is_default: false }
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
const editProvider = (p) => {
|
||||
const editProvider = async (p) => {
|
||||
editing.value = p.id
|
||||
form.value = {
|
||||
name: p.name, provider_type: p.provider_type, base_url: p.base_url,
|
||||
api_key: p.api_key, default_model: p.default_model, is_default: p.is_default
|
||||
try {
|
||||
const res = await providersAPI.get(p.id)
|
||||
if (res.success && res.data) {
|
||||
form.value = {
|
||||
name: res.data.name,
|
||||
base_url: res.data.base_url,
|
||||
api_key: res.data.api_key || '',
|
||||
default_model: res.data.default_model,
|
||||
is_default: res.data.is_default
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取Provider详情失败:', e)
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const saveProvider = async () => {
|
||||
if (!form.value.name || !form.value.base_url || !form.value.api_key) {
|
||||
if (!form.value.base_url || !form.value.api_key || !form.value.default_model) {
|
||||
formError.value = '请填写所有必填项'
|
||||
return
|
||||
}
|
||||
|
|
@ -147,11 +259,27 @@ const saveProvider = async () => {
|
|||
|
||||
const testProvider = async (p) => {
|
||||
testing.value = p.id
|
||||
testResult.value = null
|
||||
try {
|
||||
const res = await providersAPI.test(p.id)
|
||||
alert(res.data?.message || (res.data?.success ? '连接成功' : '连接失败'))
|
||||
} catch (e) { alert('测试失败: ' + e.message) }
|
||||
finally { testing.value = null }
|
||||
const isSuccess = res.data?.success === true
|
||||
testResult.value = {
|
||||
success: isSuccess,
|
||||
message: res.data?.message || (isSuccess ? '连接成功' : '连接失败'),
|
||||
status: 200,
|
||||
json: JSON.stringify(res.data || res, null, 2)
|
||||
}
|
||||
} catch (e) {
|
||||
const errorData = e.response?.data || { error: e.message }
|
||||
testResult.value = {
|
||||
success: false,
|
||||
status: e.status || e.response?.status || '未知',
|
||||
message: e.message || e.response?.data?.detail || e.response?.message || '连接失败',
|
||||
json: JSON.stringify(errorData, null, 2)
|
||||
}
|
||||
} finally {
|
||||
testing.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const deleteProvider = async (p) => {
|
||||
|
|
@ -171,53 +299,83 @@ const toggleEnabled = async (p) => {
|
|||
} catch (e) { alert('更新失败: ' + e.message) }
|
||||
}
|
||||
|
||||
onMounted(fetchProviders)
|
||||
onMounted(() => {
|
||||
fetchUserInfo()
|
||||
fetchProviders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings { padding: 0; }
|
||||
.settings h1 { font-size: 2rem; margin: 0 0 0.5rem; color: var(--text-h); }
|
||||
.subtitle { color: var(--text); margin: 0 0 2rem; }
|
||||
.actions { margin-bottom: 2rem; }
|
||||
.settings h1 { font-size: 2rem; margin: 0 0 2rem; color: var(--text-h); }
|
||||
.section { margin-bottom: 2rem; }
|
||||
.section h2 { font-size: 1.25rem; margin: 0; color: var(--text-h); }
|
||||
.user-info-card { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; margin-top: 1rem; }
|
||||
.user-info-card .info-item { display: flex; gap: 1rem; margin-bottom: 0.75rem; }
|
||||
.user-info-card .info-item:last-of-type { margin-bottom: 1rem; }
|
||||
.user-info-card .label { color: var(--text); min-width: 60px; }
|
||||
.user-info-card .value { color: var(--text-h); font-weight: 500; }
|
||||
.user-info-card .card-actions { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
|
||||
.optional { color: var(--text); font-weight: normal; font-size: 0.85rem; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
||||
.section-header h2 { margin: 0; }
|
||||
.section-header .btn-primary { min-width: 80px; text-align: center; }
|
||||
.btn-primary { padding: 0.75rem 1.5rem; background: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; }
|
||||
.btn-secondary { padding: 0.75rem 1.5rem; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
|
||||
.btn-danger { background: #dc2626; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; }
|
||||
.btn-danger { background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-danger:hover { background: var(--accent); color: white; }
|
||||
.loading, .empty, .error { text-align: center; padding: 4rem; color: var(--text); }
|
||||
.error { background: #fef2f2; border-radius: 12px; color: #dc2626; }
|
||||
.error { background: var(--accent-bg); border-radius: 12px; color: var(--accent); }
|
||||
.loading-small { padding: 2rem; text-align: center; color: var(--text); }
|
||||
.spinner-small { width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 0.5rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
|
||||
.card { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1rem; }
|
||||
.card-title { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.card-title h3 { margin: 0; color: var(--text-h); }
|
||||
.badge { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.75rem; background: var(--code-bg); color: var(--text); }
|
||||
.card { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; display: flex; flex-direction: column; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
||||
.card-title { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
|
||||
.card-title h3 { margin: 0; color: var(--text-h); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.badge { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.75rem; background: var(--code-bg); color: var(--text); flex-shrink: 0; }
|
||||
.badge.default { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; }
|
||||
.switch { position: relative; display: inline-block; width: 48px; height: 26px; }
|
||||
.switch { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; inset: 0; background-color: #ccc; transition: 0.3s; border-radius: 26px; }
|
||||
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: 0.3s; border-radius: 50%; }
|
||||
input:checked + .slider { background-color: #16a34a; }
|
||||
input:checked + .slider:before { transform: translateX(22px); }
|
||||
.card-info { margin-bottom: 1rem; }
|
||||
.info-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem; }
|
||||
.label { color: var(--text); min-width: 50px; }
|
||||
.card-info { margin-bottom: 1rem; flex: 1; }
|
||||
.info-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.info-row .label { color: var(--text); min-width: 50px; flex-shrink: 0; }
|
||||
.info-row .value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.card-actions button { padding: 0.5rem 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
|
||||
.card-actions button { padding: 0.5rem 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; min-width: 60px; text-align: center; }
|
||||
.card-actions button.btn-primary { background: var(--accent); color: white; border: none; }
|
||||
.card-actions button.btn-danger { background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); }
|
||||
.card-actions button.btn-danger:hover { background: var(--accent); color: white; }
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal { background: var(--bg); border-radius: 16px; padding: 2rem; width: 100%; max-width: 500px; }
|
||||
.result-modal { text-align: center; }
|
||||
.result-modal .result-icon { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; font-size: 2rem; }
|
||||
.result-modal.success .result-icon { background: #dcfce7; color: #16a34a; }
|
||||
.result-modal.error .result-icon { background: var(--accent-bg); color: var(--accent); }
|
||||
.result-modal h2 { margin: 0 0 0.5rem; color: var(--text-h); }
|
||||
.result-modal .result-status { color: var(--accent); font-size: 0.9rem; margin-bottom: 0.5rem; font-weight: 500; }
|
||||
.result-modal .result-message { color: var(--text); margin-bottom: 1.5rem; }
|
||||
.result-modal .result-json { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin: 1rem 0; max-height: 200px; overflow: auto; font-size: 0.85rem; text-align: left; white-space: pre-wrap; word-break: break-all; color: var(--text-h); }
|
||||
.result-modal .btn-primary { width: 100%; }
|
||||
.modal h2 { margin: 0 0 1.5rem; color: var(--text-h); }
|
||||
.form-group { margin-bottom: 1.25rem; }
|
||||
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--text-h); }
|
||||
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; font-size: 1rem; background: var(--bg); box-sizing: border-box; }
|
||||
.form-group.checkbox label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
|
||||
.radio-card { display: flex; align-items: center; padding: 1rem; border: 2px solid var(--border); border-radius: 12px; cursor: pointer; transition: all 0.2s; }
|
||||
.radio-card:hover { border-color: var(--accent); }
|
||||
.radio-card.active { border-color: var(--accent); background: var(--accent-bg); }
|
||||
.radio-card input { display: none; }
|
||||
.radio-content { display: flex; align-items: center; gap: 1rem; width: 100%; }
|
||||
.radio-icon { font-size: 1.5rem; }
|
||||
.radio-text { display: flex; flex-direction: column; }
|
||||
.radio-title { font-weight: 600; color: var(--text-h); }
|
||||
.radio-desc { font-size: 0.85rem; color: var(--text); }
|
||||
.switch-card { display: flex; align-items: center; justify-content: space-between; }
|
||||
.switch-card input { display: none; }
|
||||
.switch-content { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.switch-title { font-weight: 600; color: var(--text-h); }
|
||||
.switch-desc { font-size: 0.85rem; color: var(--text); }
|
||||
.switch-card .switch { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; }
|
||||
.switch-card .switch input { opacity: 0; width: 0; height: 0; }
|
||||
.switch-card .slider { position: absolute; cursor: pointer; inset: 0; background-color: #ccc; transition: 0.3s; border-radius: 26px; }
|
||||
.switch-card .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: 0.3s; border-radius: 50%; }
|
||||
.switch-card input:checked + .slider { background-color: var(--accent); }
|
||||
.switch-card input:checked + .slider:before { transform: translateX(22px); }
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1.5rem; }
|
||||
.spinner { width: 48px; height: 48px; border: 4px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
<h1>工具管理</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">🛠️ {{ list.length }} 个工具</div>
|
||||
<button @click="fetchData" :disabled="loading" class="btn-refresh">🔄 刷新</button>
|
||||
<div class="stat">{{ list.length }} 个工具</div>
|
||||
<button @click="fetchData" :disabled="loading" class="btn-refresh">刷新</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading"><div class="spinner"></div>加载中...</div>
|
||||
|
|
@ -12,12 +12,11 @@
|
|||
<div v-else class="grid">
|
||||
<div v-for="tool in list" :key="tool.name" class="card">
|
||||
<div class="card-header">
|
||||
<span class="icon">{{ getIcon(tool.name) }}</span>
|
||||
<span :class="['badge', tool.enabled ? 'enabled' : 'disabled']">{{ tool.enabled ? '已启用' : '已禁用' }}</span>
|
||||
</div>
|
||||
<h3>{{ tool.name }}</h3>
|
||||
<p>{{ tool.description || '暂无描述' }}</p>
|
||||
<button @click="showDetail(tool)" class="btn-info">ℹ️ 查看详情</button>
|
||||
<button @click="showDetail(tool)" class="btn-info">查看详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -66,14 +65,6 @@ const fetchData = async () => {
|
|||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const getIcon = (name) => {
|
||||
const n = name?.toLowerCase() || ''
|
||||
if (n.includes('code') || n.includes('python')) return '💻'
|
||||
if (n.includes('web') || n.includes('search') || n.includes('crawl')) return '🌐'
|
||||
if (n.includes('data')) return '📊'
|
||||
return '🛠️'
|
||||
}
|
||||
|
||||
const showDetail = (tool) => { detail.value = tool }
|
||||
|
||||
onMounted(fetchData)
|
||||
|
|
@ -84,19 +75,18 @@ onMounted(fetchData)
|
|||
.tools h1 { font-size: 2rem; margin: 0 0 1.5rem; color: var(--text-h); }
|
||||
.stats { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||
.stat { font-size: 1.1rem; color: var(--text); }
|
||||
.btn-refresh { padding: 0.5rem 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
|
||||
.btn-refresh { padding: 0.5rem 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; color: var(--accent); }
|
||||
.loading, .error-msg { text-align: center; padding: 4rem; color: var(--text); }
|
||||
.error-msg { background: #fef2f2; border-radius: 12px; }
|
||||
.error-msg { background: var(--accent-bg); border-radius: 12px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
|
||||
.card { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; }
|
||||
.card-header { display: flex; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.icon { font-size: 2.5rem; }
|
||||
.badge { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.8rem; font-weight: 500; }
|
||||
.badge.enabled { background: #d4edda; color: #155724; }
|
||||
.badge.disabled { background: #f8d7da; color: #721c24; }
|
||||
.card h3 { margin: 0 0 0.75rem; font-size: 1.25rem; color: var(--text-h); }
|
||||
.card p { color: var(--text); font-size: 0.95rem; margin: 0 0 1rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.btn-info { background: transparent; border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1rem; cursor: pointer; }
|
||||
.card { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.5rem; display: flex; flex-direction: column; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
||||
.badge { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.8rem; font-weight: 500; flex-shrink: 0; }
|
||||
.badge.enabled { background: var(--accent-bg); color: var(--accent); }
|
||||
.badge.disabled { background: var(--code-bg); color: var(--text); }
|
||||
.card h3 { margin: 0 0 0.75rem; font-size: 1.25rem; color: var(--text-h); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.card p { color: var(--text); font-size: 0.95rem; margin: 0 0 1rem; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; flex: 1; }
|
||||
.btn-info { background: transparent; border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1rem; cursor: pointer; color: var(--accent); align-self: flex-start; }
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||
.modal { background: var(--bg); border-radius: 16px; padding: 2rem; width: 100%; max-width: 600px; max-height: 80vh; overflow-y: auto; }
|
||||
.modal h2 { margin: 0 0 1rem; color: var(--text-h); }
|
||||
|
|
|
|||
|
|
@ -133,6 +133,9 @@ def update_provider(
|
|||
|
||||
# Update fields
|
||||
update_data = update.dict(exclude_unset=True)
|
||||
# Keep existing API key if the new one is empty
|
||||
if update_data.get('api_key') == '':
|
||||
update_data.pop('api_key')
|
||||
for key, value in update_data.items():
|
||||
setattr(provider, key, value)
|
||||
|
||||
|
|
@ -207,11 +210,38 @@ def test_provider(
|
|||
"max_tokens": 10
|
||||
}
|
||||
)
|
||||
return response.status_code == 200
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"success": response.status_code == 200,
|
||||
"response_body": response.text[:500] if response.text else None
|
||||
}
|
||||
|
||||
success = asyncio.run(test())
|
||||
return success_response(data={"success": success, "message": "连接成功" if success else "连接失败"})
|
||||
result = asyncio.run(test())
|
||||
if result["success"]:
|
||||
return success_response(data={
|
||||
"success": True,
|
||||
"message": "连接成功",
|
||||
"status_code": result["status_code"]
|
||||
})
|
||||
else:
|
||||
return success_response(data={
|
||||
"success": False,
|
||||
"message": f"HTTP {result['status_code']}",
|
||||
"status_code": result["status_code"],
|
||||
"response_body": result["response_body"]
|
||||
})
|
||||
except httpx.HTTPStatusError as e:
|
||||
return success_response(data={
|
||||
"success": False,
|
||||
"message": f"HTTP {e.response.status_code}: {e.response.text[:200] if e.response.text else 'Unknown error'}",
|
||||
"status_code": e.response.status_code,
|
||||
"response_body": e.response.text[:500] if e.response.text else None
|
||||
})
|
||||
except Exception as e:
|
||||
return success_response(data={"success": False, "message": f"连接失败: {str(e)}"})
|
||||
return success_response(data={
|
||||
"success": False,
|
||||
"message": f"连接失败: {str(e)}",
|
||||
"error_type": type(e).__name__
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
|
|
|||
Loading…
Reference in New Issue