前后端接口对接
This commit is contained in:
		
							
								
								
									
										322
									
								
								src/components/common/ExecutionResult/ExecutionResult.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								src/components/common/ExecutionResult/ExecutionResult.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,322 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="execution-result" ref="executionResultRef">
 | 
			
		||||
    <h4 class="section-title">
 | 
			
		||||
      <i-lucide-activity class="title-icon" />
 | 
			
		||||
      {{ title }}
 | 
			
		||||
    </h4>
 | 
			
		||||
    <div class="log-container" ref="logContainer" @mouseenter="showClearButton = true" @mouseleave="showClearButton = false">
 | 
			
		||||
      <div 
 | 
			
		||||
        class="clear-button" 
 | 
			
		||||
        :class="{ 'visible': showClearButton }"
 | 
			
		||||
        @click.stop="handleClearClick"
 | 
			
		||||
        title="清空执行结果"
 | 
			
		||||
      >
 | 
			
		||||
        <i-lucide-trash-2 class="clear-icon" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-for="(log, index) in internalLogs" :key="index" class="log-item">
 | 
			
		||||
        {{ log }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-if="internalLogs.length === 0" class="empty-logs">
 | 
			
		||||
        <i-lucide-inbox class="empty-icon" />
 | 
			
		||||
        <span>暂无执行结果</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
// 定义组件属性
 | 
			
		||||
interface Props {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  logs?: string[];
 | 
			
		||||
  autoScroll?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  title: '执行结果',
 | 
			
		||||
  logs: () => [],
 | 
			
		||||
  autoScroll: true
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 定义事件
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  clear: [];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 响应式数据
 | 
			
		||||
const showClearButton = ref(false);
 | 
			
		||||
const logContainer = ref<HTMLElement>();
 | 
			
		||||
const executionResultRef = ref<HTMLElement>();
 | 
			
		||||
const internalLogs = ref<string[]>([...props.logs]);
 | 
			
		||||
 | 
			
		||||
// 监听外部logs变化
 | 
			
		||||
watch(() => props.logs, (newLogs) => {
 | 
			
		||||
  internalLogs.value = [...newLogs];
 | 
			
		||||
  
 | 
			
		||||
  if (props.autoScroll) {
 | 
			
		||||
    scrollToBottom();
 | 
			
		||||
  }
 | 
			
		||||
}, { deep: true });
 | 
			
		||||
 | 
			
		||||
// 滚动到底部
 | 
			
		||||
const scrollToBottom = () => {
 | 
			
		||||
  if (logContainer.value) {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      if (logContainer.value) {
 | 
			
		||||
        logContainer.value.scrollTop = logContainer.value.scrollHeight;
 | 
			
		||||
      }
 | 
			
		||||
    }, 0);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 处理清空按钮点击
 | 
			
		||||
const handleClearClick = () => {
 | 
			
		||||
  // 单次点击即清空日志
 | 
			
		||||
  internalLogs.value = [];
 | 
			
		||||
  emit('clear');
 | 
			
		||||
  resetClickState();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 重置点击状态
 | 
			
		||||
const resetClickState = () => {
 | 
			
		||||
  showClearButton.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 全局点击监听,点击其他地方重置状态
 | 
			
		||||
const handleGlobalClick = (event: MouseEvent) => {
 | 
			
		||||
  const target = event.target as HTMLElement;
 | 
			
		||||
  
 | 
			
		||||
  // 如果点击的不是当前组件内的清空按钮,则重置状态
 | 
			
		||||
  if (executionResultRef.value && !executionResultRef.value.contains(target)) {
 | 
			
		||||
    resetClickState();
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 组件挂载时添加全局监听
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  document.addEventListener('click', handleGlobalClick);
 | 
			
		||||
  
 | 
			
		||||
  // 初始滚动到底部
 | 
			
		||||
  if (props.autoScroll) {
 | 
			
		||||
    scrollToBottom();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 组件卸载时移除全局监听
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  document.removeEventListener('click', handleGlobalClick);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 暴露方法给父组件
 | 
			
		||||
defineExpose({
 | 
			
		||||
  scrollToBottom
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
// 变量定义
 | 
			
		||||
$primary-color: #4a90e2;
 | 
			
		||||
$primary-light: #5ba0f2;
 | 
			
		||||
$success-color: #52c41a;
 | 
			
		||||
$error-color: #ff4d4f;
 | 
			
		||||
$bg-dark: rgba(0, 0, 0, 0.3);
 | 
			
		||||
$bg-light: rgba(255, 255, 255, 0.1);
 | 
			
		||||
$bg-light-hover: rgba(255, 255, 255, 0.2);
 | 
			
		||||
$border-light: rgba(255, 255, 255, 0.2);
 | 
			
		||||
$text-white: #ffffff;
 | 
			
		||||
$spacing-xs: 4px;
 | 
			
		||||
$spacing-sm: 8px;
 | 
			
		||||
$spacing-md: 10px;
 | 
			
		||||
$border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
// 混合器
 | 
			
		||||
@mixin flex-center {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin flex-align-center {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin icon-base($size) {
 | 
			
		||||
  width: $size;
 | 
			
		||||
  height: $size;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin transition-ease {
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.execution-result {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  .section-title {
 | 
			
		||||
    @include flex-align-center;
 | 
			
		||||
    gap: $spacing-sm;
 | 
			
		||||
    margin: 0 0 $spacing-md 0;
 | 
			
		||||
    color: $text-white;
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
 | 
			
		||||
    border-bottom: 1px solid $primary-color;
 | 
			
		||||
    padding-bottom: $spacing-sm;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .title-icon {
 | 
			
		||||
      @include icon-base(20px);
 | 
			
		||||
      filter: drop-shadow(0 2px 4px rgba(74, 144, 226, 0.3));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .sse-indicator {
 | 
			
		||||
      @include flex-center;
 | 
			
		||||
      @include icon-base(16px);
 | 
			
		||||
      margin-left: auto;
 | 
			
		||||
      position: relative;
 | 
			
		||||
      
 | 
			
		||||
      .indicator-icon {
 | 
			
		||||
        @include icon-base(16px);
 | 
			
		||||
        color: $error-color;
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
        @include transition-ease;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      &.connected .indicator-icon {
 | 
			
		||||
        color: $success-color;
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
        animation: pulse 2s infinite;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      &::after {
 | 
			
		||||
        content: '';
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        width: 6px;
 | 
			
		||||
        height: 6px;
 | 
			
		||||
        background-color: $error-color;
 | 
			
		||||
        border-radius: 50%;
 | 
			
		||||
        right: -2px;
 | 
			
		||||
        top: -2px;
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
        @include transition-ease;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      &.connected::after {
 | 
			
		||||
        background-color: $success-color;
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .log-container {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    background: $bg-dark;
 | 
			
		||||
    border: 1px solid $primary-color;
 | 
			
		||||
    border-radius: $border-radius;
 | 
			
		||||
    padding: $spacing-md;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    font-family: 'Consolas', 'Monaco', monospace;
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .clear-button {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: $spacing-sm;
 | 
			
		||||
      top: $spacing-sm;
 | 
			
		||||
      width: 24px;
 | 
			
		||||
      height: 24px;
 | 
			
		||||
      @include flex-center;
 | 
			
		||||
      background: $bg-light;
 | 
			
		||||
      border-radius: $border-radius;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
      visibility: hidden;
 | 
			
		||||
      @include transition-ease;
 | 
			
		||||
      backdrop-filter: blur(10px);
 | 
			
		||||
      border: 1px solid $border-light;
 | 
			
		||||
      z-index: 10;
 | 
			
		||||
 | 
			
		||||
      &.visible {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
        visibility: visible;
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background: $bg-light-hover;
 | 
			
		||||
        transform: scale(1.1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .clear-icon {
 | 
			
		||||
        @include icon-base(14px);
 | 
			
		||||
        color: $error-color;
 | 
			
		||||
        filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .log-item {
 | 
			
		||||
      color: $success-color;
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
      line-height: 1.4;
 | 
			
		||||
      margin-bottom: 2px;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .empty-logs {
 | 
			
		||||
      @include flex-center;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      min-height: 80px;
 | 
			
		||||
      color: $text-white;
 | 
			
		||||
      opacity: 0.5;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      
 | 
			
		||||
      .empty-icon {
 | 
			
		||||
        @include icon-base(24px);
 | 
			
		||||
        margin-bottom: $spacing-sm;
 | 
			
		||||
        color: $primary-color;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* 滚动条样式 */
 | 
			
		||||
    &::-webkit-scrollbar {
 | 
			
		||||
      width: 6px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-scrollbar-track {
 | 
			
		||||
      background: $bg-light;
 | 
			
		||||
      border-radius: 3px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &::-webkit-scrollbar-thumb {
 | 
			
		||||
      background: $primary-color;
 | 
			
		||||
      border-radius: 3px;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background: $primary-light;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes pulse {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: scale(1);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: scale(1.1);
 | 
			
		||||
    opacity: 0.8;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: scale(1);
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										74
									
								
								src/components/common/Modal/LoginModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/components/common/Modal/LoginModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-modal
 | 
			
		||||
    :open="visible"
 | 
			
		||||
    title="登录"
 | 
			
		||||
    ok-text="登录"
 | 
			
		||||
    cancel-text="取消"
 | 
			
		||||
    @close="handleClose"
 | 
			
		||||
    @cancel="handleClose"
 | 
			
		||||
    @ok="handleLogin"
 | 
			
		||||
  >
 | 
			
		||||
    <a-form :value="loginForm"
 | 
			
		||||
    :label-col="{ span: 4 }"
 | 
			
		||||
    :wrapper-col="{ span: 18 }">
 | 
			
		||||
      <a-form-item label="用户名">
 | 
			
		||||
        <a-input v-model:value="loginForm.username" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item label="密码">
 | 
			
		||||
        <a-input v-model:value="loginForm.password" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
    </a-form>
 | 
			
		||||
  </a-modal>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive, ref, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  modelValue: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'ok',
 | 
			
		||||
  'close',
 | 
			
		||||
  'cancel',
 | 
			
		||||
  'update:modelValue',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
const handleLogin = () => {
 | 
			
		||||
  console.log('loginForm: ', loginForm);
 | 
			
		||||
  console.log('ok')
 | 
			
		||||
  handleClose();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const visible = ref(props.modelValue);
 | 
			
		||||
// 关闭模态框
 | 
			
		||||
const handleClose = () => {
 | 
			
		||||
  visible.value = false;
 | 
			
		||||
  emit('update:modelValue', false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 监听外部变化
 | 
			
		||||
watch(() => props.modelValue, (newVal) => {
 | 
			
		||||
  visible.value = newVal;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听内部变化
 | 
			
		||||
watch(visible, (newVal) => {
 | 
			
		||||
  if (newVal !== props.modelValue) {
 | 
			
		||||
    emit('update:modelValue', newVal);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
const loginForm = reactive({
 | 
			
		||||
  username: '',
 | 
			
		||||
  password: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.ant-modal-header {
 | 
			
		||||
  margin-bottom: 30px !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										20
									
								
								src/components/common/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/common/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
// ExecutionResult组件相关类型定义
 | 
			
		||||
export interface ExecutionResultProps {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  logs: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 日志项类型
 | 
			
		||||
export interface LogItem {
 | 
			
		||||
  timestamp?: string;
 | 
			
		||||
  message: string;
 | 
			
		||||
  level?: 'info' | 'success' | 'warning' | 'error';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 扩展的ExecutionResult属性(支持更复杂的日志格式)
 | 
			
		||||
export interface ExtendedExecutionResultProps {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  logs: LogItem[];
 | 
			
		||||
  maxHeight?: string;
 | 
			
		||||
  showTimestamp?: boolean;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user