Ver código fonte

perf(views): 初步编写"创建组织"步骤页面

wangshun 3 semanas atrás
pai
commit
b7f6f4a9b2

+ 81 - 1
src/views/create-customer/CreateCustomer.vue

@@ -1,3 +1,83 @@
+<script setup lang="ts">
+import { computed, reactive, ref, shallowRef } from 'vue';
+import dayjs from 'dayjs';
+
+import UseGuidance from '@/layout/UseGuidance.vue';
+import CreateAccount from '@/views/create-customer/CreateAccount.vue';
+import CreateCharacter from '@/views/create-customer/CreateCharacter.vue';
+import EstablishOrganization from '@/views/create-customer/EstablishOrganization.vue';
+import { t } from '@/i18n';
+
+import type { CreateCustomer, FormRules, RegisterGatewayForm, UseGuideStepItem } from '@/types';
+
+const useForm = reactive<CreateCustomer>({
+  orgName: '',
+  logo: '',
+  themeColor: '#32bac0',
+  id: undefined,
+  imageUrl: '',
+  selectedColor: '#e2550d',
+  leaseTerm: [dayjs(), dayjs()],
+  stationsNumber: 0,
+  dataValidityPeriod: '',
+});
+
+const rules = computed<FormRules<RegisterGatewayForm>>(() => {
+  return {
+    orgName: [
+      {
+        required: true,
+        message: '不能为空',
+        trigger: 'change',
+      },
+    ],
+    leaseTerm: [
+      {
+        required: true,
+        message: '不能为空',
+        trigger: 'change',
+      },
+    ],
+    stationsNumber: [
+      {
+        required: true,
+        message: '不能为空',
+        trigger: 'change',
+      },
+    ],
+    dataValidityPeriod: [
+      {
+        required: true,
+        message: '不能为空',
+        trigger: 'change',
+      },
+    ],
+  };
+});
+
+const steps = ref<UseGuideStepItem[]>([
+  {
+    title: t('createCustomer.establishOrganization'),
+    component: shallowRef(EstablishOrganization),
+    stepDescription: '可以自定义二级域名、Logo及主题色,以后可随时修改',
+    labelAlign: 'left',
+    labelCol: { span: 2 },
+  },
+  {
+    title: t('createCustomer.createCharacter'),
+    component: shallowRef(CreateCharacter),
+    stepDescription: '可以直接使用默认角色,也可以自定义角色,权限后续可随时编辑',
+  },
+  {
+    title: t('createCustomer.createAccount'),
+    component: shallowRef(CreateAccount),
+    stepDescription: '创建账号,并为账号选择角色类型',
+    nextStepButtonText: t('common.finishSetup'),
+    isLastStep: true,
+  },
+]);
+</script>
+
 <template>
-  <div>创建客户</div>
+  <UseGuidance :title="$t('createCustomer.createCustomer')" :steps="steps" :form="useForm" :rules="rules" />
 </template>

+ 588 - 0
src/views/create-customer/EstablishOrganization.vue

@@ -0,0 +1,588 @@
+<script setup lang="ts">
+import { inject, onMounted, ref } from 'vue';
+import { ColorPicker } from 'vue-color-kit';
+import { message } from 'ant-design-vue';
+
+import SvgIcon from '@/components/SvgIcon.vue';
+import { useDictData } from '@/hooks/dict-data';
+import { useRequest } from '@/hooks/request';
+import { t } from '@/i18n';
+import { addOrganization, addUploadLogo, getInfoListByOrgId, groupList, updateOrganization } from '@/api';
+import { DictCode } from '@/constants';
+import { SET_COLOR_PRIMARY } from '@/constants/inject-key';
+
+import EquipmentLimitations from './EquipmentLimitations.vue';
+
+import type {
+  CreateCustomer,
+  EquipmentLimitationsItem,
+  EquipmentTypeItem,
+  OrgDeviceLimit,
+  UseGuideStepItemExpose,
+  UseGuideStepItemProps,
+} from '@/types';
+
+import 'vue-color-kit/dist/vue-color-kit.css';
+
+// 引入样式文件
+interface ColorStyle {
+  background: string;
+  border: string;
+  index: number;
+  color: string;
+}
+
+interface NewColor {
+  hex: string;
+}
+
+const { dictData: dataValidityPeriod, getDictData: getDataValidityPeriod } = useDictData(DictCode.DataValidityPeriod);
+const equipmentLimitationsRefs = ref<InstanceType<typeof EquipmentLimitations>[]>([]);
+const equipmentLimitationsList = ref<EquipmentLimitationsItem[]>([
+  {
+    deviceGlobalId: undefined,
+    upperLimit: 0,
+  },
+]);
+const orgDeviceLimits = ref<OrgDeviceLimit[]>([]);
+const setColorPrimary = inject(SET_COLOR_PRIMARY, undefined);
+const fileList = ref([]);
+const fileImg = ref<Blob>();
+const customizationColor = ref<boolean>(false);
+const { handleRequest } = useRequest();
+const equipmentType = ref<EquipmentTypeItem[]>([]);
+
+const props = defineProps<UseGuideStepItemProps<CreateCustomer>>();
+const colorStyle: ColorStyle[] = [
+  {
+    background: 'background:#32bac0',
+    border: 'border:1px solid #32bac0',
+    color: '#32bac0',
+    index: 0,
+  },
+  {
+    background: 'background:#256AFE',
+    border: 'border:1px solid #256AFE',
+    color: '#256AFE',
+    index: 1,
+  },
+  {
+    background: 'background:#00A94D',
+    border: 'border:1px solid #00A94D',
+    color: '#00A94D',
+    index: 2,
+  },
+  {
+    background: 'background:#7866FF',
+    border: 'border:1px solid #7866FF',
+    color: '#7866FF',
+    index: 3,
+  },
+];
+
+// 预定义颜色组
+const presetColors = ref<string[]>([
+  '#000000',
+  '#FFFFFF',
+  '#FF1900',
+  '#F47365',
+  '#FFB243',
+  '#FFE623',
+  '#6EFF2A',
+  '#1BC7B1',
+  '#00BEFF',
+  '#2E81FF',
+  '#5D61FF',
+  '#FF89CF',
+  '#FC3CAD',
+  '#BF3DCE',
+  '#8E00A7',
+]);
+
+// 颜色变化回调函数
+const handleColorChange = (newColor: NewColor) => {
+  props.form.selectedColor = newColor.hex;
+  setColorPrimary?.(props.form.selectedColor);
+  props.form.themeColor = props.form.selectedColor;
+};
+const addCustomizationColor = () => {
+  customizationColor.value = !customizationColor.value;
+};
+const colorClick = (color: string) => {
+  setColorPrimary?.(color);
+  props.form.themeColor = color;
+};
+
+const finish = async () => {
+  orgDeviceLimits.value = [];
+  // eslint-disable-next-line no-useless-catch
+  try {
+    // 创建校验任务队列(包含所有子组件+父组件自身)
+    const allTasks = [...equipmentLimitationsRefs.value.map((c) => c.formRefSubmit())];
+    // 并行执行所有校验(父+子)
+    await Promise.all(allTasks);
+
+    equipmentLimitationsList.value.forEach((item) => {
+      orgDeviceLimits.value.push({
+        deviceGlobalId: item.deviceGlobalId,
+        upperLimit: item.upperLimit,
+      });
+    });
+
+    orgDeviceLimits.value.push({
+      deviceGlobalId: -1,
+      upperLimit: props.form.stationsNumber,
+    });
+    if (props.form.id) {
+      updateOrganization({
+        orgName: props.form.orgName,
+        logo: props.form.logo,
+        themeColor: props.form.themeColor,
+        dataValidityPeriod: props.form.dataValidityPeriod,
+        enabled: '1',
+        id: props.form.id,
+        orgDeviceLimits: orgDeviceLimits.value,
+        startTenancy: props.form.leaseTerm[0].format('YYYY-MM-DD'),
+        endTenancy: props.form.leaseTerm[1].format('YYYY-MM-DD'),
+      });
+    } else {
+      props.form.id = await addOrganization({
+        orgName: props.form.orgName,
+        logo: props.form.logo,
+        themeColor: props.form.themeColor,
+        dataValidityPeriod: props.form.dataValidityPeriod,
+        enabled: '1',
+        orgDeviceLimits: orgDeviceLimits.value,
+        startTenancy: props.form.leaseTerm[0].format('YYYY-MM-DD'),
+        endTenancy: props.form.leaseTerm[1].format('YYYY-MM-DD'),
+      });
+    }
+  } catch (err) {
+    throw err;
+  }
+};
+const beforeUpload = (file: Blob) => {
+  fileImg.value = file;
+  const isJpgOrPng = file.type === 'image/jpg' || file.type === 'image/png' || file.type === 'image/jpeg';
+  console.log(isJpgOrPng);
+  if (!isJpgOrPng) {
+    message.error('请上传JPG/PNG/JPEG格式的图片!');
+    return false;
+  }
+};
+
+const addClick = () => {
+  equipmentLimitationsList.value.push({
+    deviceGlobalId: undefined,
+    upperLimit: 0,
+  });
+};
+
+const deleteClick = (index: number) => {
+  equipmentLimitationsList.value.splice(index, 1);
+};
+
+const handleChange = () => {
+  if (fileImg.value) {
+    // 上传接口
+    handleRequest(async () => {
+      const { fileName } = await addUploadLogo(fileImg.value as Blob);
+      props.form.imageUrl = URL.createObjectURL(fileImg.value as Blob);
+      props.form.logo = fileName;
+    });
+  }
+};
+
+defineExpose<UseGuideStepItemExpose>({
+  finish,
+});
+
+onMounted(() => {
+  handleRequest(async () => {
+    await getDataValidityPeriod();
+    equipmentType.value = await groupList({
+      dataType: 1,
+    });
+    if (props.form.id) {
+      equipmentLimitationsList.value = [];
+      const data = await getInfoListByOrgId(props.form.id);
+      data.forEach((item) => {
+        if (item.deviceGlobalId !== -1) {
+          equipmentLimitationsList.value.push({
+            deviceGlobalId: item.deviceGlobalId,
+            upperLimit: item.upperLimit,
+          });
+        }
+      });
+    }
+  });
+});
+</script>
+
+<template>
+  <div class="organization text">
+    <AFormItem label="组织名称" name="orgName">
+      <AInput v-model:value="form.orgName" class="organization-input" :placeholder="$t('common.pleaseEnter')" />
+    </AFormItem>
+    <AFormItem label="租期" name="leaseTerm">
+      <ARangePicker
+        class="organization-input"
+        v-model:value="form.leaseTerm"
+        :allow-clear="false"
+        :separator="$t('common.to')"
+      />
+    </AFormItem>
+    <div class="upper-limit">设置数量上限</div>
+    <AFormItem label="站房数(设备组)" name="stationsNumber">
+      <AInputNumber
+        :placeholder="t('common.pleaseEnter')"
+        v-model:value="form.stationsNumber"
+        class="organization-input"
+        addon-after="个"
+      />
+    </AFormItem>
+    <div v-for="(item, index) in equipmentLimitationsList" :key="index">
+      <EquipmentLimitations
+        ref="equipmentLimitationsRefs"
+        :index="index"
+        :form="item"
+        :equipment-type="equipmentType"
+        @addClick="addClick"
+        @deleteClick="deleteClick"
+      />
+    </div>
+
+    <AFormItem label="数据存储时长" name="dataValidityPeriod">
+      <ASelect
+        class="organization-input"
+        v-model:value="form.dataValidityPeriod"
+        :options="dataValidityPeriod"
+        :field-names="{ label: 'dictValue', value: 'dictEngValue' }"
+        :placeholder="$t('common.plzSelect')"
+      />
+    </AFormItem>
+
+    <AFormItem label="Logo">
+      <div class="upload-dev">
+        <AUpload
+          v-model:file-list="fileList"
+          name="file"
+          list-type="picture-card"
+          class="avatar-uploader"
+          action=""
+          :show-upload-list="false"
+          :before-upload="beforeUpload"
+          :custom-request="handleChange"
+        >
+          <template v-if="form.imageUrl">
+            <div class="img-dev">
+              <img class="img-style" :src="form.imageUrl" alt="avatar" />
+              <div class="background-style"></div>
+            </div>
+          </template>
+
+          <div v-else>
+            <SvgIcon class="icon-size" name="upload-pictures" />
+            <div class="text">上传图片</div>
+          </div>
+        </AUpload>
+        <div class="format-text">支持JPG/PNG/JPEG格式的图片</div>
+      </div>
+    </AFormItem>
+
+    <AFlex>
+      <AFlex align="center">
+        <div class="text">主题色</div>
+        <AFlex class="color-list">
+          <div v-for="(item, index) in colorStyle" :key="index">
+            <div @click="colorClick(item.color)">
+              <AFlex
+                justify="center"
+                align="center"
+                :style="form.themeColor === item.color ? item.border : 'border:1px solid #fff;'"
+                class="color-div"
+              >
+                <div :style="item.background" class="color-style"></div>
+              </AFlex>
+            </div>
+          </div>
+
+          <AFlex class="flex-relative">
+            <div @click="addCustomizationColor">
+              <AFlex class="color-customization" justify="space-between" align="center">
+                <div class="customization-style" :style="'background:' + form.selectedColor"></div>
+                <div class="customization-text">{{ form.selectedColor }}</div>
+              </AFlex>
+            </div>
+
+            <div class="color-selection">
+              <ColorPicker
+                v-show="customizationColor"
+                v-model:color="form.selectedColor"
+                :colors-default="presetColors"
+                theme="dark"
+                @changeColor="handleColorChange"
+                style="width: 220px"
+                class="picker-color"
+              />
+            </div>
+          </AFlex>
+        </AFlex>
+      </AFlex>
+    </AFlex>
+    <div class="display-effect">
+      <div class="display-effect-top">
+        <span class="desktop-header-dot" v-for="i in 3" :key="i"></span>
+      </div>
+      <AFlex class="desktop-content" wrap="wrap" justify="space-between">
+        <div class="desktop-content-div" v-for="i in 6" :key="i">
+          <AFlex>
+            <div class="desktop-content-top-left"></div>
+            <AFlex :vertical="true">
+              <div class="desktop-content-top-right"></div>
+              <div class="desktop-content-top-right-div"></div>
+            </AFlex>
+          </AFlex>
+          <div class="desktop-content-bottom"></div>
+          <div class="desktop-content-bottom-div"></div>
+        </div>
+      </AFlex>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.upper-limit {
+  margin-top: 40px;
+  margin-bottom: 16px;
+  font-size: 16px;
+  font-style: normal;
+  font-weight: 500;
+  line-height: 24px;
+  color: #333;
+  text-align: left;
+}
+
+.background-style {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 99999;
+  display: flex;
+  gap: 20px;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  background: rgb(0 0 0 / 50%);
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+
+.img-dev {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+.img-style {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.color-selection {
+  position: absolute;
+  top: -270px;
+  left: 80px;
+  width: 100%;
+  height: 100%;
+}
+
+.picker-color {
+  :deep(.color-alpha) {
+    display: none !important;
+  }
+
+  :deep(.color-type) {
+    display: none !important;
+  }
+
+  :deep(.hue) {
+    margin-left: 20px;
+  }
+}
+
+.customization-text {
+  font-size: 12px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 20px;
+  color: #999;
+  text-align: left;
+}
+
+.customization-style {
+  width: 24px;
+  height: 24px;
+  border-radius: 4px;
+}
+
+.flex-relative {
+  position: relative;
+}
+
+.color-customization {
+  width: 90px;
+  height: 32px;
+  padding: 4px;
+  cursor: pointer;
+  border: 1px solid #d6d6d6;
+  border-radius: 4px;
+}
+
+.desktop-content {
+  position: relative;
+  top: -30px;
+  left: 16px;
+  width: 288px;
+}
+
+.desktop-content-bottom-div {
+  width: 48px;
+  height: 3px;
+  background: #d8d8d8;
+  border-radius: 2px;
+}
+
+.desktop-content-bottom {
+  width: 71px;
+  height: 3px;
+  margin: 6px 0;
+  background: #d8d8d8;
+  border-radius: 2px;
+}
+
+.desktop-content-top-right-div {
+  width: 26px;
+  height: 3px;
+  background: #d8d8d8;
+  border-radius: 2px;
+}
+
+.desktop-content-top-right {
+  width: 40px;
+  height: 3px;
+  margin: 6px 0;
+  background: #d8d8d8;
+  border-radius: 2px;
+}
+
+.desktop-content-top-left {
+  width: 24px;
+  height: 24px;
+  margin-right: 8px;
+  background: var(--antd-color-primary-opacity-50);
+  border: 1px solid var(--antd-color-primary);
+  border-radius: 4px;
+}
+
+.desktop-content-div {
+  width: 88px;
+  height: 57px;
+  padding: 6px;
+  margin-bottom: 12px;
+  background: #fff;
+  border: 1px solid #d9dbe2;
+  border-radius: 4px;
+}
+
+.display-effect-top {
+  display: flex;
+  width: 319px;
+  height: 94px;
+  padding-top: 9px;
+  padding-left: 16px;
+  background-color: var(--antd-color-primary);
+  border-top-left-radius: 8px;
+  border-top-right-radius: 8px;
+}
+
+.display-effect {
+  width: 320px;
+  height: 206px;
+  margin-top: 24px;
+  margin-left: 138px;
+  background: #fafafa;
+  border: 1px solid var(--antd-color-primary);
+  border-radius: 8px;
+}
+
+.desktop-header-dot {
+  width: 6px;
+  height: 6px;
+  background-color: white;
+  border-radius: 50%;
+
+  & + & {
+    margin-left: 6px;
+  }
+}
+
+.color-list {
+  margin-left: 95px;
+}
+
+.color-style {
+  width: 24px;
+  height: 24px;
+  border-radius: 4px;
+}
+
+.color-div {
+  width: 32px;
+  height: 32px;
+  margin-right: 4px;
+  cursor: pointer;
+  border-radius: 6px;
+}
+
+.upload-dev {
+  margin-top: 8px;
+  margin-left: 10px;
+}
+
+.icon-size {
+  font-size: 24px;
+}
+
+.format-text {
+  font-size: 12px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 20px;
+  color: #999;
+  text-align: left;
+}
+
+.organization-input {
+  width: 256px;
+  height: 32px;
+  margin-left: 10px;
+}
+
+.text {
+  font-size: 14px;
+  font-style: normal;
+  font-weight: 400;
+  line-height: 22px;
+  color: rgb(0 0 0 / 65%);
+  text-align: left;
+}
+
+:deep(.organization-input) {
+  .ant-input-number-group .ant-input-number-group-addon {
+    background-color: #fff;
+  }
+}
+</style>