mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-23 18:19:01 +08:00
feat: 口头禅分析
This commit is contained in:
@@ -20,6 +20,8 @@ import type {
|
|||||||
RepeatRateItem,
|
RepeatRateItem,
|
||||||
ChainLengthDistribution,
|
ChainLengthDistribution,
|
||||||
HotRepeatContent,
|
HotRepeatContent,
|
||||||
|
CatchphraseAnalysis,
|
||||||
|
MemberCatchphrase,
|
||||||
} from '../../../src/types/chat'
|
} from '../../../src/types/chat'
|
||||||
|
|
||||||
// 数据库存储目录
|
// 数据库存储目录
|
||||||
@@ -774,6 +776,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
msg.id,
|
msg.id,
|
||||||
msg.sender_id as senderId,
|
msg.sender_id as senderId,
|
||||||
msg.content,
|
msg.content,
|
||||||
|
msg.ts,
|
||||||
m.platform_id as platformId,
|
m.platform_id as platformId,
|
||||||
m.name
|
m.name
|
||||||
FROM message msg
|
FROM message msg
|
||||||
@@ -786,6 +789,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
id: number
|
id: number
|
||||||
senderId: number
|
senderId: number
|
||||||
content: string
|
content: string
|
||||||
|
ts: number
|
||||||
platformId: string
|
platformId: string
|
||||||
name: string
|
name: string
|
||||||
}>
|
}>
|
||||||
@@ -802,17 +806,23 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
// 复读链长度统计
|
// 复读链长度统计
|
||||||
const chainLengthCount = new Map<number, number>() // length -> count
|
const chainLengthCount = new Map<number, number>() // length -> count
|
||||||
|
|
||||||
// 热门复读内容统计(记录最长链的原创者)
|
// 热门复读内容统计(记录最长链的原创者和最近时间戳)
|
||||||
const contentStats = new Map<string, { count: number; maxChainLength: number; originatorId: number }>()
|
const contentStats = new Map<
|
||||||
|
string,
|
||||||
|
{ count: number; maxChainLength: number; originatorId: number; lastTs: number }
|
||||||
|
>()
|
||||||
|
|
||||||
// 滑动窗口算法
|
// 滑动窗口算法
|
||||||
let currentContent: string | null = null
|
let currentContent: string | null = null
|
||||||
let repeatChain: Array<{ senderId: number; content: string }> = []
|
let repeatChain: Array<{ senderId: number; content: string; ts: number }> = []
|
||||||
let totalRepeatChains = 0
|
let totalRepeatChains = 0
|
||||||
let totalChainLength = 0 // 用于计算平均长度
|
let totalChainLength = 0 // 用于计算平均长度
|
||||||
|
|
||||||
// 处理复读链的辅助函数(至少3人参与才算复读)
|
// 处理复读链的辅助函数(至少3人参与才算复读)
|
||||||
const processRepeatChain = (chain: Array<{ senderId: number; content: string }>, breakerId?: number) => {
|
const processRepeatChain = (
|
||||||
|
chain: Array<{ senderId: number; content: string; ts: number }>,
|
||||||
|
breakerId?: number
|
||||||
|
) => {
|
||||||
if (chain.length < 3) return
|
if (chain.length < 3) return
|
||||||
|
|
||||||
totalRepeatChains++
|
totalRepeatChains++
|
||||||
@@ -835,18 +845,21 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
// 复读链长度统计
|
// 复读链长度统计
|
||||||
chainLengthCount.set(chainLength, (chainLengthCount.get(chainLength) || 0) + 1)
|
chainLengthCount.set(chainLength, (chainLengthCount.get(chainLength) || 0) + 1)
|
||||||
|
|
||||||
// 热门复读内容统计(记录最长链的原创者)
|
// 热门复读内容统计(记录最长链的原创者和时间戳)
|
||||||
const content = chain[0].content
|
const content = chain[0].content
|
||||||
|
const chainTs = chain[0].ts // 复读链的时间戳(原创者发消息的时间)
|
||||||
const existing = contentStats.get(content)
|
const existing = contentStats.get(content)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.count++
|
existing.count++
|
||||||
|
// 更新最近时间戳
|
||||||
|
existing.lastTs = Math.max(existing.lastTs, chainTs)
|
||||||
// 如果当前链更长,更新最长链信息和原创者
|
// 如果当前链更长,更新最长链信息和原创者
|
||||||
if (chainLength > existing.maxChainLength) {
|
if (chainLength > existing.maxChainLength) {
|
||||||
existing.maxChainLength = chainLength
|
existing.maxChainLength = chainLength
|
||||||
existing.originatorId = originatorId
|
existing.originatorId = originatorId
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentStats.set(content, { count: 1, maxChainLength: chainLength, originatorId })
|
contentStats.set(content, { count: 1, maxChainLength: chainLength, originatorId, lastTs: chainTs })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,7 +879,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
const lastSender = repeatChain[repeatChain.length - 1]?.senderId
|
const lastSender = repeatChain[repeatChain.length - 1]?.senderId
|
||||||
if (lastSender !== msg.senderId) {
|
if (lastSender !== msg.senderId) {
|
||||||
// 不同人发的相同内容,延续复读链
|
// 不同人发的相同内容,延续复读链
|
||||||
repeatChain.push({ senderId: msg.senderId, content })
|
repeatChain.push({ senderId: msg.senderId, content, ts: msg.ts })
|
||||||
}
|
}
|
||||||
// 同一人连续发相同内容,忽略(不算复读)
|
// 同一人连续发相同内容,忽略(不算复读)
|
||||||
} else {
|
} else {
|
||||||
@@ -875,7 +888,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
|
|
||||||
// 开始新链
|
// 开始新链
|
||||||
currentContent = content
|
currentContent = content
|
||||||
repeatChain = [{ senderId: msg.senderId, content }]
|
repeatChain = [{ senderId: msg.senderId, content, ts: msg.ts }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,6 +950,7 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
count: stats.count,
|
count: stats.count,
|
||||||
maxChainLength: stats.maxChainLength,
|
maxChainLength: stats.maxChainLength,
|
||||||
originatorName: originatorInfo?.name || '未知',
|
originatorName: originatorInfo?.name || '未知',
|
||||||
|
lastTs: stats.lastTs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 按最长复读链长度降序排序
|
// 按最长复读链长度降序排序
|
||||||
@@ -959,3 +973,98 @@ export function getRepeatAnalysis(sessionId: string, filter?: TimeFilter): Repea
|
|||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取口头禅分析数据
|
||||||
|
* 统计每个成员最常说的内容(前5个)
|
||||||
|
* - 排除:系统消息、空消息、图片消息
|
||||||
|
* - 排除:过短的内容(少于2个字符)
|
||||||
|
*/
|
||||||
|
export function getCatchphraseAnalysis(sessionId: string, filter?: TimeFilter): CatchphraseAnalysis {
|
||||||
|
const db = openDatabase(sessionId)
|
||||||
|
if (!db) {
|
||||||
|
return { members: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { clause, params } = buildTimeFilter(filter)
|
||||||
|
|
||||||
|
// 构建查询条件:排除系统消息、空消息、图片,且内容长度 >= 2
|
||||||
|
let whereClause = clause
|
||||||
|
if (whereClause.includes('WHERE')) {
|
||||||
|
whereClause +=
|
||||||
|
" AND m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
|
||||||
|
} else {
|
||||||
|
whereClause =
|
||||||
|
" WHERE m.name != '系统消息' AND msg.type = 0 AND msg.content IS NOT NULL AND LENGTH(TRIM(msg.content)) >= 2"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每个成员的发言内容及出现次数
|
||||||
|
const rows = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.id as memberId,
|
||||||
|
m.platform_id as platformId,
|
||||||
|
m.name,
|
||||||
|
TRIM(msg.content) as content,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM message msg
|
||||||
|
JOIN member m ON msg.sender_id = m.id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY m.id, TRIM(msg.content)
|
||||||
|
ORDER BY m.id, count DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(...params) as Array<{
|
||||||
|
memberId: number
|
||||||
|
platformId: string
|
||||||
|
name: string
|
||||||
|
content: string
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
// 按成员分组,取每个成员的前3个口头禅
|
||||||
|
const memberMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
memberId: number
|
||||||
|
platformId: string
|
||||||
|
name: string
|
||||||
|
catchphrases: Array<{ content: string; count: number }>
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!memberMap.has(row.memberId)) {
|
||||||
|
memberMap.set(row.memberId, {
|
||||||
|
memberId: row.memberId,
|
||||||
|
platformId: row.platformId,
|
||||||
|
name: row.name,
|
||||||
|
catchphrases: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = memberMap.get(row.memberId)!
|
||||||
|
// 只保留前5个
|
||||||
|
if (member.catchphrases.length < 5) {
|
||||||
|
member.catchphrases.push({
|
||||||
|
content: row.content,
|
||||||
|
count: row.count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按发言总次数排序(口头禅出现次数总和)
|
||||||
|
const members = Array.from(memberMap.values())
|
||||||
|
members.sort((a, b) => {
|
||||||
|
const aTotal = a.catchphrases.reduce((sum, c) => sum + c.count, 0)
|
||||||
|
const bTotal = b.catchphrases.reduce((sum, c) => sum + c.count, 0)
|
||||||
|
return bTotal - aTotal
|
||||||
|
})
|
||||||
|
|
||||||
|
return { members }
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -375,6 +375,21 @@ const mainIpcMain = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取口头禅分析数据
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'chat:getCatchphraseAnalysis',
|
||||||
|
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
|
||||||
|
try {
|
||||||
|
return database.getCatchphraseAnalysis(sessionId, filter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取口头禅分析失败:', error)
|
||||||
|
return { members: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default mainIpcMain
|
export default mainIpcMain
|
||||||
|
|||||||
2
electron/preload/index.d.ts
vendored
2
electron/preload/index.d.ts
vendored
@@ -8,6 +8,7 @@ import type {
|
|||||||
MessageType,
|
MessageType,
|
||||||
ImportProgress,
|
ImportProgress,
|
||||||
RepeatAnalysis,
|
RepeatAnalysis,
|
||||||
|
CatchphraseAnalysis,
|
||||||
} from '../../src/types/chat'
|
} from '../../src/types/chat'
|
||||||
|
|
||||||
interface TimeFilter {
|
interface TimeFilter {
|
||||||
@@ -35,6 +36,7 @@ interface ChatApi {
|
|||||||
getSupportedFormats: () => Promise<Array<{ name: string; platform: string }>>
|
getSupportedFormats: () => Promise<Array<{ name: string; platform: string }>>
|
||||||
onImportProgress: (callback: (progress: ImportProgress) => void) => () => void
|
onImportProgress: (callback: (progress: ImportProgress) => void) => () => void
|
||||||
getRepeatAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<RepeatAnalysis>
|
getRepeatAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<RepeatAnalysis>
|
||||||
|
getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<CatchphraseAnalysis>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Api {
|
interface Api {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
MessageType,
|
MessageType,
|
||||||
ImportProgress,
|
ImportProgress,
|
||||||
RepeatAnalysis,
|
RepeatAnalysis,
|
||||||
|
CatchphraseAnalysis,
|
||||||
} from '../../src/types/chat'
|
} from '../../src/types/chat'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
@@ -160,6 +161,16 @@ const chatApi = {
|
|||||||
getRepeatAnalysis: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<RepeatAnalysis> => {
|
getRepeatAnalysis: (sessionId: string, filter?: { startTs?: number; endTs?: number }): Promise<RepeatAnalysis> => {
|
||||||
return ipcRenderer.invoke('chat:getRepeatAnalysis', sessionId, filter)
|
return ipcRenderer.invoke('chat:getRepeatAnalysis', sessionId, filter)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取口头禅分析数据
|
||||||
|
*/
|
||||||
|
getCatchphraseAnalysis: (
|
||||||
|
sessionId: string,
|
||||||
|
filter?: { startTs?: number; endTs?: number }
|
||||||
|
): Promise<CatchphraseAnalysis> => {
|
||||||
|
return ipcRenderer.invoke('chat:getCatchphraseAnalysis', sessionId, filter)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import type { MemberActivity, MemberNameHistory, RepeatAnalysis } from '@/types/chat'
|
import type { MemberActivity, MemberNameHistory, RepeatAnalysis, CatchphraseAnalysis } from '@/types/chat'
|
||||||
import { MemberRankList, BarChart } from '@/components/charts'
|
import { MemberRankList, BarChart } from '@/components/charts'
|
||||||
import type { MemberRankItem, BarChartData } from '@/components/charts'
|
import type { MemberRankItem, BarChartData } from '@/components/charts'
|
||||||
|
|
||||||
@@ -25,7 +25,8 @@ const repeatRankMode = ref<'count' | 'rate'>('rate')
|
|||||||
// 转换复读数据为排行榜格式(绝对次数)
|
// 转换复读数据为排行榜格式(绝对次数)
|
||||||
const originatorRankData = computed<MemberRankItem[]>(() => {
|
const originatorRankData = computed<MemberRankItem[]>(() => {
|
||||||
if (!repeatAnalysis.value) return []
|
if (!repeatAnalysis.value) return []
|
||||||
const data = repeatRankMode.value === 'count' ? repeatAnalysis.value.originators : repeatAnalysis.value.originatorRates
|
const data =
|
||||||
|
repeatRankMode.value === 'count' ? repeatAnalysis.value.originators : repeatAnalysis.value.originatorRates
|
||||||
return data.slice(0, 10).map((m) => ({
|
return data.slice(0, 10).map((m) => ({
|
||||||
id: m.memberId.toString(),
|
id: m.memberId.toString(),
|
||||||
name: m.name,
|
name: m.name,
|
||||||
@@ -86,6 +87,33 @@ function truncateContent(content: string, maxLength = 30): string {
|
|||||||
return content.slice(0, maxLength) + '...'
|
return content.slice(0, maxLength) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(ts: number): string {
|
||||||
|
const date = new Date(ts * 1000)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 口头禅分析 ====================
|
||||||
|
const catchphraseAnalysis = ref<CatchphraseAnalysis | null>(null)
|
||||||
|
const isLoadingCatchphrase = ref(false)
|
||||||
|
|
||||||
|
// 加载口头禅分析数据
|
||||||
|
async function loadCatchphraseAnalysis() {
|
||||||
|
if (!props.sessionId) return
|
||||||
|
|
||||||
|
isLoadingCatchphrase.value = true
|
||||||
|
try {
|
||||||
|
catchphraseAnalysis.value = await window.chatApi.getCatchphraseAnalysis(props.sessionId, props.timeFilter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载口头禅分析失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoadingCatchphrase.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Top 10 排行榜数据
|
// Top 10 排行榜数据
|
||||||
const top10RankData = computed<MemberRankItem[]>(() => {
|
const top10RankData = computed<MemberRankItem[]>(() => {
|
||||||
return props.memberActivity.slice(0, 10).map((m) => ({
|
return props.memberActivity.slice(0, 10).map((m) => ({
|
||||||
@@ -162,11 +190,12 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听 sessionId 和 timeFilter 变化,重新加载复读分析
|
// 监听 sessionId 和 timeFilter 变化,重新加载复读分析和口头禅分析
|
||||||
watch(
|
watch(
|
||||||
() => [props.sessionId, props.timeFilter],
|
() => [props.sessionId, props.timeFilter],
|
||||||
() => {
|
() => {
|
||||||
loadRepeatAnalysis()
|
loadRepeatAnalysis()
|
||||||
|
loadCatchphraseAnalysis()
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
)
|
)
|
||||||
@@ -327,11 +356,7 @@ function formatPeriod(startTs: number, endTs: number | null): string {
|
|||||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">每次复读有多少人参与</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">每次复读有多少人参与</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<BarChart
|
<BarChart v-if="chainLengthChartData.labels.length > 0" :data="chainLengthChartData" :height="200" />
|
||||||
v-if="chainLengthChartData.labels.length > 0"
|
|
||||||
:data="chainLengthChartData"
|
|
||||||
:height="200"
|
|
||||||
/>
|
|
||||||
<div v-else class="py-6 text-center text-sm text-gray-400">暂无数据</div>
|
<div v-else class="py-6 text-center text-sm text-gray-400">暂无数据</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,7 +394,11 @@ function formatPeriod(startTs: number, endTs: number | null): string {
|
|||||||
{{ truncateContent(item.content) }}
|
{{ truncateContent(item.content) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="shrink-0 text-xs text-gray-500">{{ item.count }} 次</span>
|
<div class="flex shrink-0 items-center gap-2 text-xs text-gray-500">
|
||||||
|
<span>{{ item.count }} 次</span>
|
||||||
|
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||||
|
<span>{{ formatDate(item.lastTs) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="px-4 py-6 text-center text-sm text-gray-400">暂无数据</div>
|
<div v-else class="px-4 py-6 text-center text-sm text-gray-400">暂无数据</div>
|
||||||
@@ -430,5 +459,68 @@ function formatPeriod(startTs: number, endTs: number | null): string {
|
|||||||
|
|
||||||
<div v-else class="px-5 py-8 text-center text-sm text-gray-400">该群组暂无复读记录</div>
|
<div v-else class="px-5 py-8 text-center text-sm text-gray-400">该群组暂无复读记录</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 口头禅分析模块 -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
<div class="border-b border-gray-200 px-5 py-4 dark:border-gray-800">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">💬 口头禅分析</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{
|
||||||
|
isLoadingCatchphrase
|
||||||
|
? '加载中...'
|
||||||
|
: catchphraseAnalysis
|
||||||
|
? `分析了 ${catchphraseAnalysis.members.length} 位成员的高频发言`
|
||||||
|
: '暂无数据'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoadingCatchphrase" class="px-5 py-8 text-center text-sm text-gray-400">正在分析口头禅数据...</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="catchphraseAnalysis && catchphraseAnalysis.members.length > 0"
|
||||||
|
class="divide-y divide-gray-100 dark:divide-gray-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="member in catchphraseAnalysis.members.slice(0, 20)"
|
||||||
|
:key="member.memberId"
|
||||||
|
class="flex items-start gap-4 px-5 py-4"
|
||||||
|
>
|
||||||
|
<!-- 成员名称 -->
|
||||||
|
<div class="w-28 shrink-0 pt-1 font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ member.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 口头禅列表 -->
|
||||||
|
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||||||
|
<div
|
||||||
|
v-for="(phrase, index) in member.catchphrases"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
|
||||||
|
:class="
|
||||||
|
index === 0
|
||||||
|
? 'bg-amber-50 dark:bg-amber-900/20'
|
||||||
|
: index === 1
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800'
|
||||||
|
: 'bg-gray-50 dark:bg-gray-800/50'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm"
|
||||||
|
:class="
|
||||||
|
index === 0 ? 'font-medium text-amber-700 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300'
|
||||||
|
"
|
||||||
|
:title="phrase.content"
|
||||||
|
>
|
||||||
|
{{ truncateContent(phrase.content, 20) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ phrase.count }}次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="px-5 py-8 text-center text-sm text-gray-400">暂无口头禅数据</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -217,6 +217,27 @@ export interface HotRepeatContent {
|
|||||||
count: number // 被复读次数
|
count: number // 被复读次数
|
||||||
maxChainLength: number // 最长复读链长度
|
maxChainLength: number // 最长复读链长度
|
||||||
originatorName: string // 最长链的原创者名称
|
originatorName: string // 最长链的原创者名称
|
||||||
|
lastTs: number // 最近一次发生的时间戳(秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成员口头禅项
|
||||||
|
*/
|
||||||
|
export interface MemberCatchphrase {
|
||||||
|
memberId: number
|
||||||
|
platformId: string
|
||||||
|
name: string
|
||||||
|
catchphrases: Array<{
|
||||||
|
content: string
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 口头禅分析结果
|
||||||
|
*/
|
||||||
|
export interface CatchphraseAnalysis {
|
||||||
|
members: MemberCatchphrase[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user