Kaynağa Gözat

feat(views): 添加“配置协议”页面

wangcong 3 ay önce
ebeveyn
işleme
2fe4a510ce

BIN
src/assets/img/file-black.png


BIN
src/assets/img/file-white.png


+ 16 - 0
src/constants/index.ts

@@ -5,3 +5,19 @@ export const enum LanguageType {
   ZH = 'zh',
   EN = 'en',
 }
+
+/**
+ * 协议类型
+ */
+export const enum ProtocolType {
+  ModbusRTU,
+  S7,
+}
+
+/**
+ * 协议配置方式
+ */
+export const enum ProtocolConfigMethod {
+  ImportFromTemplate,
+  ManuallyCreate,
+}

+ 26 - 1
src/i18n/locales/zh.json

@@ -1,8 +1,14 @@
 {
   "common": {
     "cancel": "取消",
+    "finishSetup": "完成配置",
+    "needHelp": "需要帮助?",
+    "nextStep": "下一步",
+    "plzSelect": "请选择",
+    "return": "返回",
     "skip": "跳过",
     "tip": "提示",
+    "viewDoc": "查看文档",
     "warning": "警告"
   },
   "createCustomer": {
@@ -22,6 +28,25 @@
     "registerGateway": "注册网关"
   },
   "setupProtocol": {
-    "setupProtocol": "配置协议"
+    "canNotFindProtocolType": "没有找到我的协议类型",
+    "clickToDownloadTemplate": "点击下载模板",
+    "clickToUpload": "填写必填项后,点击下方区域上传",
+    "downloadTemplate": "下载模板",
+    "howToJudgeProtocolType": "如何判断协议类型",
+    "importFromTemplate": "从模板导入",
+    "manuallyCreate": "手动创建",
+    "protocolType": "协议类型",
+    "recognizeProgress": "识别中",
+    "recognizeTip": "预计 {time} 秒后完成识别",
+    "recognizing": "正在识别......",
+    "selectConfigMethod": "选择配置方式",
+    "selectConfigMethodTip": "推荐使用模板导入,支持语义识别、自动填充序号、自动填充非必填项;参数较少的协议可选择手动创建",
+    "selectProtocolType": "选择协议类型",
+    "selectProtocolTypeTip": "目前仅支持 modbusRTU 和 S7 协议,协议类型在完成配置后无法修改,请谨慎选择",
+    "setupProtocol": "配置协议",
+    "uploadFile": "上传文件",
+    "uploadFileFormat": "支持扩展名:.xls,xlsx",
+    "uploadFileTip": "单击或拖动文件到此区域以上传",
+    "waitingForRecognition": "等待识别"
   }
 }

+ 173 - 1
src/layout/UseGuidance.vue

@@ -1,3 +1,175 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+
+import { t } from '@/i18n';
+import { addUnit } from '@/utils';
+
+import type { Component, CSSProperties } from 'vue';
+import type { UseGuideStepItem } from '@/types';
+
+interface Props {
+  title: string;
+  steps: UseGuideStepItem[];
+  contentOffset?: number;
+}
+
+const props = defineProps<Props>();
+
+const current = ref(0);
+
+const isFirstStep = computed(() => {
+  return current.value === 0;
+});
+
+const isLastStep = computed(() => {
+  return current.value === props.steps.length - 1;
+});
+
+const nextStepButtonText = computed(() => {
+  return isLastStep.value ? t('common.finishSetup') : t('common.nextStep');
+});
+
+const currentComponent = computed<Component>(() => {
+  return props.steps[current.value].component;
+});
+
+const contentStyle = computed<CSSProperties>(() => {
+  return {
+    paddingLeft: addUnit(props.contentOffset || 312),
+  };
+});
+
+const goNextStep = () => {
+  current.value++;
+};
+
+const goPrevStep = () => {
+  current.value--;
+};
+
+const finishCurrentStep = () => {
+  if (isLastStep.value) {
+    return;
+  }
+
+  goNextStep();
+};
+</script>
+
 <template>
