首页样式优化
This commit is contained in:
20
CLAUDE.md
20
CLAUDE.md
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? '保存成功' : '创建成功');
|
||||
|
||||
Reference in New Issue
Block a user