后台管理系统开发(注册接口 前端篇)

154 阅读4分钟

注册接口非必要

实现效果

image.png

image.png

image.png

模型定义

type/dto/user

/**
 * 用户账号注册
 */
export interface UserAccountRegisterDto {
  /**
   * 验证码
   */
  code: string;
  /**
   * 用户登录密码
   */
  password: string;
  /**
   * 用户登录名称
   */
  username: string;
}

/**
 * 更新用户信息
 */
export interface UpdateUserInfoDto {
  /**
   * 用户地址
   */
  addressCode: string;
  /**
   * 头像路径
   */
  avatarUrl: string;
  /**
   * 性别
   */
  gender: number;
  /**
   * 昵称
   */
  nickName: string;
  /**
   * 手机号码
   */
  phoneNumber: string;
}

types/vo/upload.ts

/**
 * 上传文件信息 FileInfo
 */
export interface UploadFileInfo {
  attr: {
    empty: boolean;
  };
  basePath: string;
  contentType: string;
  createTime: Date;
  ext: string;
  fileAcl: { [key: string]: any };
  filename: string;
  id: string;
  metadata: { [key: string]: string };
  objectId: string;
  objectType: string;
  originalFilename: string;
  path: string;
  platform: string;
  size: number;
  thContentType: string;
  thFileAcl: { [key: string]: any };
  thFilename: string;
  thMetadata: { [key: string]: string };
  thSize: number;
  thUrl: string;
  thUserMetadata: { [key: string]: string };
  url: string;
  userMetadata: { [key: string]: string };
}

接口函数

/**
 * 用户账号注册
 *
 * @param body 注册 DTO
 */
export const postUserAccountRegisterApi = (body: UserAccountRegisterDto) => {
  return useHttpPost<LoginDetails>({
    url: '/user/register',
    body,
  });
};

/**
 * 更新当前用户信息
 */
export const postUpdateMyInfoApi = (body: Partial<UpdateUserInfoDto>) => {
  return useHttpPost({
    url: '/my/update',
    body,
  });
};

页面主内容[pages/register/index.vue]

  • BaseRegister
  • FillRegister
<script lang="ts" setup>
  import BaseRegister from './components/BaseRegister.vue';
  import FillRegister from './components/FillRegister.vue';

  definePageMeta({
    layout: 'single',
  });

  // 步骤索引
  const { count: stepsIndex, inc, dec } = useCounter(1, { min: 1, max: 3 });

  const baseRegisterRef = ref<InstanceType<typeof BaseRegister>>();
  const fillRegisterRef = ref<InstanceType<typeof FillRegister>>();

  // 重置表单
  const onResetForm = () => {
    baseRegisterRef.value?.onReset();
    fillRegisterRef.value?.onReset();
  };

  /**
   * 表单加载中
   */
  const formLoading = computed(() => {
    return baseRegisterRef.value?.loading || fillRegisterRef.value?.loading;
  });

  /**
   * 触发注册用户
   */
  const onRegisterUser = async () => {
    const b = await baseRegisterRef.value!.onSubmit();
    if (b) {
      inc(1);
    }
  };

  /**
   * 触发更新用户信息
   */
  const onUpdateUserInfo = async () => {
    const b = await fillRegisterRef.value!.onSubmit();
    if (b) {
      inc(1);
    }
  };
</script>

