Просмотр исходного кода

将账号头像改为动态首字母头像

LSamien 1 месяц назад
Родитель
Сommit
a403953250

+ 1 - 1
README.md

@@ -1,6 +1,6 @@
 # 夭夭 Web
 
-当前发布版本:**v0.3.16**,配套 iOS **1.3(176)**。详见 [发布说明](docs/releases/v0.3.16.md)。
+当前发布版本:**v0.3.17**,配套 iOS **1.3(176)**。详见 [发布说明](docs/releases/v0.3.17.md)。
 
 夭夭 Web 为网页和 iOS 提供统一的 Agent、群聊、文件库及语音配置服务。Hermes 通过标准 9119 HTTP/WebSocket 接口执行任务;无需安装 Yaoyao 插件。
 

+ 9 - 0
docs/releases/v0.3.17.md

@@ -0,0 +1,9 @@
+# v0.3.17
+
+登录账号头像改为按用户名动态生成的首字母头像。
+
+- Bot 模式底部账号栏和设置中心账号卡片使用相同的首字母头像。
+- 用户名变化时,字母与颜色会随用户名重新计算,不再使用固定的 OpenMausBot 默认头像。
+- Agent、单聊和群聊头像保持 OpenMausBot 头像体系。
+
+配套 iOS 继续为 1.3(176),无需插件升级。

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.16",
+  "version": "0.3.17",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hermes-yaoyao",
-      "version": "0.3.16",
+      "version": "0.3.17",
       "license": "BSL-1.1",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.3.16",
+  "version": "0.3.17",
   "private": true,
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 3 - 3
release.json

@@ -1,6 +1,6 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.3.16",
-  "webVersion": "0.3.16",
-  "gitTag": "v0.3.16"
+  "releaseVersion": "0.3.17",
+  "webVersion": "0.3.17",
+  "gitTag": "v0.3.17"
 }

+ 2 - 1
src/client/components/app/SettingsCenterDialog.vue

@@ -12,6 +12,7 @@ import SystemManagementPanel from '@/components/app/SystemManagementPanel.vue'
 import SystemOverviewPanel from '@/components/app/SystemOverviewPanel.vue'
 import SystemUpdatePanel from '@/components/app/SystemUpdatePanel.vue'
 import AgentAvatar from '@/components/common/AgentAvatar.vue'
+import AccountInitialAvatar from '@/components/common/AccountInitialAvatar.vue'
 import type { ProfileIdentityInput } from '@/api/profiles'
 import AppIcon from '@/components/common/AppIcon.vue'
 
