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

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) => {
try {
const { modelId, messages, title } = req.body;
const conv = await Conversation.create({ userId: req.userId, modelId, messages, title });
res.json(conv);
const { modelId, messages, title } = req.body;
// clean messages: remove system/transient or empty entries
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) {
res.status(500).json({ error: e.message });
}

View File

@@ -11,7 +11,7 @@
<div class="sidebar-bottom">
<div class="avatar-area left-align" style="position:relative;">
<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>
</button>
<div v-if="showUserMenu" class="user-menu">
@@ -69,8 +69,8 @@ function logout() { authStore.logout(); showUserMenu.value = false; router.push(
function toggleTheme() { themeStore.toggle(); }
async function handleNewConv() {
if (authStore.isAuthed) {
// create empty conversation on server and navigate
const conv = await convStore.createConversation(modelStore.currentModel, [], '新会话');
// create empty conversation on server and navigate
const conv = await convStore.createConversation(modelStore.currentModel, []);
if (conv && conv._id) {
router.push('/chat');
// let ChatView load via store currentId watcher
@@ -97,7 +97,18 @@ watch(
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>
<style scoped>

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import axios from 'axios';
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 }
interface ConversationState {
@@ -16,6 +16,16 @@ interface ConversationState {
export const useConversationStore = defineStore('conversation', {
state: (): ConversationState => ({ list: [], loading: false, error: null, currentId: null, messages: [] }),
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() {
const auth = useAuthStore();
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 } });
this.currentId = id;
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; }
finally { this.loading = false; }
},
resetCurrent() { this.currentId = null; this.messages = []; },
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 = [];
if (notify) {
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();
if (!auth.token) return null;
try {
const { data } = await axios.post('/api/conversations', { modelId, messages, title }, { headers: { Authorization: 'Bearer ' + auth.token } });
// prepend to list
this.list.unshift(data);
this.currentId = data._id;
return data;
// remove first transient placeholder if present (we'll replace it)
const phIndex = this.list.findIndex(l => l.transient);
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; }
},
pushMessage(m: ConversationMessage) { this.messages.push(m); },

View File

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