优化会话列表,优化头像显示

This commit is contained in:
2025-08-23 17:07:48 +08:00
parent b3d63e4181
commit ed39bf9e2c
4 changed files with 53 additions and 14 deletions

View File

@@ -69,9 +69,12 @@ router.post('/chat/stream', auth(false), async (req: AuthRequest, res: Response)
router.post('/conversations', auth(), async (req: AuthRequest, res: Response) => { router.post('/conversations', auth(), async (req: AuthRequest, res: Response) => {
try { try {
const { modelId, messages, title } = req.body; const { modelId, messages, title } = req.body;
const conv = await Conversation.create({ userId: req.userId, modelId, messages, title }); // clean messages: remove system/transient or empty entries
res.json(conv); const cleaned = (messages||[]).filter((m:any)=>m && typeof m.content==='string' && m.content.trim().length>0);
const computedTitle = title || (cleaned?.[0]?.content ? String(cleaned[0].content).slice(0, 60) : '新对话');
const conv = await Conversation.create({ userId: req.userId, modelId, messages: cleaned, title: computedTitle });
res.json(conv);
} catch (e:any) { } catch (e:any) {
res.status(500).json({ error: e.message }); res.status(500).json({ error: e.message });
} }

View File

@@ -11,7 +11,7 @@
<div class="sidebar-bottom"> <div class="sidebar-bottom">
<div class="avatar-area left-align" style="position:relative;"> <div class="avatar-area left-align" style="position:relative;">
<button class="avatar-btn" @click="authStore.isAuthed ? (showUserMenu = !showUserMenu) : goLogin()"> <button class="avatar-btn" @click="authStore.isAuthed ? (showUserMenu = !showUserMenu) : goLogin()">
<img v-if="authStore.user" :src="avatarUrl(userEmail)" alt="avatar" /> <img v-if="authStore.user" :src="avatarUrlForName(authStore.user.userName || authStore.user.email)" alt="avatar" />
<div v-else class="avatar-fallback">{{ userInitial }}</div> <div v-else class="avatar-fallback">{{ userInitial }}</div>
</button> </button>
<div v-if="showUserMenu" class="user-menu"> <div v-if="showUserMenu" class="user-menu">
@@ -69,8 +69,8 @@ function logout() { authStore.logout(); showUserMenu.value = false; router.push(
function toggleTheme() { themeStore.toggle(); } function toggleTheme() { themeStore.toggle(); }
async function handleNewConv() { async function handleNewConv() {
if (authStore.isAuthed) { if (authStore.isAuthed) {
// create empty conversation on server and navigate // create empty conversation on server and navigate
const conv = await convStore.createConversation(modelStore.currentModel, [], '新会话'); const conv = await convStore.createConversation(modelStore.currentModel, []);
if (conv && conv._id) { if (conv && conv._id) {
router.push('/chat'); router.push('/chat');
// let ChatView load via store currentId watcher // let ChatView load via store currentId watcher
@@ -97,7 +97,18 @@ watch(
if (authStore.isAuthed) convStore.fetchList(); if (authStore.isAuthed) convStore.fetchList();
function avatarUrl(email: string) { return ''; } function avatarUrlForName(name: string) {
// simple SVG avatar generator using initials and consistent color based on name
const initials = (name || '访客').trim().split(/\s+/).slice(0,2).map(s => s[0]).join('').toUpperCase();
// pick color from hash
let hash = 0;
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
const hue = Math.abs(hash) % 360;
const bg = `hsl(${hue} 60% 45%)`;
const fg = '#fff';
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='100%' height='100%' fill='${bg}' rx='16'/><text x='50%' y='52%' dominant-baseline='middle' text-anchor='middle' font-family='system-ui,Segoe UI,Roboto,Helvetica,Arial' font-size='32' fill='${fg}'>${initials}</text></svg>`;
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import axios from 'axios'; import axios from 'axios';
import { useAuthStore } from './authStore'; import { useAuthStore } from './authStore';
export interface ConversationSummary { _id: string; title: string; modelId: string; updatedAt: string; } export interface ConversationSummary { _id: string; title: string; modelId: string; updatedAt: string; transient?: boolean }
export interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; transient?: boolean } export interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; transient?: boolean }
interface ConversationState { interface ConversationState {
@@ -16,6 +16,16 @@ interface ConversationState {
export const useConversationStore = defineStore('conversation', { export const useConversationStore = defineStore('conversation', {
state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }), state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }),
actions: { actions: {
ensureInList(conv: any) {
if (!conv || !conv._id) return;
const idx = this.list.findIndex(l => l._id === conv._id);
const item = { _id: conv._id, title: conv.title || '未命名', modelId: conv.modelId || '', updatedAt: conv.updatedAt || new Date().toISOString() } as ConversationSummary;
if (idx >= 0) {
this.list[idx] = { ...this.list[idx], ...item };
} else {
this.list.unshift(item);
}
},
async fetchList() { async fetchList() {
const auth = useAuthStore(); const auth = useAuthStore();
if (!auth.token) { this.list = []; return; } if (!auth.token) { this.list = []; return; }
@@ -34,26 +44,41 @@ export const useConversationStore = defineStore('conversation', {
const { data } = await axios.get('/api/conversations/' + id, { headers: { Authorization: 'Bearer ' + auth.token } }); const { data } = await axios.get('/api/conversations/' + id, { headers: { Authorization: 'Bearer ' + auth.token } });
this.currentId = id; this.currentId = id;
this.messages = data.messages || []; this.messages = data.messages || [];
// ensure the list contains this conversation (may have been created elsewhere)
if (!this.list.find(l => l._id === data._id)) {
this.list.unshift({ _id: data._id, title: data.title || '未命名', modelId: data.modelId, updatedAt: data.updatedAt });
}
} catch (e:any) { this.error = e.response?.data?.error || e.message; } } catch (e:any) { this.error = e.response?.data?.error || e.message; }
finally { this.loading = false; } finally { this.loading = false; }
}, },
resetCurrent() { this.currentId = null; this.messages = []; }, resetCurrent() { this.currentId = null; this.messages = []; },
createNewConversation(notify?: string) { createNewConversation(notify?: string) {
this.currentId = null; // create a local transient placeholder conversation so the list updates immediately
const tempId = 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2,8);
const placeholder: ConversationSummary = { _id: tempId, title: '未命名', modelId: '', updatedAt: new Date().toISOString(), transient: true };
// insert placeholder at top
this.list.unshift(placeholder);
this.currentId = tempId;
this.messages = []; this.messages = [];
if (notify) { if (notify) {
this.messages.push({ role: 'system', content: notify, transient: true }); this.messages.push({ role: 'system', content: notify, transient: true });
} }
return placeholder;
}, },
async createConversation(modelId: string, messages: any[] = [], title?: string) { async createConversation(modelId: string, messages: any[] = [], title?: string) {
const auth = useAuthStore(); const auth = useAuthStore();
if (!auth.token) return null; if (!auth.token) return null;
try { try {
const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } }); const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } });
// prepend to list // prepend to list
this.list.unshift(data); // remove first transient placeholder if present (we'll replace it)
this.currentId = data._id; const phIndex = this.list.findIndex(l => l.transient);
return data; if (phIndex >= 0) this.list.splice(phIndex, 1);
this.list.unshift(data);
this.currentId = data._id;
// load messages immediately so UI has the content
await this.loadConversation(data._id);
return data;
} catch (e:any) { this.error = e.response?.data?.error || e.message; return null; } } catch (e:any) { this.error = e.response?.data?.error || e.message; return null; }
}, },
pushMessage(m: ConversationMessage) { this.messages.push(m); }, pushMessage(m: ConversationMessage) { this.messages.push(m); },

View File

@@ -10,7 +10,7 @@ export default defineConfig({
}, },
}, },
server: { server: {
port: 5173, port: 5175,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:3000',