-  <slot></slot>
+  <ALayout class="use-guide-container">
+    <ALayoutSider class="use-guide-sider" :width="268">
+      <div class="use-guide-title">{{ title }}</div>
+      <div class="use-guide-steps">
+        <ASteps v-model:current="current" :items="steps" direction="vertical" />
+      </div>
+      <AButton class="use-guide-doc-button">
+        <img src="@/assets/img/file-black.png" />
+        {{ $t('common.viewDoc') }}
+      </AButton>
+    </ALayoutSider>
+    <ALayout>
+      <ALayoutContent class="use-guide-main" :style="contentStyle">
+        <Component :is="currentComponent" />
+      </ALayoutContent>
+      <ALayoutFooter class="use-guide-footer">
+        <AButton type="text" :disabled="isLastStep" @click="goNextStep">{{ $t('common.skip') }}</AButton>
+        <AButton type="text" :disabled="isFirstStep" @click="goPrevStep">{{ $t('common.return') }}</AButton>
+        <AButton type="primary" @click="finishCurrentStep">{{ nextStepButtonText }}</AButton>
+      </ALayoutFooter>
+    </ALayout>
+  </ALayout>
 </template>
+
+<style lang="scss" scoped>
+.use-guide-container {
+  height: 100%;
+}
+
+.use-guide-sider {
+  padding-top: 46px;
+  padding-bottom: 34px;
+  background: #f0f0f0;
+
+  :deep(.ant-layout-sider-children) {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .use-guide-title {
+    margin-bottom: 26px;
+    text-align: center;
+  }
+
+  .use-guide-steps {
+    flex: 1;
+  }
+
+  :deep(.ant-steps) {
+    .ant-steps-item {
+      height: 80px;
+    }
+
+    .ant-steps-item-wait {
+      .ant-steps-item-icon {
+        background-color: initial;
+        border-color: var(--antd-color-fill);
+      }
+
+      .ant-steps-icon {
+        color: var(--antd-color-text-quaternary);
+      }
+    }
+
+    .ant-steps-item-process {
+      .ant-steps-item-title {
+        font-weight: 500;
+      }
+    }
+
+    .ant-steps-item-finish {
+      .ant-steps-item-icon {
+        background-color: initial;
+        border-color: var(--antd-color-primary);
+      }
+
+      .ant-steps-item-title {
+        color: var(--antd-color-text-secondary);
+      }
+    }
+
+    .ant-steps-item-process,
+    .ant-steps-item-wait {
+      .ant-steps-item-tail::after {
+        background-color: var(--antd-color-fill);
+      }
+    }
+  }
+
+  .use-guide-doc-button {
+    display: flex;
+    align-items: center;
+
+    img {
+      margin-right: 10px;
+    }
+  }
+}
+
+.use-guide-main {
+  padding-top: 60px;
+  overflow: auto;
+  background: var(--antd-color-bg-base);
+}
+
+.use-guide-footer {
+  padding-right: 211px;
+  padding-left: 187px;
+  background: var(--antd-color-bg-base);
+
+  button:last-of-type {
+    float: right;
+  }
+}
+</style>

+ 7 - 0
src/types/index.ts

@@ -0,0 +1,7 @@
+import type { Component } from 'vue';
+import type { StepProps } from 'ant-design-vue';
+
+export interface UseGuideStepItem extends StepProps {
+  title: string;
+  component: Component;
+}

+ 4 - 0
src/utils/index.ts

@@ -18,3 +18,7 @@ export const generateThemeCSSVar = (token: GlobalToken, prefix: string): string
 
   return `:root{${cssVar.join(';')}}`;
 };
+
+export const addUnit = (val: number, unit: string = 'px'): string => {
+  return val + unit;
+};

+ 20 - 0
src/views/setup-protocol/SelectConfigMethod.vue

@@ -0,0 +1,20 @@
+<script setup lang="ts">
+import { ProtocolConfigMethod } from '@/constants';
+</script>
+
+<template>
+  <div>
+    <div class="use-guide-title">{{ $t('setupProtocol.selectConfigMethod') }}</div>
+    <div class="use-guide-description config-description">{{ $t('setupProtocol.selectConfigMethodTip') }}</div>
+    <ARadioGroup>
+      <ARadio :value="ProtocolConfigMethod.ImportFromTemplate">{{ $t('setupProtocol.importFromTemplate') }}</ARadio>
+      <ARadio :value="ProtocolConfigMethod.ManuallyCreate">{{ $t('setupProtocol.manuallyCreate') }}</ARadio>
+    </ARadioGroup>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.config-description {
+  margin-bottom: 58px;
+}
+</style>