<template>
  <div class="register-container">
    <a-card
      :bordered="false"
      :size="'small'"
      class="pl-12 pr-6 pb-6 shadow-md rounded-3xl!"
    >
      <LogoBanner />

      <a-divider />

      <!--   填写表单数据, 仅在 1、2 步骤时显示   -->
      <a-space v-if="stepsIndex < 3" :align="'start'" fill>
        <a-steps
          :current="stepsIndex"
          :direction="'vertical'"
          :type="'dot'"
          label-placement="vertical"
        >
          <a-step description="信息">基本信息填写</a-step>
          <a-step description="非必要填写信息">附带信息填写</a-step>
          <a-step description="欢迎使用">欢迎</a-step>
        </a-steps>

        <!--   main     -->
        <BaseRegister v-if="stepsIndex === 1" ref="baseRegisterRef" />
        <FillRegister v-if="stepsIndex === 2" ref="fillRegisterRef" />
        <!--   main     -->
      </a-space>

      <!--   注册完成页面    -->
      <div v-if="stepsIndex === 3">
        <svg class="i-banner-onboarding-animate text-[18rem]" />
      </div>

      <a-space class="justify-between w-full pt-6">
        <a-space>
          <ThemeButton />

          <a-button :shape="'circle'">
            <template #icon>
              <i
                class="i-streamline-ai-settings-spark hover:i-streamline-ai-settings-spark-solid"
              />
            </template>
          </a-button>
        </a-space>

        <a-space v-if="stepsIndex < 3" class="steps-active">
          <a-button @click="onResetForm">
            <span>重置表单</span>

            <template #icon>
              <i class="i-material-symbols-refresh" />
            </template>
          </a-button>

          <!--    步骤一中注册用户      -->
          <a-button
            v-if="stepsIndex == 1"
            :loading="formLoading"
            type="primary"
            @click="onRegisterUser"
          >
            <span>下一步</span>
            <template #icon>
              <i class="i-line-md-chevron-small-double-left rotate-180" />
            </template>
          </a-button>

          <!--    直接跳过步骤二      -->
          <a-button v-if="stepsIndex == 2" type="secondary" @click="inc(1)">
            <span>跳过</span>

            <template #icon>
              <i class="i-line-md-chevron-small-double-left rotate-180" />
            </template>
          </a-button>

          <!--    步骤二中进行更新操作      -->
          <a-button
            v-if="stepsIndex == 2"
            type="primary"
            @click="onUpdateUserInfo"
          >
            <span>下一步</span>

            <template #icon>
              <i class="i-solar-cloud-upload-bold-duotone" />
            </template>
          </a-button>
        </a-space>
        <a-button
          v-else
          :shape="'round'"
          :size="'large'"
          :type="'primary'"
          @click="
            navigateTo({
              path: '/',
              replace: true,
            })
          "
        >
          <a-space>
            <i class="i-streamline-braille-blind" />
            <span>进入系统</span>
          </a-space>
        </a-button>
      </a-space>
    </a-card>
  </div>
</template>

<style scoped>
  .register-container {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;

    .register-form {
      width: 450px;
      padding-top: 0.5em;
    }
  }
</style>

页面步骤一[pages/register/components/BaseRegister.vue]

<script lang="ts" setup>
  import type Form from '@arco-design/web-vue/es/form';
  import { formRules } from '~/pages/rules';
  import { useRequest, useUserStore } from '#imports';
  import { getImageCaptchaApi } from '~/api/captcha';
  import type { UserAccountRegisterDto } from '~/types/dto/user';
  import { postUserAccountRegisterApi } from '~/api/user';

  const userStore = useUserStore();

  // 请求 图形验证码接口
  const {
    data: imageCaptchaData,
    refresh: imageCaptchaRefresh,
    loading: captchaLoading,
  } = useRequest(getImageCaptchaApi, {
    manual: false,
    onSuccess: () => {
      registerForm.value.code = '';
    },
  });

  const registerForm = ref<
    UserAccountRegisterDto & { confirmPassword: string }
  >({
    username: 'admin123',
    password: 'admin123',
    confirmPassword: 'admin123',
    code: '',
  });

  const registerFormRef = ref<InstanceType<typeof Form>>();

  /**
   * 确认密码校验
   */
  const confirmPasswordValidator = [
    {
      required: true,
      message: '用户密码不能为空',
    },
    {
      validator: (_: string, cb: (m: string) => void) => {
        const password = registerForm.value.password;
        const confirmPassword = registerForm.value.confirmPassword;

        if (!!password && !!confirmPassword && password !== confirmPassword) {
          cb('两次输入的密码不一致');
        }
      },
    },
  ];

  const { runAsync: accountRegisterRunAsync, loading: accountRegisterLoading } =
    useRequest(postUserAccountRegisterApi, {
      onError: () => {
        registerForm.value.code = '';
      },
      onSuccess: (userDetail) => {
        console.log(userDetail);
        userStore.loadUserDetails(userDetail);
      },
    });

  /**
   * 提交表单数据
   */
  const onSubmit = async () => {
    const validate = await registerFormRef.value?.validate();
    if (validate) {
      Message.info('请完成表单内容填写');
      return false;
    }
    if (registerForm.value.code.length !== 5) {
      imageCaptchaRefresh();

      registerFormRef.value?.setFields({
        code: {
          status: 'error',
          message: '验证码格式错误',
        },
      });
      return false;
    }

    // 发送注册请求
    await accountRegisterRunAsync({ ...unref(registerForm) });
    return true;
  };

  /**
   * 重置表单
   */
  const onReset = () => {
    registerFormRef.value?.resetFields();
  };

  defineExpose({
    /**
     * 重置表单
     */
    onReset,
    /**
     * 提交表单
     */
    onSubmit,
    /**
     * 提交表单中的加载状态
     */
    loading: accountRegisterLoading,
  });
</script>

