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.
Files changed (68) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
  4. package/.claude/skills/sillyspec-quick/SKILL.md +18 -4
  5. package/package.json +1 -1
  6. package/src/db.js +11 -0
  7. package/src/dispatch/backends/local-agent.js +3 -0
  8. package/src/dispatch/backends/sillyhub-mcp.js +3 -0
  9. package/src/git-helper.js +21 -8
  10. package/src/index.js +2 -0
  11. package/src/quicklog.js +2 -0
  12. package/src/run/command.js +59 -7
  13. package/src/run/complete-handlers.js +95 -1
  14. package/src/run/complete.js +23 -5
  15. package/src/run/gates.js +1 -1
  16. package/src/run/prompt.js +34 -20
  17. package/src/run/quick-audit.js +5 -6
  18. package/src/run/shared.js +45 -11
  19. package/src/run/stage.js +10 -0
  20. package/src/run.js +5 -5
  21. package/src/stage-contract-spec.js +11 -0
  22. package/src/stage-contract.js +11 -1
  23. package/src/stage-review.js +9 -1
  24. package/src/stages/archive.js +12 -26
  25. package/src/stages/brainstorm.js +22 -20
  26. package/src/stages/execute.js +48 -0
  27. package/src/stages/plan.js +17 -1
  28. package/src/stages/quick.js +5 -1
  29. package/src/stages/scan.js +2 -2
  30. package/src/stages/verify.js +2 -1
  31. package/src/sync.js +1 -1
  32. package/src/worktree-apply.js +3 -3
  33. package/src/worktree.js +83 -8
  34. package/templates/prompts/testcase-design.md +12 -0
  35. package/templates/prompts/verify-probes.md +1 -0
  36. package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
  37. package/packages/dashboard/index.html +0 -15
  38. package/packages/dashboard/package-lock.json +0 -2384
  39. package/packages/dashboard/public/favicon.jpg +0 -0
  40. package/packages/dashboard/public/logo.jpg +0 -0
  41. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  42. package/packages/dashboard/public/prototype-overview.html +0 -256
  43. package/packages/dashboard/src/App.vue +0 -558
  44. package/packages/dashboard/src/components/ActionBar.vue +0 -93
  45. package/packages/dashboard/src/components/CommandPalette.vue +0 -96
  46. package/packages/dashboard/src/components/DetailPanel.vue +0 -137
  47. package/packages/dashboard/src/components/DocPreview.vue +0 -257
  48. package/packages/dashboard/src/components/DocTree.vue +0 -114
  49. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  50. package/packages/dashboard/src/components/LogStream.vue +0 -65
  51. package/packages/dashboard/src/components/PipelineStage.vue +0 -95
  52. package/packages/dashboard/src/components/PipelineView.vue +0 -156
  53. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  54. package/packages/dashboard/src/components/ProjectList.vue +0 -210
  55. package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
  56. package/packages/dashboard/src/components/StageBadge.vue +0 -67
  57. package/packages/dashboard/src/components/StepCard.vue +0 -94
  58. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  59. package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
  60. package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
  61. package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
  62. package/packages/dashboard/src/composables/useDashboard.js +0 -213
  63. package/packages/dashboard/src/composables/useKeyboard.js +0 -119
  64. package/packages/dashboard/src/composables/useLayout.js +0 -131
  65. package/packages/dashboard/src/composables/useWebSocket.js +0 -129
  66. package/packages/dashboard/src/main.js +0 -8
  67. package/packages/dashboard/src/style.css +0 -132
  68. package/packages/dashboard/vite.config.js +0 -18