+ 38 - 0
src/views/setup-protocol/SelectProtocolType.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import { ProtocolType } from '@/constants';
+</script>
+
+<template>
+  <div>
+    <div class="use-guide-title">{{ $t('setupProtocol.selectProtocolType') }}</div>
+    <div class="use-guide-description">{{ $t('setupProtocol.selectProtocolTypeTip') }}</div>
+    <AFormItem :label="$t('setupProtocol.protocolType')" name="protocolType">
+      <ASelect class="protocol-select" :placeholder="$t('common.plzSelect')">
+        <ASelectOption :value="ProtocolType.ModbusRTU">modbusRTU</ASelectOption>
+        <ASelectOption :value="ProtocolType.S7">S7</ASelectOption>
+      </ASelect>
+    </AFormItem>
+    <div class="protocol-help">{{ $t('common.needHelp') }}</div>
+    <AButton>{{ $t('setupProtocol.howToJudgeProtocolType') }}</AButton>
+    <AButton>{{ $t('setupProtocol.canNotFindProtocolType') }}</AButton>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.protocol-select {
+  width: 224px;
+}
+
+.protocol-help {
+  margin-top: 116px;
+  margin-bottom: 7px;
+  font-size: 16px;
+  font-weight: 500;
+  line-height: 24px;
+  color: var(--antd-color-text);
+}
+
+button + button {
+  margin-left: 7px;
+}
+</style>

+ 32 - 1
src/views/setup-protocol/SetupProtocol.vue

@@ -1,3 +1,34 @@
+<script setup lang="ts">
+import UseGuidance from '@/layout/UseGuidance.vue';
+import { t } from '@/i18n';
+
+import SelectConfigMethod from './SelectConfigMethod.vue';
+import SelectProtocolType from './SelectProtocolType.vue';
+import UploadProtocolFile from './UploadProtocolFile.vue';
+import WaitingRecognition from './WaitingRecognition.vue';
+
+import type { UseGuideStepItem } from '@/types';
+
+const steps: UseGuideStepItem[] = [
+  {
+    title: t('setupProtocol.selectProtocolType'),
+    component: SelectProtocolType,
+  },
+  {
+    title: t('setupProtocol.selectConfigMethod'),
+    component: SelectConfigMethod,
+  },
+  {
+    title: t('setupProtocol.uploadFile'),
+    component: UploadProtocolFile,
+  },
+  {
+    title: t('setupProtocol.waitingForRecognition'),
+    component: WaitingRecognition,
+  },
+];
+</script>
+
 <template>
-  <div>配置协议</div>
+  <UseGuidance :title="$t('setupProtocol.setupProtocol')" :steps="steps" />
 </template>

+ 52 - 0
src/views/setup-protocol/UploadProtocolFile.vue

@@ -0,0 +1,52 @@
+<template>
+  <div>
+    <div class="use-guide-title upload-title">{{ $t('setupProtocol.uploadFile') }}</div>
+    <div class="upload-step-text">1. {{ $t('setupProtocol.clickToDownloadTemplate') }}</div>
+    <AButton class="upload-template-button" type="primary">
+      <img src="@/assets/img/file-white.png" />
+      {{ $t('setupProtocol.downloadTemplate') }}
+    </AButton>
+    <div class="upload-step-text">2. {{ $t('setupProtocol.clickToUpload') }}</div>
+    <AUploadDragger class="upload-drag-area">
+      <p class="ant-upload-drag-icon">
+        <un-i-mdi-folder-open class="upload-folder-icon" />
+      </p>
+      <p class="ant-upload-text">{{ $t('setupProtocol.uploadFileTip') }}</p>
+      <p class="ant-upload-hint">{{ $t('setupProtocol.uploadFileFormat') }}</p>
+    </AUploadDragger>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.upload-title {
+  margin-bottom: 51px;
+}
+
+.upload-step-text {
+  margin-bottom: 8px;
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--antd-color-text-secondary);
+}
+
+.upload-template-button {
+  display: flex;
+  align-items: center;
+  margin-bottom: 52px;
+
+  img {
+    margin-right: 10px;
+  }
+}
+
+.upload-drag-area {
+  :deep(.ant-upload-drag) {
+    width: 617px;
+  }
+}
+
+.upload-folder-icon {
+  font-size: 40px;
+  color: #269cf8;
+}
+</style>

+ 21 - 0
src/views/setup-protocol/WaitingRecognition.vue

@@ -0,0 +1,21 @@
+<template>
+  <div>
+    <div class="use-guide-title">{{ $t('setupProtocol.recognizing') }}</div>
+    <div class="recognize-tip">{{ $t('setupProtocol.recognizeTip', { time: 90 }) }}</div>
+    <AProgress class="recognize-progress" :percent="40" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.recognize-tip {
+  margin-top: 51px;
+  margin-bottom: 19px;
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--antd-color-text-secondary);
+}
+
+.recognize-progress {
+  max-width: 755px;
+}
+</style>