chore: 精简代码

This commit is contained in:
ViperEkura 2026-03-26 16:38:52 +08:00
parent f2866df2ea
commit 95e771cb61
11 changed files with 152 additions and 227 deletions

View File

@ -132,11 +132,7 @@ function scrollToBottom(smooth = true) {
}) })
} }
watch(() => props.messages.length, () => { watch([() => props.messages.length, () => props.streamingContent], () => {
scrollToBottom()
})
watch(() => props.streamingContent, () => {
scrollToBottom() scrollToBottom()
}) })
@ -298,52 +294,8 @@ watch(() => props.conversation?.id, () => {
padding: 0 16px; padding: 0 16px;
} }
.message-bubble { /* .message-bubble, .avatar, .message-body now in global.css */
display: flex;
gap: 12px;
margin-bottom: 16px;
width: 100%;
}
.message-bubble .message-container {
display: flex;
flex-direction: column;
max-width: 85%;
min-width: 200px;
}
.message-bubble.user .message-container {
align-items: flex-end;
}
.message-bubble.assistant .message-container {
align-items: flex-start;
}
.message-bubble .avatar {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
letter-spacing: -0.3px;
flex-shrink: 0;
background: var(--avatar-gradient);
color: white;
}
.message-bubble .message-body {
flex: 1;
min-width: 0;
overflow: hidden;
padding: 16px;
border: 1px solid var(--border-light);
border-radius: 12px;
background: var(--bg-primary);
transition: background 0.2s, border-color 0.2s;
}
</style> </style>

View File

@ -0,0 +1,12 @@
<template>
<button class="btn-close" @click="$emit('click')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</template>
<script setup>
defineEmits(['click'])
</script>

View File

