refactor: 更新UI 样式

This commit is contained in:
ViperEkura 2026-03-27 15:49:14 +08:00
parent f4cb991ed7
commit 41a4d997fd
9 changed files with 123 additions and 212 deletions

View File

@ -30,7 +30,7 @@
:project-name="currentProject.name" :project-name="currentProject.name"
/> />
</div> </div>
<div v-else class="explorer-body explorer-empty"> <div v-else class="explorer-body empty-state">
<span v-html="icons.folderLg" style="color: var(--text-tertiary); opacity: 0.5;" /> <span v-html="icons.folderLg" style="color: var(--text-tertiary); opacity: 0.5;" />
<p>当前对话未关联项目</p> <p>当前对话未关联项目</p>
</div> </div>
@ -653,17 +653,7 @@ onMounted(() => {
display: flex; display: flex;
} }
.explorer-empty { /* explorer-empty now uses global .empty-state */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text-tertiary);
font-size: 14px;
}
/* modal-overlay, modal-content, btn-icon, form-group, modal-footer, btn-secondary, btn-primary now in global.css */
.create-modal { .create-modal {
background: var(--bg-primary); background: var(--bg-primary);

View File

@ -15,15 +15,15 @@
</div> </div>
</div> </div>
<div v-if="loadingTree" class="explorer-loading"> <div v-if="loadingTree" class="empty-state">
<span class="spinner" v-html="icons.spinnerMd" /> <span class="spinner" v-html="icons.spinnerMd" />
</div> </div>
<div v-else-if="treeItems.length === 0" class="explorer-empty"> <div v-else-if="treeItems.length === 0" class="empty-state">
空项目 空项目
</div> </div>
<div v-else class="tree-container"> <div v-else class="scrollbar-thin" style="flex: 1; padding: 4px 0;">
<FileTreeItem <FileTreeItem
v-for="item in treeItems" v-for="item in treeItems"
:key="item.path" :key="item.path"
@ -80,7 +80,7 @@
</div> </div>
<!-- Empty state --> <!-- Empty state -->
<div v-else class="viewer-placeholder"> <div v-else class="empty-state">
<span v-html="icons.fileLg" style="color: var(--text-tertiary); opacity: 0.5;" /> <span v-html="icons.fileLg" style="color: var(--text-tertiary); opacity: 0.5;" />
<span>选择文件以预览</span> <span>选择文件以预览</span>
</div> </div>
@ -311,31 +311,6 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
} }
.tree-container {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.tree-container::-webkit-scrollbar {
width: 4px;
}
.tree-container::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 2px;
}
.explorer-loading,
.explorer-empty {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
color: var(--text-tertiary);
font-size: 13px;
}
/* -- Viewer -- */ /* -- Viewer -- */
.file-viewer { .file-viewer {
flex: 1; flex: 1;
@ -400,17 +375,6 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
} }
.viewer-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text-tertiary);
font-size: 13px;
}
/* -- Code pane -- */ /* -- Code pane -- */
.code-pane { .code-pane {
flex: 1; flex: 1;

View File

@ -30,13 +30,13 @@
<div class="message-footer"> <div class="message-footer">
<span class="token-count" v-if="tokenCount">{{ tokenCount }} tokens</span> <span class="token-count" v-if="tokenCount">{{ tokenCount }} tokens</span>
<span class="message-time">{{ formatTime(createdAt) }}</span> <span class="message-time">{{ formatTime(createdAt) }}</span>
<button v-if="role === 'assistant'" class="btn-regenerate" @click="$emit('regenerate')" title="重新生成"> <button v-if="role === 'assistant'" class="ghost-btn success" @click="$emit('regenerate')" title="重新生成">
<span v-html="icons.regenerate" /> <span v-html="icons.regenerate" />
</button> </button>
<button v-if="role === 'assistant'" class="btn-copy" @click="copyContent" title="复制"> <button v-if="role === 'assistant'" class="ghost-btn accent" @click="copyContent" title="复制">
<span v-html="icons.copy" /> <span v-html="icons.copy" />
</button> </button>
<button v-if="deletable" class="btn-delete-msg" @click="$emit('delete')" title="删除"> <button v-if="deletable" class="ghost-btn danger" @click="$emit('delete')" title="删除">
<span v-html="icons.trash" /> <span v-html="icons.trash" />
</button> </button>
</div> </div>
@ -140,32 +140,6 @@ function copyContent() {
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.btn-regenerate,
.btn-copy,
.btn-delete-msg {
background: none;
border: none;
color: var(--text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.15s;
display: flex;
align-items: center;
}
.btn-regenerate:hover {
color: var(--success-color);
background: var(--success-bg);
}
.btn-copy:hover {
color: var(--accent-primary);
background: var(--accent-primary-light);
}
.btn-delete-msg:hover {
color: var(--danger-color);
background: var(--danger-bg);
}
</style> </style>

View File

@ -5,7 +5,7 @@
<div v-for="(file, index) in uploadedFiles" :key="index" class="file-item"> <div v-for="(file, index) in uploadedFiles" :key="index" class="file-item">
<span class="file-icon">{{ getFileIcon(file.extension) }}</span> <span class="file-icon">{{ getFileIcon(file.extension) }}</span>
<span class="file-name">{{ file.name }}</span> <span class="file-name">{{ file.name }}</span>
<button class="btn-remove-file" @click="removeFile(index)" title="移除"> <button class="ghost-btn danger btn-remove-file" @click="removeFile(index)" title="移除">
<span v-html="icons.close" /> <span v-html="icons.close" />
</button> </button>
</div> </div>
@ -213,25 +213,11 @@ defineExpose({ focus })
} }
.btn-remove-file { .btn-remove-file {
display: flex;
align-items: center;
justify-content: center;
width: 18px; width: 18px;
height: 18px; height: 18px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
padding: 0; padding: 0;
} }
.btn-remove-file:hover {
background: var(--danger-bg);
color: var(--danger-color);
}
.input-container { .input-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,6 +1,6 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition name="modal-fade"> <Transition name="fade">
<div v-if="state.visible" class="modal-overlay" @click.self="onCancel" @keydown.escape="onCancel"> <div v-if="state.visible" class="modal-overlay" @click.self="onCancel" @keydown.escape="onCancel">
<div class="modal-dialog" role="dialog" :aria-modal="true"> <div class="modal-dialog" role="dialog" :aria-modal="true">
<h3 class="modal-title">{{ state.title }}</h3> <h3 class="modal-title">{{ state.title }}</h3>
@ -128,13 +128,6 @@ watch(() => state.visible, (v) => {
opacity: 0.85; opacity: 0.85;
} }
/* Transitions */
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity 0.2s;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
opacity: 0;
}
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="settings-panel"> <div class="settings-panel">
<div class="settings-header"> <div class="panel-header">
<div class="settings-title"> <div class="panel-title">
<span v-html="icons.settings" /> <span v-html="icons.settings" />
<h4>会话设置</h4> <h4>会话设置</h4>
</div> </div>
@ -215,37 +215,7 @@ onMounted(loadModels)
flex-direction: column; flex-direction: column;
} }
.settings-header { /* panel-header, panel-title, header-actions now in global.css */
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.settings-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
}
.settings-title svg {
color: var(--text-tertiary);
}
.settings-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
/* tab styles now in global.css */
.settings-body { .settings-body {
flex: 1; flex: 1;

View File

@ -15,21 +15,21 @@
<span class="project-name">{{ group.name }}</span> <span class="project-name">{{ group.name }}</span>
<span class="conv-count">{{ group.conversations.length }}</span> <span class="conv-count">{{ group.conversations.length }}</span>
<button <button
class="btn-group-action" class="ghost-btn"
title="新建对话" title="新建对话"
@click.stop="$emit('createInProject', { id: group.id, name: group.name })" @click.stop="$emit('createInProject', { id: group.id, name: group.name })"
> >
<span v-html="icons.plus" /> <span v-html="icons.plus" />
</button> </button>
<button <button
class="btn-group-action" class="ghost-btn"
title="浏览文件" title="浏览文件"
@click.stop="$emit('browseProject', { id: group.id, name: group.name })" @click.stop="$emit('browseProject', { id: group.id, name: group.name })"
> >
<span v-html="icons.folder" /> <span v-html="icons.folder" />
</button> </button>
<button <button
class="btn-group-action btn-delete-project" class="ghost-btn danger btn-delete-project"
title="删除项目" title="删除项目"
@click.stop="$emit('deleteProject', { id: group.id, name: group.name })" @click.stop="$emit('deleteProject', { id: group.id, name: group.name })"
> >
@ -50,7 +50,7 @@
<span>{{ conv.message_count || 0 }} 条消息 · {{ formatTime(conv.updated_at) }}</span> <span>{{ conv.message_count || 0 }} 条消息 · {{ formatTime(conv.updated_at) }}</span>
</div> </div>
</div> </div>
<button class="btn-delete" @click.stop="$emit('delete', conv.id)" title="删除"> <button class="ghost-btn danger btn-delete" @click.stop="$emit('delete', conv.id)" title="删除">
<span v-html="icons.trash" /> <span v-html="icons.trash" />
</button> </button>
</div> </div>
@ -64,7 +64,7 @@
<span class="standalone-icon" v-html="icons.chat" /> <span class="standalone-icon" v-html="icons.chat" />
<span class="conv-count">{{ groupedData.standalone.length }}</span> <span class="conv-count">{{ groupedData.standalone.length }}</span>
<button <button
class="btn-group-action" class="ghost-btn"
title="新建对话" title="新建对话"
@click.stop="$emit('createInProject', { id: null, name: null })" @click.stop="$emit('createInProject', { id: null, name: null })"
> >
@ -87,7 +87,7 @@
<span>{{ conv.message_count || 0 }} 条消息 · {{ formatTime(conv.updated_at) }}</span> <span>{{ conv.message_count || 0 }} 条消息 · {{ formatTime(conv.updated_at) }}</span>
</div> </div>
</div> </div>
<button class="btn-delete" @click.stop="$emit('delete', conv.id)" title="删除"> <button class="ghost-btn danger btn-delete" @click.stop="$emit('delete', conv.id)" title="删除">
<span v-html="icons.trash" /> <span v-html="icons.trash" />
</button> </button>
</div> </div>
@ -229,10 +229,6 @@ function onScroll(e) {
padding: 0 16px 16px; padding: 0 16px 16px;
} }
.conversation-list::-webkit-scrollbar {
width: 4px;
}
.conversation-list::-webkit-scrollbar-thumb { .conversation-list::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-sidebar); background: var(--scrollbar-thumb-sidebar);
border-radius: 2px; border-radius: 2px;
@ -297,16 +293,8 @@ function onScroll(e) {
} }
.btn-group-action { .btn-group-action {
display: flex;
align-items: center;
justify-content: center;
width: 24px; width: 24px;
height: 24px; height: 24px;
border: none;
background: none;
color: var(--text-tertiary);
cursor: pointer;
border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
transition: all 0.15s; transition: all 0.15s;
opacity: 0.5; opacity: 0.5;
@ -316,15 +304,10 @@ function onScroll(e) {
opacity: 1; opacity: 1;
} }
.btn-group-action:hover {
color: var(--accent-primary);
background: var(--accent-primary-light);
opacity: 1;
}
.btn-delete-project:hover { .btn-delete-project:hover {
color: var(--danger-color); color: var(--danger-color);
background: var(--danger-bg); background: var(--danger-bg);
opacity: 1;
} }
.conversation-item { .conversation-item {
@ -369,24 +352,12 @@ function onScroll(e) {
.btn-delete { .btn-delete {
opacity: 0; opacity: 0;
background: none;
border: none;
color: var(--text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.15s;
flex-shrink: 0;
} }
.conversation-item:hover .btn-delete { .conversation-item:hover .btn-delete {
opacity: 1; opacity: 1;
} }
.btn-delete:hover {
color: var(--danger-color);
background: var(--danger-bg);
}
.loading-more, .loading-more,
.empty-hint { .empty-hint {
@ -407,16 +378,13 @@ function onScroll(e) {
} }
.btn-footer { .btn-footer {
display: flex;
align-items: center;
justify-content: center;
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 8px;
border: none; border: none;
background: none; background: none;
color: var(--text-tertiary); color: var(--text-tertiary);
cursor: pointer; cursor: pointer;
border-radius: 8px;
transition: all 0.15s; transition: all 0.15s;
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="stats-panel"> <div class="stats-panel">
<div class="stats-header"> <div class="panel-header">
<div class="stats-title"> <div class="panel-title">
<span v-html="icons.stats" /> <span v-html="icons.stats" />
<h4>使用统计</h4> <h4>使用统计</h4>
</div> </div>
@ -324,37 +324,7 @@ onMounted(loadStats)
padding: 0; padding: 0;
} }
.stats-header { /* panel-header, panel-title, header-actions now in global.css */
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.stats-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
}
.stats-title svg {
color: var(--text-tertiary);
}
.stats-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
/* tab styles now in global.css */
.stats-loading { .stats-loading {
display: flex; display: flex;

View File

@ -751,5 +751,101 @@ input[type="range"]::-moz-range-thumb {
color: var(--danger-color); color: var(--danger-color);
} }
/* ============ Ghost Button (base for small icon buttons) ============ */
.ghost-btn {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
color: var(--text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.15s;
}
.ghost-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.ghost-btn.danger:hover {
background: var(--danger-bg);
color: var(--danger-color);
}
.ghost-btn.accent:hover {
background: var(--accent-primary-light);
color: var(--accent-primary);
}
.ghost-btn.success:hover {
background: var(--success-bg);
color: var(--success-color);
}
/* ============ Empty / Loading State ============ */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 12px;
color: var(--text-tertiary);
font-size: 13px;
}
/* ============ Thin Scrollbar (4px) ============ */
.scrollbar-thin {
overflow-y: auto;
}
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 2px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
/* ============ Panel Header (title + actions) ============ */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
}
.panel-title svg {
color: var(--text-tertiary);
}
.panel-title h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}