zhangwei
2025-06-25 ce5e84197b43dec8c01717b116cb77535ad3c91e
'登录注册'
11个文件已修改
4个文件已添加
981 ■■■■ 已修改文件
mock/login.ts 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login/index.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/register/index.ts 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/user.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/util.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/home.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/remaining.ts 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/utils.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/multiTags.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/home/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/register/index.vue 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/register/registersucess.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mock/login.ts
@@ -1,42 +1,42 @@
// 根据角色动态生成路由
import { defineFakeRoute } from "vite-plugin-fake-server/client";
// // 根据角色动态生成路由
// import { defineFakeRoute } from "vite-plugin-fake-server/client";
export default defineFakeRoute([
  {
    url: "/login",
    method: "post",
    response: ({ body }) => {
      if (body.username === "admin") {
        return {
          success: true,
          data: {
            avatar: "https://avatars.githubusercontent.com/u/44761321",
            username: "admin",
            nickname: "小铭",
            // 一个用户可能有多个角色
            roles: ["admin"],
            // 按钮级别权限
            permissions: ["*:*:*"],
            accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
            refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
            expires: "2030/10/30 00:00:00"
          }
        };
      } else {
        return {
          success: true,
          data: {
            avatar: "https://avatars.githubusercontent.com/u/52823142",
            username: "common",
            nickname: "小林",
            roles: ["common"],
            permissions: ["permission:btn:add", "permission:btn:edit"],
            accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
            refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
            expires: "2030/10/30 00:00:00"
          }
        };
      }
    }
  }
]);
// export default defineFakeRoute([
//   {
//     url: "/login",
//     method: "post",
//     response: ({ body }) => {
//       if (body.username === "admin") {
//         return {
//           success: true,
//           data: {
//             avatar: "https://avatars.githubusercontent.com/u/44761321",
//             username: "admin",
//             nickname: "小铭",
//             // 一个用户可能有多个角色
//             roles: ["admin"],
//             // 按钮级别权限
//             permissions: ["*:*:*"],
//             accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
//             refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
//             expires: "2030/10/30 00:00:00"
//           }
//         };
//       } else {
//         return {
//           success: true,
//           data: {
//             avatar: "https://avatars.githubusercontent.com/u/52823142",
//             username: "common",
//             nickname: "小林",
//             roles: ["common"],
//             permissions: ["permission:btn:add", "permission:btn:edit"],
//             accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
//             refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
//             expires: "2030/10/30 00:00:00"
//           }
//         };
//       }
//     }
//   }
// ]);
src/api/login/index.ts
New file
@@ -0,0 +1,17 @@
/**
 * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参)
 *
 * 注册api接口集合
 * @method login 登录
 */
import { http } from "@/utils/http";
import { baseUrlApi } from "../util";
type Result = {
  success: boolean;
  data: Array<any>;
};
export const login = (data?: object) => {
  return http.request("post", baseUrlApi("/api/auth/loginPhone"), { data });
};
src/api/register/index.ts
New file
@@ -0,0 +1,41 @@
/**
 * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参)
 *
 * 注册api接口集合
 * @method register 注册
 * @method captcha 获取验证码
 */
import { http } from "@/utils/http";
import { baseUrlApi } from "../util";
type Result = {
  success: boolean;
  data: Array<any>;
};
export const register = (data?: object) => {
  return http.request(
    "post",
    baseUrlApi("/api/customer/customerRegistration"),
    { data }
  );
};
export const captcha = () => {
  return http.request<Result>("get", baseUrlApi("/api/zCSMS/captcha"));
};
//获取角色
export const exRole = () => {
  return http.request<Result>("get", baseUrlApi("/api/customer/exRole"));
};
// 获取手机验证码
export const phoneNumberCode = (params?: object) => {
  return http.request(
    "post",
    baseUrlApi(
      `/api/zCSMS/sendSMS/${params.phone}/${params.code}/${params.codeId}`
    )
  );
};
src/api/user.ts
@@ -1,4 +1,5 @@
import { http } from "@/utils/http";
import { baseUrlApi } from "./util";
export type UserResult = {
  success: boolean;
@@ -36,10 +37,11 @@
/** 登录 */
export const getLogin = (data?: object) => {
  return http.request<UserResult>("post", "/login", { data });
  return http.request("post", baseUrlApi("/api/auth/loginPhone"), { data });
  // return http.request<UserResult>("post", "/login", { data });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
  return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
  return http.request<RefreshTokenResult>("post", "/refresh-token1", { data });
};
src/api/util.ts
New file
@@ -0,0 +1,4 @@
export const baseUrlApi = (url: string) =>
  process.env.NODE_ENV === "development"
    ? `/api${url}`
    : `http://192.168.0.36:5005${url}`;
src/router/index.ts
@@ -106,6 +106,7 @@
}
/** 路由白名单 */
// const whiteList = ["/login"];
const whiteList = [];
const { VITE_HIDE_HOME } = import.meta.env;
src/router/modules/home.ts
@@ -8,7 +8,7 @@
  // redirect: "/welcome",
  meta: {
    icon: "ep/home-filled",
    title: "首页",
    title: "主页",
    rank: 0
  },
  children: [
@@ -17,7 +17,7 @@
      name: "Welcome",
      component: () => import("@/views/welcome/index.vue"),
      meta: {
        title: "首页",
        title: "主页",
        showLink: VITE_HIDE_HOME === "true" ? false : true
      }
    }
