openclaw-agent-dashboard 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +56 -0
- package/README.md +302 -0
- package/docs/CHANGELOG_AGENT_MODIFICATIONS.md +132 -0
- package/docs/RELEASE-LATEST.md +189 -0
- package/docs/RELEASE-MODEL-CONFIG.md +95 -0
- package/docs/release-guide.md +259 -0
- package/docs/release-operations-manual.md +167 -0
- package/docs/specs/tr3-install-system.md +580 -0
- package/docs/windows-collaboration-model-paths-troubleshooting.md +0 -0
- package/frontend/index.html +12 -0
- package/frontend/package-lock.json +1240 -0
- package/frontend/package.json +19 -0
- package/frontend/src/App.vue +331 -0
- package/frontend/src/components/AgentCard.vue +796 -0
- package/frontend/src/components/AgentConfigPanel.vue +539 -0
- package/frontend/src/components/AgentDetailPanel.vue +738 -0
- package/frontend/src/components/ErrorAnalysisView.vue +546 -0
- package/frontend/src/components/ErrorCenterPanel.vue +844 -0
- package/frontend/src/components/PerformanceMonitor.vue +515 -0
- package/frontend/src/components/SettingsPanel.vue +236 -0
- package/frontend/src/components/TokenAnalysisPanel.vue +683 -0
- package/frontend/src/components/chain/ChainEdge.vue +85 -0
- package/frontend/src/components/chain/ChainNode.vue +166 -0
- package/frontend/src/components/chain/TaskChainView.vue +425 -0
- package/frontend/src/components/chain/index.ts +3 -0
- package/frontend/src/components/chain/types.ts +70 -0
- package/frontend/src/components/collaboration/CollaborationFlowSection.vue +1032 -0
- package/frontend/src/components/collaboration/CollaborationFlowWrapper.vue +113 -0
- package/frontend/src/components/performance/PerformancePanel.vue +119 -0
- package/frontend/src/components/performance/PerformanceSection.vue +1137 -0
- package/frontend/src/components/tasks/TaskStatusSection.vue +973 -0
- package/frontend/src/components/timeline/TimelineConnector.vue +31 -0
- package/frontend/src/components/timeline/TimelineRound.vue +135 -0
- package/frontend/src/components/timeline/TimelineStep.vue +691 -0
- package/frontend/src/components/timeline/TimelineToolLink.vue +109 -0
- package/frontend/src/components/timeline/TimelineView.vue +540 -0
- package/frontend/src/components/timeline/index.ts +5 -0
- package/frontend/src/components/timeline/types.ts +120 -0
- package/frontend/src/composables/index.ts +7 -0
- package/frontend/src/composables/useDebounce.ts +48 -0
- package/frontend/src/composables/useRealtime.ts +52 -0
- package/frontend/src/composables/useState.ts +52 -0
- package/frontend/src/composables/useThrottle.ts +46 -0
- package/frontend/src/composables/useVirtualScroll.ts +106 -0
- package/frontend/src/main.ts +4 -0
- package/frontend/src/managers/EventDispatcher.ts +127 -0
- package/frontend/src/managers/RealtimeDataManager.ts +293 -0
- package/frontend/src/managers/StateManager.ts +128 -0
- package/frontend/src/managers/index.ts +5 -0
- package/frontend/src/types/collaboration.ts +135 -0
- package/frontend/src/types/index.ts +20 -0
- package/frontend/src/types/performance.ts +105 -0
- package/frontend/src/types/task.ts +38 -0
- package/frontend/vite.config.ts +18 -0
- package/package.json +22 -0
- package/plugin/README.md +99 -0
- package/plugin/config.json.example +1 -0
- package/plugin/index.js +250 -0
- package/plugin/openclaw.plugin.json +17 -0
- package/plugin/package.json +21 -0
- package/scripts/build-plugin.js +67 -0
- package/scripts/bundle.sh +62 -0
- package/scripts/install-plugin.sh +162 -0
- package/scripts/install-python-deps.js +346 -0
- package/scripts/install-python-deps.sh +226 -0
- package/scripts/install.js +512 -0
- package/scripts/install.sh +367 -0
- package/scripts/lib/common.js +490 -0
- package/scripts/lib/common.sh +137 -0
- package/scripts/release-pack.sh +110 -0
- package/scripts/start.js +50 -0
- package/scripts/test_available_models.py +284 -0
- package/scripts/test_websocket_ping.py +44 -0
- package/src/backend/agents.py +73 -0
- package/src/backend/api/__init__.py +1 -0
- package/src/backend/api/agent_config_api.py +90 -0
- package/src/backend/api/agents.py +73 -0
- package/src/backend/api/agents_config.py +75 -0
- package/src/backend/api/chains.py +126 -0
- package/src/backend/api/collaboration.py +902 -0
- package/src/backend/api/debug_paths.py +39 -0
- package/src/backend/api/error_analysis.py +146 -0
- package/src/backend/api/errors.py +281 -0
- package/src/backend/api/performance.py +784 -0
- package/src/backend/api/subagents.py +770 -0
- package/src/backend/api/timeline.py +144 -0
- package/src/backend/api/websocket.py +251 -0
- package/src/backend/collaboration.py +405 -0
- package/src/backend/data/__init__.py +1 -0
- package/src/backend/data/agent_config_manager.py +270 -0
- package/src/backend/data/chain_reader.py +299 -0
- package/src/backend/data/config_reader.py +153 -0
- package/src/backend/data/error_analyzer.py +430 -0
- package/src/backend/data/session_reader.py +445 -0
- package/src/backend/data/subagent_reader.py +244 -0
- package/src/backend/data/task_history.py +118 -0
- package/src/backend/data/timeline_reader.py +981 -0
- package/src/backend/errors.py +63 -0
- package/src/backend/main.py +89 -0
- package/src/backend/mechanism_reader.py +131 -0
- package/src/backend/mechanisms.py +32 -0
- package/src/backend/performance.py +474 -0
- package/src/backend/requirements.txt +5 -0
- package/src/backend/session_reader.py +238 -0
- package/src/backend/status/__init__.py +1 -0
- package/src/backend/status/error_detector.py +122 -0
- package/src/backend/status/status_calculator.py +301 -0
- package/src/backend/status_calculator.py +121 -0
- package/src/backend/subagent_reader.py +229 -0
- package/src/backend/watchers/__init__.py +4 -0
- package/src/backend/watchers/file_watcher.py +159 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Timeline 类型定义
|
|
2
|
+
|
|
3
|
+
/** 步骤类型 */
|
|
4
|
+
export type StepType =
|
|
5
|
+
| 'user' // 用户消息
|
|
6
|
+
| 'thinking' // Agent 思考
|
|
7
|
+
| 'text' // Agent 文本响应
|
|
8
|
+
| 'toolCall' // 工具调用
|
|
9
|
+
| 'toolResult' // 工具结果
|
|
10
|
+
| 'error' // 错误
|
|
11
|
+
|
|
12
|
+
/** 步骤状态 */
|
|
13
|
+
export type StepStatus = 'pending' | 'running' | 'success' | 'error'
|
|
14
|
+
|
|
15
|
+
/** Token 使用 */
|
|
16
|
+
export interface TokenUsage {
|
|
17
|
+
input: number
|
|
18
|
+
output: number
|
|
19
|
+
cumulative?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 时序步骤 */
|
|
23
|
+
export interface TimelineStep {
|
|
24
|
+
id: string
|
|
25
|
+
type: StepType
|
|
26
|
+
status: StepStatus
|
|
27
|
+
timestamp: number
|
|
28
|
+
duration?: number
|
|
29
|
+
|
|
30
|
+
// 内容
|
|
31
|
+
content?: string
|
|
32
|
+
thinking?: string
|
|
33
|
+
|
|
34
|
+
// 工具调用
|
|
35
|
+
toolName?: string
|
|
36
|
+
toolCallId?: string
|
|
37
|
+
toolArguments?: Record<string, unknown>
|
|
38
|
+
toolResult?: string
|
|
39
|
+
toolResultStatus?: 'ok' | 'error'
|
|
40
|
+
toolResultError?: string // 工具失败时的错误信息
|
|
41
|
+
|
|
42
|
+
// 工具链路关联
|
|
43
|
+
pairedToolCallId?: string // toolResult 专用:对应的 toolCall ID
|
|
44
|
+
pairedToolResultId?: string // toolCall 专用:对应的 toolResult ID
|
|
45
|
+
executionTime?: number // 工具执行耗时(ms),toolResult 专用
|
|
46
|
+
|
|
47
|
+
// 错误
|
|
48
|
+
errorMessage?: string
|
|
49
|
+
errorType?: string
|
|
50
|
+
|
|
51
|
+
// 统计
|
|
52
|
+
tokens?: TokenUsage
|
|
53
|
+
|
|
54
|
+
// 展示控制
|
|
55
|
+
collapsed?: boolean
|
|
56
|
+
|
|
57
|
+
// 消息来源(用于区分真实用户和其他 Agent)
|
|
58
|
+
senderId?: string // 发送者 Agent ID(如 'main')
|
|
59
|
+
senderName?: string // 发送者显示名(如 '老K')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 时序统计 */
|
|
63
|
+
export interface TimelineStats {
|
|
64
|
+
totalDuration: number
|
|
65
|
+
totalInputTokens: number
|
|
66
|
+
totalOutputTokens: number
|
|
67
|
+
toolCallCount: number
|
|
68
|
+
stepCount: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** LLM 轮次 */
|
|
72
|
+
export interface LLMRound {
|
|
73
|
+
id: string // round_1, round_2, ...
|
|
74
|
+
index: number // 轮次序号(从1开始)
|
|
75
|
+
trigger: 'user_input' | 'tool_result' | 'subagent_result' | 'start'
|
|
76
|
+
triggerBy?: string // 触发来源描述
|
|
77
|
+
stepIds: string[] // 该轮次包含的步骤 ID 列表
|
|
78
|
+
duration: number // 该轮次耗时(ms)
|
|
79
|
+
tokens?: TokenUsage // 该轮次的 token 使用
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 时序会话响应 */
|
|
83
|
+
export interface TimelineResponse {
|
|
84
|
+
sessionId: string | null
|
|
85
|
+
agentId: string
|
|
86
|
+
agentName?: string
|
|
87
|
+
model?: string
|
|
88
|
+
startedAt: number | null
|
|
89
|
+
status: 'running' | 'completed' | 'error' | 'empty' | 'no_sessions'
|
|
90
|
+
steps: TimelineStep[]
|
|
91
|
+
stats: TimelineStats
|
|
92
|
+
message?: string
|
|
93
|
+
// LLM 轮次分组
|
|
94
|
+
rounds?: LLMRound[]
|
|
95
|
+
roundMode?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 步骤图标和颜色配置 */
|
|
99
|
+
export const stepConfig: Record<StepType, { icon: string; bgColor: string; borderColor: string; label: string }> = {
|
|
100
|
+
user: { icon: '👤', bgColor: '#f0f9ff', borderColor: '#3b82f6', label: '用户' },
|
|
101
|
+
thinking: { icon: '🧠', bgColor: '#fef3c7', borderColor: '#f59e0b', label: '思考' },
|
|
102
|
+
text: { icon: '🤖', bgColor: '#f0fdf4', borderColor: '#22c55e', label: '回复' },
|
|
103
|
+
toolCall: { icon: '🔧', bgColor: '#f5f3ff', borderColor: '#8b5cf6', label: '调用' },
|
|
104
|
+
toolResult: { icon: '✅', bgColor: '#ecfdf5', borderColor: '#10b981', label: '结果' },
|
|
105
|
+
error: { icon: '⚠️', bgColor: '#fef2f2', borderColor: '#dc2626', label: '错误' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取用户步骤的显示标签
|
|
110
|
+
* 如果有 senderName,显示发送者名称;否则显示"用户"
|
|
111
|
+
*/
|
|
112
|
+
export function getUserStepLabel(step: TimelineStep): string {
|
|
113
|
+
if (step.senderName) {
|
|
114
|
+
return step.senderName
|
|
115
|
+
}
|
|
116
|
+
if (step.senderId) {
|
|
117
|
+
return step.senderId
|
|
118
|
+
}
|
|
119
|
+
return '用户'
|
|
120
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 防抖组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, onUnmounted } from 'vue'
|
|
6
|
+
|
|
7
|
+
export function useDebounce<T extends (...args: unknown[]) => unknown>(
|
|
8
|
+
fn: T,
|
|
9
|
+
delay: number = 300
|
|
10
|
+
): { debouncedFn: T; cancel: () => void; flush: () => void } {
|
|
11
|
+
const timer = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
12
|
+
let lastArgs: unknown[] | null = null
|
|
13
|
+
|
|
14
|
+
const debouncedFn = ((...args: unknown[]) => {
|
|
15
|
+
lastArgs = args
|
|
16
|
+
if (timer.value) {
|
|
17
|
+
clearTimeout(timer.value)
|
|
18
|
+
}
|
|
19
|
+
timer.value = setTimeout(() => {
|
|
20
|
+
fn(...args)
|
|
21
|
+
timer.value = null
|
|
22
|
+
lastArgs = null
|
|
23
|
+
}, delay)
|
|
24
|
+
}) as T
|
|
25
|
+
|
|
26
|
+
const cancel = () => {
|
|
27
|
+
if (timer.value) {
|
|
28
|
+
clearTimeout(timer.value)
|
|
29
|
+
timer.value = null
|
|
30
|
+
lastArgs = null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const flush = () => {
|
|
35
|
+
if (timer.value && lastArgs) {
|
|
36
|
+
clearTimeout(timer.value)
|
|
37
|
+
fn(...lastArgs)
|
|
38
|
+
timer.value = null
|
|
39
|
+
lastArgs = null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onUnmounted(() => {
|
|
44
|
+
cancel()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { debouncedFn, cancel, flush }
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 实时数据组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
6
|
+
import { getRealtimeManager } from '../managers'
|
|
7
|
+
import type { ConnectionState } from '../types'
|
|
8
|
+
|
|
9
|
+
export function useRealtime() {
|
|
10
|
+
const manager = getRealtimeManager()
|
|
11
|
+
const connectionState = ref<ConnectionState>(manager.getConnectionState())
|
|
12
|
+
const isConnected = ref(false)
|
|
13
|
+
|
|
14
|
+
let unsubscribeState: (() => void) | null = null
|
|
15
|
+
|
|
16
|
+
const subscribe = <T>(event: string, callback: (data: T) => void) => {
|
|
17
|
+
return manager.subscribe(event, callback as (data: unknown) => void)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const connect = () => {
|
|
21
|
+
manager.connect()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const disconnect = () => {
|
|
25
|
+
manager.disconnect()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
unsubscribeState = manager.onStateChange((state) => {
|
|
30
|
+
connectionState.value = state
|
|
31
|
+
isConnected.value = state.status === 'connected'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!manager.isConnected()) {
|
|
35
|
+
manager.connect()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
onUnmounted(() => {
|
|
40
|
+
if (unsubscribeState) {
|
|
41
|
+
unsubscribeState()
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
connectionState,
|
|
47
|
+
isConnected,
|
|
48
|
+
subscribe,
|
|
49
|
+
connect,
|
|
50
|
+
disconnect
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态管理组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { computed, type Ref } from 'vue'
|
|
6
|
+
import { getStateManager } from '../managers'
|
|
7
|
+
|
|
8
|
+
export function useState<T>(key: string, defaultValue: T): Ref<T> {
|
|
9
|
+
const manager = getStateManager()
|
|
10
|
+
return manager.useStore(key, defaultValue)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useCache<T>(key: string, fetcher: () => Promise<T>, ttl?: number) {
|
|
14
|
+
const manager = getStateManager()
|
|
15
|
+
const data = useState<T | null>(`cache:${key}`, null)
|
|
16
|
+
const loading = useState(`cache:${key}:loading`, false)
|
|
17
|
+
const error = useState<Error | null>(`cache:${key}:error`, null)
|
|
18
|
+
|
|
19
|
+
const fetch = async () => {
|
|
20
|
+
// 检查缓存
|
|
21
|
+
if (manager.hasValidCache(key)) {
|
|
22
|
+
data.value = manager.getCache<T>(key) ?? null
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
loading.value = true
|
|
27
|
+
error.value = null
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await fetcher()
|
|
31
|
+
data.value = result
|
|
32
|
+
manager.setCache(key, result, ttl)
|
|
33
|
+
} catch (e) {
|
|
34
|
+
error.value = e as Error
|
|
35
|
+
} finally {
|
|
36
|
+
loading.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const invalidate = () => {
|
|
41
|
+
manager.invalidateCache(key)
|
|
42
|
+
data.value = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
data: computed(() => data.value),
|
|
47
|
+
loading: computed(() => loading.value),
|
|
48
|
+
error: computed(() => error.value),
|
|
49
|
+
fetch,
|
|
50
|
+
invalidate
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 节流组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, onUnmounted } from 'vue'
|
|
6
|
+
|
|
7
|
+
export function useThrottle<T extends (...args: unknown[]) => unknown>(
|
|
8
|
+
fn: T,
|
|
9
|
+
interval: number = 100
|
|
10
|
+
): { throttledFn: T; cancel: () => void } {
|
|
11
|
+
const lastExec = ref(0)
|
|
12
|
+
const timer = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
13
|
+
|
|
14
|
+
const throttledFn = ((...args: unknown[]) => {
|
|
15
|
+
const now = Date.now()
|
|
16
|
+
const timeSinceLastExec = now - lastExec.value
|
|
17
|
+
|
|
18
|
+
if (timeSinceLastExec >= interval) {
|
|
19
|
+
lastExec.value = now
|
|
20
|
+
fn(...args)
|
|
21
|
+
} else {
|
|
22
|
+
// 确保最后一次调用会被执行
|
|
23
|
+
if (timer.value) {
|
|
24
|
+
clearTimeout(timer.value)
|
|
25
|
+
}
|
|
26
|
+
timer.value = setTimeout(() => {
|
|
27
|
+
lastExec.value = Date.now()
|
|
28
|
+
fn(...args)
|
|
29
|
+
timer.value = null
|
|
30
|
+
}, interval - timeSinceLastExec)
|
|
31
|
+
}
|
|
32
|
+
}) as T
|
|
33
|
+
|
|
34
|
+
const cancel = () => {
|
|
35
|
+
if (timer.value) {
|
|
36
|
+
clearTimeout(timer.value)
|
|
37
|
+
timer.value = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onUnmounted(() => {
|
|
42
|
+
cancel()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return { throttledFn, cancel }
|
|
46
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 虚拟滚动组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
interface VirtualScrollOptions {
|
|
8
|
+
itemHeight: number
|
|
9
|
+
bufferSize?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useVirtualScroll(
|
|
13
|
+
containerRef: Ref<HTMLElement | null>,
|
|
14
|
+
totalCount: Ref<number>,
|
|
15
|
+
options: VirtualScrollOptions
|
|
16
|
+
) {
|
|
17
|
+
const { itemHeight, bufferSize = 5 } = options
|
|
18
|
+
|
|
19
|
+
const scrollTop = ref(0)
|
|
20
|
+
const containerHeight = ref(0)
|
|
21
|
+
|
|
22
|
+
const visibleCount = computed(() => {
|
|
23
|
+
return Math.ceil(containerHeight.value / itemHeight) + bufferSize * 2
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const startIndex = computed(() => {
|
|
27
|
+
const rawStart = Math.floor(scrollTop.value / itemHeight) - bufferSize
|
|
28
|
+
return Math.max(0, rawStart)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const endIndex = computed(() => {
|
|
32
|
+
return Math.min(totalCount.value, startIndex.value + visibleCount.value)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const offsetY = computed(() => {
|
|
36
|
+
return startIndex.value * itemHeight
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const totalHeight = computed(() => {
|
|
40
|
+
return totalCount.value * itemHeight
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const visibleItems = computed(() => {
|
|
44
|
+
const items: { index: number; style: { transform: string } }[] = []
|
|
45
|
+
for (let i = startIndex.value; i < endIndex.value; i++) {
|
|
46
|
+
items.push({
|
|
47
|
+
index: i,
|
|
48
|
+
style: {
|
|
49
|
+
transform: `translateY(${i * itemHeight}px)`
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
return items
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const scrollToIndex = (index: number) => {
|
|
57
|
+
if (containerRef.value) {
|
|
58
|
+
containerRef.value.scrollTop = index * itemHeight
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleScroll = () => {
|
|
63
|
+
if (containerRef.value) {
|
|
64
|
+
scrollTop.value = containerRef.value.scrollTop
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updateContainerHeight = () => {
|
|
69
|
+
if (containerRef.value) {
|
|
70
|
+
containerHeight.value = containerRef.value.clientHeight
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let resizeObserver: ResizeObserver | null = null
|
|
75
|
+
|
|
76
|
+
onMounted(() => {
|
|
77
|
+
if (containerRef.value) {
|
|
78
|
+
containerRef.value.addEventListener('scroll', handleScroll, { passive: true })
|
|
79
|
+
|
|
80
|
+
resizeObserver = new ResizeObserver(() => {
|
|
81
|
+
updateContainerHeight()
|
|
82
|
+
})
|
|
83
|
+
resizeObserver.observe(containerRef.value)
|
|
84
|
+
updateContainerHeight()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
onUnmounted(() => {
|
|
89
|
+
if (containerRef.value) {
|
|
90
|
+
containerRef.value.removeEventListener('scroll', handleScroll)
|
|
91
|
+
}
|
|
92
|
+
if (resizeObserver) {
|
|
93
|
+
resizeObserver.disconnect()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
startIndex,
|
|
99
|
+
endIndex,
|
|
100
|
+
offsetY,
|
|
101
|
+
totalHeight,
|
|
102
|
+
visibleItems,
|
|
103
|
+
scrollToIndex,
|
|
104
|
+
updateContainerHeight
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件分发器
|
|
3
|
+
* 负责组件间事件通信
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
type EventHandler = (payload?: unknown) => void
|
|
7
|
+
|
|
8
|
+
interface QueuedEvent {
|
|
9
|
+
event: string
|
|
10
|
+
payload?: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EventDispatcher {
|
|
14
|
+
private listeners: Map<string, Set<EventHandler>> = new Map()
|
|
15
|
+
private eventQueue: QueuedEvent[] = []
|
|
16
|
+
private isFlushing = false
|
|
17
|
+
private maxQueueSize = 100
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 分发事件
|
|
21
|
+
*/
|
|
22
|
+
emit(event: string, payload?: unknown): void {
|
|
23
|
+
const handlers = this.listeners.get(event)
|
|
24
|
+
if (handlers) {
|
|
25
|
+
handlers.forEach(handler => {
|
|
26
|
+
try {
|
|
27
|
+
handler(payload)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`Error in event handler for ${event}:`, e)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 监听事件
|
|
37
|
+
* 返回取消监听函数
|
|
38
|
+
*/
|
|
39
|
+
on(event: string, handler: EventHandler): () => void {
|
|
40
|
+
if (!this.listeners.has(event)) {
|
|
41
|
+
this.listeners.set(event, new Set())
|
|
42
|
+
}
|
|
43
|
+
this.listeners.get(event)!.add(handler)
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
this.listeners.get(event)?.delete(handler)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 监听一次事件
|
|
52
|
+
*/
|
|
53
|
+
once(event: string, handler: EventHandler): void {
|
|
54
|
+
const wrapper: EventHandler = (payload) => {
|
|
55
|
+
this.off(event, wrapper)
|
|
56
|
+
handler(payload)
|
|
57
|
+
}
|
|
58
|
+
this.on(event, wrapper)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 取消监听
|
|
63
|
+
*/
|
|
64
|
+
off(event: string, handler: EventHandler): void {
|
|
65
|
+
this.listeners.get(event)?.delete(handler)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 将事件加入队列
|
|
70
|
+
*/
|
|
71
|
+
enqueue(event: string, payload?: unknown): void {
|
|
72
|
+
if (this.eventQueue.length >= this.maxQueueSize) {
|
|
73
|
+
console.warn('Event queue is full, dropping oldest event')
|
|
74
|
+
this.eventQueue.shift()
|
|
75
|
+
}
|
|
76
|
+
this.eventQueue.push({ event, payload })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 刷新队列中的所有事件
|
|
81
|
+
*/
|
|
82
|
+
flush(): void {
|
|
83
|
+
if (this.isFlushing) return
|
|
84
|
+
|
|
85
|
+
this.isFlushing = true
|
|
86
|
+
try {
|
|
87
|
+
while (this.eventQueue.length > 0) {
|
|
88
|
+
const { event, payload } = this.eventQueue.shift()!
|
|
89
|
+
this.emit(event, payload)
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
this.isFlushing = false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取队列长度
|
|
98
|
+
*/
|
|
99
|
+
getQueueLength(): number {
|
|
100
|
+
return this.eventQueue.length
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 清空队列
|
|
105
|
+
*/
|
|
106
|
+
clearQueue(): void {
|
|
107
|
+
this.eventQueue = []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 清除所有监听器
|
|
112
|
+
*/
|
|
113
|
+
clearAll(): void {
|
|
114
|
+
this.listeners.clear()
|
|
115
|
+
this.eventQueue = []
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 单例实例
|
|
120
|
+
let instance: EventDispatcher | null = null
|
|
121
|
+
|
|
122
|
+
export function getEventDispatcher(): EventDispatcher {
|
|
123
|
+
if (!instance) {
|
|
124
|
+
instance = new EventDispatcher()
|
|
125
|
+
}
|
|
126
|
+
return instance
|
|
127
|
+
}
|