mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-11 16:41:02 +08:00
feat: 时间筛选支持更多灵活选择
This commit is contained in:
@@ -0,0 +1,588 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import dayjs from 'dayjs'
|
||||
import { formatDateRange } from '@/utils'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import DatePicker from '@/components/UI/DatePicker.vue'
|
||||
|
||||
// ==================== 类型定义(导出供父组件使用) ====================
|
||||
|
||||
export type TimeSelectMode = 'recent' | 'quarter' | 'year' | 'custom'
|
||||
|
||||
/** 组件内部状态快照,用于父组件 URL 序列化 */
|
||||
export interface TimeSelectState {
|
||||
mode: TimeSelectMode
|
||||
recentDays?: number // 最近模式:天数 (180/365/730/1825/0=全部)
|
||||
year?: number // 按年模式:年份
|
||||
quarterYear?: number // 按季模式:年份
|
||||
quarter?: number // 按季模式:季度 (1-4)
|
||||
customStart?: string // 自定义模式:开始日期 YYYY-MM-DD
|
||||
customEnd?: string // 自定义模式:结束日期 YYYY-MM-DD
|
||||
}
|
||||
|
||||
/** v-model 绑定值 */
|
||||
export interface TimeRangeValue {
|
||||
startTs: number
|
||||
endTs: number
|
||||
/** 显示标签,用于父组件展示(如 "最近一年" / "2024年 第3季度") */
|
||||
displayLabel: string
|
||||
/** 是否选中「全部」范围(父组件据此决定使用 session 总数还是筛选数) */
|
||||
isFullRange: boolean
|
||||
/** 内部状态快照,便于父组件 URL 序列化 */
|
||||
state: TimeSelectState
|
||||
}
|
||||
|
||||
// ==================== Props & Emits ====================
|
||||
|
||||
interface Props {
|
||||
sessionId: string | undefined
|
||||
modelValue: TimeRangeValue | null
|
||||
visible?: boolean
|
||||
/** 初始状态(通常从 URL query 构建) */
|
||||
initialState?: Partial<TimeSelectState>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: TimeRangeValue): void
|
||||
(e: 'update:fullRange', value: { start: number; end: number } | null): void
|
||||
(e: 'update:availableYears', value: number[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// ==================== 内部数据 ====================
|
||||
|
||||
const isLoaded = ref(false)
|
||||
const availableYears = ref<number[]>([])
|
||||
const fullTimeRange = ref<{ start: number; end: number } | null>(null)
|
||||
|
||||
// 模式
|
||||
const mode = ref<TimeSelectMode>('recent')
|
||||
// 最近
|
||||
const recentPeriod = ref<number>(365)
|
||||
// 按年
|
||||
const selectedYear = ref<number>(0)
|
||||
// 按季
|
||||
const selectedQuarterYear = ref<number>(0)
|
||||
const selectedQuarter = ref<number>(1)
|
||||
// 自定义
|
||||
const customStartDate = ref<string>('')
|
||||
const customEndDate = ref<string>('')
|
||||
|
||||
// 是否正在内部初始化(防止 watcher 重复 emit)
|
||||
const isInitializing = ref(false)
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
function getQuarterFromTs(ts: number): { year: number; quarter: number } {
|
||||
const date = new Date(ts * 1000)
|
||||
return {
|
||||
year: date.getFullYear(),
|
||||
quarter: Math.floor(date.getMonth() / 3) + 1,
|
||||
}
|
||||
}
|
||||
|
||||
function getQuarterRange(year: number, quarter: number): { startTs: number; endTs: number } {
|
||||
const startMonth = (quarter - 1) * 3
|
||||
const startDate = new Date(year, startMonth, 1, 0, 0, 0)
|
||||
const endDate = new Date(year, startMonth + 3, 0, 23, 59, 59) // 季度最后一天
|
||||
return {
|
||||
startTs: Math.floor(startDate.getTime() / 1000),
|
||||
endTs: Math.floor(endDate.getTime() / 1000),
|
||||
}
|
||||
}
|
||||
|
||||
function getYearRange(year: number): { startTs: number; endTs: number } {
|
||||
const startDate = new Date(year, 0, 1, 0, 0, 0)
|
||||
const endDate = new Date(year, 11, 31, 23, 59, 59)
|
||||
return {
|
||||
startTs: Math.floor(startDate.getTime() / 1000),
|
||||
endTs: Math.floor(endDate.getTime() / 1000),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 选项配置 ====================
|
||||
|
||||
const modeOptions = computed(() => [
|
||||
{ label: t('mode.recent'), value: 'recent' as const },
|
||||
{ label: t('mode.quarter'), value: 'quarter' as const },
|
||||
{ label: t('mode.year'), value: 'year' as const },
|
||||
{ label: t('mode.custom'), value: 'custom' as const },
|
||||
])
|
||||
|
||||
const recentOptions = computed(() => [
|
||||
{ label: t('recent.halfYear'), value: 180 },
|
||||
{ label: t('recent.oneYear'), value: 365 },
|
||||
{ label: t('recent.twoYears'), value: 730 },
|
||||
{ label: t('recent.fiveYears'), value: 1825 },
|
||||
{ label: t('recent.all'), value: 0 },
|
||||
])
|
||||
|
||||
// ==================== 导航边界 ====================
|
||||
|
||||
const minQuarter = computed(() => {
|
||||
if (!fullTimeRange.value) return { year: 0, quarter: 1 }
|
||||
return getQuarterFromTs(fullTimeRange.value.start)
|
||||
})
|
||||
|
||||
const maxQuarter = computed(() => {
|
||||
if (!fullTimeRange.value) return { year: 0, quarter: 1 }
|
||||
return getQuarterFromTs(fullTimeRange.value.end)
|
||||
})
|
||||
|
||||
const canPrevQuarter = computed(() => {
|
||||
const min = minQuarter.value
|
||||
return (
|
||||
selectedQuarterYear.value > min.year ||
|
||||
(selectedQuarterYear.value === min.year && selectedQuarter.value > min.quarter)
|
||||
)
|
||||
})
|
||||
|
||||
const canNextQuarter = computed(() => {
|
||||
const max = maxQuarter.value
|
||||
return (
|
||||
selectedQuarterYear.value < max.year ||
|
||||
(selectedQuarterYear.value === max.year && selectedQuarter.value < max.quarter)
|
||||
)
|
||||
})
|
||||
|
||||
const canPrevYear = computed(() => {
|
||||
if (availableYears.value.length === 0) return false
|
||||
const currentIdx = availableYears.value.indexOf(selectedYear.value)
|
||||
return currentIdx < availableYears.value.length - 1 // years 降序
|
||||
})
|
||||
|
||||
const canNextYear = computed(() => {
|
||||
if (availableYears.value.length === 0) return false
|
||||
const currentIdx = availableYears.value.indexOf(selectedYear.value)
|
||||
return currentIdx > 0 // years 降序
|
||||
})
|
||||
|
||||
// ==================== 显示标签 ====================
|
||||
|
||||
const quarterDisplayLabel = computed(() => {
|
||||
return t('quarter.label', {
|
||||
year: selectedQuarterYear.value,
|
||||
quarter: selectedQuarter.value,
|
||||
})
|
||||
})
|
||||
|
||||
const yearDisplayLabel = computed(() => {
|
||||
return t('year.label', { year: selectedYear.value })
|
||||
})
|
||||
|
||||
// ==================== 核心:构建输出值 ====================
|
||||
|
||||
/** 最近模式 displayLabel 映射 */
|
||||
function getRecentDisplayLabel(days: number): string {
|
||||
const map: Record<number, string> = {
|
||||
180: t('display.recent180'),
|
||||
365: t('display.recent365'),
|
||||
730: t('display.recent730'),
|
||||
1825: t('display.recent1825'),
|
||||
}
|
||||
return map[days] || ''
|
||||
}
|
||||
|
||||
function buildValue(): TimeRangeValue | null {
|
||||
if (!fullTimeRange.value) return null
|
||||
const stateBase: TimeSelectState = { mode: mode.value }
|
||||
|
||||
switch (mode.value) {
|
||||
case 'recent': {
|
||||
stateBase.recentDays = recentPeriod.value
|
||||
if (recentPeriod.value === 0) {
|
||||
// 全部
|
||||
return {
|
||||
startTs: fullTimeRange.value.start,
|
||||
endTs: fullTimeRange.value.end,
|
||||
displayLabel: formatDateRange(fullTimeRange.value.start, fullTimeRange.value.end),
|
||||
isFullRange: true,
|
||||
state: stateBase,
|
||||
}
|
||||
}
|
||||
const endTs = fullTimeRange.value.end
|
||||
const startTs = Math.max(endTs - recentPeriod.value * 86400, fullTimeRange.value.start)
|
||||
return {
|
||||
startTs,
|
||||
endTs,
|
||||
displayLabel: getRecentDisplayLabel(recentPeriod.value),
|
||||
isFullRange: false,
|
||||
state: stateBase,
|
||||
}
|
||||
}
|
||||
case 'quarter': {
|
||||
stateBase.quarterYear = selectedQuarterYear.value
|
||||
stateBase.quarter = selectedQuarter.value
|
||||
const range = getQuarterRange(selectedQuarterYear.value, selectedQuarter.value)
|
||||
return {
|
||||
...range,
|
||||
displayLabel: quarterDisplayLabel.value,
|
||||
isFullRange: false,
|
||||
state: stateBase,
|
||||
}
|
||||
}
|
||||
case 'year': {
|
||||
stateBase.year = selectedYear.value
|
||||
const range = getYearRange(selectedYear.value)
|
||||
return {
|
||||
...range,
|
||||
displayLabel: yearDisplayLabel.value,
|
||||
isFullRange: false,
|
||||
state: stateBase,
|
||||
}
|
||||
}
|
||||
case 'custom': {
|
||||
stateBase.customStart = customStartDate.value
|
||||
stateBase.customEnd = customEndDate.value
|
||||
if (!customStartDate.value || !customEndDate.value) return null
|
||||
let startTs = dayjs(customStartDate.value).startOf('day').unix()
|
||||
let endTs = dayjs(customEndDate.value).endOf('day').unix()
|
||||
// 如果开始 > 结束,交换
|
||||
if (startTs > endTs) [startTs, endTs] = [endTs, startTs]
|
||||
return {
|
||||
startTs,
|
||||
endTs,
|
||||
displayLabel: formatDateRange(startTs, endTs),
|
||||
isFullRange: false,
|
||||
state: stateBase,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitCurrentValue() {
|
||||
const value = buildValue()
|
||||
if (value) emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// ==================== 导航方法 ====================
|
||||
|
||||
function navigateQuarter(direction: number) {
|
||||
let y = selectedQuarterYear.value
|
||||
let q = selectedQuarter.value + direction
|
||||
if (q > 4) {
|
||||
y++
|
||||
q = 1
|
||||
}
|
||||
if (q < 1) {
|
||||
y--
|
||||
q = 4
|
||||
}
|
||||
const min = minQuarter.value
|
||||
const max = maxQuarter.value
|
||||
if (y < min.year || (y === min.year && q < min.quarter)) return
|
||||
if (y > max.year || (y === max.year && q > max.quarter)) return
|
||||
selectedQuarterYear.value = y
|
||||
selectedQuarter.value = q
|
||||
emitCurrentValue()
|
||||
}
|
||||
|
||||
function navigateYear(direction: number) {
|
||||
const years = availableYears.value // 降序
|
||||
const currentIdx = years.indexOf(selectedYear.value)
|
||||
// direction 1 = 向后(更新年份,idx 更小),-1 = 向前(更旧年份,idx 更大)
|
||||
const newIdx = currentIdx - direction
|
||||
if (newIdx >= 0 && newIdx < years.length) {
|
||||
selectedYear.value = years[newIdx]
|
||||
emitCurrentValue()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 模式切换默认值 ====================
|
||||
|
||||
function initModeDefaults(newMode: TimeSelectMode) {
|
||||
if (!fullTimeRange.value) return
|
||||
switch (newMode) {
|
||||
case 'recent':
|
||||
if (![180, 365, 730, 1825, 0].includes(recentPeriod.value)) {
|
||||
recentPeriod.value = 365
|
||||
}
|
||||
break
|
||||
case 'quarter': {
|
||||
const { year, quarter } = getQuarterFromTs(fullTimeRange.value.end)
|
||||
selectedQuarterYear.value = year
|
||||
selectedQuarter.value = quarter
|
||||
break
|
||||
}
|
||||
case 'year': {
|
||||
selectedYear.value =
|
||||
availableYears.value[0] || new Date(fullTimeRange.value.end * 1000).getFullYear()
|
||||
break
|
||||
}
|
||||
case 'custom': {
|
||||
const endTs = fullTimeRange.value.end
|
||||
const startTs = Math.max(endTs - 30 * 86400, fullTimeRange.value.start)
|
||||
customStartDate.value = dayjs.unix(startTs).format('YYYY-MM-DD')
|
||||
customEndDate.value = dayjs.unix(endTs).format('YYYY-MM-DD')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 双向绑定模型 ====================
|
||||
|
||||
/** 模式选择器(USelect v-model) */
|
||||
const modeModel = computed({
|
||||
get: () => mode.value,
|
||||
set: (val: TimeSelectMode) => {
|
||||
mode.value = val
|
||||
isInitializing.value = true
|
||||
initModeDefaults(val)
|
||||
isInitializing.value = false
|
||||
emitCurrentValue()
|
||||
},
|
||||
})
|
||||
|
||||
/** 最近选项(UITabs v-model) */
|
||||
const recentPeriodModel = computed({
|
||||
get: () => recentPeriod.value,
|
||||
set: (val: number) => {
|
||||
recentPeriod.value = val
|
||||
emitCurrentValue()
|
||||
},
|
||||
})
|
||||
|
||||
/** 自定义开始日期 */
|
||||
const customStartModel = computed({
|
||||
get: () => customStartDate.value,
|
||||
set: (val: string) => {
|
||||
customStartDate.value = val
|
||||
if (customEndDate.value) emitCurrentValue()
|
||||
},
|
||||
})
|
||||
|
||||
/** 自定义结束日期 */
|
||||
const customEndModel = computed({
|
||||
get: () => customEndDate.value,
|
||||
set: (val: string) => {
|
||||
customEndDate.value = val
|
||||
if (customStartDate.value) emitCurrentValue()
|
||||
},
|
||||
})
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
|
||||
async function loadData() {
|
||||
if (!props.sessionId) {
|
||||
availableYears.value = []
|
||||
fullTimeRange.value = null
|
||||
emit('update:fullRange', null)
|
||||
emit('update:availableYears', [])
|
||||
isLoaded.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const [years, range] = await Promise.all([
|
||||
window.chatApi.getAvailableYears(props.sessionId),
|
||||
window.chatApi.getTimeRange(props.sessionId),
|
||||
])
|
||||
availableYears.value = years
|
||||
fullTimeRange.value = range
|
||||
emit('update:fullRange', range)
|
||||
emit('update:availableYears', years)
|
||||
|
||||
// 从 initialState 或默认值初始化
|
||||
const init = props.initialState
|
||||
const initMode = init?.mode ?? 'recent'
|
||||
mode.value = initMode
|
||||
|
||||
isInitializing.value = true
|
||||
switch (initMode) {
|
||||
case 'recent':
|
||||
recentPeriod.value = init?.recentDays ?? 365
|
||||
break
|
||||
case 'quarter': {
|
||||
if (init?.quarterYear && init?.quarter) {
|
||||
selectedQuarterYear.value = init.quarterYear
|
||||
selectedQuarter.value = init.quarter
|
||||
} else if (range) {
|
||||
const { year, quarter } = getQuarterFromTs(range.end)
|
||||
selectedQuarterYear.value = year
|
||||
selectedQuarter.value = quarter
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'year': {
|
||||
if (init?.year && years.includes(init.year)) {
|
||||
selectedYear.value = init.year
|
||||
} else {
|
||||
selectedYear.value = years[0] || 0
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'custom': {
|
||||
if (init?.customStart && init?.customEnd) {
|
||||
customStartDate.value = init.customStart
|
||||
customEndDate.value = init.customEnd
|
||||
} else if (range) {
|
||||
const endTs = range.end
|
||||
const startTs = Math.max(endTs - 30 * 86400, range.start)
|
||||
customStartDate.value = dayjs.unix(startTs).format('YYYY-MM-DD')
|
||||
customEndDate.value = dayjs.unix(endTs).format('YYYY-MM-DD')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
isInitializing.value = false
|
||||
|
||||
emitCurrentValue()
|
||||
} catch (error) {
|
||||
console.error('TimeSelect 加载数据失败:', error)
|
||||
availableYears.value = []
|
||||
fullTimeRange.value = null
|
||||
emit('update:fullRange', null)
|
||||
emit('update:availableYears', [])
|
||||
} finally {
|
||||
isLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
() => {
|
||||
isLoaded.value = false
|
||||
loadData()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible && isLoaded" class="flex items-center gap-2">
|
||||
<!-- 模式选择器 -->
|
||||
<USelect
|
||||
v-model="modeModel"
|
||||
:items="modeOptions"
|
||||
size="md"
|
||||
class="w-28 shrink-0"
|
||||
/>
|
||||
|
||||
<!-- 最近模式:UITabs 选择时间段 -->
|
||||
<UITabs
|
||||
v-if="mode === 'recent'"
|
||||
v-model="recentPeriodModel"
|
||||
:items="recentOptions"
|
||||
size="sm"
|
||||
class="min-w-0 shrink"
|
||||
/>
|
||||
|
||||
<!-- 按季模式:箭头导航 -->
|
||||
<div v-else-if="mode === 'quarter'" class="flex items-center">
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:disabled="!canPrevQuarter"
|
||||
@click="navigateQuarter(-1)"
|
||||
/>
|
||||
<span class="whitespace-nowrap px-0.5 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ quarterDisplayLabel }}
|
||||
</span>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-right"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:disabled="!canNextQuarter"
|
||||
@click="navigateQuarter(1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 按年模式:箭头导航 -->
|
||||
<div v-else-if="mode === 'year'" class="flex items-center">
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-left"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:disabled="!canPrevYear"
|
||||
@click="navigateYear(-1)"
|
||||
/>
|
||||
<span class="whitespace-nowrap px-0.5 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ yearDisplayLabel }}
|
||||
</span>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-right"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:disabled="!canNextYear"
|
||||
@click="navigateYear(1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 自定义模式:双日期选择器 -->
|
||||
<div v-else-if="mode === 'custom'" class="flex items-center gap-1">
|
||||
<DatePicker v-model="customStartModel" width-class="w-28" :clearable="false" />
|
||||
<span class="text-xs text-gray-400">-</span>
|
||||
<DatePicker v-model="customEndModel" width-class="w-28" :clearable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"zh-CN": {
|
||||
"mode": {
|
||||
"recent": "最近",
|
||||
"quarter": "按季",
|
||||
"year": "按年",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"recent": {
|
||||
"halfYear": "近半年",
|
||||
"oneYear": "近一年",
|
||||
"twoYears": "近两年",
|
||||
"fiveYears": "近五年",
|
||||
"all": "全部"
|
||||
},
|
||||
"display": {
|
||||
"recent180": "最近半年",
|
||||
"recent365": "最近一年",
|
||||
"recent730": "最近两年",
|
||||
"recent1825": "最近五年"
|
||||
},
|
||||
"quarter": {
|
||||
"label": "{year}年 第{quarter}季度"
|
||||
},
|
||||
"year": {
|
||||
"label": "{year}年"
|
||||
}
|
||||
},
|
||||
"en-US": {
|
||||
"mode": {
|
||||
"recent": "Recent",
|
||||
"quarter": "Quarter",
|
||||
"year": "Year",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"recent": {
|
||||
"halfYear": "Last 6M",
|
||||
"oneYear": "Last 1Y",
|
||||
"twoYears": "Last 2Y",
|
||||
"fiveYears": "Last 5Y",
|
||||
"all": "All"
|
||||
},
|
||||
"display": {
|
||||
"recent180": "Last 6 months",
|
||||
"recent365": "Last 1 year",
|
||||
"recent730": "Last 2 years",
|
||||
"recent1825": "Last 5 years"
|
||||
},
|
||||
"quarter": {
|
||||
"label": "{year} Q{quarter}"
|
||||
},
|
||||
"year": {
|
||||
"label": "{year}"
|
||||
}
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
@@ -5,3 +5,4 @@ export { useAsyncData, useMultipleAsyncData } from './useAsyncData'
|
||||
export { usePageAnchors, type AnchorItem } from './usePageAnchors'
|
||||
export { useAIChat, type ChatMessage, type SourceMessage } from './useAIChat'
|
||||
export { useScreenCapture, type ScreenCaptureOptions } from './useScreenCapture'
|
||||
export { useTimeSelect } from './useTimeSelect'
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* useTimeSelect — 管理 TimeSelect 组件的状态、派生计算与 URL 同步
|
||||
*
|
||||
* 从 private-chat/index.vue 和 group-chat/index.vue 中提取的共通逻辑,
|
||||
* 消除两个页面间 ~50 行重复代码。
|
||||
*/
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
|
||||
import type {
|
||||
TimeRangeValue,
|
||||
TimeSelectState,
|
||||
TimeSelectMode,
|
||||
} from '@/components/common/TimeSelect.vue'
|
||||
|
||||
interface UseTimeSelectOptions {
|
||||
/** 当前激活的 Tab ref(用于 URL 同步) */
|
||||
activeTab: Ref<string>
|
||||
/** 是否处于初始加载(URL 同步 guard) */
|
||||
isInitialLoad: Ref<boolean>
|
||||
/** 当前会话 ID ref(用于 timeRangeValue watch guard) */
|
||||
currentSessionId: Ref<string | null>
|
||||
/** timeRangeValue 变化时的回调(通常用于重新加载分析数据) */
|
||||
onTimeRangeChange?: () => void
|
||||
}
|
||||
|
||||
export function useTimeSelect(
|
||||
route: RouteLocationNormalizedLoaded,
|
||||
router: Router,
|
||||
options: UseTimeSelectOptions
|
||||
) {
|
||||
const { activeTab, isInitialLoad, currentSessionId, onTimeRangeChange } = options
|
||||
|
||||
// ==================== 核心状态 ====================
|
||||
|
||||
/** TimeSelect v-model 绑定值 */
|
||||
const timeRangeValue = ref<TimeRangeValue | null>(null)
|
||||
|
||||
/** 完整时间范围(由 TimeSelect 通过 emit 设置) */
|
||||
const fullTimeRange = ref<{ start: number; end: number } | null>(null)
|
||||
|
||||
/** 可选年份列表(由 TimeSelect 通过 emit 设置,group-chat 的 ViewTab 需要) */
|
||||
const availableYears = ref<number[]>([])
|
||||
|
||||
// ==================== 派生计算 ====================
|
||||
|
||||
/** 时间过滤参数(用于 API 调用) */
|
||||
const timeFilter = computed(() => {
|
||||
const v = timeRangeValue.value
|
||||
if (!v) return undefined
|
||||
return { startTs: v.startTs, endTs: v.endTs }
|
||||
})
|
||||
|
||||
/** Tab 内容 key(确保时间筛选切换时组件能正确刷新) */
|
||||
const timeFilterKey = computed(() => {
|
||||
const v = timeRangeValue.value
|
||||
if (!v) return 'init'
|
||||
return `${v.startTs}-${v.endTs}`
|
||||
})
|
||||
|
||||
/** 用于 OverviewTab / ViewTab 的 selectedYear(null=全部,number=指定年份) */
|
||||
const selectedYearForOverview = computed(() => {
|
||||
const v = timeRangeValue.value
|
||||
if (!v || v.isFullRange) return null
|
||||
return new Date(v.startTs * 1000).getFullYear()
|
||||
})
|
||||
|
||||
/** 从 URL query 构建 TimeSelect 初始状态 */
|
||||
const initialTimeState = computed<Partial<TimeSelectState>>(() => {
|
||||
const q = route.query
|
||||
const m = q.timeMode as TimeSelectMode | undefined
|
||||
return {
|
||||
mode: m ?? undefined,
|
||||
recentDays: q.timeDays ? Number(q.timeDays) : undefined,
|
||||
year: q.timeYear ? Number(q.timeYear) : undefined,
|
||||
quarterYear: q.timeYear ? Number(q.timeYear) : undefined,
|
||||
quarter: q.timeQuarter ? Number(q.timeQuarter) : undefined,
|
||||
customStart: (q.timeStart as string) || undefined,
|
||||
customEnd: (q.timeEnd as string) || undefined,
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== URL 同步 ====================
|
||||
|
||||
watch([activeTab, timeRangeValue], ([newTab, newTimeRange]) => {
|
||||
if (isInitialLoad.value || !newTimeRange) return
|
||||
|
||||
const state = (newTimeRange as TimeRangeValue).state
|
||||
const query: Record<string, string | number | undefined> = {
|
||||
tab: newTab as string,
|
||||
timeMode: state.mode,
|
||||
}
|
||||
if (state.mode === 'recent') query.timeDays = state.recentDays
|
||||
if (state.mode === 'year') query.timeYear = state.year
|
||||
if (state.mode === 'quarter') {
|
||||
query.timeYear = state.quarterYear
|
||||
query.timeQuarter = state.quarter
|
||||
}
|
||||
if (state.mode === 'custom') {
|
||||
query.timeStart = state.customStart
|
||||
query.timeEnd = state.customEnd
|
||||
}
|
||||
|
||||
router.replace({ query })
|
||||
})
|
||||
|
||||
// ==================== timeRangeValue 变化监听 ====================
|
||||
|
||||
watch(
|
||||
timeRangeValue,
|
||||
(val) => {
|
||||
if (!val || !currentSessionId.value) return
|
||||
onTimeRangeChange?.()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ==================== 重置方法 ====================
|
||||
|
||||
/** 切换会话时调用,清空时间范围状态 */
|
||||
function resetTimeRange() {
|
||||
timeRangeValue.value = null
|
||||
fullTimeRange.value = null
|
||||
availableYears.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
timeRangeValue,
|
||||
fullTimeRange,
|
||||
availableYears,
|
||||
// 派生计算
|
||||
timeFilter,
|
||||
timeFilterKey,
|
||||
selectedYearForOverview,
|
||||
initialTimeState,
|
||||
// 方法
|
||||
resetTimeRange,
|
||||
}
|
||||
}
|
||||
+45
-103
@@ -5,9 +5,8 @@ import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/analysis'
|
||||
import { formatDateRange } from '@/utils'
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import OverviewTab from './components/OverviewTab.vue'
|
||||
import ViewTab from './components/ViewTab.vue'
|
||||
@@ -19,6 +18,7 @@ import IncrementalImportModal from '@/components/analysis/IncrementalImportModal
|
||||
import LoadingState from '@/components/UI/LoadingState.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useTimeSelect } from '@/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -46,12 +46,7 @@ const memberActivity = ref<MemberActivity[]>([])
|
||||
const hourlyActivity = ref<HourlyActivity[]>([])
|
||||
const dailyActivity = ref<DailyActivity[]>([])
|
||||
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
||||
const timeRange = ref<{ start: number; end: number } | null>(null)
|
||||
|
||||
// 年份筛选
|
||||
const availableYears = ref<number[]>([])
|
||||
const selectedYear = ref<number>(0) // 0 表示全部
|
||||
const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发,并控制首屏加载状态
|
||||
const isInitialLoad = ref(true)
|
||||
|
||||
// Tab 配置
|
||||
const allTabs = [
|
||||
@@ -67,27 +62,21 @@ const tabs = computed(() => allTabs)
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'overview')
|
||||
|
||||
// 计算时间过滤参数
|
||||
const timeFilter = computed(() => {
|
||||
if (selectedYear.value === 0) {
|
||||
return undefined
|
||||
}
|
||||
// 计算年份的开始和结束时间戳
|
||||
const startDate = new Date(selectedYear.value, 0, 1, 0, 0, 0)
|
||||
const endDate = new Date(selectedYear.value, 11, 31, 23, 59, 59)
|
||||
return {
|
||||
startTs: Math.floor(startDate.getTime() / 1000),
|
||||
endTs: Math.floor(endDate.getTime() / 1000),
|
||||
}
|
||||
})
|
||||
|
||||
// 年份选项
|
||||
const yearOptions = computed(() => {
|
||||
const options = [{ label: t('analysis.yearFilter.allTime'), value: 0 }]
|
||||
for (const year of availableYears.value) {
|
||||
options.push({ label: t('analysis.yearFilter.year', { year }), value: year })
|
||||
}
|
||||
return options
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const {
|
||||
timeRangeValue,
|
||||
fullTimeRange,
|
||||
availableYears,
|
||||
timeFilter,
|
||||
timeFilterKey,
|
||||
selectedYearForOverview,
|
||||
initialTimeState,
|
||||
resetTimeRange,
|
||||
} = useTimeSelect(route, router, {
|
||||
activeTab,
|
||||
isInitialLoad,
|
||||
currentSessionId,
|
||||
onTimeRangeChange: () => loadAnalysisData(),
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
@@ -107,15 +96,6 @@ const filteredMemberCount = computed(() => {
|
||||
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
||||
})
|
||||
|
||||
// 格式化时间范围显示
|
||||
const dateRangeText = computed(() => {
|
||||
if (selectedYear.value) {
|
||||
return t('analysis.yearFilter.year', { year: selectedYear.value })
|
||||
}
|
||||
if (!timeRange.value) return ''
|
||||
return formatDateRange(timeRange.value.start, timeRange.value.end)
|
||||
})
|
||||
|
||||
// Sync route param to store
|
||||
function syncSession() {
|
||||
const id = route.params.id as string
|
||||
@@ -128,33 +108,13 @@ function syncSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载基础数据(不受年份筛选影响)
|
||||
// 加载基础数据(仅会话信息,时间范围由 TimeSelect 内部拉取)
|
||||
async function loadBaseData() {
|
||||
if (!currentSessionId.value) return
|
||||
|
||||
try {
|
||||
const [sessionData, years, range] = await Promise.all([
|
||||
window.chatApi.getSession(currentSessionId.value),
|
||||
window.chatApi.getAvailableYears(currentSessionId.value),
|
||||
window.chatApi.getTimeRange(currentSessionId.value),
|
||||
])
|
||||
|
||||
const sessionData = await window.chatApi.getSession(currentSessionId.value)
|
||||
session.value = sessionData
|
||||
availableYears.value = years
|
||||
timeRange.value = range
|
||||
|
||||
// 初始化年份选择
|
||||
// 1. 优先使用 URL 参数中的年份
|
||||
// 2. 否则默认选择最近的年份(years 已按降序排列)
|
||||
// 3. 如果没有年份数据,选 0 (全部)
|
||||
const queryYear = Number(route.query.year)
|
||||
if (queryYear === 0 || (queryYear && years.includes(queryYear))) {
|
||||
selectedYear.value = queryYear
|
||||
} else if (years.length > 0) {
|
||||
selectedYear.value = years[0]
|
||||
} else {
|
||||
selectedYear.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载基础数据失败:', error)
|
||||
}
|
||||
@@ -189,12 +149,10 @@ async function loadAnalysisData() {
|
||||
|
||||
// 加载所有数据
|
||||
async function loadData() {
|
||||
// 如果没有会话 ID,保持 loading 状态,等待 syncSession 设置后再触发
|
||||
if (!currentSessionId.value) return
|
||||
|
||||
isInitialLoad.value = true
|
||||
await loadBaseData()
|
||||
await loadAnalysisData()
|
||||
isInitialLoad.value = false
|
||||
}
|
||||
|
||||
@@ -215,37 +173,18 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 监听会话变化 (syncSession 会触发 currentSessionId 变化)
|
||||
// 监听会话变化(切换会话时清空时间范围,等待 TimeSelect 重新拉取)
|
||||
watch(
|
||||
currentSessionId,
|
||||
() => {
|
||||
// 年份筛选会在 loadBaseData 中自动设置为最近年份
|
||||
(newId, oldId) => {
|
||||
if (oldId !== undefined && newId !== oldId) {
|
||||
resetTimeRange()
|
||||
}
|
||||
loadData()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听年份筛选变化(仅用户手动切换年份时触发)
|
||||
watch(selectedYear, () => {
|
||||
// 跳过初始加载时的触发,避免重复加载
|
||||
if (isInitialLoad.value) return
|
||||
loadAnalysisData()
|
||||
})
|
||||
|
||||
// 同步状态到 URL
|
||||
watch([activeTab, selectedYear], ([newTab, newYear]) => {
|
||||
// 避免在初始化过程中频繁更新 URL
|
||||
if (isInitialLoad.value) return
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
tab: newTab,
|
||||
year: newYear,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
syncSession()
|
||||
})
|
||||
@@ -263,9 +202,11 @@ onMounted(() => {
|
||||
:title="session.name"
|
||||
:description="
|
||||
t('analysis.groupChat.description', {
|
||||
dateRange: dateRangeText,
|
||||
memberCount: selectedYear ? filteredMemberCount : session.memberCount,
|
||||
messageCount: selectedYear ? filteredMessageCount : session.messageCount,
|
||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
||||
memberCount:
|
||||
timeRangeValue?.isFullRange !== false ? session.memberCount : filteredMemberCount,
|
||||
messageCount:
|
||||
timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
||||
})
|
||||
"
|
||||
:avatar="session.groupAvatar"
|
||||
@@ -320,13 +261,14 @@ onMounted(() => {
|
||||
<span class="whitespace-nowrap">{{ t(tab.labelKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 年份选择器靠右,允许收缩(AI实验室时隐藏) -->
|
||||
<UITabs
|
||||
v-if="activeTab !== 'ai'"
|
||||
v-model="selectedYear"
|
||||
:items="yearOptions"
|
||||
size="sm"
|
||||
class="min-w-0 shrink"
|
||||
<!-- 时间范围选择器靠右(AI实验室时隐藏) -->
|
||||
<TimeSelect
|
||||
v-model="timeRangeValue"
|
||||
:session-id="currentSessionId ?? undefined"
|
||||
:visible="activeTab !== 'ai'"
|
||||
:initial-state="initialTimeState"
|
||||
@update:full-range="fullTimeRange = $event"
|
||||
@update:available-years="availableYears = $event"
|
||||
/>
|
||||
</div>
|
||||
</PageHeader>
|
||||
@@ -340,7 +282,7 @@ onMounted(() => {
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
<OverviewTab
|
||||
v-if="activeTab === 'overview'"
|
||||
:key="'overview-' + selectedYear"
|
||||
:key="'overview-' + timeFilterKey"
|
||||
:session="session"
|
||||
:member-activity="memberActivity"
|
||||
:top-members="topMembers"
|
||||
@@ -348,30 +290,30 @@ onMounted(() => {
|
||||
:message-types="messageTypes"
|
||||
:hourly-activity="hourlyActivity"
|
||||
:daily-activity="dailyActivity"
|
||||
:time-range="timeRange"
|
||||
:selected-year="selectedYear"
|
||||
:time-range="fullTimeRange"
|
||||
:selected-year="selectedYearForOverview"
|
||||
:filtered-message-count="filteredMessageCount"
|
||||
:filtered-member-count="filteredMemberCount"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<ViewTab
|
||||
v-else-if="activeTab === 'view'"
|
||||
:key="'view-' + selectedYear"
|
||||
:key="'view-' + timeFilterKey"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
:member-activity="memberActivity"
|
||||
:selected-year="selectedYear"
|
||||
:selected-year="selectedYearForOverview ?? undefined"
|
||||
:available-years="availableYears"
|
||||
/>
|
||||
<QuotesTab
|
||||
v-else-if="activeTab === 'quotes'"
|
||||
:key="'quotes-' + selectedYear"
|
||||
:key="'quotes-' + timeFilterKey"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<MemberTab
|
||||
v-else-if="activeTab === 'members'"
|
||||
:key="'members-' + selectedYear"
|
||||
:key="'members-' + timeFilterKey"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
@data-changed="loadData"
|
||||
|
||||
@@ -5,9 +5,8 @@ import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { MemberActivity, HourlyActivity, DailyActivity } from '@/types/analysis'
|
||||
import { formatDateRange } from '@/utils'
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import UITabs from '@/components/UI/Tabs.vue'
|
||||
import TimeSelect from '@/components/common/TimeSelect.vue'
|
||||
import AITab from '@/components/analysis/AITab.vue'
|
||||
import OverviewTab from './components/OverviewTab.vue'
|
||||
import ViewTab from './components/ViewTab.vue'
|
||||
@@ -19,6 +18,7 @@ import IncrementalImportModal from '@/components/analysis/IncrementalImportModal
|
||||
import LoadingState from '@/components/UI/LoadingState.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useTimeSelect } from '@/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -46,12 +46,7 @@ const memberActivity = ref<MemberActivity[]>([])
|
||||
const hourlyActivity = ref<HourlyActivity[]>([])
|
||||
const dailyActivity = ref<DailyActivity[]>([])
|
||||
const messageTypes = ref<Array<{ type: MessageType; count: number }>>([])
|
||||
const timeRange = ref<{ start: number; end: number } | null>(null)
|
||||
|
||||
// 年份筛选
|
||||
const availableYears = ref<number[]>([])
|
||||
const selectedYear = ref<number>(0) // 0 表示全部
|
||||
const isInitialLoad = ref(true) // 用于跳过初始加载时的 watch 触发,并控制首屏加载状态
|
||||
const isInitialLoad = ref(true)
|
||||
|
||||
// Tab 配置 - 私聊有总览、视图、语录、成员、AI实验室
|
||||
const tabs = [
|
||||
@@ -64,27 +59,20 @@ const tabs = [
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'overview')
|
||||
|
||||
// 计算时间过滤参数
|
||||
const timeFilter = computed(() => {
|
||||
if (selectedYear.value === 0) {
|
||||
return undefined
|
||||
}
|
||||
// 计算年份的开始和结束时间戳
|
||||
const startDate = new Date(selectedYear.value, 0, 1, 0, 0, 0)
|
||||
const endDate = new Date(selectedYear.value, 11, 31, 23, 59, 59)
|
||||
return {
|
||||
startTs: Math.floor(startDate.getTime() / 1000),
|
||||
endTs: Math.floor(endDate.getTime() / 1000),
|
||||
}
|
||||
})
|
||||
|
||||
// 年份选项
|
||||
const yearOptions = computed(() => {
|
||||
const options = [{ label: t('analysis.yearFilter.allTime'), value: 0 }]
|
||||
for (const year of availableYears.value) {
|
||||
options.push({ label: t('analysis.yearFilter.year', { year }), value: year })
|
||||
}
|
||||
return options
|
||||
// 时间范围筛选(composable 统一管理状态、派生计算、URL 同步)
|
||||
const {
|
||||
timeRangeValue,
|
||||
fullTimeRange,
|
||||
timeFilter,
|
||||
timeFilterKey,
|
||||
selectedYearForOverview,
|
||||
initialTimeState,
|
||||
resetTimeRange,
|
||||
} = useTimeSelect(route, router, {
|
||||
activeTab,
|
||||
isInitialLoad,
|
||||
currentSessionId,
|
||||
onTimeRangeChange: () => loadAnalysisData(),
|
||||
})
|
||||
|
||||
// 当前筛选后的消息总数
|
||||
@@ -97,15 +85,6 @@ const filteredMemberCount = computed(() => {
|
||||
return memberActivity.value.filter((m) => m.messageCount > 0).length
|
||||
})
|
||||
|
||||
// 格式化时间范围显示
|
||||
const dateRangeText = computed(() => {
|
||||
if (selectedYear.value) {
|
||||
return t('analysis.yearFilter.year', { year: selectedYear.value })
|
||||
}
|
||||
if (!timeRange.value) return ''
|
||||
return formatDateRange(timeRange.value.start, timeRange.value.end)
|
||||
})
|
||||
|
||||
// Sync route param to store
|
||||
function syncSession() {
|
||||
const id = route.params.id as string
|
||||
@@ -118,36 +97,19 @@ function syncSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载基础数据(不受年份筛选影响)
|
||||
// 加载基础数据(仅会话信息,时间范围由 TimeSelect 内部拉取)
|
||||
async function loadBaseData() {
|
||||
if (!currentSessionId.value) return
|
||||
|
||||
try {
|
||||
const [sessionData, years, range] = await Promise.all([
|
||||
window.chatApi.getSession(currentSessionId.value),
|
||||
window.chatApi.getAvailableYears(currentSessionId.value),
|
||||
window.chatApi.getTimeRange(currentSessionId.value),
|
||||
])
|
||||
|
||||
const sessionData = await window.chatApi.getSession(currentSessionId.value)
|
||||
session.value = sessionData
|
||||
availableYears.value = years
|
||||
timeRange.value = range
|
||||
|
||||
// 初始化年份选择
|
||||
const queryYear = Number(route.query.year)
|
||||
if (queryYear === 0 || (queryYear && years.includes(queryYear))) {
|
||||
selectedYear.value = queryYear
|
||||
} else if (years.length > 0) {
|
||||
selectedYear.value = years[0]
|
||||
} else {
|
||||
selectedYear.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载基础数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分析数据(受年份筛选影响)
|
||||
// 加载分析数据(受时间范围筛选影响)
|
||||
async function loadAnalysisData() {
|
||||
if (!currentSessionId.value) return
|
||||
|
||||
@@ -176,12 +138,10 @@ async function loadAnalysisData() {
|
||||
|
||||
// 加载所有数据
|
||||
async function loadData() {
|
||||
// 如果没有会话 ID,保持 loading 状态,等待 syncSession 设置后再触发
|
||||
if (!currentSessionId.value) return
|
||||
|
||||
isInitialLoad.value = true
|
||||
await loadBaseData()
|
||||
await loadAnalysisData()
|
||||
isInitialLoad.value = false
|
||||
}
|
||||
|
||||
@@ -198,34 +158,18 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 监听会话变化
|
||||
// 监听会话变化(切换会话时清空时间范围,等待 TimeSelect 重新拉取)
|
||||
watch(
|
||||
currentSessionId,
|
||||
() => {
|
||||
(newId, oldId) => {
|
||||
if (oldId !== undefined && newId !== oldId) {
|
||||
resetTimeRange()
|
||||
}
|
||||
loadData()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听年份筛选变化
|
||||
watch(selectedYear, () => {
|
||||
if (isInitialLoad.value) return
|
||||
loadAnalysisData()
|
||||
})
|
||||
|
||||
// 同步状态到 URL
|
||||
watch([activeTab, selectedYear], ([newTab, newYear]) => {
|
||||
if (isInitialLoad.value) return
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
tab: newTab,
|
||||
year: newYear,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// 获取对方头像
|
||||
const otherMemberAvatar = computed(() => {
|
||||
if (!session.value || memberActivity.value.length === 0) return null
|
||||
@@ -267,8 +211,9 @@ onMounted(() => {
|
||||
:title="session.name"
|
||||
:description="
|
||||
t('analysis.privateChat.description', {
|
||||
dateRange: dateRangeText,
|
||||
messageCount: selectedYear ? filteredMessageCount : session.messageCount,
|
||||
dateRange: timeRangeValue?.displayLabel ?? '',
|
||||
messageCount:
|
||||
timeRangeValue?.isFullRange !== false ? session.messageCount : filteredMessageCount,
|
||||
})
|
||||
"
|
||||
:avatar="otherMemberAvatar"
|
||||
@@ -323,13 +268,13 @@ onMounted(() => {
|
||||
<span class="whitespace-nowrap">{{ t(tab.labelKey) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 年份选择器靠右(AI实验室时隐藏) -->
|
||||
<UITabs
|
||||
v-if="activeTab !== 'ai'"
|
||||
v-model="selectedYear"
|
||||
:items="yearOptions"
|
||||
size="sm"
|
||||
class="min-w-0 shrink"
|
||||
<!-- 时间范围选择器靠右(AI实验室时隐藏) -->
|
||||
<TimeSelect
|
||||
v-model="timeRangeValue"
|
||||
:session-id="currentSessionId ?? undefined"
|
||||
:visible="activeTab !== 'ai'"
|
||||
:initial-state="initialTimeState"
|
||||
@update:full-range="fullTimeRange = $event"
|
||||
/>
|
||||
</div>
|
||||
</PageHeader>
|
||||
@@ -343,27 +288,27 @@ onMounted(() => {
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
<OverviewTab
|
||||
v-if="activeTab === 'overview'"
|
||||
:key="'overview-' + selectedYear"
|
||||
:key="'overview-' + timeFilterKey"
|
||||
:session="session"
|
||||
:member-activity="memberActivity"
|
||||
:message-types="messageTypes"
|
||||
:hourly-activity="hourlyActivity"
|
||||
:daily-activity="dailyActivity"
|
||||
:time-range="timeRange"
|
||||
:selected-year="selectedYear"
|
||||
:time-range="fullTimeRange"
|
||||
:selected-year="selectedYearForOverview"
|
||||
:filtered-message-count="filteredMessageCount"
|
||||
:filtered-member-count="filteredMemberCount"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<ViewTab
|
||||
v-else-if="activeTab === 'view'"
|
||||
:key="'view-' + selectedYear"
|
||||
:key="'view-' + timeFilterKey"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
<QuotesTab
|
||||
v-else-if="activeTab === 'quotes'"
|
||||
:key="'quotes-' + selectedYear"
|
||||
:key="'quotes-' + timeFilterKey"
|
||||
:session-id="currentSessionId!"
|
||||
:time-filter="timeFilter"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user