feat(1.5.0)-beta1): 完成日志接入ws
This commit is contained in:
		
							parent
							
								
									2408463d7c
								
							
						
					
					
						commit
						d7fd6c040c
					
				@ -1,447 +0,0 @@
 | 
				
			|||||||
<script setup lang="tsx">
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  NCard,
 | 
					 | 
				
			||||||
  NCollapse,
 | 
					 | 
				
			||||||
  NCollapseItem,
 | 
					 | 
				
			||||||
  NDivider,
 | 
					 | 
				
			||||||
  NEmpty,
 | 
					 | 
				
			||||||
  NScrollbar,
 | 
					 | 
				
			||||||
  NSpin,
 | 
					 | 
				
			||||||
  NVirtualList,
 | 
					 | 
				
			||||||
  type VirtualListInst
 | 
					 | 
				
			||||||
} from 'naive-ui';
 | 
					 | 
				
			||||||
import { defineComponent, nextTick, onBeforeUnmount, ref, watch } from 'vue';
 | 
					 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					 | 
				
			||||||
import ButtonIcon from '@/components/custom/button-icon.vue';
 | 
					 | 
				
			||||||
import { initWebSocket } from '@/utils/websocket';
 | 
					 | 
				
			||||||
const terminalSocket = ref<WebSocket | undefined>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
defineOptions({
 | 
					 | 
				
			||||||
  name: 'LogWsDrawer'
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Props {
 | 
					 | 
				
			||||||
  title?: string;
 | 
					 | 
				
			||||||
  drawer?: boolean;
 | 
					 | 
				
			||||||
  type?: 'job' | 'retry';
 | 
					 | 
				
			||||||
  taskData?: Api.Job.JobTask | Api.RetryTask.RetryTask;
 | 
					 | 
				
			||||||
  modelValue?: Api.JobLog.JobMessage[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = withDefaults(defineProps<Props>(), {
 | 
					 | 
				
			||||||
  title: undefined,
 | 
					 | 
				
			||||||
  drawer: true,
 | 
					 | 
				
			||||||
  type: 'job',
 | 
					 | 
				
			||||||
  taskData: undefined,
 | 
					 | 
				
			||||||
  modelValue: () => []
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const visible = defineModel<boolean>('show', {
 | 
					 | 
				
			||||||
  default: false
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isFullscreen = ref(true);
 | 
					 | 
				
			||||||
const expandedNames = ref<string[]>([]);
 | 
					 | 
				
			||||||
const virtualListInst = ref<VirtualListInst>();
 | 
					 | 
				
			||||||
const logList = ref<Api.JobLog.JobMessage[]>([]);
 | 
					 | 
				
			||||||
const interval = ref<NodeJS.Timeout>();
 | 
					 | 
				
			||||||
const finished = ref<boolean>(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const pauseLog = () => {
 | 
					 | 
				
			||||||
  finished.value = true;
 | 
					 | 
				
			||||||
  clearTimeout(interval.value);
 | 
					 | 
				
			||||||
  interval.value = undefined;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const stopLog = () => {
 | 
					 | 
				
			||||||
  pauseLog();
 | 
					 | 
				
			||||||
  logList.value = [];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getLogList() {
 | 
					 | 
				
			||||||
  if (terminalSocket.value) {
 | 
					 | 
				
			||||||
    terminalSocket.value.close();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (props.type === 'job') {
 | 
					 | 
				
			||||||
    const newSocket = initWebSocket('JOB_LOG_SCENE');
 | 
					 | 
				
			||||||
    terminalSocket.value = newSocket ?? undefined;
 | 
					 | 
				
			||||||
    terminalSocket!.value!.onopen = () => {
 | 
					 | 
				
			||||||
      const taskData = props.taskData! as Api.Job.JobTask;
 | 
					 | 
				
			||||||
      const msg = {
 | 
					 | 
				
			||||||
        taskBatchId: taskData.taskBatchId,
 | 
					 | 
				
			||||||
        taskId: taskData.id
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      terminalSocket!.value?.send(JSON.stringify(msg));
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  terminalSocket!.value!.onmessage = (event: any) => {
 | 
					 | 
				
			||||||
    if (event.data === 'END') {
 | 
					 | 
				
			||||||
      finished.value = true;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      logList.value.push(JSON.parse(event.data));
 | 
					 | 
				
			||||||
      virtualListInst.value?.scrollTo({ position: 'bottom', debounce: false });
 | 
					 | 
				
			||||||
      nextTick(() => {
 | 
					 | 
				
			||||||
        virtualListInst.value?.scrollTo({ position: 'bottom', debounce: false });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onBeforeUnmount(() => {
 | 
					 | 
				
			||||||
  stopLog();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
  () => visible.value,
 | 
					 | 
				
			||||||
  async val => {
 | 
					 | 
				
			||||||
    if (val) {
 | 
					 | 
				
			||||||
      if (props.modelValue) {
 | 
					 | 
				
			||||||
        logList.value = [...props.modelValue];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      terminalSocket?.value?.close();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if ((val || !props.drawer) && props.type && props.taskData) {
 | 
					 | 
				
			||||||
      finished.value = false;
 | 
					 | 
				
			||||||
      await getLogList();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!val && props.drawer) {
 | 
					 | 
				
			||||||
      stopLog();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  { immediate: true }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function timestampToDate(timestamp: string): string {
 | 
					 | 
				
			||||||
  const date = new Date(Number.parseInt(timestamp?.toString(), 10));
 | 
					 | 
				
			||||||
  const year = date.getFullYear();
 | 
					 | 
				
			||||||
  const month =
 | 
					 | 
				
			||||||
    (date.getMonth() + 1)?.toString().length === 1 ? `0${date.getMonth() + 1}` : (date.getMonth() + 1)?.toString();
 | 
					 | 
				
			||||||
  const day = date.getDate()?.toString().length === 1 ? `0${date.getDate()}` : date.getDate()?.toString();
 | 
					 | 
				
			||||||
  const hours = date.getHours()?.toString().length === 1 ? `0${date.getHours()}` : date.getHours()?.toString();
 | 
					 | 
				
			||||||
  const minutes = date.getMinutes()?.toString().length === 1 ? `0${date.getMinutes()}` : date.getMinutes()?.toString();
 | 
					 | 
				
			||||||
  const seconds = date.getSeconds()?.toString().length === 1 ? `0${date.getSeconds()}` : date.getSeconds()?.toString();
 | 
					 | 
				
			||||||
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${date.getMilliseconds()}`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const router = useRouter();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function openNewTab() {
 | 
					 | 
				
			||||||
  let query;
 | 
					 | 
				
			||||||
  if (props.type === 'job') {
 | 
					 | 
				
			||||||
    query = {
 | 
					 | 
				
			||||||
      type: props.type,
 | 
					 | 
				
			||||||
      taskBatchId: (props.taskData as Api.Job.JobTask).taskBatchId,
 | 
					 | 
				
			||||||
      jobId: (props.taskData as Api.Job.JobTask).jobId,
 | 
					 | 
				
			||||||
      taskId: (props.taskData as Api.Job.JobTask).id
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (props.type === 'retry') {
 | 
					 | 
				
			||||||
    query = {
 | 
					 | 
				
			||||||
      type: props.type,
 | 
					 | 
				
			||||||
      groupName: (props.taskData as Api.RetryTask.RetryTask | Api.Retry.Retry).groupName,
 | 
					 | 
				
			||||||
      retryTaskId: (props.taskData as Api.RetryTask.RetryTask).id
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const url = router.resolve({ path: '/log', query });
 | 
					 | 
				
			||||||
  window.open(url.href);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const SnailLogComponent = defineComponent({
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    if (finished.value && logList.value.length === 0) {
 | 
					 | 
				
			||||||
      return () => <NEmpty class="h-full flex-center" size="huge" />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const throwableComponent = (message: Api.JobLog.JobMessage) => {
 | 
					 | 
				
			||||||
      const throwable = message.throwable;
 | 
					 | 
				
			||||||
      if (!throwable) {
 | 
					 | 
				
			||||||
        return <></>;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const firstLine = throwable.match(/^.+/m);
 | 
					 | 
				
			||||||
      if (!firstLine) {
 | 
					 | 
				
			||||||
        return <></>;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const restOfText = throwable.replace(/^.+(\n|$)/m, '');
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        <NCollapseItem title={firstLine[0]} name={`throwable-${message.index}`}>
 | 
					 | 
				
			||||||
          <NScrollbar content-class="p-8px" class="message-scroll-body">{`${restOfText}`}</NScrollbar>
 | 
					 | 
				
			||||||
        </NCollapseItem>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const messageComponent = (message: Api.JobLog.JobMessage) => {
 | 
					 | 
				
			||||||
      const msg = message.message;
 | 
					 | 
				
			||||||
      if (!msg) {
 | 
					 | 
				
			||||||
        return <></>;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const firstLine = msg.match(/^.+/m);
 | 
					 | 
				
			||||||
      if (!firstLine) {
 | 
					 | 
				
			||||||
        return <></>;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const restOfText = msg.replace(/^.+(\n|$)/m, '').replaceAll('\n', '\n - ');
 | 
					 | 
				
			||||||
      if (restOfText) {
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          <NCollapseItem title={firstLine[0]} name={`message-${message.index}`}>
 | 
					 | 
				
			||||||
            <NScrollbar content-class="p-8px" class="message-scroll-body">{` - ${restOfText}`}</NScrollbar>
 | 
					 | 
				
			||||||
          </NCollapseItem>
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return <div class="pl-6px">- {`${msg}`}</div>;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleUpdateExpanded = (val: string[]) => {
 | 
					 | 
				
			||||||
      expandedNames.value = val;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleResize = (_: ResizeObserverEntry) => {
 | 
					 | 
				
			||||||
      expandedNames.value = [];
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return () => (
 | 
					 | 
				
			||||||
      <code class="snail-log">
 | 
					 | 
				
			||||||
        <NCollapse
 | 
					 | 
				
			||||||
          accordion
 | 
					 | 
				
			||||||
          v-model:expanded-names={expandedNames.value}
 | 
					 | 
				
			||||||
          on-update:expanded-names={handleUpdateExpanded}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <NVirtualList
 | 
					 | 
				
			||||||
            ref={virtualListInst}
 | 
					 | 
				
			||||||
            class="virtual-list"
 | 
					 | 
				
			||||||
            itemSize={85}
 | 
					 | 
				
			||||||
            paddingBottom={16}
 | 
					 | 
				
			||||||
            items={logList.value}
 | 
					 | 
				
			||||||
            scrollbarProps={{ xScrollable: true }}
 | 
					 | 
				
			||||||
            onResize={handleResize}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {{
 | 
					 | 
				
			||||||
              default: ({ item: message }: { item: Api.JobLog.JobMessage }) => (
 | 
					 | 
				
			||||||
                <pre key={message.index} class="min-h-85px min-w-full">
 | 
					 | 
				
			||||||
                  <div>
 | 
					 | 
				
			||||||
                    <span class="log-hljs-time inline-block">{timestampToDate(message.time_stamp)}</span>
 | 
					 | 
				
			||||||
                    <span
 | 
					 | 
				
			||||||
                      class={`log-hljs-level-${message.level} ml-12px mr-12px inline-block`}
 | 
					 | 
				
			||||||
                    >{`${message.level}`}</span>
 | 
					 | 
				
			||||||
                    <span class="log-hljs-thread mr-12px inline-block">{`[${message.host}:${message.port}]`}</span>
 | 
					 | 
				
			||||||
                    <span class="log-hljs-thread mr-12px inline-block">{`[${message.thread}]`}</span>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                  <div class="log-hljs-location">{`${message.location}: `}</div>
 | 
					 | 
				
			||||||
                  <div>{messageComponent(message)}</div>
 | 
					 | 
				
			||||||
                  <div>{throwableComponent(message)}</div>
 | 
					 | 
				
			||||||
                  <NDivider />
 | 
					 | 
				
			||||||
                </pre>
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          </NVirtualList>
 | 
					 | 
				
			||||||
        </NCollapse>
 | 
					 | 
				
			||||||
      </code>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <NDrawer
 | 
					 | 
				
			||||||
    v-if="drawer"
 | 
					 | 
				
			||||||
    v-model:show="visible"
 | 
					 | 
				
			||||||
    :width="isFullscreen ? '100%' : '50%'"
 | 
					 | 
				
			||||||
    display-directive="if"
 | 
					 | 
				
			||||||
    :auto-focus="false"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <NDrawerContent closable>
 | 
					 | 
				
			||||||
      <template #header>
 | 
					 | 
				
			||||||
        <div class="flex items-center justify-between" :class="`tool-header${isFullscreen ? '-full' : ''}`">
 | 
					 | 
				
			||||||
          <div class="flex-center">
 | 
					 | 
				
			||||||
            <NTooltip v-if="finished">
 | 
					 | 
				
			||||||
              <template #trigger>
 | 
					 | 
				
			||||||
                <icon-material-symbols:check-circle class="text-20px color-success" />
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
              日志加载完成
 | 
					 | 
				
			||||||
            </NTooltip>
 | 
					 | 
				
			||||||
            <NTooltip v-else>
 | 
					 | 
				
			||||||
              <template #trigger>
 | 
					 | 
				
			||||||
                <NSpin size="small">
 | 
					 | 
				
			||||||
                  <template #icon>
 | 
					 | 
				
			||||||
                    <icon-nonicons:loading-16 />
 | 
					 | 
				
			||||||
                  </template>
 | 
					 | 
				
			||||||
                </NSpin>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
              日志正在加载
 | 
					 | 
				
			||||||
            </NTooltip>
 | 
					 | 
				
			||||||
            <span class="ml-6px">{{ title }}</span>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div class="flex-center">
 | 
					 | 
				
			||||||
            <ButtonIcon
 | 
					 | 
				
			||||||
              size="tiny"
 | 
					 | 
				
			||||||
              icon="hugeicons:share-01"
 | 
					 | 
				
			||||||
              tooltip-content="在新标签页打开"
 | 
					 | 
				
			||||||
              class="ml-6px"
 | 
					 | 
				
			||||||
              @click="openNewTab"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <ButtonIcon
 | 
					 | 
				
			||||||
              size="tiny"
 | 
					 | 
				
			||||||
              :tooltip-content="isFullscreen ? '半屏' : '全屏'"
 | 
					 | 
				
			||||||
              @click="() => (isFullscreen = !isFullscreen)"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <icon-material-symbols:close-fullscreen-rounded v-if="isFullscreen" />
 | 
					 | 
				
			||||||
              <icon-material-symbols:open-in-full-rounded v-else />
 | 
					 | 
				
			||||||
            </ButtonIcon>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </template>
 | 
					 | 
				
			||||||
      <div v-if="logList.length === 0" class="empty-height flex-center">
 | 
					 | 
				
			||||||
        <NEmpty v-if="logList.length === 0 && finished" />
 | 
					 | 
				
			||||||
        <NSpin v-if="logList.length === 0 && !finished" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <SnailLogComponent v-if="logList.length > 0" />
 | 
					 | 
				
			||||||
    </NDrawerContent>
 | 
					 | 
				
			||||||
  </NDrawer>
 | 
					 | 
				
			||||||
  <NCard v-else :bordered="false" :title="title" size="small" class="h-full sm:flex-1-hidden card-wrapper">
 | 
					 | 
				
			||||||
    <template #header-extra>
 | 
					 | 
				
			||||||
      <div class="flex items-center">
 | 
					 | 
				
			||||||
        <NTooltip v-if="finished">
 | 
					 | 
				
			||||||
          <template #trigger>
 | 
					 | 
				
			||||||
            <icon-material-symbols:check-circle class="text-20px color-success" />
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          日志加载完成
 | 
					 | 
				
			||||||
        </NTooltip>
 | 
					 | 
				
			||||||
        <NTooltip v-else>
 | 
					 | 
				
			||||||
          <template #trigger>
 | 
					 | 
				
			||||||
            <NSpin size="small">
 | 
					 | 
				
			||||||
              <template #icon>
 | 
					 | 
				
			||||||
                <icon-nonicons:loading-16 />
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
            </NSpin>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          日志正在加载
 | 
					 | 
				
			||||||
        </NTooltip>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </template>
 | 
					 | 
				
			||||||
    <div v-if="logList.length === 0" class="h-full flex-center">
 | 
					 | 
				
			||||||
      <NEmpty v-if="logList.length === 0 && finished" />
 | 
					 | 
				
			||||||
      <NSpin v-if="logList.length === 0 && !finished" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <SnailLogComponent />
 | 
					 | 
				
			||||||
  </NCard>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss">
 | 
					 | 
				
			||||||
.snail-log {
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .virtual-list {
 | 
					 | 
				
			||||||
    min-height: calc(100vh - 101px);
 | 
					 | 
				
			||||||
    max-height: calc(100vh - 101px);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .v-vl {
 | 
					 | 
				
			||||||
    min-height: calc(100vh - 101px);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .v-vl-items {
 | 
					 | 
				
			||||||
    min-height: calc(100vh - 101px - 16px) !important;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .n-divider:not(.n-divider--vertical) {
 | 
					 | 
				
			||||||
    margin-top: 6px;
 | 
					 | 
				
			||||||
    margin-bottom: 6px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  pre {
 | 
					 | 
				
			||||||
    // white-space: pre-wrap;
 | 
					 | 
				
			||||||
    word-break: break-word;
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    font-size: 16px;
 | 
					 | 
				
			||||||
    color: #333639;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .log-hljs {
 | 
					 | 
				
			||||||
    &-time {
 | 
					 | 
				
			||||||
      color: #2db7f5;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-level {
 | 
					 | 
				
			||||||
      &-DEBUG {
 | 
					 | 
				
			||||||
        color: #2647cc;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &-INFO {
 | 
					 | 
				
			||||||
        color: #5c962c;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &-WARN {
 | 
					 | 
				
			||||||
        color: #da9816;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &-ERROR {
 | 
					 | 
				
			||||||
        color: #dc3f41;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-thread {
 | 
					 | 
				
			||||||
      color: #00a3a3;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &-location {
 | 
					 | 
				
			||||||
      color: #a771bf;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dark {
 | 
					 | 
				
			||||||
  .snail-log {
 | 
					 | 
				
			||||||
    background-color: #1e1f22;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pre {
 | 
					 | 
				
			||||||
      color: #ffffffe6;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped>
 | 
					 | 
				
			||||||
:deep(.n-spin) {
 | 
					 | 
				
			||||||
  height: 18px !important;
 | 
					 | 
				
			||||||
  width: 18px !important;
 | 
					 | 
				
			||||||
  font-size: 18px !important;
 | 
					 | 
				
			||||||
  margin-right: 6px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.tool-header-full {
 | 
					 | 
				
			||||||
  width: calc(100vw - 72px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.tool-header {
 | 
					 | 
				
			||||||
  width: calc(50vw - 72px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.empty-height {
 | 
					 | 
				
			||||||
  min-height: calc(100vh - 101px);
 | 
					 | 
				
			||||||
  max-height: calc(100vh - 101px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:deep(.n-collapse-item__content-inner) {
 | 
					 | 
				
			||||||
  padding-top: 0 !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:deep(.v-vl-items) {
 | 
					 | 
				
			||||||
  display: inline-block !important;
 | 
					 | 
				
			||||||
  min-width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:deep(.message-scroll-body) {
 | 
					 | 
				
			||||||
  margin-top: 6px;
 | 
					 | 
				
			||||||
  max-height: 150px;
 | 
					 | 
				
			||||||
  border: 1px solid rgb(239, 239, 245);
 | 
					 | 
				
			||||||
  border-radius: var(--n-border-radius);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
:deep(.dark .message-scroll-body) {
 | 
					 | 
				
			||||||
  border: 1px solid rgba(255, 255, 255, 0.09) !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@ -14,8 +14,12 @@ import {
 | 
				
			|||||||
} from 'naive-ui';
 | 
					} from 'naive-ui';
 | 
				
			||||||
import { defineComponent, nextTick, onBeforeUnmount, ref, watch } from 'vue';
 | 
					import { defineComponent, nextTick, onBeforeUnmount, ref, watch } from 'vue';
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import type { UseWebSocketReturn } from '@vueuse/core';
 | 
				
			||||||
 | 
					import { useWebSocket } from '@vueuse/core';
 | 
				
			||||||
import { fetchJobLogList, fetchRetryLogList } from '@/service/api/log';
 | 
					import { fetchJobLogList, fetchRetryLogList } from '@/service/api/log';
 | 
				
			||||||
import ButtonIcon from '@/components/custom/button-icon.vue';
 | 
					import ButtonIcon from '@/components/custom/button-icon.vue';
 | 
				
			||||||
 | 
					import { initWebSocketUrl } from '@/utils/websocket';
 | 
				
			||||||
 | 
					import { generateRandomString } from '@/utils/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineOptions({
 | 
					defineOptions({
 | 
				
			||||||
  name: 'LogDrawer'
 | 
					  name: 'LogDrawer'
 | 
				
			||||||
@ -25,6 +29,7 @@ interface Props {
 | 
				
			|||||||
  title?: string;
 | 
					  title?: string;
 | 
				
			||||||
  drawer?: boolean;
 | 
					  drawer?: boolean;
 | 
				
			||||||
  type?: 'job' | 'retry';
 | 
					  type?: 'job' | 'retry';
 | 
				
			||||||
 | 
					  fetchType?: 'ws' | 'http';
 | 
				
			||||||
  taskData?: Api.Job.JobTask | Api.RetryTask.RetryTask;
 | 
					  taskData?: Api.Job.JobTask | Api.RetryTask.RetryTask;
 | 
				
			||||||
  modelValue?: Api.JobLog.JobMessage[];
 | 
					  modelValue?: Api.JobLog.JobMessage[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -33,6 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
 | 
				
			|||||||
  title: undefined,
 | 
					  title: undefined,
 | 
				
			||||||
  drawer: true,
 | 
					  drawer: true,
 | 
				
			||||||
  type: 'job',
 | 
					  type: 'job',
 | 
				
			||||||
 | 
					  fetchType: 'ws',
 | 
				
			||||||
  taskData: undefined,
 | 
					  taskData: undefined,
 | 
				
			||||||
  modelValue: () => []
 | 
					  modelValue: () => []
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -47,6 +53,7 @@ const expandedNames = ref<string[]>([]);
 | 
				
			|||||||
const virtualListInst = ref<VirtualListInst>();
 | 
					const virtualListInst = ref<VirtualListInst>();
 | 
				
			||||||
const syncTime = ref(1);
 | 
					const syncTime = ref(1);
 | 
				
			||||||
const logList = ref<Api.JobLog.JobMessage[]>([]);
 | 
					const logList = ref<Api.JobLog.JobMessage[]>([]);
 | 
				
			||||||
 | 
					const websocket = ref<UseWebSocketReturn<any>>();
 | 
				
			||||||
const interval = ref<NodeJS.Timeout>();
 | 
					const interval = ref<NodeJS.Timeout>();
 | 
				
			||||||
let controller = new AbortController();
 | 
					let controller = new AbortController();
 | 
				
			||||||
const finished = ref<boolean>(true);
 | 
					const finished = ref<boolean>(true);
 | 
				
			||||||
@ -59,7 +66,11 @@ const pauseLog = () => {
 | 
				
			|||||||
  interval.value = undefined;
 | 
					  interval.value = undefined;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const stopLog = () => {
 | 
					const stopLogByWs = () => {
 | 
				
			||||||
 | 
					  websocket.value?.close();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const stopLogByHttp = async () => {
 | 
				
			||||||
  if (!finished.value) controller.abort();
 | 
					  if (!finished.value) controller.abort();
 | 
				
			||||||
  pauseLog();
 | 
					  pauseLog();
 | 
				
			||||||
  startId = '0';
 | 
					  startId = '0';
 | 
				
			||||||
@ -67,7 +78,46 @@ const stopLog = () => {
 | 
				
			|||||||
  logList.value = [];
 | 
					  logList.value = [];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const stopLog = () => {
 | 
				
			||||||
 | 
					  if (props.fetchType === 'http') {
 | 
				
			||||||
 | 
					    stopLogByHttp();
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  stopLogByWs();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getLogList() {
 | 
					async function getLogList() {
 | 
				
			||||||
 | 
					  if (props.fetchType === 'http') {
 | 
				
			||||||
 | 
					    await getLogListByHttp();
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  getLogListByWs();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getLogListByWs() {
 | 
				
			||||||
 | 
					  finished.value = false;
 | 
				
			||||||
 | 
					  logList.value = [];
 | 
				
			||||||
 | 
					  websocket.value?.open();
 | 
				
			||||||
 | 
					  if (props.type === 'job') {
 | 
				
			||||||
 | 
					    const taskData = props.taskData! as Api.Job.JobTask;
 | 
				
			||||||
 | 
					    const msg = {
 | 
				
			||||||
 | 
					      taskBatchId: taskData.taskBatchId,
 | 
				
			||||||
 | 
					      taskId: taskData.id
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    websocket.value?.send(JSON.stringify(msg));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (props.type === 'retry') {
 | 
				
			||||||
 | 
					    const taskData = props.taskData! as Api.RetryTask.RetryTask;
 | 
				
			||||||
 | 
					    const msg = {
 | 
				
			||||||
 | 
					      groupName: taskData.groupName,
 | 
				
			||||||
 | 
					      retryTaskId: taskData.id
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    websocket.value?.send(JSON.stringify(msg));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getLogListByHttp() {
 | 
				
			||||||
  clearTimeout(interval.value);
 | 
					  clearTimeout(interval.value);
 | 
				
			||||||
  let logData = null;
 | 
					  let logData = null;
 | 
				
			||||||
  let logError;
 | 
					  let logError;
 | 
				
			||||||
@ -110,7 +160,12 @@ async function getLogList() {
 | 
				
			|||||||
      logList.value.push(...logData.message);
 | 
					      logList.value.push(...logData.message);
 | 
				
			||||||
      logList.value
 | 
					      logList.value
 | 
				
			||||||
        .sort((a, b) => Number.parseInt(a.time_stamp, 10) - Number.parseInt(b.time_stamp, 10))
 | 
					        .sort((a, b) => Number.parseInt(a.time_stamp, 10) - Number.parseInt(b.time_stamp, 10))
 | 
				
			||||||
        .forEach((item, index) => (item.index = index));
 | 
					        .forEach((item, index) => {
 | 
				
			||||||
 | 
					          item.index = index;
 | 
				
			||||||
 | 
					          if (!item.key) {
 | 
				
			||||||
 | 
					            item.key = `${item.time_stamp}-${generateRandomString(16)}`;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    nextTick(() => {
 | 
					    nextTick(() => {
 | 
				
			||||||
      if (isAutoScroll.value) virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true });
 | 
					      if (isAutoScroll.value) virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true });
 | 
				
			||||||
@ -162,22 +217,58 @@ watch(
 | 
				
			|||||||
  () => visible.value,
 | 
					  () => visible.value,
 | 
				
			||||||
  async val => {
 | 
					  async val => {
 | 
				
			||||||
    if (val) {
 | 
					    if (val) {
 | 
				
			||||||
 | 
					      logList.value = [];
 | 
				
			||||||
      if (props.modelValue) {
 | 
					      if (props.modelValue) {
 | 
				
			||||||
        logList.value = [...props.modelValue];
 | 
					        logList.value = [...props.modelValue];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ((val || !props.drawer) && props.type && props.taskData) {
 | 
					 | 
				
			||||||
      finished.value = false;
 | 
					 | 
				
			||||||
      controller = new AbortController();
 | 
					 | 
				
			||||||
      await getLogList();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!val && props.drawer) {
 | 
					    if (!val && props.drawer) {
 | 
				
			||||||
      stopLog();
 | 
					      stopLog();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					
 | 
				
			||||||
  { immediate: true }
 | 
					    if (((val && props.drawer) || !props.drawer) && props.type && props.taskData) {
 | 
				
			||||||
 | 
					      finished.value = false;
 | 
				
			||||||
 | 
					      controller = new AbortController();
 | 
				
			||||||
 | 
					      if (props.fetchType === 'ws') {
 | 
				
			||||||
 | 
					        const url = initWebSocketUrl('JOB_LOG_SCENE', props.taskData.id);
 | 
				
			||||||
 | 
					        if (!url) {
 | 
				
			||||||
 | 
					          window.$message?.error('Token 失效');
 | 
				
			||||||
 | 
					          visible.value = false;
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        websocket.value = useWebSocket(url, {
 | 
				
			||||||
 | 
					          immediate: false,
 | 
				
			||||||
 | 
					          autoConnect: false,
 | 
				
			||||||
 | 
					          autoReconnect: {
 | 
				
			||||||
 | 
					            // 重连最大次数
 | 
				
			||||||
 | 
					            retries: 3,
 | 
				
			||||||
 | 
					            // 重连间隔
 | 
				
			||||||
 | 
					            delay: 1000,
 | 
				
			||||||
 | 
					            onFailed() {
 | 
				
			||||||
 | 
					              window.$message?.error('websocket 连接失败');
 | 
				
			||||||
 | 
					              visible.value = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onMessage: (_, e) => {
 | 
				
			||||||
 | 
					            if (e.data !== 'END') {
 | 
				
			||||||
 | 
					              const data = JSON.parse(e.data) as Api.JobLog.JobMessage;
 | 
				
			||||||
 | 
					              data.key = `${data.time_stamp}-${generateRandomString(16)}`;
 | 
				
			||||||
 | 
					              logList.value.push(data);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              finished.value = true;
 | 
				
			||||||
 | 
					              stopLogByWs();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        getLogListByWs();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await getLogList();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function timestampToDate(timestamp: string): string {
 | 
					function timestampToDate(timestamp: string): string {
 | 
				
			||||||
@ -284,7 +375,7 @@ const SnailLogComponent = defineComponent({
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      const restOfText = throwable.replace(/^.+(\n|$)/m, '');
 | 
					      const restOfText = throwable.replace(/^.+(\n|$)/m, '');
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <NCollapseItem title={firstLine[0]} name={`throwable-${message.index}`}>
 | 
					        <NCollapseItem title={firstLine[0]} name={`throwable-${message.key}`}>
 | 
				
			||||||
          <NScrollbar content-class="p-8px" class="message-scroll-body">{`${restOfText}`}</NScrollbar>
 | 
					          <NScrollbar content-class="p-8px" class="message-scroll-body">{`${restOfText}`}</NScrollbar>
 | 
				
			||||||
        </NCollapseItem>
 | 
					        </NCollapseItem>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@ -302,7 +393,7 @@ const SnailLogComponent = defineComponent({
 | 
				
			|||||||
      const restOfText = msg.replace(/^.+(\n|$)/m, '').replaceAll('\n', '\n - ');
 | 
					      const restOfText = msg.replace(/^.+(\n|$)/m, '').replaceAll('\n', '\n - ');
 | 
				
			||||||
      if (restOfText) {
 | 
					      if (restOfText) {
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
          <NCollapseItem title={firstLine[0]} name={`message-${message.index}`}>
 | 
					          <NCollapseItem title={firstLine[0]} name={`message-${message.key}`}>
 | 
				
			||||||
            <NScrollbar content-class="p-8px" class="message-scroll-body">{` - ${restOfText}`}</NScrollbar>
 | 
					            <NScrollbar content-class="p-8px" class="message-scroll-body">{` - ${restOfText}`}</NScrollbar>
 | 
				
			||||||
          </NCollapseItem>
 | 
					          </NCollapseItem>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
@ -336,8 +427,8 @@ const SnailLogComponent = defineComponent({
 | 
				
			|||||||
            onResize={handleResize}
 | 
					            onResize={handleResize}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            {{
 | 
					            {{
 | 
				
			||||||
              default: ({ item: message }: { item: Api.JobLog.JobMessage }) => (
 | 
					              default: ({ item: message }: { item: Api.JobLog.JobMessage; index: number }) => (
 | 
				
			||||||
                <pre key={message.index} class="min-h-85px min-w-full">
 | 
					                <pre key={message.key} class="min-h-85px min-w-full">
 | 
				
			||||||
                  <div>
 | 
					                  <div>
 | 
				
			||||||
                    <span class="log-hljs-time inline-block">{timestampToDate(message.time_stamp)}</span>
 | 
					                    <span class="log-hljs-time inline-block">{timestampToDate(message.time_stamp)}</span>
 | 
				
			||||||
                    <span
 | 
					                    <span
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref, watch } from 'vue';
 | 
					import { ref, watch } from 'vue';
 | 
				
			||||||
import { contentTypeRecord } from '@/constants/business';
 | 
					import { contentTypeRecord, workFlowNodeStatusRecord } from '@/constants/business';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineOptions({
 | 
					defineOptions({
 | 
				
			||||||
  name: 'CallbackDetail'
 | 
					  name: 'CallbackDetail'
 | 
				
			||||||
@ -48,6 +48,9 @@ const onClose = () => {
 | 
				
			|||||||
      <NDescriptionsItem label="密钥">
 | 
					      <NDescriptionsItem label="密钥">
 | 
				
			||||||
        {{ modelValue.callback?.secret }}
 | 
					        {{ modelValue.callback?.secret }}
 | 
				
			||||||
      </NDescriptionsItem>
 | 
					      </NDescriptionsItem>
 | 
				
			||||||
 | 
					      <NDescriptionsItem label="回调通知状态">
 | 
				
			||||||
 | 
					        {{ $t(workFlowNodeStatusRecord[modelValue.workflowNodeStatus!]) }}
 | 
				
			||||||
 | 
					      </NDescriptionsItem>
 | 
				
			||||||
    </NDescriptions>
 | 
					    </NDescriptions>
 | 
				
			||||||
  </DetailDrawer>
 | 
					  </DetailDrawer>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,7 @@ const getTaskName = (id: string) => {
 | 
				
			|||||||
      <NDescriptionsItem label="失败策略">
 | 
					      <NDescriptionsItem label="失败策略">
 | 
				
			||||||
        {{ $t(failStrategyRecord[modelValue.failStrategy!]) }}
 | 
					        {{ $t(failStrategyRecord[modelValue.failStrategy!]) }}
 | 
				
			||||||
      </NDescriptionsItem>
 | 
					      </NDescriptionsItem>
 | 
				
			||||||
      <NDescriptionsItem label="工作流状态">
 | 
					      <NDescriptionsItem label="任务状态">
 | 
				
			||||||
        {{ $t(workFlowNodeStatusRecord[modelValue.workflowNodeStatus!]) }}
 | 
					        {{ $t(workFlowNodeStatusRecord[modelValue.workflowNodeStatus!]) }}
 | 
				
			||||||
      </NDescriptionsItem>
 | 
					      </NDescriptionsItem>
 | 
				
			||||||
    </NDescriptions>
 | 
					    </NDescriptions>
 | 
				
			||||||
 | 
				
			|||||||
@ -98,8 +98,8 @@ const rules: FormRules = {
 | 
				
			|||||||
        </NFormItem>
 | 
					        </NFormItem>
 | 
				
			||||||
        <NFormItem
 | 
					        <NFormItem
 | 
				
			||||||
          name="workflowNodeStatus"
 | 
					          name="workflowNodeStatus"
 | 
				
			||||||
          label="工作流状态"
 | 
					          label="回调通知状态"
 | 
				
			||||||
          :rules="[{ required: true, message: '请选择工作流状态', trigger: 'change' }]"
 | 
					          :rules="[{ required: true, message: '请选择回调通知状态', trigger: 'change' }]"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <NRadioGroup v-model:value="form.workflowNodeStatus">
 | 
					          <NRadioGroup v-model:value="form.workflowNodeStatus">
 | 
				
			||||||
            <NSpace>
 | 
					            <NSpace>
 | 
				
			||||||
 | 
				
			|||||||
@ -141,7 +141,7 @@ const jobTaskChange = (_: string, option: { label: string; value: number }) => {
 | 
				
			|||||||
            </NSpace>
 | 
					            </NSpace>
 | 
				
			||||||
          </NRadioGroup>
 | 
					          </NRadioGroup>
 | 
				
			||||||
        </NFormItem>
 | 
					        </NFormItem>
 | 
				
			||||||
        <NFormItem path="workflowNodeStatus" label="节点状态">
 | 
					        <NFormItem path="workflowNodeStatus" label="任务状态">
 | 
				
			||||||
          <NRadioGroup v-model:value="form.workflowNodeStatus">
 | 
					          <NRadioGroup v-model:value="form.workflowNodeStatus">
 | 
				
			||||||
            <NSpace>
 | 
					            <NSpace>
 | 
				
			||||||
              <NRadio
 | 
					              <NRadio
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								src/typings/api.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/typings/api.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1350,6 +1350,7 @@ declare namespace Api {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    type JobMessage = {
 | 
					    type JobMessage = {
 | 
				
			||||||
      index: number;
 | 
					      index: number;
 | 
				
			||||||
 | 
					      key: string;
 | 
				
			||||||
      level: JobLevel;
 | 
					      level: JobLevel;
 | 
				
			||||||
      host: string;
 | 
					      host: string;
 | 
				
			||||||
      port: string;
 | 
					      port: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -274,3 +274,19 @@ export function debounce(func: () => any, wait: number, immediate?: boolean) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 生成 随机数
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param length - 长度
 | 
				
			||||||
 | 
					 * @returns 随机数
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function generateRandomString(length: number) {
 | 
				
			||||||
 | 
					  const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 | 
				
			||||||
 | 
					  let randomString = '';
 | 
				
			||||||
 | 
					  for (let i = 0; i < length; i += 1) {
 | 
				
			||||||
 | 
					    const randomNumber = Math.floor(Math.random() * chars.length);
 | 
				
			||||||
 | 
					    randomString += chars.substring(randomNumber, randomNumber + 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return randomString;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,28 +1,24 @@
 | 
				
			|||||||
/** socket 通信 */
 | 
					/** socket 通信 */
 | 
				
			||||||
import { getServiceBaseURL } from '@/utils/service';
 | 
					import { getServiceBaseURL } from '@/utils/service';
 | 
				
			||||||
import { localStg } from './storage';
 | 
					import { localStg } from './storage';
 | 
				
			||||||
const { baseURL } = getServiceBaseURL(import.meta.env, false);
 | 
					import { generateRandomString } from './common';
 | 
				
			||||||
const url = baseURL.replace('http://', 'ws://');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** 生成 token */
 | 
					/**
 | 
				
			||||||
function generateToken(length: number) {
 | 
					 * 初始化 websocket
 | 
				
			||||||
  const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 | 
					 *
 | 
				
			||||||
  let token = 'SID_';
 | 
					 * @param scene - 场景
 | 
				
			||||||
  for (let i = 0; i < length; i += 1) {
 | 
					 * @param sid - 会话 id
 | 
				
			||||||
    const randomNumber = Math.floor(Math.random() * chars.length);
 | 
					 * @returns
 | 
				
			||||||
    token += chars.substring(randomNumber, randomNumber + 1);
 | 
					 */
 | 
				
			||||||
  }
 | 
					export function initWebSocketUrl(scene: string, sid?: string) {
 | 
				
			||||||
  return token;
 | 
					  const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
 | 
				
			||||||
}
 | 
					  const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
 | 
				
			||||||
 | 
					  const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
 | 
				
			||||||
// 初始化socket
 | 
					  const url =
 | 
				
			||||||
export function initWebSocket(scene: string) {
 | 
					    import.meta.env.MODE === 'test' ? import.meta.env.VITE_SERVICE_BASE_URL : protocol + window.location.host + baseURL;
 | 
				
			||||||
  const token = localStg.get('token');
 | 
					  const token = localStg.get('token');
 | 
				
			||||||
  if (import.meta.env.VITE_APP_WEBSOCKET === 'N' || !token) {
 | 
					  if (!token) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  return `${url}/websocket?Snail-Job-Auth=${token}&sid=${sid ?? generateRandomString(32)}&scene=${scene}`;
 | 
				
			||||||
  const sid = generateToken(32);
 | 
					 | 
				
			||||||
  // 初始化 websocket
 | 
					 | 
				
			||||||
  return new WebSocket(`${url}/websocket?Snail-Job-Auth=${token}&sid=${sid}&scene=${scene}`);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ const title = computed(() => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="h-full">
 | 
					  <div class="h-full">
 | 
				
			||||||
    <LogDrawerWs :drawer="false" :title="title" :type="type" :task-data="taskData" />
 | 
					    <LogDrawer :drawer="false" :title="title" :type="type" :task-data="taskData" />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -75,7 +75,7 @@ async function retry() {
 | 
				
			|||||||
    </NTabs>
 | 
					    </NTabs>
 | 
				
			||||||
  </DetailDrawer>
 | 
					  </DetailDrawer>
 | 
				
			||||||
  <!--  <LogDrawer v-model:show="logShow" :title="$t('page.log.title')" :task-data="taskData" />-->
 | 
					  <!--  <LogDrawer v-model:show="logShow" :title="$t('page.log.title')" :task-data="taskData" />-->
 | 
				
			||||||
  <LogDrawerWs v-model:show="logShow" :title="$t('page.log.title')" :task-data="taskData" />
 | 
					  <LogDrawer v-model:show="logShow" :title="$t('page.log.title')" :task-data="taskData" />
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user