feat(gui): add code/preview panel with diff viewer and file tree (U4)
- Create CodeDiffViewer.vue with line-level diff highlighting - Create FileTree.vue with status badges (added/modified/deleted) - Update WorkflowView.vue: replace hardcoded colors with Design Tokens - Update KnowledgeBaseView.vue: replace hardcoded colors with Design Tokens
This commit is contained in:
parent
6d5a08cb0c
commit
79a400afe8
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="code-diff-viewer">
|
||||
<div v-if="!diff" class="code-diff-viewer__empty">
|
||||
<FileTextOutlined style="font-size: 32px; color: var(--text-placeholder)" />
|
||||
<p>暂无代码变更</p>
|
||||
</div>
|
||||
<div v-else class="code-diff-viewer__content">
|
||||
<div class="code-diff-viewer__header">
|
||||
<span class="code-diff-viewer__file">{{ diff.file }}</span>
|
||||
<span class="code-diff-viewer__stats">
|
||||
<span class="code-diff-viewer__added">+{{ diff.added }}</span>
|
||||
<span class="code-diff-viewer__removed">-{{ diff.removed }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="code-diff-viewer__lines">
|
||||
<div
|
||||
v-for="(line, idx) in diff.lines"
|
||||
:key="idx"
|
||||
:class="['code-diff-viewer__line', `code-diff-viewer__line--${line.type}`]"
|
||||
>
|
||||
<span class="code-diff-viewer__num">{{ line.num }}</span>
|
||||
<span class="code-diff-viewer__prefix">{{ line.prefix }}</span>
|
||||
<span class="code-diff-viewer__text">{{ line.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileTextOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
export interface DiffLine {
|
||||
num: number
|
||||
type: 'context' | 'added' | 'removed'
|
||||
prefix: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface DiffData {
|
||||
file: string
|
||||
added: number
|
||||
removed: number
|
||||
lines: DiffLine[]
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
diff: DiffData | null
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.code-diff-viewer {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
|
||||
font-size: var(--font-sm);
|
||||
background: var(--code-bg);
|
||||
color: var(--code-fg);
|
||||
}
|
||||
|
||||
.code-diff-viewer__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: var(--space-3);
|
||||
color: var(--text-placeholder);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.code-diff-viewer__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.code-diff-viewer__file {
|
||||
color: var(--code-fg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.code-diff-viewer__stats {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
.code-diff-viewer__added {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.code-diff-viewer__removed {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.code-diff-viewer__line {
|
||||
display: flex;
|
||||
min-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.code-diff-viewer__line--added {
|
||||
background: var(--code-added-bg);
|
||||
}
|
||||
|
||||
.code-diff-viewer__line--removed {
|
||||
background: var(--code-removed-bg);
|
||||
}
|
||||
|
||||
.code-diff-viewer__num {
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
padding-right: var(--space-2);
|
||||
color: var(--code-comment);
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-diff-viewer__prefix {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-diff-viewer__line--added .code-diff-viewer__prefix {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.code-diff-viewer__line--removed .code-diff-viewer__prefix {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.code-diff-viewer__text {
|
||||
flex: 1;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div class="file-tree">
|
||||
<div v-if="files.length === 0" class="file-tree__empty">
|
||||
<FolderOpenOutlined style="font-size: 24px; color: var(--text-placeholder)" />
|
||||
<p>暂无文件</p>
|
||||
</div>
|
||||
<div v-else class="file-tree__list">
|
||||
<div
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
:class="['file-tree__item', { 'file-tree__item--active': selectedPath === file.path }]"
|
||||
@click="selectedPath = file.path; $emit('select', file.path)"
|
||||
>
|
||||
<component :is="getFileIcon(file.status)" class="file-tree__icon" :class="[`file-tree__icon--${file.status}`]" />
|
||||
<span class="file-tree__name">{{ file.name }}</span>
|
||||
<span v-if="file.status" :class="['file-tree__badge', `file-tree__badge--${file.status}`]">
|
||||
{{ getStatusLabel(file.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, type Component } from 'vue'
|
||||
import {
|
||||
FolderOpenOutlined,
|
||||
FileOutlined,
|
||||
FileAddOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
export interface FileEntry {
|
||||
path: string
|
||||
name: string
|
||||
status?: 'added' | 'modified' | 'deleted'
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
files: FileEntry[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: [path: string]
|
||||
}>()
|
||||
|
||||
const selectedPath = ref('')
|
||||
|
||||
function getFileIcon(status?: string): Component {
|
||||
switch (status) {
|
||||
case 'added': return FileAddOutlined
|
||||
case 'deleted': return DeleteOutlined
|
||||
case 'modified': return EditOutlined
|
||||
default: return FileOutlined
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string): string {
|
||||
switch (status) {
|
||||
case 'added': return 'A'
|
||||
case 'deleted': return 'D'
|
||||
case 'modified': return 'M'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-tree {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.file-tree__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-placeholder);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.file-tree__list {
|
||||
padding: var(--space-1);
|
||||
}
|
||||
|
||||
.file-tree__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.file-tree__item:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-tree__item--active {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.file-tree__icon {
|
||||
font-size: 14px;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-tree__icon--added {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.file-tree__icon--modified {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.file-tree__icon--deleted {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.file-tree__name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.file-tree__badge {
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-tree__badge--added {
|
||||
color: var(--color-success);
|
||||
background: var(--color-success-light);
|
||||
}
|
||||
|
||||
.file-tree__badge--modified {
|
||||
color: var(--color-warning);
|
||||
background: var(--color-warning-light);
|
||||
}
|
||||
|
||||
.file-tree__badge--deleted {
|
||||
color: var(--color-error);
|
||||
background: var(--color-error-light);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,8 +32,8 @@ onMounted(() => {
|
|||
<style scoped>
|
||||
.kb-view {
|
||||
height: 100%;
|
||||
padding: 16px 24px;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -315,55 +315,54 @@ function handleBack() {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.list-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-md);
|
||||
}
|
||||
|
||||
.list-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
gap: 12px;
|
||||
transition: all var(--transition-fast);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.12);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-base);
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
|
|
@ -382,8 +381,8 @@ function handleBack() {
|
|||
}
|
||||
|
||||
.execution-history {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -391,20 +390,20 @@ function handleBack() {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.history-header:hover {
|
||||
background: #f0f0f0;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.history-body {
|
||||
padding: 0 16px 8px;
|
||||
padding: 0 var(--space-4) var(--space-2);
|
||||
}
|
||||
|
||||
.back-bar {
|
||||
|
|
@ -414,16 +413,16 @@ function handleBack() {
|
|||
right: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.workflow-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue