Merge pull request #7 from xzhseh/xzhseh/feat-avatar-name-highlighting

feat: highlight avatar names with unique colors in event panel
This commit is contained in:
4thfever
2026-01-04 20:52:34 +08:00
committed by GitHub
2 changed files with 69 additions and 1 deletions

View File

@@ -2,6 +2,8 @@
import { computed, ref, watch, nextTick } from 'vue'
import { useWorldStore } from '../../stores/world'
import { NSelect } from 'naive-ui'
import { highlightAvatarNames, buildAvatarColorMap } from '../../utils/eventHelper'
import type { GameEvent } from '../../types/core'
const worldStore = useWorldStore()
const filterValue = ref('all')
@@ -67,6 +69,15 @@ const emptyEventMessage = computed(() => (
function formatEventDate(event: { year: number; month: number }) {
return `${event.year}${event.month}`
}
// 构建角色名 -> 颜色映射表。
const avatarColorMap = computed(() => buildAvatarColorMap(worldStore.avatarList))
// 渲染事件内容,高亮角色名。
function renderEventContent(event: GameEvent): string {
const text = event.content || event.text || ''
return highlightAvatarNames(text, avatarColorMap.value)
}
</script>
<template>
@@ -84,7 +95,7 @@ function formatEventDate(event: { year: number; month: number }) {
<div v-else class="event-list" ref="eventListRef">
<div v-for="event in filteredEvents" :key="event.id" class="event-item">
<div class="event-date">{{ formatEventDate(event) }}</div>
<div class="event-content">{{ event.content || event.text }}</div>
<div class="event-content" v-html="renderEventContent(event)"></div>
</div>
</div>
</section>

View File

@@ -58,3 +58,60 @@ export function mergeAndSortEvents(existingEvents: GameEvent[], newEvents: GameE
return combined;
}
/**
* 根据角色 ID 哈希生成一致的 HSL 颜色。
*/
export function avatarIdToColor(id: string): string {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = ((hash << 5) - hash) + id.charCodeAt(i);
hash |= 0;
}
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 65%)`;
}
/**
* 根据角色列表构建 name -> color 映射表。
*/
export function buildAvatarColorMap(
avatars: Array<{ id: string; name?: string }>
): Map<string, string> {
const map = new Map<string, string>();
for (const av of avatars) {
if (av.name) {
map.set(av.name, avatarIdToColor(av.id));
}
}
return map;
}
const HTML_ESCAPE_MAP: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
/**
* 高亮文本中的角色名,返回 HTML 字符串。
*/
export function highlightAvatarNames(
text: string,
colorMap: Map<string, string>
): string {
if (!text || colorMap.size === 0) return text;
// 按名字长度倒序排列,避免部分匹配(如 "张三" 匹配到 "张三丰")。
const names = [...colorMap.keys()].sort((a, b) => b.length - a.length);
let result = text;
for (const name of names) {
const color = colorMap.get(name)!;
const escaped = name.replace(/[&<>"']/g, c => HTML_ESCAPE_MAP[c] || c);
result = result.replaceAll(name, `<span style="color:${color}">${escaped}</span>`);
}
return result;
}