src/router/modules/remaining.ts
@@ -5,7 +5,11 @@
    path: "/",
    name: "Main",
    redirect: "/home",
    meta: {}
    meta: {
      title: "首页",
      showLink: false,
      rank: 101
    }
  },
  {
    path: "/home",
@@ -20,7 +24,7 @@
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/home/index.vue"),
    component: () => import("@/views/login/index.vue"),
    meta: {
      title: "登录",
      showLink: false,
@@ -34,7 +38,17 @@
    meta: {
      title: "注册",
      showLink: false,
      rank: 101
      rank: 104
    }
  },
  {
    path: "/registersucess",
    name: "RegisterSucess",
    component: () => import("@/views/register/registersucess.vue"),
    meta: {
      title: "注册成功",
      showLink: false,
      rank: 103
    }
  },
  {
src/router/utils.ts
@@ -33,7 +33,7 @@
  const { name, path, parentId, meta } = routeInfo;
  return isAllEmpty(parentId)
    ? isAllEmpty(meta?.rank) ||
      (meta?.rank === 0 && name !== "Home" && path !== "/")
      (meta?.rank === 0 && name !== "Welcome" && path !== "/welcome")
      ? true
      : false
    : false;
@@ -151,7 +151,7 @@
/** 处理动态路由(后端返回的路由) */
function handleAsyncRoutes(routeList) {
  if (routeList.length === 0) {
  if (routeList?.length === 0) {
    usePermissionStoreHook().handleWholeMenus(routeList);
  } else {
    formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
@@ -171,7 +171,7 @@
          if (!router.hasRoute(v?.name)) router.addRoute(v);
          const flattenRouters: any = router
            .getRoutes()
            .find(n => n.path === "/");
            .find(n => n.path === "/welcome");
          // 保持router.options.routes[0].children与path为"/"的children一致,防止数据不一致导致异常
          flattenRouters.children = router.options.routes[0].children;
          router.addRoute(flattenRouters);
@@ -205,8 +205,8 @@
    } else {
      return new Promise(resolve => {
        getAsyncRoutes().then(({ data }) => {
          handleAsyncRoutes(cloneDeep(data));
          storageLocal().setItem(key, data);
          // handleAsyncRoutes(cloneDeep(data));
          // storageLocal().setItem(key, data);
          resolve(router);
        });
      });
@@ -214,7 +214,7 @@
  } else {
    return new Promise(resolve => {
      getAsyncRoutes().then(({ data }) => {
        handleAsyncRoutes(cloneDeep(data));
        // handleAsyncRoutes(cloneDeep(data));
        resolve(router);
      });
    });
@@ -227,7 +227,7 @@
 * @returns 返回处理后的一维路由
 */
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
  if (routesList.length === 0) return routesList;
  if (routesList?.length === 0) return routesList;
  let hierarchyList = buildHierarchyTree(routesList);
  for (let i = 0; i < hierarchyList.length; i++) {
    if (hierarchyList[i].children) {
@@ -246,10 +246,10 @@
 * @returns 返回将一维数组重新处理成规定路由的格式
 */
function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
  if (routesList.length === 0) return routesList;
  if (routesList?.length === 0) return routesList;
  const newRoutesList: RouteRecordRaw[] = [];
  routesList.forEach((v: RouteRecordRaw) => {
    if (v.path === "/") {
    if (v.path === "/welcome") {
      newRoutesList.push({
        component: v.component,
        name: v.name,
@@ -387,6 +387,7 @@
    usePermissionStoreHook().wholeMenus[0]?.children[0]
  );
  tag && useMultiTagsStoreHook().handleTags("push", topMenu);
  console.log(topMenu, "topMenu");
  return topMenu;
}
src/store/modules/multiTags.ts
@@ -79,10 +79,10 @@
            // showLink:false 不添加到标签页
            if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
              return;
            const tagPath = tagVal.path;
            const tagPath = tagVal?.path;
            // 判断tag是否已存在
            const tagHasExits = this.multiTags.some(tag => {
              return tag.path === tagPath;
              return tag?.path === tagPath;
            });
            // 判断tag中的query键值是否相等
src/views/home/index.vue
@@ -42,8 +42,8 @@
    </div>
    <div class="right">
      <el-tabs v-model="activeName" class="demo-tabs">
        <el-tab-pane label="工程招标" name="first">
          工程招标
        <el-tab-pane label="意向公开" name="first">
          意向公开
          <!-- <div class="item">
            <span
              ><span style="color: #145ccd; font-weight: 600">·</span
@@ -94,8 +94,9 @@
            <span>2024-04-15 18:10</span>
          </div> -->
        </el-tab-pane>
        <el-tab-pane label="货物招标" name="second">货物招标</el-tab-pane>
        <el-tab-pane label="服务招标" name="third">服务招标</el-tab-pane>
        <el-tab-pane label="工程招标" name="second">工程招标</el-tab-pane>
        <el-tab-pane label="货物招标" name="third">货物招标</el-tab-pane>
        <el-tab-pane label="服务招标" name="fourth">服务招标</el-tab-pane>
        <el-tab-pane label="网上竞价" name="fourth">网上竞价</el-tab-pane>
      </el-tabs>
    </div>
@@ -147,9 +148,13 @@
              src="@/assets/home/car1.png"
              alt=""
            />
            采购人/招标人
            采购人
          </div>
          <div>注册<span class="m-2">|</span>登录</div>
          <div>
            <span class="hover:cursor-pointer" @click="toRegister">注册</span
            ><span class="m-2">|</span
            ><span class="hover:cursor-pointer" @click="toLogin">登录</span>
          </div>
        </div>
        <div class="item">
          <div class="box">
@@ -159,20 +164,13 @@
              src="@/assets/home/car.png"
              alt=""
            />
            采购人
            代理机构
          </div>
          <div>注册<span class="m-2">|</span>登录</div>
        </div>
        <div class="item">
          <div class="box">
            <img
              width="18px"
              height="18px"
              src="@/assets/home/car.png"
              alt=""
            />招标代理机构
          <div>
            <span class="hover:cursor-pointer" @click="toRegister">注册</span
            ><span class="m-2">|</span
            ><span class="hover:cursor-pointer" @click="toLogin">登录</span>
          </div>
          <div>注册<span class="m-2">|</span>登录</div>
        </div>
        <div class="item">
          <div class="box">
@@ -183,7 +181,11 @@
              alt=""
            />供应商
          </div>
          <div>注册<span class="m-2">|</span>登录</div>
          <div>
            <span class="hover:cursor-pointer" @click="toRegister">注册</span
            ><span class="m-2">|</span
            ><span class="hover:cursor-pointer" @click="toLogin">登录</span>
          </div>
        </div>
        <div class="item">
          <div class="box">
@@ -194,7 +196,11 @@
              alt=""
            />评审专家
          </div>
          <div>注册<span class="m-2">|</span>登录</div>
          <div>
            <span class="hover:cursor-pointer" @click="toRegister">注册</span
            ><span class="m-2">|</span
            ><span class="hover:cursor-pointer" @click="toLogin">登录</span>
          </div>
        </div>
      </div>
      <div class="right" />
@@ -396,6 +402,17 @@
import { ref } from "vue";
import myFooter from "./component/myFooter.vue";
let activeName = ref("first");
import { useRoute, useRouter } from "vue-router";
defineOptions({
  name: "Home"
});
const router = useRouter();
const toRegister = () => {
  router.push({ name: "Register" });
};
const toLogin = () => {
  router.push({ name: "Login" });
};
</script>
<style lang="scss" scoped>
@@ -518,7 +535,7 @@
  padding: 40px 0;
  .all {
    width: 72%;
    height: 482px;
    height: 385px;
    background: #fff;
    margin: 0 auto;
    display: flex;
@@ -535,7 +552,7 @@
        justify-content: space-between;
        align-items: center;
        padding: 0 30px;
        height: 20%;
        height: 25%;
        text-align: left;
        color: #5f5f5f;
        .box {
src/views/login/index.vue
@@ -3,7 +3,15 @@
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import { ref, reactive, toRaw } from "vue";
import {
  reactive,
  computed,
  ref,
  onMounted,
  defineAsyncComponent,
  onUnmounted,
  watch
} from "vue";
import { debounce } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import { useEventListener } from "@vueuse/core";
@@ -12,13 +20,17 @@
import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, logo1 } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useRoute } from "vue-router";
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
const route = useRoute();
// import dayIcon from "@/assets/svg/day.svg?component";
// import darkIcon from "@/assets/svg/dark.svg?component";
import Lock from "~icons/ri/lock-fill";
import User from "~icons/ri/user-3-fill";
import { captcha, phoneNumberCode, exRole } from "@/api/register/index.ts";
defineOptions({
  name: "Login"
@@ -35,12 +47,114 @@
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value);
const { title } = useNav();
// 获取验证码
const getCaptcha = async () => {
  // if (!state.captchaEnabled) return;
const ruleForm = reactive({
  username: "admin",
  password: "admin123"
  state.ruleForm.code = "";
  const res = await captcha();
  console.log(res);
  state.captchaImage = "data:text/html;base64," + res.result?.img;
  state.expirySeconds = res.result?.expirySeconds;
  state.ruleForm.codeId = res.result?.id;
};
const state = reactive({
  isShowPassword: false,
  ruleForm: {
    account: "",
    nickName: "",
    phone: "",
    phoneVCode: "",
    // tenantId: props.tenantInfo.id,
    code: "",
    codeId: 0,
    email: "",
    exRoleCode: ""
  },
  rules: {
    code: [
      {
        required: true,
        message: "请输入手机验证码",
        trigger: "blur"
      }
    ],
    phone: [
      {
        required: true,
        message: "请输入您的手机号码",
        trigger: "blur"
      }
    ],
    exRoleCode: [
      {
        required: true,
        message: "请选择角色",
        trigger: "blur"
      }
    ]
    // code: [{ required: true, message: t('message.account.placeholder4'), trigger: 'blur' }],
  },
  loading: {
    signIn: false
  },
  captchaImage: "",
  rotateVerifyVisible: false,
  // rotateVerifyImg: verifyImg,
  // rotateVerifyImg: themeConfig.value.logoUrl,
  secondVerEnabled: false,
  // captchaEnabled: false,
  isPassRotate: false,
  capsLockVisible: false,
  hideTenantForLogin: false,
  expirySeconds: 60, // 验证码过期时间
  phoneSeconds: 0, // 手机验证码倒计时
  roleList: []
});
// 验证码过期计时器
let timer: any = null;
let phonetimer: any = null;
// 页面初始化
onMounted(async () => {
  // 若URL带有Token参数(第三方登录)
  const accessToken = route.query.token;
  // if (accessToken) await saveTokenAndInitRoutes(accessToken);
  // watch(
  //   () => themeConfig.value.isLoaded,
  //   isLoaded => {
  //     if (isLoaded) {
  // 获取登录配置
  // state.hideTenantForLogin = themeConfig.value.hideTenantForLogin ?? true;
  // state.secondVerEnabled = themeConfig.value.secondVer ?? true;
  // state.captchaEnabled = themeConfig.value.captcha ?? true;
  // 获取验证码
  getCaptcha();
  exRole().then(res => {
    state.roleList = res.result;
  });
  // 注册验证码过期计时器
  // if (state.captchaEnabled) {
  timer = setInterval(() => {
    if (state.expirySeconds > 0) state.expirySeconds -= 1;
  }, 1000);
  // }
  // }
  // },
  // { immediate: true }
  // );
});
// 页面卸载
onUnmounted(() => {
  // 销毁验证码过期计时器
  clearInterval(timer);
  timer = null;
  clearInterval(phonetimer);
  phonetimer = null;
});
const onLogin = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate(valid => {
@@ -48,30 +162,46 @@
      loading.value = true;
      useUserStoreHook()
        .loginByUsername({
          username: ruleForm.username,
          password: ruleForm.password
          phone: state.ruleForm.phone,
          code: state.ruleForm.phoneVCode,
          exRuleCode: state.ruleForm.exRoleCode
        })
        .then(res => {
          if (res.success) {
          if (res.code == 200) {
            // 获取后端路由
            return initRouter().then(() => {
              disabled.value = true;
              router
                .push(getTopMenu(true).path)
                .push(getTopMenu(true)?.path)
                .then(() => {
                  message("登录成功", { type: "success" });
                })
                .finally(() => (disabled.value = false));
            });
          } else {
            message("登录失败", { type: "error" });
            message(res?.message || "登录失败", { type: "error" });
          }
        })
        .finally(() => (loading.value = false));
    }
  });
};
const sendValidationCode = async () => {
  if (!state.ruleForm.phone) {
    return message("请先输入手机号", { type: "warning" });
  }
  if (!state.ruleForm.code) {
    return message("请先输入验证码", { type: "warning" });
  }
  const res = await phoneNumberCode(state.ruleForm);
  if (res?.code != 200) {
    return message(res?.message, { type: "warning" });
  }
  state.phoneSeconds = 60;
  phonetimer = setInterval(() => {
    if (state.phoneSeconds > 0) state.phoneSeconds -= 1;
  }, 1000);
};
const immediateDebounce: any = debounce(
  formRef => onLogin(formRef),
  1000,
@@ -117,32 +247,76 @@
          <el-form
            ref="ruleFormRef"
            :model="ruleForm"
            :model="state.ruleForm"
            :rules="loginRules"
            size="large"
          >
            <Motion :delay="150">
              <el-form-item prop="exRoleCode">
                <el-radio-group v-model="state.ruleForm.exRoleCode">
                  <el-radio
                    v-for="item in state.roleList"
                    :key="item.id"
                    :value="item.code"
                    >{{ item.name }}</el-radio
                  >
                </el-radio-group>
              </el-form-item>
            </Motion>
            <Motion :delay="100">
              <el-form-item
                :rules="[
                  {
                    required: true,
                    message: '请输入账号',
                    message: '请输入手机号',
                    trigger: 'blur'
                  }
                ]"
                prop="username"
                prop="phone"
              >
                <el-input
                  v-model="ruleForm.username"
                  v-model="state.ruleForm.phone"
                  clearable
                  placeholder="账号"
                  :prefix-icon="useRenderIcon(User)"
                  placeholder="手机号"
                />
              </el-form-item>
            </Motion>
            <Motion :delay="150">
              <el-form-item prop="password">
              <el-form-item class="login-animation3" prop="code">
                <el-col :span="15">
                  <el-input
                    ref="codeRef"
                    v-model="state.ruleForm.code"
                    text
                    maxlength="4"
                    placeholder="请输入验证码"
                    clearable
                    autocomplete="off"
                  />
                </el-col>
                <el-col :span="1" />
                <el-col :span="8">
                  <div
                    :class="[
                      state.expirySeconds > 0
                        ? 'login-content-code'
                        : 'login-content-code-expired'
                    ]"
                    @click="getCaptcha"
                  >
                    <img
                      class="login-content-code-img"
                      width="130px"
                      height="38px"
                      :src="state.captchaImage"
                      style="cursor: pointer"
                    />
                  </div>
                </el-col>
              </el-form-item>
              <!-- <el-form-item prop="password">
                <el-input
                  v-model="ruleForm.password"
                  clearable
@@ -150,9 +324,34 @@
                  placeholder="密码"
                  :prefix-icon="useRenderIcon(Lock)"
                />
              </el-form-item> -->
            </Motion>
            <Motion :delay="150">
              <el-form-item prop="phoneVCode">
                <el-input
                  v-model.number="state.ruleForm.phoneVCode"
                  class="form-input"
                  placeholder="请输入验证码"
                >
                  <template #suffix>
                    <span v-if="state.phoneSeconds == 0" id="suffix-span">
                      <span
                        id="suffix-span-2"
                        ref="spanRef"
                        @click="sendValidationCode(state.ruleForm.phone)"
                      >
                        获取验证码
                      </span>
                    </span>
                    <span v-else id="suffix-span">
                      <span id="suffix-span-2" ref="spanRef">
                        {{ state.phoneSeconds }}秒后重新获取
                      </span>
                    </span>
                  </template>
                </el-input>
              </el-form-item>
            </Motion>
            <Motion :delay="250">
              <el-button
                class="w-full mt-4!"
@@ -180,4 +379,7 @@
:deep(.el-input-group__append, .el-input-group__prepend) {
  padding: 0;
}
#suffix-span {
  cursor: pointer;
}
</style>
src/views/register/index.vue
@@ -18,18 +18,22 @@
          <el-form
            ref="ruleFormRef"
            style="max-width: 600px"
            :model="ruleForm"
            :rules="rules"
            :model="state.ruleForm"
            :rules="state.rules"
            label-width="auto"
            size="large"
          >
            <el-form-item label="注册身份" prop="resource">
              <el-radio-group v-model="ruleForm.resource">
                <el-radio value="Sponsorship">供应商</el-radio>
                <el-radio value="Venue">代理机构</el-radio>
                <el-radio value="cgr">采购人</el-radio>
            <el-form-item label="注册身份" prop="exRoleCode">
              <el-radio-group v-model="state.ruleForm.exRoleCode">
                <el-radio
                  v-for="item in state.roleList"
                  :key="item.id"
                  :value="item.code"
                  >{{ item.name }}</el-radio
                >
              </el-radio-group>
            </el-form-item>
            <el-form-item label="企业名称" prop="name">
            <!-- <el-form-item label="企业名称" prop="name">
              <el-input
                v-model="ruleForm.name"
                placeholder="请输入营业执照上的企业名称"
@@ -37,8 +41,20 @@
            </el-form-item>
            <el-form-item label="用户名" prop="region">
              <el-input v-model="ruleForm.region" placeholder="请输入用户名" />
            </el-form-item> -->
            <el-form-item label="姓名" prop="nickName">
              <el-input
                v-model="state.ruleForm.nickName"
                placeholder="请输入姓名"
              />
            </el-form-item>
            <el-form-item label="登录密码" prop="password">
            <el-form-item label="手机号码" prop="phone">
              <el-input
                v-model="state.ruleForm.phone"
                placeholder="请输入您的手机号码"
              />
            </el-form-item>
            <!-- <el-form-item label="登录密码" prop="password">
              <el-input
                v-model="ruleForm.password"
                placeholder="8-20位数字+大小写字母+特殊字符的组合"
@@ -55,48 +71,74 @@
                v-model="ruleForm.repassword"
                placeholder="请输入联系人姓名"
              />
            </el-form-item>
            <el-form-item label="邮箱" prop="repassword">
            </el-form-item> -->
            <el-form-item label="邮箱" prop="email">
              <el-input
                v-model="ruleForm.repassword"
                v-model="state.ruleForm.email"
                placeholder="请输入联系邮箱"
              />
            </el-form-item>
            <el-form-item label="手机号码" prop="repassword">
              <el-input
                v-model="ruleForm.repassword"
                placeholder="请输入您的手机号码"
              />
            <el-form-item label="验证码" class="login-animation3" prop="code">
              <el-col :span="15">
                <el-input
                  ref="codeRef"
                  v-model="state.ruleForm.code"
                  text
                  maxlength="4"
                  placeholder="请输入验证码"
                  clearable
                  autocomplete="off"
                />
              </el-col>
              <el-col :span="1" />
              <el-col :span="8">
                <div
                  :class="[
                    state.expirySeconds > 0
                      ? 'login-content-code'
                      : 'login-content-code-expired'
                  ]"
                  @click="getCaptcha"
                >
                  <img
                    class="login-content-code-img"
                    width="130px"
                    height="38px"
                    :src="state.captchaImage"
                    style="cursor: pointer"
                  />
                </div>
              </el-col>
            </el-form-item>
            <el-form-item label="手机号码" prop="repassword">
            <el-form-item prop="phoneVCode" label="手机验证码">
              <el-input
                v-model="ruleForm.repassword"
                placeholder="请输入您的手机号码"
              />
            </el-form-item>
            <el-form-item prop="validationCode" label="手机验证码">
              <el-input
                v-model.number="ruleForm.validationCode"
                v-model.number="state.ruleForm.phoneVCode"
                class="form-input"
                placeholder="请输入验证码"
              >
                <template #suffix>
                  <span id="suffix-span">
                  <span v-if="state.phoneSeconds == 0" id="suffix-span">
                    <span
                      id="suffix-span-2"
                      ref="spanRef"
                      @click="sendValidationCode(ruleForm.repassword)"
                      @click="sendValidationCode(state.ruleForm.phone)"
                    >
                      {{ isSendValidationCode }}
                      获取验证码
                    </span>
                  </span>
                  <span v-else id="suffix-span">
                    <span id="suffix-span-2" ref="spanRef">
                      {{ state.phoneSeconds }}秒后重新获取
                    </span>
                  </span>
                </template>
              </el-input>
            </el-form-item>
            <el-form-item prop="repassword" label=" ">
              <el-checkbox value="Online activities" name="type">
                我已阅读并同意 《非政采招标采购交易平台用户协议》
              </el-checkbox>
            <el-form-item label=" ">
              <el-checkbox v-model="checked1" name="type" />我已阅读并同意
              <el-link type="primary" :underline="false"
                >《非政采招标采购交易平台用户协议》</el-link
              >
            </el-form-item>
            <el-form-item label=" ">
              <el-button type="primary" @click="submitForm(ruleFormRef)">
@@ -104,7 +146,8 @@
              </el-button>
            </el-form-item>
            <el-form-item label=" ">
              <span>已有账号?立即登录</span>
              <span>已有账号?</span>
              <el-link type="primary" :underline="false">立即登录</el-link>
            </el-form-item>
          </el-form>
        </div>
@@ -114,120 +157,197 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from "vue";
import {
  reactive,
  computed,
  ref,
  onMounted,
  defineAsyncComponent,
  onUnmounted,
  watch
} from "vue";
import type { FormInstance, FormRules } from "element-plus";
interface RuleForm {
  name: string;
  region: string;
  count: string;
  password: string;
  repassword: string;
  validationCode: string;
  date1: string;
  date2: string;
  delivery: boolean;
  location: string;
  type: string[];
  resource: string;
  desc: string;
}
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive<RuleForm>({
  name: "",
  region: "",
  password: "",
  validationCode: "",
  repassword: "",
  count: "",
  date1: "",
  date2: "",
  delivery: false,
  location: "",
  type: [],
  resource: "",
  desc: ""
import {
  captcha,
  phoneNumberCode,
  register,
  exRole
} from "@/api/register/index.ts";
import { useRoute, useRouter } from "vue-router";
import { message } from "@/utils/message";
defineOptions({
  name: "Register"
});
const route = useRoute();
const router = useRouter();
const checked1 = ref(false);
const state = reactive({
  isShowPassword: false,
  ruleForm: {
    account: "",
    nickName: "",
    phone: "",
    phoneVCode: "",
    // tenantId: props.tenantInfo.id,
    code: "",
    codeId: 0,
    email: "",
    exRoleCode: ""
  },
  rules: {
    phoneVCode: [
      {
        required: true,
        message: "请输入手机验证码",
        trigger: "blur"
      }
    ],
    nickName: [
      {
        required: true,
        message: "请输入姓名",
        trigger: "blur"
      }
    ],
    phone: [
      {
        required: true,
        message: "请输入您的手机号码",
        trigger: "blur"
      }
    ],
    exRoleCode: [
      {
        required: true,
        message: "请选择角色",
        trigger: "blur"
      }
    ]
    // code: [{ required: true, message: t('message.account.placeholder4'), trigger: 'blur' }],
  },
  loading: {
    signIn: false
  },
  captchaImage: "",
  rotateVerifyVisible: false,
  // rotateVerifyImg: verifyImg,
  // rotateVerifyImg: themeConfig.value.logoUrl,
  secondVerEnabled: false,
  // captchaEnabled: false,
  isPassRotate: false,
  capsLockVisible: false,
  hideTenantForLogin: false,
  expirySeconds: 60, // 验证码过期时间
  phoneSeconds: 0, // 手机验证码倒计时
  roleList: []
});
// 验证码过期计时器
let timer: any = null;
let phonetimer: any = null;
// 页面初始化
onMounted(async () => {
  // 若URL带有Token参数(第三方登录)
  const accessToken = route.query.token;
  // if (accessToken) await saveTokenAndInitRoutes(accessToken);
  // watch(
  //   () => themeConfig.value.isLoaded,
  //   isLoaded => {
  //     if (isLoaded) {
  // 获取登录配置
  // state.hideTenantForLogin = themeConfig.value.hideTenantForLogin ?? true;
  // state.secondVerEnabled = themeConfig.value.secondVer ?? true;
  // state.captchaEnabled = themeConfig.value.captcha ?? true;
  // 获取验证码
  getCaptcha();
  exRole().then(res => {
    state.roleList = res.result;
  });
  // 注册验证码过期计时器
  // if (state.captchaEnabled) {
  timer = setInterval(() => {
    if (state.expirySeconds > 0) state.expirySeconds -= 1;
  }, 1000);
  // }
  // }
  // },
  // { immediate: true }
  // );
  // 检测大小写按键/CapsLK
  document.addEventListener("keyup", handleKeyPress);
});
// 页面卸载
onUnmounted(() => {
  // 销毁验证码过期计时器
  clearInterval(timer);
  timer = null;
  clearInterval(phonetimer);
  phonetimer = null;
  document.removeEventListener("keyup", handleKeyPress);
});
// 检测大小写按键
const handleKeyPress = (e: KeyboardEvent) => {
  if (e.getModifierState != undefined)
    state.capsLockVisible = e.getModifierState("CapsLock");
};
// 获取验证码
const getCaptcha = async () => {
  // if (!state.captchaEnabled) return;
  state.ruleForm.code = "";
  const res = await captcha();
  console.log(res);
  state.captchaImage = "data:text/html;base64," + res.result?.img;
  state.expirySeconds = res.result?.expirySeconds;
  state.ruleForm.codeId = res.result?.id;
};
const ruleFormRef = ref<FormInstance>();
const locationOptions = ["Home", "Company", "School"];
const rules = reactive<FormRules<RuleForm>>({
  name: [
    { required: true, message: "Please input Activity name", trigger: "blur" },
    { min: 3, max: 5, message: "Length should be 3 to 5", trigger: "blur" }
  ],
  region: [
    {
      required: true,
      message: "Please select Activity zone",
      trigger: "change"
    }
  ],
  count: [
    {
      required: true,
      message: "Please select Activity count",
      trigger: "change"
    }
  ],
  date1: [
    {
      type: "date",
      required: true,
      message: "Please pick a date",
      trigger: "change"
    }
  ],
  date2: [
    {
      type: "date",
      required: true,
      message: "Please pick a time",
      trigger: "change"
    }
  ],
  location: [
    {
      required: true,
      message: "Please select a location",
      trigger: "change"
    }
  ],
  type: [
    {
      type: "array",
      required: true,
      message: "Please select at least one activity type",
      trigger: "change"
    }
  ],
  resource: [
    {
      required: true,
      message: "Please select activity resource",
      trigger: "change"
    }
  ],
  desc: [
    { required: true, message: "Please input activity form", trigger: "blur" }
  ]
});
// 验证码区域文字说明
const spanRef = ref();
const isSendValidationCode = ref<string>("获取验证码");
const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log("submit!");
    } else {
      console.log("error submit!", fields);
    }
      state.ruleForm.account = state.ruleForm.phone;
      register(state.ruleForm).then(res => {
        if (res?.code == 200) {
          router.replace("/RegisterSucess");
          return message("注册成功!", { type: "success" });
        } else {
          return message(res?.message, { type: "warning" });
        }
      });
    } else if (!checked1.value)
      return message("请勾选用户协议", { type: "warning" });
  });
};
const sendValidationCode = () => {};
const sendValidationCode = async () => {
  if (!state.ruleForm.phone) {
    return message("请先输入手机号", { type: "warning" });
  }
  if (!state.ruleForm.code) {
    return message("请先输入验证码", { type: "warning" });
  }
  const res = await phoneNumberCode(state.ruleForm);
  if (res?.code != 200) {
    return message(res?.message, { type: "warning" });
  }
  state.phoneSeconds = 60;
  phonetimer = setInterval(() => {
    if (state.phoneSeconds > 0) state.phoneSeconds -= 1;
  }, 1000);
};
const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
@@ -296,4 +416,49 @@
    }
  }
}
.el-form-item {
  align-items: center;
}
#suffix-span {
  cursor: pointer;
}
.login-content-code {
  display: flex;
  align-items: center;
  justify-content: space-around;
  position: relative;
  .login-content-code-img {
    width: 100%;
    height: 40px;
    line-height: 40px;
    background-color: #ffffff;
    border: 1px solid rgb(220, 223, 230);
    cursor: pointer;
    transition: all ease 0.2s;
    border-radius: 4px;
    user-select: none;
    &:hover {
      border-color: #c0c4cc;
      transition: all ease 0.2s;
    }
  }
}
.login-content-code-expired {
  @extend .login-content-code;
  &::before {
    content: "验证码已过期";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 4px;
    background-color: rgba(0, 0, 0, 0.5);
    color: #ffffff;
    text-align: center;
  }
}
</style>
src/views/register/registersucess.vue
New file
@@ -0,0 +1,36 @@
<template>
  <div class="content">
    <div class="header">
      <div class="headimg">
        <img width="167px" height="44px" src="@/assets/home/logo.png" alt="" />
      </div>
    </div>
    <div class="center w-[70%] h-[584px] bg-white mx-auto mt-25">
      <el-button type="primary">马上登录</el-button>
    </div>
  </div>
</template>
<script setup>
defineOptions({
  name: "RegisterSucess"
});
</script>
<style lang="scss" scoped>
.content {
  background-color: #f8f8f8;
  width: 100%;
  height: 100%;
  .header {
    display: flex;
    align-items: center;
    height: 80px;
    width: 100%;
    margin: 0 auto;
    background-color: #fff;
    .headimg {
      width: 1200px;
      margin: 0 auto;
    }
  }
}
</style>
vite.config.ts
@@ -24,7 +24,14 @@
      port: VITE_PORT,
      host: "0.0.0.0",
      // 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
      proxy: {},
      proxy: {
        "/api": {
          // 这里填写后端地址
          target: "http://192.168.0.36:5005",
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, "")
        }
      },
      // 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
      warmup: {
        clientFiles: ["./index.html", "./src/{views,components}/*"]