@@ -1,96 +0,0 @@
1
- <template>
2
- <n-modal :show="isOpen" @update:show="$emit('close')" :mask-closable="true" transform-origin="center" style="max-width: 480px;">
3
- <div class="overflow-hidden rounded-md" style="background: #FFFFFF; border: 1px solid #E5E5EA; box-shadow: 0 25px 60px rgba(0,0,0,0.1);">
4
- <!-- Search -->
5
- <div class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
6
- <n-input v-model:value="searchQuery" placeholder="搜索项目或命令..." ref="searchInput" @keydown="handleKeydown" size="small">
7
- <template #prefix>
8
- <svg class="w-3.5 h-3.5" style="color: #6B7280;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
9
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
10
- </svg>
11
- </template>
12
- </n-input>
13
- </div>
14
-
15
- <!-- Results -->
16
- <div class="max-h-72 overflow-y-auto">
17
- <n-empty v-if="filteredItems.length === 0" description="无结果" style="padding: 40px 0;" />
18
- <div v-else class="py-0.5">
19
- <div
20
- v-for="(item, index) in filteredItems"
21
- :key="item.id"
22
- :class="['px-4 py-2.5 cursor-pointer transition-colors duration-100 flex items-center gap-3']"
23
- :style="{ background: selectedIndex === index ? 'rgba(217,119,6,0.06)' : 'transparent' }"
24
- @click="selectItem(item)"
25
- @mouseenter="selectedIndex = index"
26
- >
27
- <div class="w-6 h-6 rounded-sm flex items-center justify-center text-[10px] font-[JetBrains_Mono,monospace] flex-shrink-0" style="background: #F0F0F3; border: 1px solid #F0F0F3; color: #6B7280;">
28
- {{ item.type === 'project' ? '□' : '◇' }}
29
- </div>
30
- <div class="flex-1 min-w-0">
31
- <div class="text-[12px] font-medium truncate font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">{{ item.title }}</div>
32
- <div class="text-[10px] truncate" style="color: #6B7280;">{{ item.subtitle }}</div>
33
- </div>
34
- <StageBadge v-if="item.status" :status="item.status" size="sm" />
35
- </div>
36
- </div>
37
- </div>
38
-
39
- <!-- Footer -->
40
- <div class="px-4 py-2 flex items-center gap-4 text-[9px] font-mono-log" style="border-top: 1px solid #F0F0F3; color: #D1D1D6;">
41
- <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↑↓</kbd> 导航</span>
42
- <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↵</kbd> 打开</span>
43
- <span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">esc</kbd> 关闭</span>
44
- </div>
45
- </div>
46
- </n-modal>
47
- </template>
48
-
49
- <script setup>
50
- import { ref, computed, watch, nextTick } from 'vue'
51
- import StageBadge from './StageBadge.vue'
52
-
53
- const props = defineProps({ isOpen: { type: Boolean, default: false }, projects: { type: Array, default: () => [] } })
54
- const emit = defineEmits(['close', 'select-project', 'select-stage'])
55
-
56
- const searchQuery = ref('')
57
- const searchInput = ref(null)
58
- const selectedIndex = ref(0)
59
-
60
- const stageNames = [
61
- { id: 'scan', name: '代码扫描' },
62
- { id: 'brainstorm', name: '头脑风暴' },
63
- { id: 'plan', name: '规划' },
64
- { id: 'execute', name: '执行' },
65
- { id: 'verify', name: '验证' },
66
- { id: 'archive', name: '归档' },
67
- { id: 'quick', name: '快速任务' },
68
- { id: 'explore', name: '自由探索' }
69
- ]
70
-
71
- const filteredItems = computed(() => {
72
- const items = []
73
- for (const project of props.projects) {
74
- const pm = !searchQuery.value || project.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || project.path.toLowerCase().includes(searchQuery.value.toLowerCase())
75
- if (pm) items.push({ id: `project-${project.name}`, type: 'project', title: project.name, subtitle: project.path, data: project, status: getProjectStatus(project) })
76
- for (const stage of stageNames) {
77
- const sm = !searchQuery.value || stage.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || stage.id.toLowerCase().includes(searchQuery.value.toLowerCase())
78
- if (sm || pm) items.push({ id: `stage-${project.name}-${stage.id}`, type: 'stage', title: `${project.name} / ${stage.name}`, subtitle: `Jump to ${stage.name}`, data: { project, stage: stage.id }, status: getStageStatus(project, stage.id) })
79
- }
80
- }
81
- return items
82
- })
83
-
84
- function getProjectStatus(p) { const s = p.state?.progress?.stages?.[p.state?.currentStage]?.steps || []; if (s.some(x => x.status === 'failed')) return 'failed'; if (s.some(x => x.status === 'in-progress')) return 'in-progress'; if (s.every(x => x.status === 'completed')) return 'completed'; return 'pending' }
85
- function getStageStatus(p, id) { const s = p.state?.progress?.stages?.[id]?.steps || []; if (!s.length) return 'pending'; if (s.some(x => x.status === 'failed')) return 'failed'; if (s.some(x => x.status === 'in-progress')) return 'in-progress'; if (s.every(x => x.status === 'completed')) return 'completed'; return 'pending' }
86
- function close() { searchQuery.value = ''; selectedIndex.value = 0; emit('close') }
87
- function selectItem(item) { if (item.type === 'project') emit('select-project', item.data); else emit('select-stage', item.data); close() }
88
- function handleKeydown(e) {
89
- if (e.key === 'ArrowDown') { e.preventDefault(); selectedIndex.value = Math.min(selectedIndex.value + 1, filteredItems.value.length - 1) }
90
- else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIndex.value = Math.max(selectedIndex.value - 1, 0) }
91
- else if (e.key === 'Enter') { e.preventDefault(); if (filteredItems.value[selectedIndex.value]) selectItem(filteredItems.value[selectedIndex.value]) }
92
- else if (e.key === 'Escape') { e.preventDefault(); close() }
93
- }
94
- watch(filteredItems, () => { selectedIndex.value = 0 })
95
- watch(() => props.isOpen, (v) => { if (v) { nextTick(() => { searchInput.value?.focus() }) } })
96
- </script>
@@ -1,137 +0,0 @@
1
- <template>
2
- <div
3
- :class="['flex flex-col transition-all duration-300', isOpen ? 'w-[340px]' : 'w-0 opacity-0 overflow-hidden']"
4
- style="background: #F5F5F7;"
5
- >
6
- <!-- Header -->
7
- <div class="px-4 py-3 flex items-center justify-between flex-shrink-0" style="border-bottom: 1px solid #F0F0F3;">
8
- <h2 class="text-[11px] font-semibold uppercase tracking-[0.2em] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ detailType ? detailTitle : '详情' }}</h2>
9
- <n-button quaternary size="tiny" @click="handleClose">
10
- <template #icon>
11
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
13
- </svg>
14
- </template>
15
- </n-button>
16
- </div>
17
-
18
- <!-- Content -->
19
- <div class="flex-1 overflow-y-auto">
20
- <!-- Empty state -->
21
- <n-empty v-if="!activeStep && !detailType" description="选择一个步骤" style="margin: auto; padding: 48px 0;" />
22
-
23
- <!-- Detail views -->
24
- <template v-if="detailType === 'git'">
25
- <GitDetail :data="detailData" />
26
- </template>
27
- <template v-else-if="detailType === 'tech'">
28
- <TechDetail :data="detailData" />
29
- </template>
30
- <template v-else-if="detailType === 'docs'">
31
- <DocsDetail :data="detailData" @open-file="$emit('open-doc-file', $event)" />
32
- </template>
33
-
34
- <!-- Step detail (original) -->
35
- <div v-else-if="activeStep">
36
- <!-- Title -->
37
- <div class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
38
- <h3 class="text-[13px] font-semibold font-[JetBrains_Mono,monospace]" style="color: #D97706;">
39
- {{ activeStep.title || activeStep.name }}
40
- </h3>
41
- <div v-if="activeStep.status" class="mt-2">
42
- <StageBadge :status="activeStep.status" />
43
- </div>
44
- </div>
45
-
46
- <!-- Description -->
47
- <div v-if="activeStep.description || activeStep.summary" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
48
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">描述</h4>
49
- <p class="text-[11px] leading-relaxed" style="color: #636366;">{{ activeStep.description || activeStep.summary }}</p>
50
- </div>
51
-
52
- <!-- Conclusion -->
53
- <div v-if="activeStep.conclusion" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
54
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">结论</h4>
55
- <p class="text-[11px] leading-relaxed" style="color: #1C1C1E;">{{ activeStep.conclusion }}</p>
56
- </div>
57
-
58
- <!-- Decision -->
59
- <div v-if="activeStep.decision" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
60
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">决策</h4>
61
- <p class="text-[11px] leading-relaxed" style="color: #1C1C1E;">{{ activeStep.decision }}</p>
62
- </div>
63
-
64
- <!-- User Query -->
65
- <div v-if="activeStep.userQuery" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
66
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">用户提问</h4>
67
- <div class="px-3 py-2 rounded-md" style="background: #F5F5F7; border: 1px solid #F0F0F3;">
68
- <p class="text-[11px] italic" style="color: #636366;">"{{ activeStep.userQuery }}"</p>
69
- </div>
70
- </div>
71
-
72
- <!-- Metadata -->
73
- <div v-if="activeStep.duration || activeStep.timestamp" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
74
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">元信息</h4>
75
- <div class="space-y-1 text-[11px]" style="color: #6B7280;">
76
- <div v-if="activeStep.duration"><span style="color: #636366;">耗时:</span> {{ activeStep.duration }}</div>
77
- <div v-if="activeStep.timestamp"><span style="color: #636366;">时间:</span> {{ formatTimestamp(activeStep.timestamp) }}</div>
78
- </div>
79
- </div>
80
-
81
- <!-- Output -->
82
- <div v-if="activeStep.output || activeStep.files" class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
83
- <h4 class="text-[9px] font-semibold uppercase tracking-[0.2em] mb-1.5 font-[JetBrains_Mono,monospace]" style="color: #6B7280;">输出</h4>
84
- <n-code v-if="activeStep.output" :code="activeStep.output" language="text" word-wrap style="max-height: 300px; overflow-y: auto; padding: 8px; border-radius: 4px; background: #F5F5F7;" />
85
- <div v-if="activeStep.files" class="mt-2 space-y-1">
86
- <div v-for="(file, i) in activeStep.files" :key="i" class="flex items-center gap-2 text-[10px]" style="color: #6B7280;">
87
- <svg class="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
88
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
89
- </svg>
90
- <span class="truncate">{{ file }}</span>
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
-
97
- <!-- Log Stream -->
98
- <div class="flex-shrink-0" style="height: 200px; border-top: 1px solid #F0F0F3;">
99
- <LogStream :logs="logs" @clear="$emit('clear-logs')" />
100
- </div>
101
- </div>
102
- </template>
103
-
104
- <script setup>
105
- import { computed } from 'vue'
106
- import StageBadge from './StageBadge.vue'
107
- import LogStream from './LogStream.vue'
108
- import GitDetail from './detail/GitDetail.vue'
109
- import TechDetail from './detail/TechDetail.vue'
110
- import DocsDetail from './detail/DocsDetail.vue'
111
-
112
- const props = defineProps({
113
- isOpen: { type: Boolean, default: true },
114
- activeStep: { type: Object, default: null },
115
- logs: { type: Array, default: () => [] },
116
- detailType: { type: String, default: null },
117
- detailData: { type: [Object, Array], default: null }
118
- })
119
-
120
- const emit = defineEmits(['close', 'clear-logs', 'open-doc-file'])
121
-
122
- const detailTitle = computed(() => {
123
- if (props.detailType === 'git') return 'Git 详情'
124
- if (props.detailType === 'tech') return '技术栈详情'
125
- if (props.detailType === 'docs') return '文档列表'
126
- return '详情'
127
- })
128
-
129
- function handleClose() {
130
- emit('close')
131
- }
132
-
133
- function formatTimestamp(ts) {
134
- if (!ts) return ''
135
- return new Date(ts).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
136
- }
137
- </script>
@@ -1,257 +0,0 @@
1
- <template>
2
- <div class="doc-preview-shell">
3
- <div v-if="!content && !loading" class="doc-empty">
4
- <p>选择一个文档查看内容</p>
5
- </div>
6
- <div v-else-if="loading" class="doc-empty">
7
- <p>加载中...</p>
8
- </div>
9
- <template v-else>
10
- <div class="doc-toolbar">
11
- <div class="doc-title" :title="fileTitle">{{ fileTitle }}</div>
12
- <n-button size="tiny" type="primary" @click="isModalOpen = true">弹窗阅读</n-button>
13
- </div>
14
- <div class="doc-preview-scroll">
15
- <div class="doc-preview" v-html="renderedContent"></div>
16
- </div>
17
- </template>
18
-
19
- <n-modal
20
- v-model:show="isModalOpen"
21
- preset="card"
22
- :title="fileTitle"
23
- style="width: min(920px, calc(100vw - 48px));"
24
- >
25
- <div class="doc-modal-body">
26
- <div class="doc-preview doc-preview-large" v-html="renderedContent"></div>
27
- </div>
28
- </n-modal>
29
- </div>
30
- </template>
31
-
32
- <script setup>
33
- import { computed, ref, watch } from 'vue'
34
- import { marked } from 'marked'
35
-
36
- const props = defineProps({
37
- content: { type: String, default: '' },
38
- loading: { type: Boolean, default: false },
39
- selectedFile: { type: Object, default: null }
40
- })
41
-
42
- const isModalOpen = ref(false)
43
-
44
- marked.setOptions({
45
- breaks: true,
46
- gfm: true
47
- })
48
-
49
- watch(() => props.selectedFile?.path, () => {
50
- isModalOpen.value = false
51
- })
52
-
53
- const fileTitle = computed(() => {
54
- return props.selectedFile?.title || props.selectedFile?.name || '文档'
55
- })
56
-
57
- const renderedContent = computed(() => {
58
- if (!props.content) return ''
59
- return marked.parse(props.content)
60
- })
61
- </script>
62
-
63
- <style scoped>
64
- .doc-preview-shell {
65
- height: 100%;
66
- min-height: 0;
67
- display: flex;
68
- flex-direction: column;
69
- background: #FFFFFF;
70
- }
71
-
72
- .doc-empty {
73
- height: 100%;
74
- display: flex;
75
- align-items: center;
76
- justify-content: center;
77
- padding: 24px;
78
- text-align: center;
79
- font-size: 12px;
80
- color: #636366;
81
- font-family: 'JetBrains Mono', monospace;
82
- }
83
-
84
- .doc-toolbar {
85
- flex-shrink: 0;
86
- display: flex;
87
- align-items: center;
88
- justify-content: space-between;
89
- gap: 12px;
90
- padding: 10px 14px;
91
- border-bottom: 1px solid #F0F0F3;
92
- }
93
-
94
- .doc-title {
95
- min-width: 0;
96
- overflow: hidden;
97
- text-overflow: ellipsis;
98
- white-space: nowrap;
99
- font-size: 12px;
100
- font-weight: 600;
101
- color: #1C1C1E;
102
- }
103
-
104
- .doc-preview-scroll {
105
- flex: 1;
106
- min-height: 0;
107
- overflow: auto;
108
- padding: 14px 16px 22px;
109
- }
110
-
111
- .doc-modal-body {
112
- max-height: min(72vh, 760px);
113
- overflow: auto;
114
- padding-right: 4px;
115
- }
116
-
117
- .doc-preview {
118
- font-size: 13px;
119
- line-height: 1.7;
120
- color: #374151;
121
- overflow-wrap: anywhere;
122
- }
123
-
124
- .doc-preview-large {
125
- font-size: 14px;
126
- line-height: 1.75;
127
- }
128
-
129
- .doc-preview :deep(h1) {
130
- color: #D97706;
131
- font-size: 18px;
132
- font-weight: 700;
133
- margin: 0 0 16px;
134
- border-bottom: 1px solid #E5E5EA;
135
- padding-bottom: 8px;
136
- }
137
-
138
- .doc-preview-large :deep(h1) {
139
- font-size: 22px;
140
- }
141
-
142
- .doc-preview :deep(h2) {
143
- color: #1C1C1E;
144
- font-size: 15px;
145
- font-weight: 600;
146
- margin: 20px 0 10px;
147
- }
148
-
149
- .doc-preview-large :deep(h2) {
150
- font-size: 17px;
151
- }
152
-
153
- .doc-preview :deep(h3) {
154
- color: #D97706;
155
- font-size: 14px;
156
- font-weight: 600;
157
- margin: 16px 0 8px;
158
- }
159
-
160
- .doc-preview :deep(p) {
161
- margin: 8px 0;
162
- }
163
-
164
- .doc-preview :deep(strong) {
165
- color: #1C1C1E;
166
- font-weight: 600;
167
- }
168
-
169
- .doc-preview :deep(a) {
170
- color: #D97706;
171
- text-decoration: none;
172
- }
173
-
174
- .doc-preview :deep(a:hover) {
175
- text-decoration: underline;
176
- }
177
-
178
- .doc-preview :deep(code) {
179
- background: #E5E5EA;
180
- color: #1C1C1E;
181
- padding: 1px 5px;
182
- border-radius: 3px;
183
- font-size: 12px;
184
- font-family: 'JetBrains Mono', monospace;
185
- }
186
-
187
- .doc-preview :deep(pre) {
188
- background: #1C1C1E;
189
- color: #E5E5E7;
190
- border: 1px solid #E5E5EA;
191
- border-radius: 6px;
192
- padding: 12px 16px;
193
- overflow-x: auto;
194
- font-size: 12px;
195
- margin: 8px 0;
196
- line-height: 1.5;
197
- }
198
-
199
- .doc-preview :deep(pre code) {
200
- background: none;
201
- color: inherit;
202
- padding: 0;
203
- border-radius: 0;
204
- font-size: inherit;
205
- }
206
-
207
- .doc-preview :deep(ul),
208
- .doc-preview :deep(ol) {
209
- padding-left: 20px;
210
- margin: 8px 0;
211
- color: #374151;
212
- }
213
-
214
- .doc-preview :deep(li) {
215
- margin: 4px 0;
216
- }
217
-
218
- .doc-preview :deep(blockquote) {
219
- border-left: 3px solid #D97706;
220
- padding-left: 12px;
221
- margin: 12px 0;
222
- color: #636366;
223
- font-style: italic;
224
- }
225
-
226
- .doc-preview :deep(table) {
227
- width: 100%;
228
- border-collapse: collapse;
229
- margin: 12px 0;
230
- font-size: 12px;
231
- }
232
-
233
- .doc-preview :deep(thead th) {
234
- background: #F0F0F3;
235
- color: #1C1C1E;
236
- font-weight: 600;
237
- text-align: left;
238
- padding: 8px 12px;
239
- border: 1px solid #E5E5EA;
240
- }
241
-
242
- .doc-preview :deep(tbody td) {
243
- padding: 6px 12px;
244
- border: 1px solid #E5E5EA;
245
- color: #374151;
246
- }
247
-
248
- .doc-preview :deep(tbody tr:hover) {
249
- background: #F9FAFB;
250
- }
251
-
252
- .doc-preview :deep(hr) {
253
- border: none;
254
- border-top: 1px solid #E5E5EA;
255
- margin: 16px 0;
256
- }
257
- </style>
@@ -1,114 +0,0 @@
1
- <template>
2
- <div class="doc-tree">
3
- <div class="doc-tree-search">
4
- <n-input
5
- v-model:value="query"
6
- size="small"
7
- clearable
8
- placeholder="搜索文档"
9
- />
10
- </div>
11
-
12
- <n-empty v-if="groups.length === 0" description="暂无文档" style="margin: auto;" />
13
- <n-empty v-else-if="treeData.length === 0" description="无匹配文档" style="margin: auto;" />
14
- <n-tree
15
- v-else
16
- class="doc-tree-list"
17
- :data="treeData"
18
- :selected-keys="selectedKeys"
19
- :default-expand-all="true"
20
- selectable
21
- block-line
22
- @update:selected-keys="handleSelect"
23
- />
24
- </div>
25
- </template>
26
-
27
- <script setup>
28
- import { computed, ref } from 'vue'
29
-
30
- const props = defineProps({
31
- groups: { type: Array, default: () => [] },
32
- selectedFile: { type: Object, default: null }
33
- })
34
-
35
- const emit = defineEmits(['select-file'])
36
- const query = ref('')
37
-
38
- const selectedKeys = computed(() => {
39
- return props.selectedFile?.path ? [props.selectedFile.path] : []
40
- })
41
-
42
- const groupIcons = {
43
- design: '📋',
44
- plan: '🧾',
45
- archive: '📦',
46
- changes: '⚙️',
47
- scan: '🔍',
48
- quicklog: '⚡'
49
- }
50
-
51
- const treeData = computed(() => {
52
- const keyword = query.value.trim().toLowerCase()
53
-
54
- return props.groups
55
- .map(group => {
56
- const groupLabel = group.project ? `${group.label} (${group.project})` : group.label
57
- const groupMatched = !keyword || groupLabel.toLowerCase().includes(keyword)
58
- const files = group.files.filter(file => {
59
- if (groupMatched) return true
60
- return `${file.title || ''} ${file.name || ''}`.toLowerCase().includes(keyword)
61
- })
62
-
63
- if (files.length === 0) return null
64
-
65
- return {
66
- key: `group-${group.key}`,
67
- label: groupLabel,
68
- prefix: () => groupIcons[group.key] || '📄',
69
- children: files.map(file => ({
70
- key: file.path,
71
- label: file.title || file.name,
72
- prefix: () => '📄',
73
- data: file
74
- }))
75
- }
76
- })
77
- .filter(Boolean)
78
- })
79
-
80
- function handleSelect(keys, option) {
81
- if (option?.[0]?.data) {
82
- emit('select-file', option[0].data)
83
- }
84
- }
85
- </script>
86
-
87
- <style scoped>
88
- .doc-tree {
89
- height: 100%;
90
- min-height: 0;
91
- display: flex;
92
- flex-direction: column;
93
- background: #FFFFFF;
94
- }
95
-
96
- .doc-tree-search {
97
- flex-shrink: 0;
98
- padding: 10px 12px 8px;
99
- border-bottom: 1px solid #F0F0F3;
100
- }
101
-
102
- .doc-tree-list {
103
- flex: 1;
104
- min-height: 0;
105
- overflow: auto;
106
- padding: 8px 12px 14px;
107
- }
108
-
109
- .doc-tree-list :deep(.n-tree-node-content__text) {
110
- white-space: normal;
111
- line-height: 1.35;
112
- overflow-wrap: anywhere;
113
- }
114
- </style>
@@ -1,48 +0,0 @@
1
- <template>
2
- <div
3
- class="h-resize-handle"
4
- :class="{ 'dragging': isDragging }"
5
- @mousedown="handleMouseDown"
6
- ></div>
7
- </template>
8
-
9
- <script setup>
10
- const props = defineProps({
11
- isDragging: { type: Boolean, default: false },
12
- colIndex: { type: Number, required: true }
13
- })
14
-
15
- const emit = defineEmits(['drag-start', 'drag-end', 'resize'])
16
-
17
- function handleMouseDown(e) {
18
- e.preventDefault()
19
- emit('drag-start', { type: 'horizontal', colIndex: props.colIndex, startX: e.clientX })
20
-
21
- const onMove = (ev) => {
22
- emit('resize', { deltaX: ev.clientX - e.clientX })
23
- }
24
-
25
- const onUp = () => {
26
- emit('drag-end')
27
- window.removeEventListener('mousemove', onMove)
28
- window.removeEventListener('mouseup', onUp)
29
- }
30
-
31
- window.addEventListener('mousemove', onMove)
32
- window.addEventListener('mouseup', onUp)
33
- }
34
- </script>
35
-
36
- <style scoped>
37
- .h-resize-handle {
38
- width: 4px;
39
- background: #E5E7EB;
40
- cursor: col-resize;
41
- transition: background 0.2s;
42
- }
43
-
44
- .h-resize-handle:hover,
45
- .h-resize-handle.dragging {
46
- background: #D97706;
47
- }
48
- </style>