<template>
  <a-form
    ref="registerFormRef"
    :model="registerForm"
    :rules="formRules"
    :wrapper-col-props="{ span: 20, offset: 2 }"
    class="register-form"
  >
    <a-form-item :validate-trigger="'input'" field="username" hide-asterisk>
      <a-input
        v-model="registerForm.username"
        :max-length="30"
        placeholder="请输入账号"
      >
        <template #prefix>
          <i class="i-streamline-user-profile-focus pr-1" />
        </template>
      </a-input>
    </a-form-item>

    <a-form-item :validate-trigger="'input'" field="password" hide-asterisk>
      <a-input-password
        v-model="registerForm.password"
        :max-length="30"
        placeholder="请输入密码"
      >
        <template #prefix>
          <i
            class="i-streamline-interface-user-lock-actions-lock-geometric-human-person-single-up-user pr-1"
          />
        </template>
      </a-input-password>
    </a-form-item>

    <a-form-item
      :rules="confirmPasswordValidator"
      :validate-trigger="'input'"
      field="confirmPassword"
      hide-asterisk
    >
      <a-input-password
        v-model="registerForm.confirmPassword"
        :max-length="30"
        placeholder="请重新输入密码"
      >
        <template #prefix>
          <i
            class="i-streamline-interface-user-lock-actions-lock-geometric-human-person-single-up-user pr-1"
          />
        </template>
      </a-input-password>
    </a-form-item>

    <a-form-item
      :rules="[]"
      :validate-trigger="'input'"
      field="code"
      hide-asterisk
    >
      <a-space :size="'large'" class="pt-2.5">
        <a-spin :loading="captchaLoading">
          <a-image
            :height="58"
            :preview="false"
            :src="imageCaptchaData?.base64Data"
            :width="120"
            class="rounded-md! cursor-pointer"
            @click="imageCaptchaRefresh"
          />
        </a-spin>

        <a-verification-code
          v-model="registerForm.code"
          :length="5"
          style="width: 230px"
        />
      </a-space>
    </a-form-item>
  </a-form>
</template>

<style scoped></style>

页面步骤二[pages/register/components/FillRegister.vue]

<script lang="ts" setup>
  import type Form from '@arco-design/web-vue/es/form';
  import type { UpdateUserInfoDto } from '~/types/dto/user';
  import { useRequest } from '#imports';
  import { postUpdateMyInfoApi } from '~/api/user';

  const fillForm = ref<UpdateUserInfoDto>({
    avatarUrl: '',
    nickName: '',
    gender: 0,
    phoneNumber: '',
    addressCode: '',
  });

  const fillFormRef = ref<InstanceType<typeof Form>>();

  const { loading: updateMyInfoLoading, runAsync: updateMyInfoRunAsync } =
    useRequest(postUpdateMyInfoApi);

  // 重置表单数据
  const onReset = () => {
    fillFormRef.value?.resetFields();
  };

  // 提交表单
  const onSubmit = async () => {
    const validate = await fillFormRef.value?.validate();
    if (validate) {
      Message.info('请完成表单内容填写');
      return false;
    }

    // 发送更新请求
    await updateMyInfoRunAsync({ ...unref(fillForm) });
    return true;
  };

  defineExpose({
    // 重置表单数据
    onReset,
    // 提交表单 加载中
    loading: updateMyInfoLoading,
    // 提交表单
    onSubmit,
  });
</script>

<template>
  <a-form
    ref="fillFormRef"
    :model="fillForm"
    :wrapper-col-props="{ span: 20, offset: 2 }"
    class="register-form"
  >
    <a-form-item>
      <a-space class="justify-between w-full">
        <svg class="i-banner-upload_image w-[10em] h-[10em]" />
        <UploadAvatar
          v-model="fillForm.avatarUrl"
          action-url="http://localhost:7000/my/upload/avatar"
        />
      </a-space>
    </a-form-item>
    <a-form-item
      :rules="[
        {
          match: /^[\u4E00-\u9FA5a-zA-Z0-9·._\s]{1,20}$/,
          message: '用户名称格式错误',
        },
      ]"
      hide-asterisk
    >
      <a-input
        v-model="fillForm.nickName"
        :max-length="30"
        placeholder="请输入名称"
      >
        <template #prefix>
          <i class="i-streamline-user-profile-focus pr-1" />
        </template>
      </a-input>
    </a-form-item>

    <a-form-item>
      <AreaCascader
        v-model:address="fillForm.addressCode"
        placeholder="请选择地址"
      />
    </a-form-item>

    // TODO 请求后端获取字典
    <a-form-item>
      <a-radio-group v-model="fillForm.gender" :type="'radio'">
        <a-radio :value="0" label="true">男性</a-radio>
        <a-radio :value="1" label="true">女性</a-radio>
        <a-radio :value="2" label="true">保密</a-radio>
      </a-radio-group>
    </a-form-item>
  </a-form>
</template>

<style scoped></style>