@ -90,35 +90,7 @@ function copyContent() {
</script> </script>
<style scoped> <style scoped>
.message-bubble { /* .message-bubble, .avatar, .message-body now in global.css */
display: flex;
gap: 12px;
margin-bottom: 16px;
width: 100%;
}
.message-bubble.user {
flex-direction: row-reverse;
}
.message-container {
display: flex;
flex-direction: column;
min-width: 200px;
width: 100%;
}
.message-bubble.user .message-container {
align-items: flex-end;
width: fit-content;
max-width: 85%;
}
.message-bubble.assistant .message-container {
align-items: flex-start;
flex: 1 1 auto;
min-width: 0;
}
.attachments-list { .attachments-list {
display: flex; display: flex;
@ -154,48 +126,6 @@ function copyContent() {
font-weight: 500; font-weight: 500;
} }
.message-bubble.assistant .message-body {
width: 100%;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
flex-shrink: 0;
}
.user .avatar {
background: linear-gradient(135deg, #2563eb, #3b82f6);
color: white;
font-size: 12px;
font-weight: 700;
letter-spacing: -0.3px;
}
.assistant .avatar {
background: var(--avatar-gradient);
color: white;
font-size: 12px;
font-weight: 700;
letter-spacing: -0.3px;
}
.message-body {
flex: 1;
min-width: 0;
padding: 16px;
border: 1px solid var(--border-light);
border-radius: 12px;
background: var(--bg-primary);
transition: background 0.2s, border-color 0.2s;
}
.message-footer { .message-footer {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -74,6 +74,7 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { renderMarkdown } from '../utils/markdown' import { renderMarkdown } from '../utils/markdown'
import { formatJson, truncate } from '../utils/format'
import { useCodeEnhancement } from '../composables/useCodeEnhancement' import { useCodeEnhancement } from '../composables/useCodeEnhancement'
const props = defineProps({ const props = defineProps({
@ -97,18 +98,6 @@ function toggleItem(key) {
expandedKeys.value[key] = !expandedKeys.value[key] expandedKeys.value[key] = !expandedKeys.value[key]
} }
function formatJson(value) {
if (value == null) return ''
const str = typeof value === 'string' ? value : JSON.stringify(value)
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
}
function truncate(text, max = 60) {
if (!text) return ''
const str = text.replace(/\s+/g, ' ').trim()
return str.length > max ? str.slice(0, max) + '…' : str
}
function getResultSummary(result) { function getResultSummary(result) {
try { try {
const parsed = typeof result === 'string' ? JSON.parse(result) : result const parsed = typeof result === 'string' ? JSON.parse(result) : result

View File

@ -53,7 +53,7 @@
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<h3>创建项目</h3> <h3>创建项目</h3>
<button class="btn-close" @click="showCreateModal = false">&times;</button> <CloseButton @click="showCreateModal = false" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@ -79,7 +79,7 @@
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<h3>上传文件夹</h3> <h3>上传文件夹</h3>
<button class="btn-close" @click="closeUploadModal">&times;</button> <CloseButton @click="closeUploadModal" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
@ -124,7 +124,7 @@
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<h3>确认删除</h3> <h3>确认删除</h3>
<button class="btn-close" @click="showDeleteModal = false">&times;</button> <CloseButton @click="showDeleteModal = false" />
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>确定要删除项目 <strong>{{ projectToDelete?.name }}</strong> </p> <p>确定要删除项目 <strong>{{ projectToDelete?.name }}</strong> </p>
@ -144,9 +144,10 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { projectApi } from '../api' import { projectApi } from '../api'
import CloseButton from './CloseButton.vue'
const props = defineProps({ const props = defineProps({
currentProject: Object, currentProject: { type: Object, default: null },
}) })
const emit = defineEmits(['select', 'created', 'deleted']) const emit = defineEmits(['select', 'created', 'deleted'])
@ -316,23 +317,7 @@ defineExpose({
color: var(--text-primary); color: var(--text-primary);
} }
.btn-icon { /* .btn-icon is defined in global.css */
padding: 6px;
border: none;
background: transparent;
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: var(--bg-hover);
color: var(--accent-primary);
}
.btn-icon.danger:hover { .btn-icon.danger:hover {
background: var(--danger-bg); background: var(--danger-bg);

View File

@ -18,12 +18,7 @@
{{ t.label }} {{ t.label }}
</button> </button>
</div> </div>
<button class="btn-close" @click="$emit('close')"> <CloseButton @click="$emit('close')" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div> </div>
</div> </div>
@ -132,6 +127,7 @@
import { reactive, ref, watch, onMounted } from 'vue' import { reactive, ref, watch, onMounted } from 'vue'
import { modelApi, conversationApi } from '../api' import { modelApi, conversationApi } from '../api'
import { useTheme } from '../composables/useTheme' import { useTheme } from '../composables/useTheme'
import CloseButton from './CloseButton.vue'
const props = defineProps({ const props = defineProps({
visible: { type: Boolean, default: false }, visible: { type: Boolean, default: false },
@ -251,34 +247,7 @@ onMounted(loadModels)
gap: 12px; gap: 12px;
} }
.period-tabs { /* tab styles now in global.css */
display: flex;
gap: 2px;
background: var(--bg-input);
padding: 3px;
border-radius: 8px;
}
.tab {
padding: 4px 12px;
border: none;
background: none;
color: var(--text-tertiary);
font-size: 12px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.tab:hover {
color: var(--text-secondary);
}
.tab.active {
background: var(--accent-primary);
color: white;
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3);
}
.settings-body { .settings-body {
flex: 1; flex: 1;

View File

@ -109,7 +109,8 @@ function selectProject(project) {
} }
function onProjectCreated(project) { function onProjectCreated(project) {
// Auto-select newly created project // Auto-select newly created project and refresh list
projectManagerRef.value?.loadProjects()
emit('selectProject', project) emit('selectProject', project)
} }
@ -132,7 +133,6 @@ function onScroll(e) {
.sidebar { .sidebar {
width: 20%; width: 20%;
min-width: 220px; min-width: 220px;
max-width: 320px;
flex-shrink: 0; flex-shrink: 0;
background: color-mix(in srgb, var(--bg-primary) 75%, transparent); background: color-mix(in srgb, var(--bg-primary) 75%, transparent);
backdrop-filter: blur(40px); backdrop-filter: blur(40px);

View File

@ -20,12 +20,7 @@
{{ p.label }} {{ p.label }}
</button> </button>
</div> </div>
<button class="btn-close" @click="$emit('close')"> <CloseButton @click="$emit('close')" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div> </div>
</div> </div>
@ -224,6 +219,7 @@ import { ref, computed, onMounted } from 'vue'
import { statsApi } from '../api' import { statsApi } from '../api'
import { useTheme } from '../composables/useTheme' import { useTheme } from '../composables/useTheme'
import { formatNumber } from '../utils/format' import { formatNumber } from '../utils/format'
import CloseButton from './CloseButton.vue'
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
@ -377,34 +373,7 @@ onMounted(loadStats)
gap: 12px; gap: 12px;
} }
.period-tabs { /* tab styles now in global.css */
display: flex;
gap: 2px;
background: var(--bg-input);
padding: 3px;
border-radius: 8px;
}
.tab {
padding: 4px 12px;
border: none;
background: none;
color: var(--text-tertiary);
font-size: 12px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.tab:hover {
color: var(--text-secondary);
}
.tab.active {
background: var(--accent-primary);
color: white;
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3);
}
.stats-loading { .stats-loading {
display: flex; display: flex;

View File

@ -509,6 +509,107 @@ input[type="range"]::-moz-range-thumb {
cursor: not-allowed; cursor: not-allowed;
} }
/* ============ Tab Navigation ============ */
.period-tabs {
display: flex;
gap: 2px;
background: var(--bg-input);
padding: 3px;
border-radius: 8px;
}
.tab {
padding: 4px 12px;
border: none;
background: none;
color: var(--text-tertiary);
font-size: 12px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.tab:hover {
color: var(--text-secondary);
}
.tab.active {
background: var(--accent-primary);
color: white;
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3);
}
/* ============ Message Bubble Shared ============ */
.message-bubble {
display: flex;
gap: 12px;
margin-bottom: 16px;
width: 100%;
}
.message-bubble.user {
flex-direction: row-reverse;
}
.message-container {
display: flex;
flex-direction: column;
min-width: 200px;
width: 100%;
}
.message-bubble.user .message-container {
align-items: flex-end;
width: fit-content;
max-width: 85%;
}
.message-bubble.assistant .message-container {
align-items: flex-start;
flex: 1 1 auto;
width: 100%;
min-width: 0;
}
.message-bubble.assistant .message-body {
width: 100%;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
letter-spacing: -0.3px;
flex-shrink: 0;
}
.user .avatar {
background: linear-gradient(135deg, #2563eb, #3b82f6);
color: white;
font-size: 12px;
}
.assistant .avatar {
background: var(--avatar-gradient);
color: white;
font-size: 12px;
}
.message-body {
flex: 1;
min-width: 0;
padding: 16px;
border: 1px solid var(--border-light);
border-radius: 12px;
background: var(--bg-primary);
transition: background 0.2s, border-color 0.2s;
}
/* ============ Form ============ */ /* ============ Form ============ */
.form-group { .form-group {
margin-bottom: 16px; margin-bottom: 16px;

View File

@ -23,3 +23,21 @@ export function formatNumber(num) {
if (num >= 1000) return (num / 1000).toFixed(1) + 'K' if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString() return num.toString()
} }
/**
* Format a value as pretty-printed JSON string.
*/
export function formatJson(value) {
if (value == null) return ''
const str = typeof value === 'string' ? value : JSON.stringify(value)
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
}
/**
* Truncate text to max characters with ellipsis.
*/
export function truncate(text, max = 60) {
if (!text) return ''
const str = text.replace(/\s+/g, ' ').trim()
return str.length > max ? str.slice(0, max) + '\u2026' : str
}

View File

@ -75,6 +75,9 @@ export function renderMarkdown(text) {
return marked.parse(text) return marked.parse(text)
} }
const COPY_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'
const CHECK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>'
/** /**
* 后处理 HTML为所有代码块包裹 .code-block 容器 * 后处理 HTML为所有代码块包裹 .code-block 容器
* 添加语言标签和复制按钮在组件 onMounted / updated 中调用 * 添加语言标签和复制按钮在组件 onMounted / updated 中调用
@ -101,9 +104,6 @@ export function enhanceCodeBlocks(container) {
langSpan.className = 'code-lang' langSpan.className = 'code-lang'
langSpan.textContent = lang langSpan.textContent = lang
const COPY_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'
const CHECK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>'
const copyBtn = document.createElement('button') const copyBtn = document.createElement('button')
copyBtn.className = 'code-copy-btn' copyBtn.className = 'code-copy-btn'
copyBtn.title = '复制' copyBtn.title = '复制'