sillyspec 3.26.3 → 3.26.5
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/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -4
- package/package.json +1 -1
- package/src/db.js +11 -0
- package/src/dispatch/backends/local-agent.js +3 -0
- package/src/dispatch/backends/sillyhub-mcp.js +3 -0
- package/src/git-helper.js +21 -8
- package/src/index.js +2 -0
- package/src/quicklog.js +2 -0
- package/src/run/command.js +59 -7
- package/src/run/complete-handlers.js +95 -1
- package/src/run/complete.js +23 -5
- package/src/run/gates.js +1 -1
- package/src/run/prompt.js +34 -20
- package/src/run/quick-audit.js +5 -6
- package/src/run/shared.js +45 -11
- package/src/run/stage.js +10 -0
- package/src/run.js +5 -5
- package/src/stage-contract-spec.js +11 -0
- package/src/stage-contract.js +11 -1
- package/src/stage-review.js +9 -1
- package/src/stages/archive.js +12 -26
- package/src/stages/brainstorm.js +22 -20
- package/src/stages/execute.js +48 -0
- package/src/stages/plan.js +17 -1
- package/src/stages/quick.js +5 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +2 -1
- package/src/sync.js +1 -1
- package/src/worktree-apply.js +3 -3
- package/src/worktree.js +83 -8
- package/templates/prompts/testcase-design.md +12 -0
- package/templates/prompts/verify-probes.md +1 -0
- package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
- package/packages/dashboard/index.html +0 -15
- package/packages/dashboard/package-lock.json +0 -2384
- package/packages/dashboard/public/favicon.jpg +0 -0
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/public/prototype-dashboard.html +0 -836
- package/packages/dashboard/public/prototype-overview.html +0 -256
- package/packages/dashboard/src/App.vue +0 -558
- package/packages/dashboard/src/components/ActionBar.vue +0 -93
- package/packages/dashboard/src/components/CommandPalette.vue +0 -96
- package/packages/dashboard/src/components/DetailPanel.vue +0 -137
- package/packages/dashboard/src/components/DocPreview.vue +0 -257
- package/packages/dashboard/src/components/DocTree.vue +0 -114
- package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
- package/packages/dashboard/src/components/LogStream.vue +0 -65
- package/packages/dashboard/src/components/PipelineStage.vue +0 -95
- package/packages/dashboard/src/components/PipelineView.vue +0 -156
- package/packages/dashboard/src/components/ProjectCard.vue +0 -187
- package/packages/dashboard/src/components/ProjectList.vue +0 -210
- package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
- package/packages/dashboard/src/components/StageBadge.vue +0 -67
- package/packages/dashboard/src/components/StepCard.vue +0 -94
- package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
- package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
- package/packages/dashboard/src/composables/useDashboard.js +0 -213
- package/packages/dashboard/src/composables/useKeyboard.js +0 -119
- package/packages/dashboard/src/composables/useLayout.js +0 -131
- package/packages/dashboard/src/composables/useWebSocket.js +0 -129
- package/packages/dashboard/src/main.js +0 -8
- package/packages/dashboard/src/style.css +0 -132
- package/packages/dashboard/vite.config.js +0 -18
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div v-if="data" class="px-4 py-3">
|
|
3
|
-
<!-- Branch -->
|
|
4
|
-
<div class="mb-4">
|
|
5
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">当前分支</h4>
|
|
6
|
-
<div class="text-[18px] font-bold font-[JetBrains_Mono,monospace]" style="color: #D97706;">{{ data.branch || '—' }}</div>
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
|
-
<!-- Commits -->
|
|
10
|
-
<div v-if="data.commits?.length" class="mb-4">
|
|
11
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">最近提交</h4>
|
|
12
|
-
<div class="space-y-2">
|
|
13
|
-
<div v-for="c in data.commits" :key="c.hash" class="px-3 py-2 rounded-md" style="background: #F5F5F7;">
|
|
14
|
-
<div class="flex items-center gap-2">
|
|
15
|
-
<span class="text-[11px] font-mono font-semibold" style="color: #D97706;">{{ c.hash }}</span>
|
|
16
|
-
<span class="text-[11px] flex-1 truncate" style="color: #1C1C1E;">{{ c.message }}</span>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="flex items-center gap-2 mt-1 text-[10px]" style="color: #6B7280;">
|
|
19
|
-
<span>{{ c.author }}</span>
|
|
20
|
-
<span>·</span>
|
|
21
|
-
<span>{{ relativeTime(c.date) }}</span>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<!-- Untracked -->
|
|
28
|
-
<div v-if="data.untracked?.length">
|
|
29
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">未提交文件</h4>
|
|
30
|
-
<div class="space-y-1">
|
|
31
|
-
<div v-for="f in data.untracked" :key="f.file" class="flex items-center gap-2 px-2 py-1 rounded text-[11px] font-[JetBrains_Mono,monospace]">
|
|
32
|
-
<span class="font-semibold w-5 text-center" :style="{ color: statusColor(f.status) }">{{ f.status }}</span>
|
|
33
|
-
<span class="truncate" style="color: #1C1C1E;">{{ f.file }}</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
<n-empty v-else description="无 Git 信息" style="padding: 48px 0;" />
|
|
39
|
-
</template>
|
|
40
|
-
|
|
41
|
-
<script setup>
|
|
42
|
-
defineProps({ data: { type: Object, default: null } })
|
|
43
|
-
|
|
44
|
-
function statusColor(s) {
|
|
45
|
-
if (s.includes('M')) return '#F59E0B'
|
|
46
|
-
if (s.includes('A') || s === '?') return '#10B981'
|
|
47
|
-
if (s.includes('D')) return '#EF4444'
|
|
48
|
-
return '#9CA3AF'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function relativeTime(iso) {
|
|
52
|
-
if (!iso) return ''
|
|
53
|
-
const d = new Date(iso)
|
|
54
|
-
const diff = Date.now() - d.getTime()
|
|
55
|
-
if (diff < 60000) return '刚刚'
|
|
56
|
-
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
|
57
|
-
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
|
58
|
-
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`
|
|
59
|
-
return `${d.getMonth() + 1}/${d.getDate()}`
|
|
60
|
-
}
|
|
61
|
-
</script>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div v-if="data" class="px-4 py-3">
|
|
3
|
-
<!-- Frameworks -->
|
|
4
|
-
<div v-if="data.frameworks?.length" class="mb-4">
|
|
5
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">识别框架</h4>
|
|
6
|
-
<div class="flex flex-wrap gap-2">
|
|
7
|
-
<n-tag v-for="f in data.frameworks" :key="f" type="warning" size="small" round>{{ f }}</n-tag>
|
|
8
|
-
</div>
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<!-- Dependencies -->
|
|
12
|
-
<div v-if="depEntries.length" class="mb-4">
|
|
13
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">dependencies ({{ depEntries.length }})</h4>
|
|
14
|
-
<div class="space-y-1">
|
|
15
|
-
<div v-for="[name, ver] in depEntries" :key="name" class="flex items-center gap-2 px-2 py-1 rounded text-[11px]" style="background: #F5F5F7;">
|
|
16
|
-
<span class="font-mono font-medium truncate" style="color: #1C1C1E;">{{ name }}</span>
|
|
17
|
-
<span class="font-mono ml-auto flex-shrink-0" style="color: #9CA3AF;">{{ ver }}</span>
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<!-- Dev Dependencies -->
|
|
23
|
-
<div v-if="devDepEntries.length">
|
|
24
|
-
<h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-2 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">devDependencies ({{ devDepEntries.length }})</h4>
|
|
25
|
-
<div class="space-y-1">
|
|
26
|
-
<div v-for="[name, ver] in devDepEntries" :key="name" class="flex items-center gap-2 px-2 py-1 rounded text-[11px]" style="background: #F5F5F7;">
|
|
27
|
-
<span class="font-mono font-medium truncate" style="color: #1C1C1E;">{{ name }}</span>
|
|
28
|
-
<span class="font-mono ml-auto flex-shrink-0" style="color: #9CA3AF;">{{ ver }}</span>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
<n-empty v-else description="无依赖信息" style="padding: 48px 0;" />
|
|
34
|
-
</template>
|
|
35
|
-
|
|
36
|
-
<script setup>
|
|
37
|
-
import { computed } from 'vue'
|
|
38
|
-
|
|
39
|
-
const props = defineProps({ data: { type: Object, default: null } })
|
|
40
|
-
|
|
41
|
-
const depEntries = computed(() => props.data ? Object.entries(props.data.dependencies || {}) : [])
|
|
42
|
-
const devDepEntries = computed(() => props.data ? Object.entries(props.data.devDependencies || {}) : [])
|
|
43
|
-
</script>
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { reactive, computed } from 'vue'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dashboard state management composable
|
|
5
|
-
* @returns {object} Dashboard state and methods
|
|
6
|
-
*/
|
|
7
|
-
export function useDashboard() {
|
|
8
|
-
// Reactive state
|
|
9
|
-
const state = reactive({
|
|
10
|
-
projects: [],
|
|
11
|
-
activeProject: null,
|
|
12
|
-
activeStep: null,
|
|
13
|
-
logs: [],
|
|
14
|
-
isPanelOpen: true,
|
|
15
|
-
executingProject: null,
|
|
16
|
-
isLoading: true,
|
|
17
|
-
activeTab: 'pipeline',
|
|
18
|
-
docs: { groups: [] },
|
|
19
|
-
selectedDocFile: null,
|
|
20
|
-
docContent: '',
|
|
21
|
-
docLoading: false
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get project by name
|
|
26
|
-
* @param {string} name - Project name
|
|
27
|
-
* @returns {object|null} Project or null
|
|
28
|
-
*/
|
|
29
|
-
function getProject(path) {
|
|
30
|
-
return state.projects.find(p => p.path === path) || null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Select a project as active
|
|
35
|
-
* @param {object|string} project - Project object or path
|
|
36
|
-
*/
|
|
37
|
-
function selectProject(project) {
|
|
38
|
-
const proj = typeof project === 'string'
|
|
39
|
-
? getProject(project)
|
|
40
|
-
: project
|
|
41
|
-
|
|
42
|
-
if (proj) {
|
|
43
|
-
state.activeProject = proj
|
|
44
|
-
state.activeStep = null
|
|
45
|
-
state.logs = []
|
|
46
|
-
state.selectedDocFile = null
|
|
47
|
-
state.docContent = ''
|
|
48
|
-
|
|
49
|
-
// Load initial logs from project state if available
|
|
50
|
-
if (proj.state?.progress?.currentLogs) {
|
|
51
|
-
appendLog(proj.state.progress.currentLogs)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Select a step within active project
|
|
58
|
-
* @param {object} step - Step object
|
|
59
|
-
*/
|
|
60
|
-
function selectStep(step) {
|
|
61
|
-
state.activeStep = step
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Append log lines to logs buffer
|
|
66
|
-
* @param {string|string[]} lines - Log line(s) to append
|
|
67
|
-
*/
|
|
68
|
-
function appendLog(lines) {
|
|
69
|
-
const newLines = Array.isArray(lines) ? lines : [lines]
|
|
70
|
-
|
|
71
|
-
for (const line of newLines) {
|
|
72
|
-
const timestamp = new Date().toISOString()
|
|
73
|
-
state.logs.push({
|
|
74
|
-
id: `${timestamp}-${Math.random().toString(36).substr(2, 9)}`,
|
|
75
|
-
timestamp,
|
|
76
|
-
content: line,
|
|
77
|
-
type: 'info'
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Keep only last 500 lines
|
|
82
|
-
if (state.logs.length > 500) {
|
|
83
|
-
state.logs = state.logs.slice(-500)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Clear logs buffer
|
|
89
|
-
*/
|
|
90
|
-
function clearLogs() {
|
|
91
|
-
state.logs = []
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Toggle detail panel open/closed
|
|
96
|
-
*/
|
|
97
|
-
function togglePanel() {
|
|
98
|
-
state.isPanelOpen = !state.isPanelOpen
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Open detail panel
|
|
103
|
-
*/
|
|
104
|
-
function openPanel() {
|
|
105
|
-
state.isPanelOpen = true
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Close detail panel
|
|
110
|
-
*/
|
|
111
|
-
function closePanel() {
|
|
112
|
-
state.isPanelOpen = false
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Update projects list
|
|
117
|
-
* @param {array} projects - New projects array
|
|
118
|
-
*/
|
|
119
|
-
function updateProjects(projects) {
|
|
120
|
-
state.isLoading = false
|
|
121
|
-
state.projects = projects
|
|
122
|
-
|
|
123
|
-
// Restore active project if it still exists
|
|
124
|
-
if (state.activeProject) {
|
|
125
|
-
const updated = getProject(state.activeProject.path)
|
|
126
|
-
if (updated) {
|
|
127
|
-
state.activeProject = updated
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
state.activeProject = state.projects[0] || null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Merge a single project update into the current project list.
|
|
137
|
-
* @param {object} project - Partial project payload containing at least path
|
|
138
|
-
*/
|
|
139
|
-
function updateProject(project) {
|
|
140
|
-
if (!project?.path) return
|
|
141
|
-
|
|
142
|
-
const index = state.projects.findIndex(p => p.path === project.path)
|
|
143
|
-
if (index === -1) return
|
|
144
|
-
|
|
145
|
-
const updated = {
|
|
146
|
-
...state.projects[index],
|
|
147
|
-
...project,
|
|
148
|
-
state: project.state ?? state.projects[index].state,
|
|
149
|
-
overview: project.overview ?? state.projects[index].overview
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
state.projects.splice(index, 1, updated)
|
|
153
|
-
|
|
154
|
-
if (state.activeProject?.path === project.path) {
|
|
155
|
-
state.activeProject = updated
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Set executing project state
|
|
161
|
-
* @param {string|null} projectName - Project name or null
|
|
162
|
-
*/
|
|
163
|
-
function setExecuting(projectName) {
|
|
164
|
-
state.executingProject = projectName
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Check if a project is currently executing
|
|
169
|
-
* @param {string} projectName - Project name
|
|
170
|
-
* @returns {boolean}
|
|
171
|
-
*/
|
|
172
|
-
function isExecuting(projectName) {
|
|
173
|
-
return state.executingProject === projectName
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Computed properties
|
|
177
|
-
const activeProjectName = computed(() => state.activeProject?.name || null)
|
|
178
|
-
const activeProjectPath = computed(() => state.activeProject?.path || null)
|
|
179
|
-
const activeProjectStage = computed(() => state.activeProject?.state?.currentStage || null)
|
|
180
|
-
const hasProjects = computed(() => state.projects.length > 0)
|
|
181
|
-
const activeProjectSteps = computed(() => {
|
|
182
|
-
if (!state.activeProject?.state?.progress?.stages) return []
|
|
183
|
-
const currentStage = state.activeProject.state.currentStage
|
|
184
|
-
const stageData = state.activeProject.state.progress.stages[currentStage]
|
|
185
|
-
return stageData?.steps || []
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
state,
|
|
190
|
-
getProject,
|
|
191
|
-
selectProject,
|
|
192
|
-
selectStep,
|
|
193
|
-
appendLog,
|
|
194
|
-
clearLogs,
|
|
195
|
-
togglePanel,
|
|
196
|
-
openPanel,
|
|
197
|
-
closePanel,
|
|
198
|
-
updateProjects,
|
|
199
|
-
updateProject,
|
|
200
|
-
setExecuting,
|
|
201
|
-
isExecuting,
|
|
202
|
-
activeProjectName,
|
|
203
|
-
activeProjectPath,
|
|
204
|
-
activeProjectStage,
|
|
205
|
-
hasProjects,
|
|
206
|
-
activeProjectSteps,
|
|
207
|
-
setActiveTab(tab) { state.activeTab = tab },
|
|
208
|
-
updateDocs(docs) { state.docs = docs },
|
|
209
|
-
selectDocFile(file) { state.selectedDocFile = file },
|
|
210
|
-
setDocContent(content) { state.docContent = content },
|
|
211
|
-
setDocLoading(loading) { state.docLoading = loading }
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { onMounted, onUnmounted } from 'vue'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Keyboard shortcuts composable
|
|
5
|
-
* @param {object} options - Configuration options
|
|
6
|
-
* @returns {object} Keyboard API
|
|
7
|
-
*/
|
|
8
|
-
export function useKeyboard(options = {}) {
|
|
9
|
-
const {
|
|
10
|
-
onCmdK = null,
|
|
11
|
-
onJ = null,
|
|
12
|
-
onK = null,
|
|
13
|
-
onEscape = null,
|
|
14
|
-
onEnter = null,
|
|
15
|
-
onArrowUp = null,
|
|
16
|
-
onArrowDown = null,
|
|
17
|
-
disabled: initialDisabled = false
|
|
18
|
-
} = options
|
|
19
|
-
|
|
20
|
-
let isDisabled = initialDisabled
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Handle keyboard events
|
|
24
|
-
* @param {KeyboardEvent} event
|
|
25
|
-
*/
|
|
26
|
-
function handleKeyDown(event) {
|
|
27
|
-
if (isDisabled) return
|
|
28
|
-
|
|
29
|
-
// Ignore if in input field
|
|
30
|
-
const target = event.target
|
|
31
|
-
if (target.tagName === 'INPUT' ||
|
|
32
|
-
target.tagName === 'TEXTAREA' ||
|
|
33
|
-
target.contentEditable === 'true') {
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
|
38
|
-
const cmdOrCtrl = isMac ? event.metaKey : event.ctrlKey
|
|
39
|
-
|
|
40
|
-
// Cmd/Ctrl + K
|
|
41
|
-
if (cmdOrCtrl && event.key === 'k') {
|
|
42
|
-
event.preventDefault()
|
|
43
|
-
if (onCmdK) onCmdK()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// J - navigate down
|
|
47
|
-
if (event.key === 'j' && !cmdOrCtrl) {
|
|
48
|
-
event.preventDefault()
|
|
49
|
-
if (onJ) onJ()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// K - navigate up
|
|
53
|
-
if (event.key === 'k' && !cmdOrCtrl) {
|
|
54
|
-
event.preventDefault()
|
|
55
|
-
if (onK) onK()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Escape
|
|
59
|
-
if (event.key === 'Escape') {
|
|
60
|
-
event.preventDefault()
|
|
61
|
-
if (onEscape) onEscape()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Enter
|
|
65
|
-
if (event.key === 'Enter' && !cmdOrCtrl) {
|
|
66
|
-
event.preventDefault()
|
|
67
|
-
if (onEnter) onEnter()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Arrow Up
|
|
71
|
-
if (event.key === 'ArrowUp' && !cmdOrCtrl) {
|
|
72
|
-
event.preventDefault()
|
|
73
|
-
if (onArrowUp) onArrowUp()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Arrow Down
|
|
77
|
-
if (event.key === 'ArrowDown' && !cmdOrCtrl) {
|
|
78
|
-
event.preventDefault()
|
|
79
|
-
if (onArrowDown) onArrowDown()
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
onMounted(() => {
|
|
84
|
-
window.addEventListener('keydown', handleKeyDown)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
onUnmounted(() => {
|
|
88
|
-
window.removeEventListener('keydown', handleKeyDown)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
disable: () => { isDisabled = true },
|
|
93
|
-
enable: () => { isDisabled = false }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Global keyboard shortcuts for dashboard
|
|
99
|
-
* @param {object} callbacks - Callback functions for each shortcut
|
|
100
|
-
* @returns {object} Keyboard API
|
|
101
|
-
*/
|
|
102
|
-
export function useDashboardKeyboard(callbacks = {}) {
|
|
103
|
-
const {
|
|
104
|
-
onOpenCommandPalette,
|
|
105
|
-
onNavigateDown,
|
|
106
|
-
onNavigateUp,
|
|
107
|
-
onClose,
|
|
108
|
-
onSelect,
|
|
109
|
-
onPanelToggle
|
|
110
|
-
} = callbacks
|
|
111
|
-
|
|
112
|
-
return useKeyboard({
|
|
113
|
-
onCmdK: onOpenCommandPalette,
|
|
114
|
-
onJ: onNavigateDown,
|
|
115
|
-
onK: onNavigateUp,
|
|
116
|
-
onEscape: onClose,
|
|
117
|
-
onEnter: onSelect
|
|
118
|
-
})
|
|
119
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 布局管理 Composable
|
|
3
|
-
* 管理双层布局的概览/详情比例和三栏宽度
|
|
4
|
-
*/
|
|
5
|
-
import { reactive, ref, onMounted } from 'vue'
|
|
6
|
-
|
|
7
|
-
const STORAGE_KEY = 'dashboard-layout-v2'
|
|
8
|
-
|
|
9
|
-
// 默认值
|
|
10
|
-
const DEFAULT_OVERVIEW_HEIGHT = 30 // 概览区域百分比
|
|
11
|
-
const DEFAULT_COLUMN_WIDTHS = [33.33, 33.33, 33.33] // 三栏宽度百分比
|
|
12
|
-
|
|
13
|
-
// 最小值限制
|
|
14
|
-
const MIN_OVERVIEW_HEIGHT = 15
|
|
15
|
-
const MAX_OVERVIEW_HEIGHT = 75
|
|
16
|
-
const MIN_COLUMN_WIDTH = 10
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 布局管理 Hook
|
|
20
|
-
*/
|
|
21
|
-
export function useLayout() {
|
|
22
|
-
// 布局状态
|
|
23
|
-
const layout = reactive({
|
|
24
|
-
overviewHeight: DEFAULT_OVERVIEW_HEIGHT,
|
|
25
|
-
columnWidths: [...DEFAULT_COLUMN_WIDTHS]
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// 拖动状态
|
|
29
|
-
const isDragging = ref(false)
|
|
30
|
-
const dragType = ref(null) // 'vertical' | 'horizontal'
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 加载保存的布局
|
|
34
|
-
*/
|
|
35
|
-
function loadLayout() {
|
|
36
|
-
try {
|
|
37
|
-
const saved = localStorage.getItem(STORAGE_KEY)
|
|
38
|
-
if (saved) {
|
|
39
|
-
const parsed = JSON.parse(saved)
|
|
40
|
-
if (parsed.overviewHeight !== undefined) {
|
|
41
|
-
layout.overviewHeight = Math.max(MIN_OVERVIEW_HEIGHT, Math.min(MAX_OVERVIEW_HEIGHT, parsed.overviewHeight))
|
|
42
|
-
}
|
|
43
|
-
if (Array.isArray(parsed.columnWidths) && parsed.columnWidths.length === 3) {
|
|
44
|
-
layout.columnWidths = parsed.columnWidths
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
console.error('Failed to load layout:', e)
|
|
49
|
-
// 使用默认值
|
|
50
|
-
resetLayout()
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 保存布局到 localStorage
|
|
56
|
-
*/
|
|
57
|
-
function saveLayout() {
|
|
58
|
-
try {
|
|
59
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
60
|
-
overviewHeight: layout.overviewHeight,
|
|
61
|
-
columnWidths: layout.columnWidths
|
|
62
|
-
}))
|
|
63
|
-
} catch (e) {
|
|
64
|
-
console.error('Failed to save layout:', e)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 重置布局为默认值
|
|
70
|
-
*/
|
|
71
|
-
function resetLayout() {
|
|
72
|
-
layout.overviewHeight = DEFAULT_OVERVIEW_HEIGHT
|
|
73
|
-
layout.columnWidths = [...DEFAULT_COLUMN_WIDTHS]
|
|
74
|
-
saveLayout()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 设置概览区域高度(百分比)
|
|
79
|
-
*/
|
|
80
|
-
function setOverviewHeight(percent) {
|
|
81
|
-
layout.overviewHeight = Math.max(MIN_OVERVIEW_HEIGHT, Math.min(MAX_OVERVIEW_HEIGHT, percent))
|
|
82
|
-
saveLayout()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 设置列宽度(索引,百分比)
|
|
87
|
-
*/
|
|
88
|
-
function setColumnWidth(index, percent) {
|
|
89
|
-
if (index >= 0 && index < 3) {
|
|
90
|
-
layout.columnWidths[index] = Math.max(MIN_COLUMN_WIDTH, percent)
|
|
91
|
-
saveLayout()
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 开始拖动
|
|
97
|
-
*/
|
|
98
|
-
function startDrag(type) {
|
|
99
|
-
isDragging.value = true
|
|
100
|
-
dragType.value = type
|
|
101
|
-
document.body.classList.add('resizing')
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 结束拖动
|
|
106
|
-
*/
|
|
107
|
-
function endDrag() {
|
|
108
|
-
isDragging.value = false
|
|
109
|
-
dragType.value = null
|
|
110
|
-
document.body.classList.remove('resizing')
|
|
111
|
-
saveLayout()
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 组件挂载时加载布局
|
|
115
|
-
onMounted(() => {
|
|
116
|
-
loadLayout()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
layout,
|
|
121
|
-
isDragging,
|
|
122
|
-
dragType,
|
|
123
|
-
loadLayout,
|
|
124
|
-
saveLayout,
|
|
125
|
-
resetLayout,
|
|
126
|
-
setOverviewHeight,
|
|
127
|
-
setColumnWidth,
|
|
128
|
-
startDrag,
|
|
129
|
-
endDrag
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WebSocket connection composable with auto-reconnect
|
|
5
|
-
* @returns {object} WebSocket API
|
|
6
|
-
*/
|
|
7
|
-
export function useWebSocket() {
|
|
8
|
-
const ws = ref(null)
|
|
9
|
-
const connected = ref(false)
|
|
10
|
-
const reconnectTimeout = ref(null)
|
|
11
|
-
const handlers = new Map()
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Connect to WebSocket server
|
|
15
|
-
*/
|
|
16
|
-
function connect() {
|
|
17
|
-
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
18
|
-
const wsUrl = `${protocol}//${location.host}`
|
|
19
|
-
|
|
20
|
-
ws.value = new WebSocket(wsUrl)
|
|
21
|
-
|
|
22
|
-
ws.value.onopen = () => {
|
|
23
|
-
connected.value = true
|
|
24
|
-
console.log('WebSocket connected')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
ws.value.onclose = () => {
|
|
28
|
-
connected.value = false
|
|
29
|
-
console.log('WebSocket disconnected, reconnecting in 3s...')
|
|
30
|
-
|
|
31
|
-
// Auto-reconnect after 3 seconds
|
|
32
|
-
reconnectTimeout.value = setTimeout(() => {
|
|
33
|
-
connect()
|
|
34
|
-
}, 3000)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
ws.value.onerror = (err) => {
|
|
38
|
-
console.error('WebSocket error:', err)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
ws.value.onmessage = (event) => {
|
|
42
|
-
try {
|
|
43
|
-
const message = JSON.parse(event.data)
|
|
44
|
-
const { type, data } = message
|
|
45
|
-
|
|
46
|
-
// Call registered handlers for this message type
|
|
47
|
-
const typeHandlers = handlers.get(type) || []
|
|
48
|
-
typeHandlers.forEach(handler => handler(data))
|
|
49
|
-
|
|
50
|
-
// Call wildcard handlers
|
|
51
|
-
const wildcardHandlers = handlers.get('*') || []
|
|
52
|
-
wildcardHandlers.forEach(handler => handler(message))
|
|
53
|
-
} catch (err) {
|
|
54
|
-
console.error('Error parsing WebSocket message:', err)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Register event handler for a message type
|
|
61
|
-
* @param {string} type - Message type
|
|
62
|
-
* @param {function} handler - Handler function
|
|
63
|
-
* @returns {function} Unsubscribe function
|
|
64
|
-
*/
|
|
65
|
-
function on(type, handler) {
|
|
66
|
-
if (!handlers.has(type)) {
|
|
67
|
-
handlers.set(type, [])
|
|
68
|
-
}
|
|
69
|
-
handlers.get(type).push(handler)
|
|
70
|
-
|
|
71
|
-
// Return unsubscribe function
|
|
72
|
-
return () => {
|
|
73
|
-
const typeHandlers = handlers.get(type)
|
|
74
|
-
if (typeHandlers) {
|
|
75
|
-
const index = typeHandlers.indexOf(handler)
|
|
76
|
-
if (index > -1) {
|
|
77
|
-
typeHandlers.splice(index, 1)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Send message to server
|
|
85
|
-
* @param {object} data - Message data
|
|
86
|
-
*/
|
|
87
|
-
function send(data) {
|
|
88
|
-
if (ws.value && connected.value) {
|
|
89
|
-
ws.value.send(JSON.stringify(data))
|
|
90
|
-
} else {
|
|
91
|
-
console.warn('Cannot send message: WebSocket not connected')
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Disconnect WebSocket
|
|
97
|
-
*/
|
|
98
|
-
function disconnect() {
|
|
99
|
-
if (reconnectTimeout.value) {
|
|
100
|
-
clearTimeout(reconnectTimeout.value)
|
|
101
|
-
reconnectTimeout.value = null
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (ws.value) {
|
|
105
|
-
ws.value.close()
|
|
106
|
-
ws.value = null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
connected.value = false
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Auto-connect on mount
|
|
113
|
-
onMounted(() => {
|
|
114
|
-
connect()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// Cleanup on unmount
|
|
118
|
-
onUnmounted(() => {
|
|
119
|
-
disconnect()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
connected,
|
|
124
|
-
on,
|
|
125
|
-
send,
|
|
126
|
-
disconnect,
|
|
127
|
-
reconnect: connect
|
|
128
|
-
}
|
|
129
|
-
}
|