feat(gui): refactor terminal panel with One Dark Pro theme and Ant Design Modal (U5)

- TerminalView: replace native HTML confirmation with Ant Design Modal,
  make command history sidebar collapsible (default collapsed)
- TerminalEmulator: use One Dark Pro CSS variables for ANSI colors,
  replace all hardcoded colors with Design Tokens
- CommandHistory: replace all hardcoded colors with Design Tokens
This commit is contained in:
chiguyong 2026-06-13 02:41:46 +08:00
parent 79a400afe8
commit 3dc5c68135
3 changed files with 139 additions and 178 deletions

View File

@ -15,8 +15,7 @@
>
<div class="command-history__item-header">
<span
class="command-history__exit-code"
:class="record.exit_code === 0 ? 'success' : 'error'"
:class="['command-history__exit-code', record.exit_code === 0 ? 'command-history__exit-code--success' : 'command-history__exit-code--error']"
>
{{ record.exit_code === 0 ? '✓' : '✗' }}
</span>
@ -37,6 +36,7 @@
</template>
<script setup lang="ts">
import { Button as AButton } from 'ant-design-vue'
import { useTerminalStore } from '@/stores/terminal'
const terminalStore = useTerminalStore()
@ -60,61 +60,60 @@ function formatTime(timestamp: number): string {
display: flex;
flex-direction: column;
height: 100%;
background: #fafafa;
border-left: 1px solid #f0f0f0;
background: var(--bg-primary);
}
.command-history__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
padding: var(--space-3) var(--space-4);
font-weight: var(--font-weight-semibold);
font-size: var(--font-base);
border-bottom: 1px solid var(--border-color);
}
.command-history__list {
flex: 1;
overflow-y: auto;
padding: 8px;
padding: var(--space-2);
}
.command-history__item {
padding: 8px 12px;
border-radius: 4px;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
margin-bottom: var(--space-1);
transition: background var(--transition-fast);
}
.command-history__item:hover {
background: #e6f4ff;
background: var(--color-primary-light);
}
.command-history__item-header {
display: flex;
align-items: center;
gap: 6px;
gap: var(--space-1);
}
.command-history__exit-code {
font-size: 12px;
font-weight: 600;
font-size: var(--font-xs);
font-weight: var(--font-weight-semibold);
}
.command-history__exit-code.success {
color: #52c41a;
.command-history__exit-code--success {
color: var(--color-success);
}
.command-history__exit-code.error {
color: #f5222d;
.command-history__exit-code--error {
color: var(--color-error);
}
.command-history__command {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 12px;
color: #333;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-xs);
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -122,20 +121,20 @@ function formatTime(timestamp: number): string {
.command-history__item-meta {
display: flex;
gap: 8px;
gap: var(--space-2);
margin-top: 2px;
font-size: 11px;
color: #999;
color: var(--text-placeholder);
}
.command-history__duration {
color: #1677ff;
color: var(--color-primary);
}
.command-history__empty {
text-align: center;
padding: 24px;
color: #999;
font-size: 13px;
padding: var(--space-6);
color: var(--text-placeholder);
font-size: var(--font-sm);
}
</style>

View File

@ -77,15 +77,17 @@ function historyDown(): void {
}
function ansiToHtml(text: string): string {
// Basic ANSI color code conversion
// Basic ANSI color code conversion using One Dark Pro palette
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\x1b\[32m/g, '<span style="color:#4caf50">')
.replace(/\x1b\[33m/g, '<span style="color:#ff9800">')
.replace(/\x1b\[31m/g, '<span style="color:#f44336">')
.replace(/\x1b\[36m/g, '<span style="color:#00bcd4">')
.replace(/\x1b\[32m/g, '<span class="ansi-green">')
.replace(/\x1b\[33m/g, '<span class="ansi-yellow">')
.replace(/\x1b\[31m/g, '<span class="ansi-red">')
.replace(/\x1b\[36m/g, '<span class="ansi-cyan">')
.replace(/\x1b\[34m/g, '<span class="ansi-blue">')
.replace(/\x1b\[35m/g, '<span class="ansi-magenta">')
.replace(/\x1b\[0m/g, '</span>')
}
</script>
@ -95,38 +97,38 @@ function ansiToHtml(text: string): string {
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
border-radius: 6px;
background: var(--code-bg);
border-radius: var(--radius-md);
overflow: hidden;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
}
.terminal-emulator__output {
flex: 1;
overflow-y: auto;
padding: 12px;
font-size: 13px;
line-height: 1.5;
color: #d4d4d4;
padding: var(--space-3);
font-size: var(--font-sm);
line-height: var(--leading-normal);
color: var(--code-fg);
}
.terminal-emulator__welcome {
color: #888;
color: var(--code-comment);
font-style: italic;
}
.terminal-emulator__input {
display: flex;
align-items: center;
padding: 8px 12px;
border-top: 1px solid #333;
background: #252526;
padding: var(--space-2) var(--space-3);
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
}
.terminal-emulator__prompt {
color: #4caf50;
margin-right: 8px;
font-size: 13px;
color: var(--code-string);
margin-right: var(--space-2);
font-size: var(--font-sm);
white-space: nowrap;
}
@ -135,17 +137,25 @@ function ansiToHtml(text: string): string {
background: transparent;
border: none;
outline: none;
color: #d4d4d4;
color: var(--code-fg);
font-family: inherit;
font-size: 13px;
font-size: var(--font-sm);
}
.terminal-emulator__input-field::placeholder {
color: #555;
color: var(--code-comment);
}
.terminal-line {
white-space: pre-wrap;
word-break: break-all;
}
/* ANSI color classes using One Dark Pro palette */
.terminal-line :deep(.ansi-green) { color: var(--code-string); }
.terminal-line :deep(.ansi-yellow) { color: var(--code-number); }
.terminal-line :deep(.ansi-red) { color: var(--code-variable); }
.terminal-line :deep(.ansi-cyan) { color: var(--code-function); }
.terminal-line :deep(.ansi-blue) { color: var(--code-function); }
.terminal-line :deep(.ansi-magenta) { color: var(--code-keyword); }
</style>

View File

@ -2,59 +2,56 @@
<div class="terminal-view">
<div class="terminal-view__main">
<TerminalEmulator />
<!-- Confirmation dialog -->
<div v-if="terminalStore.pendingConfirmation" class="terminal-view__confirmation">
<div class="terminal-view__confirmation-content">
<div class="terminal-view__confirmation-header">
<span class="terminal-view__confirmation-icon"></span>
<span>命令确认</span>
</div>
<div class="terminal-view__confirmation-command">
{{ terminalStore.pendingConfirmation.command }}
</div>
<div class="terminal-view__confirmation-reason">
{{ terminalStore.pendingConfirmation.reason }}
</div>
<div class="terminal-view__confirmation-actions">
<label class="terminal-view__whitelist-check">
<input
v-model="addToWhitelist"
type="checkbox"
/>
添加到会话白名单
</label>
<div class="terminal-view__confirmation-buttons">
<button
class="terminal-view__btn terminal-view__btn--reject"
@click="rejectConfirmation"
>
拒绝
</button>
<button
class="terminal-view__btn terminal-view__btn--approve"
@click="approveConfirmation"
>
确认执行
</button>
</div>
</div>
</div>
</div>
<div :class="['terminal-view__sidebar', { 'terminal-view__sidebar--collapsed': sidebarCollapsed }]">
<button class="terminal-view__sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
<HistoryOutlined />
</button>
<div v-if="!sidebarCollapsed" class="terminal-view__sidebar-content">
<CommandHistory @select="handleHistorySelect" />
</div>
</div>
<div class="terminal-view__sidebar">
<CommandHistory @select="handleHistorySelect" />
</div>
<!-- Confirmation dialog using Ant Design Modal -->
<a-modal
v-model:open="showConfirmation"
title="命令确认"
:ok-text="'确认执行'"
:cancel-text="'拒绝'"
@ok="approveConfirmation"
@cancel="rejectConfirmation"
:ok-button-props="{ danger: true }"
>
<div class="terminal-view__modal-body">
<div class="terminal-view__modal-command">
{{ terminalStore.pendingConfirmation?.command }}
</div>
<div class="terminal-view__modal-reason">
{{ terminalStore.pendingConfirmation?.reason }}
</div>
<a-checkbox v-model:checked="addToWhitelist">
添加到会话白名单
</a-checkbox>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
import { Modal as AModal, Checkbox as ACheckbox } from 'ant-design-vue'
import { HistoryOutlined } from '@ant-design/icons-vue'
import { useTerminalStore } from '@/stores/terminal'
import TerminalEmulator from '@/components/terminal/TerminalEmulator.vue'
import CommandHistory from '@/components/terminal/CommandHistory.vue'
const terminalStore = useTerminalStore()
const addToWhitelist = ref(false)
const sidebarCollapsed = ref(true)
const showConfirmation = computed({
get: () => !!terminalStore.pendingConfirmation,
set: () => { /* modal handles close via cancel */ },
})
onUnmounted(() => {
terminalStore.disconnectWebSocket()
@ -91,110 +88,65 @@ function rejectConfirmation(): void {
.terminal-view__main {
flex: 1;
overflow: hidden;
padding: 16px;
padding: var(--space-2);
position: relative;
}
.terminal-view__sidebar {
width: 280px;
display: flex;
border-left: 1px solid var(--border-color);
background: var(--bg-primary);
transition: width var(--transition-normal);
overflow: hidden;
}
.terminal-view__confirmation {
position: absolute;
bottom: 60px;
left: 24px;
right: 24px;
z-index: 10;
.terminal-view__sidebar--collapsed {
width: 32px;
}
.terminal-view__confirmation-content {
background: #2d2d2d;
border: 1px solid #ff9800;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
.terminal-view__sidebar:not(.terminal-view__sidebar--collapsed) {
width: 240px;
}
.terminal-view__confirmation-header {
.terminal-view__sidebar-toggle {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #ff9800;
margin-bottom: 8px;
justify-content: center;
width: 32px;
height: 100%;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
flex-shrink: 0;
transition: all var(--transition-fast);
}
.terminal-view__confirmation-icon {
font-size: 16px;
.terminal-view__sidebar-toggle:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.terminal-view__confirmation-command {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 13px;
color: #d4d4d4;
background: #1e1e1e;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 8px;
.terminal-view__sidebar-content {
flex: 1;
overflow: hidden;
min-width: 0;
}
.terminal-view__modal-command {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-sm);
color: var(--text-primary);
background: var(--bg-tertiary);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
margin-bottom: var(--space-3);
word-break: break-all;
}
.terminal-view__confirmation-reason {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
.terminal-view__confirmation-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-view__whitelist-check {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #aaa;
cursor: pointer;
}
.terminal-view__whitelist-check input {
cursor: pointer;
}
.terminal-view__confirmation-buttons {
display: flex;
gap: 8px;
}
.terminal-view__btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.terminal-view__btn--reject {
background: #555;
color: #d4d4d4;
}
.terminal-view__btn--reject:hover {
background: #666;
}
.terminal-view__btn--approve {
background: #ff9800;
color: #1e1e1e;
font-weight: 600;
}
.terminal-view__btn--approve:hover {
background: #ffa726;
.terminal-view__modal-reason {
font-size: var(--font-sm);
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
</style>