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:
parent
79a400afe8
commit
3dc5c68135
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue