mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-23 01:39:37 +08:00
feat: 支持统计龙王和潜水排名
This commit is contained in:
@@ -21,6 +21,10 @@ import type {
|
|||||||
TimeRankItem,
|
TimeRankItem,
|
||||||
ConsecutiveNightRecord,
|
ConsecutiveNightRecord,
|
||||||
NightOwlChampion,
|
NightOwlChampion,
|
||||||
|
DragonKingAnalysis,
|
||||||
|
DragonKingRankItem,
|
||||||
|
DivingAnalysis,
|
||||||
|
DivingRankItem,
|
||||||
} from '../../../src/types/chat'
|
} from '../../../src/types/chat'
|
||||||
import { openDatabase } from './core'
|
import { openDatabase } from './core'
|
||||||
|
|
||||||
@@ -1012,3 +1016,138 @@ export function getNightOwlAnalysis(sessionId: string, filter?: TimeFilter): Nig
|
|||||||
db.close()
|
db.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取龙王排名
|
||||||
|
* 每天发言最多的人+1,统计所有天数
|
||||||
|
*/
|
||||||
|
export function getDragonKingAnalysis(sessionId: string, filter?: TimeFilter): DragonKingAnalysis {
|
||||||
|
const db = openDatabase(sessionId)
|
||||||
|
const emptyResult: DragonKingAnalysis = {
|
||||||
|
rank: [],
|
||||||
|
totalDays: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db) return emptyResult
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { clause, params } = buildTimeFilter(filter)
|
||||||
|
const clauseWithSystem = buildSystemMessageFilter(clause)
|
||||||
|
|
||||||
|
// 查询每天每个人的发言数,找出每天的龙王
|
||||||
|
const dailyTopSpeakers = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
WITH daily_counts AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime') as date,
|
||||||
|
msg.sender_id,
|
||||||
|
m.platform_id,
|
||||||
|
m.name,
|
||||||
|
COUNT(*) as msg_count
|
||||||
|
FROM message msg
|
||||||
|
JOIN member m ON msg.sender_id = m.id
|
||||||
|
${clauseWithSystem}
|
||||||
|
GROUP BY date, msg.sender_id
|
||||||
|
),
|
||||||
|
daily_max AS (
|
||||||
|
SELECT date, MAX(msg_count) as max_count
|
||||||
|
FROM daily_counts
|
||||||
|
GROUP BY date
|
||||||
|
)
|
||||||
|
SELECT dc.sender_id, dc.platform_id, dc.name, COUNT(*) as dragon_days
|
||||||
|
FROM daily_counts dc
|
||||||
|
JOIN daily_max dm ON dc.date = dm.date AND dc.msg_count = dm.max_count
|
||||||
|
GROUP BY dc.sender_id
|
||||||
|
ORDER BY dragon_days DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(...params) as Array<{
|
||||||
|
sender_id: number
|
||||||
|
platform_id: string
|
||||||
|
name: string
|
||||||
|
dragon_days: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
// 获取总天数
|
||||||
|
const totalDaysRow = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT COUNT(DISTINCT strftime('%Y-%m-%d', msg.ts, 'unixepoch', 'localtime')) as total
|
||||||
|
FROM message msg
|
||||||
|
JOIN member m ON msg.sender_id = m.id
|
||||||
|
${clauseWithSystem}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(...params) as { total: number }
|
||||||
|
|
||||||
|
const totalDays = totalDaysRow.total
|
||||||
|
|
||||||
|
const rank: DragonKingRankItem[] = dailyTopSpeakers.map((item) => ({
|
||||||
|
memberId: item.sender_id,
|
||||||
|
platformId: item.platform_id,
|
||||||
|
name: item.name,
|
||||||
|
count: item.dragon_days,
|
||||||
|
percentage: totalDays > 0 ? Math.round((item.dragon_days / totalDays) * 10000) / 100 : 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { rank, totalDays }
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取潜水排名
|
||||||
|
* 所有人的最后一次发言记录,按时间倒序(最久没发言的在前面)
|
||||||
|
*/
|
||||||
|
export function getDivingAnalysis(sessionId: string, filter?: TimeFilter): DivingAnalysis {
|
||||||
|
const db = openDatabase(sessionId)
|
||||||
|
const emptyResult: DivingAnalysis = {
|
||||||
|
rank: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db) return emptyResult
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { clause, params } = buildTimeFilter(filter)
|
||||||
|
const clauseWithSystem = buildSystemMessageFilter(clause)
|
||||||
|
|
||||||
|
// 查询每个成员的最后发言时间
|
||||||
|
const lastMessages = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.id as member_id,
|
||||||
|
m.platform_id,
|
||||||
|
m.name,
|
||||||
|
MAX(msg.ts) as last_ts
|
||||||
|
FROM member m
|
||||||
|
JOIN message msg ON m.id = msg.sender_id
|
||||||
|
${clauseWithSystem.replace('msg.', 'msg.')}
|
||||||
|
GROUP BY m.id
|
||||||
|
ORDER BY last_ts ASC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(...params) as Array<{
|
||||||
|
member_id: number
|
||||||
|
platform_id: string
|
||||||
|
name: string
|
||||||
|
last_ts: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
const rank: DivingRankItem[] = lastMessages.map((item) => ({
|
||||||
|
memberId: item.member_id,
|
||||||
|
platformId: item.platform_id,
|
||||||
|
name: item.name,
|
||||||
|
lastMessageTs: item.last_ts,
|
||||||
|
daysSinceLastMessage: Math.floor((now - item.last_ts) / 86400),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { rank }
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export {
|
|||||||
getRepeatAnalysis,
|
getRepeatAnalysis,
|
||||||
getCatchphraseAnalysis,
|
getCatchphraseAnalysis,
|
||||||
getNightOwlAnalysis,
|
getNightOwlAnalysis,
|
||||||
|
getDragonKingAnalysis,
|
||||||
|
getDivingAnalysis,
|
||||||
} from './analysis'
|
} from './analysis'
|
||||||
|
|
||||||
// 类型导出
|
// 类型导出
|
||||||
|
|||||||
@@ -427,6 +427,36 @@ const mainIpcMain = (win: BrowserWindow) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取龙王分析数据
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'chat:getDragonKingAnalysis',
|
||||||
|
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
|
||||||
|
try {
|
||||||
|
return database.getDragonKingAnalysis(sessionId, filter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取龙王分析失败:', error)
|
||||||
|
return { rank: [], totalDays: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取潜水分析数据
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
'chat:getDivingAnalysis',
|
||||||
|
async (_, sessionId: string, filter?: { startTs?: number; endTs?: number }) => {
|
||||||
|
try {
|
||||||
|
return database.getDivingAnalysis(sessionId, filter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取潜水分析失败:', error)
|
||||||
|
return { rank: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default mainIpcMain
|
export default mainIpcMain
|
||||||
|
|||||||
4
electron/preload/index.d.ts
vendored
4
electron/preload/index.d.ts
vendored
@@ -11,6 +11,8 @@ import type {
|
|||||||
RepeatAnalysis,
|
RepeatAnalysis,
|
||||||
CatchphraseAnalysis,
|
CatchphraseAnalysis,
|
||||||
NightOwlAnalysis,
|
NightOwlAnalysis,
|
||||||
|
DragonKingAnalysis,
|
||||||
|
DivingAnalysis,
|
||||||
} from '../../src/types/chat'
|
} from '../../src/types/chat'
|
||||||
|
|
||||||
interface TimeFilter {
|
interface TimeFilter {
|
||||||
@@ -41,6 +43,8 @@ interface ChatApi {
|
|||||||
getRepeatAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<RepeatAnalysis>
|
getRepeatAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<RepeatAnalysis>
|
||||||
getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<CatchphraseAnalysis>
|
getCatchphraseAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<CatchphraseAnalysis>
|
||||||
getNightOwlAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<NightOwlAnalysis>
|
getNightOwlAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<NightOwlAnalysis>
|
||||||
|
getDragonKingAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<DragonKingAnalysis>
|
||||||
|
getDivingAnalysis: (sessionId: string, filter?: TimeFilter) => Promise<DivingAnalysis>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Api {
|
interface Api {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
RepeatAnalysis,
|
RepeatAnalysis,
|
||||||
CatchphraseAnalysis,
|
CatchphraseAnalysis,
|
||||||
NightOwlAnalysis,
|
NightOwlAnalysis,
|
||||||
|
DragonKingAnalysis,
|
||||||
|
DivingAnalysis,
|
||||||
} from '../../src/types/chat'
|
} from '../../src/types/chat'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
@@ -190,6 +192,26 @@ const chatApi = {
|
|||||||
): Promise<NightOwlAnalysis> => {
|
): Promise<NightOwlAnalysis> => {
|
||||||
return ipcRenderer.invoke('chat:getNightOwlAnalysis', sessionId, filter)
|
return ipcRenderer.invoke('chat:getNightOwlAnalysis', sessionId, filter)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取龙王分析数据
|
||||||
|
*/
|
||||||
|
getDragonKingAnalysis: (
|
||||||
|
sessionId: string,
|
||||||
|
filter?: { startTs?: number; endTs?: number }
|
||||||
|
): Promise<DragonKingAnalysis> => {
|
||||||
|
return ipcRenderer.invoke('chat:getDragonKingAnalysis', sessionId, filter)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取潜水分析数据
|
||||||
|
*/
|
||||||
|
getDivingAnalysis: (
|
||||||
|
sessionId: string,
|
||||||
|
filter?: { startTs?: number; endTs?: number }
|
||||||
|
): Promise<DivingAnalysis> => {
|
||||||
|
return ipcRenderer.invoke('chat:getDivingAnalysis', sessionId, filter)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -249,7 +249,13 @@ onMounted(loadData)
|
|||||||
:hourly-activity="hourlyActivity"
|
:hourly-activity="hourlyActivity"
|
||||||
:time-filter="timeFilter"
|
:time-filter="timeFilter"
|
||||||
/>
|
/>
|
||||||
<TimelineTab v-else-if="activeTab === 'timeline'" :daily-activity="dailyActivity" :time-range="timeRange" />
|
<TimelineTab
|
||||||
|
v-else-if="activeTab === 'timeline'"
|
||||||
|
:session-id="currentSessionId!"
|
||||||
|
:daily-activity="dailyActivity"
|
||||||
|
:time-range="timeRange"
|
||||||
|
:time-filter="timeFilter"
|
||||||
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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, CatchphraseAnalysis } from '@/types/chat'
|
import type { MemberActivity, MemberNameHistory, RepeatAnalysis, CatchphraseAnalysis, DragonKingAnalysis } from '@/types/chat'
|
||||||
import { RankListPro, BarChart, ListPro } from '@/components/charts'
|
import { RankListPro, BarChart, ListPro } from '@/components/charts'
|
||||||
import type { RankItem, BarChartData } from '@/components/charts'
|
import type { RankItem, BarChartData } from '@/components/charts'
|
||||||
|
|
||||||
@@ -15,6 +15,35 @@ const props = defineProps<{
|
|||||||
timeFilter?: TimeFilter
|
timeFilter?: TimeFilter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// ==================== 龙王分析 ====================
|
||||||
|
const dragonKingAnalysis = ref<DragonKingAnalysis | null>(null)
|
||||||
|
const isLoadingDragonKing = ref(false)
|
||||||
|
|
||||||
|
// 加载龙王分析数据
|
||||||
|
async function loadDragonKingAnalysis() {
|
||||||
|
if (!props.sessionId) return
|
||||||
|
|
||||||
|
isLoadingDragonKing.value = true
|
||||||
|
try {
|
||||||
|
dragonKingAnalysis.value = await window.chatApi.getDragonKingAnalysis(props.sessionId, props.timeFilter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载龙王分析失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoadingDragonKing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 龙王排行数据
|
||||||
|
const dragonKingRankData = computed<RankItem[]>(() => {
|
||||||
|
if (!dragonKingAnalysis.value) return []
|
||||||
|
return dragonKingAnalysis.value.rank.map((m) => ({
|
||||||
|
id: m.memberId.toString(),
|
||||||
|
name: m.name,
|
||||||
|
value: m.count,
|
||||||
|
percentage: m.percentage,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
// ==================== 复读分析 ====================
|
// ==================== 复读分析 ====================
|
||||||
const repeatAnalysis = ref<RepeatAnalysis | null>(null)
|
const repeatAnalysis = ref<RepeatAnalysis | null>(null)
|
||||||
const isLoadingRepeat = ref(false)
|
const isLoadingRepeat = ref(false)
|
||||||
@@ -178,10 +207,11 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听 sessionId 和 timeFilter 变化,重新加载复读分析和口头禅分析
|
// 监听 sessionId 和 timeFilter 变化,重新加载分析数据
|
||||||
watch(
|
watch(
|
||||||
() => [props.sessionId, props.timeFilter],
|
() => [props.sessionId, props.timeFilter],
|
||||||
() => {
|
() => {
|
||||||
|
loadDragonKingAnalysis()
|
||||||
loadRepeatAnalysis()
|
loadRepeatAnalysis()
|
||||||
loadCatchphraseAnalysis()
|
loadCatchphraseAnalysis()
|
||||||
},
|
},
|
||||||
@@ -215,6 +245,21 @@ function formatPeriod(startTs: number, endTs: number | null): string {
|
|||||||
<!-- 成员活跃度排行 -->
|
<!-- 成员活跃度排行 -->
|
||||||
<RankListPro :members="memberRankData" title="成员活跃度排行" />
|
<RankListPro :members="memberRankData" title="成员活跃度排行" />
|
||||||
|
|
||||||
|
<!-- 龙王排名 -->
|
||||||
|
<div
|
||||||
|
v-if="isLoadingDragonKing"
|
||||||
|
class="rounded-xl border border-gray-200 bg-white px-5 py-8 text-center text-sm text-gray-400 shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||||||
|
>
|
||||||
|
正在统计龙王数据...
|
||||||
|
</div>
|
||||||
|
<RankListPro
|
||||||
|
v-else-if="dragonKingRankData.length > 0"
|
||||||
|
:members="dragonKingRankData"
|
||||||
|
title="🐉 龙王排名"
|
||||||
|
:description="`每天发言最多的人+1(共 ${dragonKingAnalysis?.totalDays ?? 0} 天)`"
|
||||||
|
unit="天"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 昵称变更记录区域 -->
|
<!-- 昵称变更记录区域 -->
|
||||||
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
<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">
|
<div class="border-b border-gray-200 px-5 py-4 dark:border-gray-800">
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import type { DailyActivity } from '@/types/chat'
|
import type { DailyActivity, DivingAnalysis } from '@/types/chat'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { LineChart } from '@/components/charts'
|
import { LineChart } from '@/components/charts'
|
||||||
import type { LineChartData } from '@/components/charts'
|
import type { LineChartData } from '@/components/charts'
|
||||||
|
|
||||||
|
interface TimeFilter {
|
||||||
|
startTs?: number
|
||||||
|
endTs?: number
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
sessionId: string
|
||||||
dailyActivity: DailyActivity[]
|
dailyActivity: DailyActivity[]
|
||||||
timeRange: { start: number; end: number } | null
|
timeRange: { start: number; end: number } | null
|
||||||
|
timeFilter?: TimeFilter
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 检测是否跨年
|
// 检测是否跨年
|
||||||
@@ -53,6 +60,47 @@ const totalDays = computed(() => {
|
|||||||
const end = dayjs.unix(props.timeRange.end)
|
const end = dayjs.unix(props.timeRange.end)
|
||||||
return end.diff(start, 'day') + 1
|
return end.diff(start, 'day') + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ==================== 潜水分析 ====================
|
||||||
|
const divingAnalysis = ref<DivingAnalysis | null>(null)
|
||||||
|
const isLoadingDiving = ref(false)
|
||||||
|
|
||||||
|
// 加载潜水分析数据
|
||||||
|
async function loadDivingAnalysis() {
|
||||||
|
if (!props.sessionId) return
|
||||||
|
|
||||||
|
isLoadingDiving.value = true
|
||||||
|
try {
|
||||||
|
divingAnalysis.value = await window.chatApi.getDivingAnalysis(props.sessionId, props.timeFilter)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载潜水分析失败:', error)
|
||||||
|
} finally {
|
||||||
|
isLoadingDiving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化最后发言时间(精确到时分秒)
|
||||||
|
function formatLastMessageTime(ts: number): string {
|
||||||
|
return dayjs.unix(ts).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化天数显示
|
||||||
|
function formatDaysSince(days: number): string {
|
||||||
|
if (days === 0) return '今天'
|
||||||
|
if (days === 1) return '昨天'
|
||||||
|
if (days < 30) return `${days} 天前`
|
||||||
|
if (days < 365) return `${Math.floor(days / 30)} 个月前`
|
||||||
|
return `${Math.floor(days / 365)} 年前`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 sessionId 和 timeFilter 变化
|
||||||
|
watch(
|
||||||
|
() => [props.sessionId, props.timeFilter],
|
||||||
|
() => {
|
||||||
|
loadDivingAnalysis()
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -103,5 +151,78 @@ const totalDays = computed(() => {
|
|||||||
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">每日消息趋势</h3>
|
<h3 class="mb-4 font-semibold text-gray-900 dark:text-white">每日消息趋势</h3>
|
||||||
<LineChart :data="dailyChartData" :height="288" />
|
<LineChart :data="dailyChartData" :height="288" />
|
||||||
</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">
|
||||||
|
按最后发言时间排序,最久没发言的在前面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoadingDiving" class="px-5 py-8 text-center text-sm text-gray-400">
|
||||||
|
正在统计潜水数据...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="divingAnalysis && divingAnalysis.rank.length > 0"
|
||||||
|
class="divide-y divide-gray-100 dark:divide-gray-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(member, index) in divingAnalysis.rank"
|
||||||
|
:key="member.memberId"
|
||||||
|
class="flex items-center gap-4 px-5 py-4 transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
|
>
|
||||||
|
<!-- 排名 -->
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-bold"
|
||||||
|
:class="
|
||||||
|
index === 0
|
||||||
|
? 'bg-gradient-to-r from-blue-400 to-cyan-500 text-white'
|
||||||
|
: index === 1
|
||||||
|
? 'bg-gradient-to-r from-blue-300 to-cyan-400 text-white'
|
||||||
|
: index === 2
|
||||||
|
? 'bg-gradient-to-r from-blue-200 to-cyan-300 text-gray-700'
|
||||||
|
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 名字 -->
|
||||||
|
<div class="w-32 shrink-0">
|
||||||
|
<p class="truncate font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ member.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最后发言时间 -->
|
||||||
|
<div class="flex flex-1 items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ formatLastMessageTime(member.lastMessageTs) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 距今天数 -->
|
||||||
|
<div class="shrink-0 text-right">
|
||||||
|
<span
|
||||||
|
class="text-sm font-medium"
|
||||||
|
:class="
|
||||||
|
member.daysSinceLastMessage > 30
|
||||||
|
? 'text-red-600 dark:text-red-400'
|
||||||
|
: member.daysSinceLastMessage > 7
|
||||||
|
? 'text-orange-600 dark:text-orange-400'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ formatDaysSince(member.daysSinceLastMessage) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="px-5 py-8 text-center text-sm text-gray-400">暂无数据</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -212,6 +212,43 @@ export interface NightOwlChampion {
|
|||||||
consecutiveDays: number // 最长连续天数
|
consecutiveDays: number // 最长连续天数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 龙王排名项(每天发言最多的人)
|
||||||
|
*/
|
||||||
|
export interface DragonKingRankItem {
|
||||||
|
memberId: number
|
||||||
|
platformId: string
|
||||||
|
name: string
|
||||||
|
count: number // 成为龙王的天数
|
||||||
|
percentage: number // 占总天数的百分比
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 龙王分析结果
|
||||||
|
*/
|
||||||
|
export interface DragonKingAnalysis {
|
||||||
|
rank: DragonKingRankItem[]
|
||||||
|
totalDays: number // 统计的总天数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 潜水排名项(最后发言时间)
|
||||||
|
*/
|
||||||
|
export interface DivingRankItem {
|
||||||
|
memberId: number
|
||||||
|
platformId: string
|
||||||
|
name: string
|
||||||
|
lastMessageTs: number // 最后发言时间戳(秒)
|
||||||
|
daysSinceLastMessage: number // 距离最后发言的天数
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 潜水分析结果
|
||||||
|
*/
|
||||||
|
export interface DivingAnalysis {
|
||||||
|
rank: DivingRankItem[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 夜猫分析完整结果
|
* 夜猫分析完整结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user