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:
chiguyong 2026-06-13 02:40:14 +08:00
parent 6d5a08cb0c
commit 79a400afe8
4 changed files with 341 additions and 34 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>