首页样式优化

This commit is contained in:
2026-05-28 11:10:03 +08:00
parent 4174c424fc
commit 9bd4a44ab6
4 changed files with 20 additions and 315 deletions

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is the GFast UI project, a Vue 3 admin management system based on the vue-next-admin template, customized for a digital advertising/trading platform.
This is the **GFast UI** project (`gfast-ui`), a Vue 3 admin management system based on the vue-next-admin template, customized for a digital advertising/trading platform.
## Commands
@@ -60,11 +60,14 @@ This is the GFast UI project, a Vue 3 admin management system based on the vue-n
2. **API Requests**:
- API methods are organized by domain in `src/api/` (each subdomain has its own directory structure)
- The Axios instance in `src/utils/request.ts` automatically:
- Adds Bearer token from session storage
- Sends only changed fields for PUT requests (diff comparison with original data)
- Handles token expiration globally with redirect to login
- The Axios instance in `src/utils/request.ts` is pre-configured with:
- 50 second request timeout
- `qs` serialization of query parameters with dot notation for nested objects
- Automatically adds Bearer token (from cookies via Session utility) to all requests
- Automatically sends only changed fields for PUT requests (diff comparison with original data via `_originalData` field)
- Handles token expiration globally with redirect to login (prevents multiple overlapping popups)
- Provides error message extraction from multiple response formats (`message`, `msg`, `error`, `detail`)
- Error throttling: maximum one error message every 2 seconds
- Supports error mode configuration via `requestOptions.errorMode`:
- `global`: (default) Global error popup with backend message
- `page`: No automatic popup, reject the error for page-level handling
@@ -78,9 +81,14 @@ This is the GFast UI project, a Vue 3 admin management system based on the vue-n
- `getUpFileUrl`, `handleTree`, `useDict`, `selectDictLabel`, `parseTime`, `getItems`, `setItems`, `getOptionValue`, `isEmpty`
- Global components:
- `pagination` - Reusable pagination component
- Global plugins registered:
- `vue-simple-uploader` - Large file upload component
- Global event bus via mitt: `app.config.globalProperties.mittBus`
4. **Authentication**: Token is stored in session storage via the `Session` utility. 401 responses (HTTP or business code) trigger automatic logout.
4. **Authentication**:
- Token is stored in cookies via `js-cookie` (through the `Session` utility). Other user session data is stored in `sessionStorage`.
- The `Session.clearAuth()` method only clears authentication-related keys (`token`, `userInfo`, `userMenu`, `permissions`), preserving other local preferences.
- 401 responses (HTTP or business code) containing token expiration signals trigger automatic logout with a single confirmation popup.
5. **Keep-alive**: Route components can be cached via keep-alive, managed by the `useKeepALiveNames` store.

View File

@@ -3,18 +3,10 @@
<div class="chat-divider">今天 15:14</div>
<div v-for="msg in messages" :key="msg.id" class="message-row" :class="{ 'is-user': msg.isUser }">
<div v-if="!msg.isUser" class="avatar-wrap">
<div class="ai-avatar">AI</div>
</div>
<div class="bubble-wrap">
<div class="bubble">{{ msg.content }}</div>
<div class="time">{{ msg.time }}</div>
</div>
<div v-if="msg.isUser" class="avatar-wrap">
<div class="user-avatar"></div>
</div>
</div>
</div>
</template>
@@ -48,42 +40,6 @@ const messages = ref<Message[]>([
time: '09:31',
isUser: false,
},
{
id: 4,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: true,
},
{
id: 5,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: false,
},
{
id: 6,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: true,
},
{
id: 7,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: false,
},
{
id: 8,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: true,
},
{
id: 9,
content: '当然可以,这个系统提供了多种功能:日记、文件、快捷指令、快捷回复,以及技能管理和模型管理入口。你想先看哪一块?',
time: '09:31',
isUser: false,
},
]);
</script>
@@ -121,49 +77,6 @@ const messages = ref<Message[]>([
}
}
.avatar-wrap {
width: 36px;
height: 36px;
flex-shrink: 0;
display: flex;
align-items: flex-start;
padding-top: 4px;
}
.ai-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: #1e40af;
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
border: 1px solid #93c5fd;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 2px 8px rgba(59, 130, 246, 0.15);
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: #ffffff;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border: 1px solid #f59e0b;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
0 2px 8px rgba(245, 158, 11, 0.2);
}
.bubble-wrap {
display: flex;
flex-direction: column;

View File

@@ -1,32 +1,12 @@
<template>
<div class="sidebar">
<div class="sidebar-header">
<div class="brand-dot"></div>
<div class="user-info">
<div class="ai-avatar">
<span class="ai-icon">AI</span>
</div>
<div class="user-details">
<div class="user-name">AI 助手</div>
<div class="user-status"><span class="status-dot"></span>在线</div>
</div>
</div>
<el-button class="new-chat-btn" @click="handleNewChat">
<el-icon><Plus /></el-icon>
新增对话
</el-button>
</div>
<div class="sidebar-menu">
<div v-for="item in menuItems" :key="item.key" class="menu-item" :class="{ active: activeMenu === item.key }" @click="handleMenuClick(item)">
<el-icon :size="18" class="menu-icon">
<component :is="item.icon" />
</el-icon>
<span class="menu-label">{{ item.label }}</span>
<el-badge v-if="item.badge" :value="item.badge" class="menu-badge" />
</div>
</div>
<div v-if="historyList.length" class="history-section">
<div class="history-title">历史对话</div>
<div class="history-list">
@@ -47,27 +27,11 @@
</div>
</div>
</div>
<div class="sidebar-footer">
<el-button text class="footer-btn" @click="handleSettings">
<el-icon><Setting /></el-icon>
设置
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { Document, Folder, ChatDotRound, MagicStick, Cpu, VideoPlay, Setting, Plus, Delete } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
interface MenuItem {
key: string;
label: string;
icon: any;
badge?: number;
route?: string;
}
import { Plus, Delete } from '@element-plus/icons-vue';
interface HistoryItem {
id: number;
@@ -90,30 +54,11 @@ interface Emits {
defineProps<Props>();
const emit = defineEmits<Emits>();
const router = useRouter();
const menuItems: MenuItem[] = [
{ key: 'chat', label: '对话', icon: ChatDotRound },
{ key: 'models', label: '模型管理', icon: Cpu, route: '/settings/modelConfig/modelModule' },
{ key: 'creation', label: '内容创作', icon: VideoPlay, route: '/settings/creation' },
];
const handleMenuClick = (item: MenuItem) => {
if (item.route) {
router.push(item.route);
} else {
emit('menu-change', item.key);
}
};
const handleNewChat = () => {
emit('new-chat');
};
const handleSettings = () => {
router.push('/personal');
};
const handleSelectHistory = (id: number) => {
emit('select-history', id);
};
@@ -142,7 +87,7 @@ const handleDeleteHistory = (id: number) => {
}
.new-chat-btn {
margin-top: 12px;
margin-top: 0;
width: 100%;
height: 40px;
border-radius: 12px;
@@ -163,152 +108,11 @@ const handleDeleteHistory = (id: number) => {
}
}
.brand-dot {
position: absolute;
right: 16px;
top: 18px;
width: 8px;
height: 8px;
border-radius: 999px;
background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 10px 8px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(147, 197, 253, 0.04) 100%);
border: 1px solid rgba(59, 130, 246, 0.18);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.ai-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 50%, #1d4ed8 100%);
box-shadow:
inset 0 2px 8px rgba(255, 255, 255, 0.35),
0 4px 12px rgba(37, 99, 235, 0.28);
flex-shrink: 0;
}
.ai-icon {
color: #fff;
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 4px;
letter-spacing: 0.2px;
}
.user-status {
font-size: 12px;
color: #10b981;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.sidebar-menu {
flex-shrink: 0;
padding: 12px 10px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
margin-bottom: 6px;
border-radius: 11px;
cursor: pointer;
transition: all 0.22s ease;
color: #64748b;
position: relative;
border: 1px solid transparent;
&:hover {
background: #f3f7ff;
color: #1f2937;
border-color: #e3ecfb;
}
&.active {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.16) 0%, rgba(37, 99, 235, 0.12) 100%);
color: #1f4db8;
font-weight: 600;
border-color: rgba(59, 130, 246, 0.2);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
&::before {
content: '';
position: absolute;
left: -2px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
border-radius: 0 4px 4px 0;
}
}
}
.menu-icon {
opacity: 0.92;
}
.menu-label {
flex: 1;
font-size: 13px;
}
.menu-badge {
:deep(.el-badge__content) {
background: #ef4444;
border: none;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25);
}
}
.history-section {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 8px;
border-top: 1px solid rgba(233, 238, 247, 0.8);
min-height: 0;
background: linear-gradient(180deg, rgba(249, 251, 255, 0.5) 0%, rgba(249, 251, 255, 0.9) 100%);
}
@@ -396,25 +200,4 @@ const handleDeleteHistory = (id: number) => {
font-size: 11px;
color: #94a3b8;
}
.sidebar-footer {
flex-shrink: 0;
padding: 10px;
border-top: 1px solid #e9eef7;
}
.footer-btn {
width: 100%;
justify-content: flex-start;
gap: 8px;
color: #64748b;
font-size: 13px;
border-radius: 10px;
padding: 10px 12px;
&:hover {
background: #f4f7fd;
color: #1f2937;
}
}
</style>

View File

@@ -650,11 +650,12 @@ const onSaveknowledge = async () => {
});
} else {
// 创建知识库
await createknowledge({
const params = {
name: knowledgeForm.name,
datasetType: knowledgeForm.datasetType!,
datasetType: knowledgeForm.datasetType as number,
description: knowledgeForm.description,
});
};
await createknowledge(params);
}
ElMessage.success(knowledgeForm.id ? '保存成功' : '创建成功');