@@ -288,7 +289,7 @@ function requestModeSwitch() {
                   </div>
                 </div>
                 <div v-else class="settings-account-summary" aria-label="当前账号">
-                  <AgentAvatar :name="accountName" :size="34" />
+                  <AccountInitialAvatar :name="accountName" :size="34" />
                   <span><strong>{{ accountName }}</strong><small>当前账号</small></span>
                 </div>
 

+ 5 - 2
src/client/components/app/WorkspaceShell.vue

@@ -3,6 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import type { Profile } from '@shared/types'
 import AgentAvatar from '@/components/common/AgentAvatar.vue'
+import AccountInitialAvatar from '@/components/common/AccountInitialAvatar.vue'
 import type { ProfileIdentityInput } from '@/api/profiles'
 import AppIcon from '@/components/common/AppIcon.vue'
 import BrandMark from '@/components/common/BrandMark.vue'
@@ -455,7 +456,8 @@ onBeforeUnmount(() => {
       <div class="sidebar-footer">
         <div class="sidebar-account-switcher">
           <button class="sidebar-account-switcher__main" type="button" :title="applicationWorkspace ? '当前账号' : `切换 Agent:${profileTitle(activeProfile)}`" :aria-haspopup="applicationWorkspace ? undefined : 'listbox'" :aria-expanded="applicationWorkspace ? undefined : profileMenuOpen" @click="applicationWorkspace ? openSettings('account-security', $event) : toggleProfileMenu($event)">
-            <AgentAvatar :name="applicationWorkspace ? userName : profileTitle(activeProfile)" :avatar="applicationWorkspace ? '' : activeProfile?.agentAvatar || ''" :size="30" />
+            <AccountInitialAvatar v-if="applicationWorkspace" :name="userName" :size="30" />
+            <AgentAvatar v-else :name="profileTitle(activeProfile)" :avatar="activeProfile?.agentAvatar || ''" :size="30" />
             <span class="account-copy">
               <strong>{{ applicationWorkspace ? userName : profileTitle(activeProfile) }}</strong>
               <span>{{ applicationWorkspace ? '当前账号' : activeProfile?.name || userName || '未选择 Agent' }}</span>
@@ -561,7 +563,8 @@ onBeforeUnmount(() => {
       <div class="sidebar-footer mobile-drawer__footer">
         <div class="sidebar-account-switcher">
           <button class="sidebar-account-switcher__main" type="button" :title="applicationWorkspace ? '当前账号' : `切换 Agent:${profileTitle(activeProfile)}`" :aria-haspopup="applicationWorkspace ? undefined : 'listbox'" :aria-expanded="applicationWorkspace ? undefined : profileMenuOpen" @click="applicationWorkspace ? openSettings('account-security', $event) : toggleProfileMenu($event)">
-            <AgentAvatar :name="applicationWorkspace ? userName : profileTitle(activeProfile)" :avatar="applicationWorkspace ? '' : activeProfile?.agentAvatar || ''" :size="30" />
+            <AccountInitialAvatar v-if="applicationWorkspace" :name="userName" :size="30" />
+            <AgentAvatar v-else :name="profileTitle(activeProfile)" :avatar="activeProfile?.agentAvatar || ''" :size="30" />
             <span class="account-copy">
               <strong>{{ applicationWorkspace ? userName : profileTitle(activeProfile) }}</strong>
               <span>{{ applicationWorkspace ? '当前账号' : activeProfile?.name || userName || '未选择 Agent' }}</span>

+ 32 - 0
src/client/components/common/AccountInitialAvatar.vue

@@ -0,0 +1,32 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const props = withDefaults(defineProps<{
+  name: string
+  size?: number
+}>(), { size: 30 })
+
+const initial = computed(() => Array.from(props.name.trim())[0]?.toLocaleUpperCase() || '?')
+const colors = ['#1488ff', '#00a17a', '#9655f7', '#e05d2f', '#b7791f', '#5166d6'] as const
+const background = computed(() => {
+  let hash = 2166136261
+  for (const byte of new TextEncoder().encode(props.name.trim().toLocaleLowerCase() || '?')) {
+    hash ^= byte
+    hash = Math.imul(hash, 16777619) >>> 0
+  }
+  return colors[hash % colors.length]
+})
+</script>
+
+<template>
+  <span
+    class="account-initial-avatar"
+    :style="{ width: `${size}px`, height: `${size}px`, backgroundColor: background, fontSize: `${Math.max(11, Math.round(size * .42))}px` }"
+    role="img"
+    :aria-label="`${name || '当前账号'} 的账号头像`"
+  ><span aria-hidden="true">{{ initial }}</span></span>
+</template>
+
+<style scoped>
+.account-initial-avatar { display: inline-grid; flex: 0 0 auto; place-items: center; border-radius: 50%; color: #fff; font-weight: 700; line-height: 1; text-transform: uppercase; user-select: none; }
+</style>

+ 1 - 0
tests/client/settingsCenterDialog.test.ts

@@ -100,6 +100,7 @@ describe('Settings center dialog', () => {
     expect(wrapper.find('.settings-agent-selector').exists()).toBe(false)
     expect(wrapper.get('.settings-account-summary').text()).toContain('owner')
     expect(wrapper.get('.settings-account-summary').text()).toContain('当前账号')
+    expect(wrapper.get('.settings-account-summary .account-initial-avatar').text()).toBe('O')
 
     const voiceButton = navigationButton(wrapper, '双流语音')
     expect(voiceButton.text()).toContain('全局')

+ 2 - 0
tests/client/workspaceShell.test.ts

@@ -151,6 +151,8 @@ it('uses the reference Bot list header and keeps mode changes in settings', asyn
   expect(rail.find('.sidebar-primary-action').exists()).toBe(false)
   expect(rail.find('.sidebar-footer').exists()).toBe(true)
   expect(rail.get('.sidebar-account-switcher__main').text()).toContain('owner')
+  expect(rail.get('.account-initial-avatar').text()).toBe('O')
+  expect(rail.find('.sidebar-account-switcher__main .agent-avatar').exists()).toBe(false)
   expect(rail.find('.bot-logo-trigger').exists()).toBe(true)
   expect(rail.find('.bot-account-trigger').exists()).toBe(false)
   expect(rail.find('.sidebar-collapse').exists()).toBe(false)

+ 2 - 0
tests/e2e/workspace-chat.spec.ts

@@ -40,11 +40,13 @@ test('created roles and editable teams share a durable chat list without plugin
   await expect(rail.locator('.bot-logo-trigger .brand-mark')).toBeVisible()
   await expect(rail.locator('.sidebar-footer')).toBeVisible()
   await expect(rail.locator('.sidebar-account-switcher__main')).toContainText('fixture')
+  await expect(rail.locator('.sidebar-account-switcher__main .account-initial-avatar')).toHaveText('F')
   await rail.getByRole('button', { name: '设置与模式', exact: true }).click()
   await expect(page.getByRole('menu', { name: '设置与模式' }).getByRole('menuitem')).toHaveText(['进入设置', '进入聊天模式'])
   await page.getByRole('menuitem', { name: '进入设置', exact: true }).click()
   const settings = page.getByRole('dialog', { name: '设置中心', exact: true })
   await expect(settings.locator('.settings-account-summary')).toContainText('fixture')
+  await expect(settings.locator('.settings-account-summary .account-initial-avatar')).toHaveText('F')
   await settings.getByRole('button', { name: '身份与头像', exact: true }).click()
   await settings.getByRole('button', { name: '造型:星星', exact: true }).click()
   await settings.getByRole('button', { name: '使用颜色 #1488ff', exact: true }).click()