xyh 2 лет назад
Родитель
Сommit
f487d0fb08

BIN
src/assets/images/register/imgPlaceholder.webp


BIN
src/assets/images/register/phoneIcon.webp


BIN
src/assets/images/register/success.webp


+ 0 - 5
src/components/.gitkeep

@@ -1,5 +0,0 @@
-# To ensure that empty folders can also be submitted
-
-# You can delete this file
-
-# This type of file has been entered in .gitignore, and this type of file will not be submitted to git

+ 15 - 0
src/components/button/Button.vue

@@ -0,0 +1,15 @@
+<script setup lang='ts'>
+import {ElButton} from 'element-plus';
+
+defineOptions({name: 'ButtonComponent'});
+</script>
+
+<template>
+  <ElButton class="button-component">
+    <slot />
+  </ElButton>
+</template>
+
+<style scoped lang='css'>
+@import './index.css';
+</style>

+ 7 - 0
src/components/button/index.css

@@ -0,0 +1,7 @@
+.button-component {
+  color: white;
+  text-align: center;
+  background-color: var(--primary-color);
+  border-radius: 9999999px;
+  box-shadow: 0 2px 28px 1px rgb(204 212 255 / 59%);
+}

+ 1 - 0
src/components/button/index.ts

@@ -0,0 +1 @@
+export {default as Button} from './Button.vue';

+ 1 - 0
src/components/index.ts

@@ -1 +1,2 @@
 export * from './login-field';
+export * from './button';

+ 1 - 0
src/components/login-field/Field.vue

@@ -23,6 +23,7 @@ const {value, errorMessage, name: inputName} = useField<string>(name.value);
     :name="inputName"
     :placeholder="placeholder"
     :type="type"
+    class="field"
   >
     <template #prefix>
       <slot name="prefix" />

+ 58 - 0
src/components/login-field/Upload.vue

@@ -0,0 +1,58 @@
+<script setup lang='ts'>
+import {toRefs} from 'vue';
+import placeholder from '@assets/images/register/imgPlaceholder.webp';
+import {useI18n} from 'vue-i18n';
+import {useDropFile, useUpload} from './hooks';
+import {useField} from 'vee-validate';
+import {ElImage} from 'element-plus';
+
+defineOptions({name: 'LoginUpload'});
+
+type Props = {
+  title: string;
+  name: string;
+};
+const props = defineProps<Props>();
+const {title, name} = toRefs(props);
+
+const {t} = useI18n();
+
+const {value, setValue, errorMessage} = useField<string>(name);
+const [{uploadRef}, {onUpload, onUploadClick}] = useUpload(setValue);
+const {zoneRef, isOverDropZone} = useDropFile(onUpload);
+</script>
+
+<template>
+  <div
+    ref="zoneRef"
+    :class="['upload-wrapper', {'drop-active': isOverDropZone}]"
+  >
+    <p class="title">{{title}}</p>
+    <ElImage
+      fit="cover"
+      :src="value ? value : placeholder"
+      :class="[
+        'placeholder',
+        {'preview-img': !!value}
+      ]"
+      :preview-src-list="value ? [value] : void 0"
+/>
+    <p class="drap-tip">
+      {{t('register.uploadTip')}}<span @click="onUploadClick">
+        {{t('register.upload')}}
+      </span>
+    </p>
+    <p
+      v-show="!value"
+      :class="['max-size-tip', {'upload-error-tip': !!errorMessage}]"
+    >
+      {{t(errorMessage || 'register.maxSizeTip')}}
+    </p>
+
+    <input v-show="false" ref="uploadRef" type="file" accept="image/*" />
+  </div>
+</template>
+
+<style scoped lang='css'>
+@import './index.css';
+</style>

+ 45 - 0
src/components/login-field/hooks.ts

@@ -0,0 +1,45 @@
+import {useDropZone, useEventListener} from '@vueuse/core';
+import {ref} from 'vue';
+import {ElMessage} from 'element-plus';
+
+export function useUpload(setValue: (val: string) => void) {
+  const uploadRef = ref<HTMLInputElement>();
+
+  function onUpload(files?: File | File[] | null) {
+    if (!files) {
+      return;
+    }
+    const file = Array.isArray(files) ? files[0] : files;
+    if (!file.type.includes('image')) {
+      ElMessage.error('请上传图片');
+      return;
+    }
+
+    const reader = new FileReader();
+    reader.readAsDataURL(file);
+    reader.onload = function() {
+      setValue(this.result as string);
+    };
+  }
+
+  function onUploadClick() {
+    uploadRef.value?.click();
+  }
+
+  useEventListener(uploadRef, 'change', function(e) {
+    const target = e.target as HTMLInputElement;
+
+    onUpload(target.files?.item(0));
+  });
+
+  return [{uploadRef}, {onUpload, onUploadClick}] as const;
+}
+
+export function useDropFile(onUpload: (files: File[] | null) => void) {
+  const zoneRef = ref<HTMLImageElement>();
+  const {isOverDropZone} = useDropZone(zoneRef, function(file) {
+    onUpload(file);
+  });
+
+  return {zoneRef, isOverDropZone};
+}

+ 71 - 0
src/components/login-field/index.css

@@ -10,3 +10,74 @@
 .error-top-hidden {
   visibility: hidden;
 }
+
+.field {
+  & :deep(.el-input__wrapper) {
+    --el-input-height: 46px;
+    --el-input-border-radius: 23px;
+    --el-input-text-color: #333;
+
+    padding: 0 32px;
+    margin-top: 12px;
+    background-color: #f3f5fa;
+    box-shadow: none;
+  }
+}
+
+.upload-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 260px;
+  padding: 24px 16px 20px;
+  background: #f3f5fa;
+  border: 1px dashed #dedfe2;
+  border-radius: 10px;
+}
+
+.title {
+  font-size: 16px;
+  color: #000;
+  text-align: center;
+}
+
+.placeholder {
+  width: 80px;
+  margin: auto 0;
+
+  & img {
+    width: 100%;
+  }
+}
+
+.drap-tip {
+  font-size: 16px;
+
+  & span {
+    color: var(--primary-color);
+    cursor: pointer;
+  }
+}
+
+.max-size-tip {
+  margin-top: 16px;
+  font-size: 14px;
+}
+
+.drop-active {
+  border-color: var(--primary-color);
+  opacity: 0.4;
+}
+
+.upload-error-tip {
+  color: red;
+}
+
+.preview-img {
+  width: 200px;
+  height: 120px;
+  object-fit: cover;
+  cursor: pointer;
+}

+ 1 - 0
src/components/login-field/index.ts

@@ -1 +1,2 @@
 export {default as LoginField} from './Field.vue';
+export {default as LoginUpload} from './Upload.vue';

+ 3 - 0
src/locales/index.ts

@@ -1,5 +1,6 @@
 import {createI18n} from 'vue-i18n';
 import loginMessage from './login';
+import registerMessage from './register';
 
 function initLocalLanguage() {
   const language = navigator.language.toLocaleLowerCase();
@@ -18,9 +19,11 @@ const i18n = createI18n({
   messages: {
     zh: {
       login: loginMessage.cn,
+      register: registerMessage.cn,
     },
     ko: {
       login: loginMessage.ko,
+      register: registerMessage.ko,
     },
   },
   legacy: false,

+ 19 - 0
src/locales/register.ts

@@ -0,0 +1,19 @@
+export default {
+  cn: {
+    tabs: ['① 提交基础信息', '② 申请成功'],
+    title: '填写企业信息,申请成为供应商!',
+    placeholder: {enterprise: '填写企业名称', phone: '填写手机联系方式'},
+    idcard: {title: '上传法人身份证'},
+    license: {title: '上传营业执照'},
+    maxSizeTip: '单张图片体积不超过20MB',
+    upload: '上传',
+    uploadTip: '拖拽图片到这里,或点击',
+    confirm: '下一步',
+    reset: '重置',
+    dropOverTip: '请松开鼠标',
+    successTitle: '恭喜,供应商注册申请成功',
+    successSubtitle: '审核结果将在5个工作日以内发送至您的邮箱,请您耐心等待~',
+    notifyButtonText: '通知企业审核',
+  },
+  ko: {},
+};

+ 60 - 0
src/pages/register/content/index.css

@@ -0,0 +1,60 @@
+.title {
+  font-size: 18px;
+  font-weight: 500;
+  color: #333;
+}
+
+.content-info {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  height: 100%;
+  padding: 0 200px;
+}
+
+.form-group {
+  display: flex;
+  gap: 40px;
+  justify-content: space-between;
+  margin-top: 30px;
+}
+
+.form-item {
+  flex: 1;
+}
+
+.input-icon {
+  width: 18px;
+}
+
+.upload {
+  margin-top: 16px;
+}
+
+.btn-group {
+  display: flex;
+  gap: 52px;
+  margin: auto auto 0;
+
+  & button {
+    width: 189px;
+    height: 46px;
+    font-size: 16px;
+    line-height: 43px;
+    color: #666;
+    cursor: pointer;
+    background-color: transparent;
+    border: none;
+    border-radius: 23px;
+
+    &[type='reset'] {
+      border: 2px solid #6a8bf5;
+    }
+
+    &[type='submit'] {
+      color: white;
+      background-color: var(--primary-color);
+      box-shadow: 0 2px 28px 1px rgb(204 212 255 / 59%);
+    }
+  }
+}

+ 58 - 0
src/pages/register/content/index.vue

@@ -0,0 +1,58 @@
+<script setup lang='ts'>
+import {LoginField, LoginUpload} from '@components';
+import {useI18n} from 'vue-i18n';
+import enterpriseIcon from '@assets/images/login/enterprise.webp';
+import phoneIcon from '@assets/images/register/phoneIcon.webp';
+
+defineOptions({name: 'RegisterContent'});
+
+const {t} = useI18n();
+</script>
+
+<template>
+  <div class="content-info">
+    <h2 v-t="'register.title'" class="title" />
+
+    <div class="form-group">
+      <div class="form-item">
+        <LoginField
+          name="enterprise"
+          :placeholder="t('register.placeholder.enterprise')"
+        >
+          <template #prefix>
+            <img :src="enterpriseIcon" class="input-icon" />
+          </template>
+        </LoginField>
+
+        <LoginUpload
+          class="upload"
+          :title="t('register.idcard.title')" name="idCardImage"
+        />
+      </div>
+      <div class="form-item">
+        <LoginField
+          name="phone"
+          :placeholder="t('register.placeholder.phone')"
+        >
+          <template #prefix>
+            <img :src="phoneIcon" class="input-icon" />
+          </template>
+        </LoginField>
+
+        <LoginUpload
+          class="upload"
+          :title="t('register.license.title')" name="licenseImage"
+        />
+      </div>
+    </div>
+
+    <div class="btn-group">
+      <button type="reset">{{t('register.reset')}}</button>
+      <button type="submit">{{t('register.confirm')}}</button>
+    </div>
+  </div>
+</template>
+
+<style scoped lang='css'>
+@import './index.css';
+</style>

+ 40 - 0
src/pages/register/hooks.ts

@@ -0,0 +1,40 @@
+import {toTypedSchema} from '@vee-validate/zod';
+import {useForm} from 'vee-validate';
+import {Ref} from 'vue';
+import {object, string} from 'zod';
+
+type FormState = {
+  enterprise: string;
+  phone: string;
+  idCardImage: string;
+  licenseImage: string;
+};
+
+const validate = object({
+  enterprise: string({errorMap: () => ({message: '请输入企业名称'})})
+    .min(1, '请输入企业名称'),
+  phone: string({errorMap: () => ({message: '请输入企业名称'})})
+    .min(1, '请输入企业名称'),
+  idCardImage: string({errorMap: () => ({message: '请上传法人身份证'})})
+    .min(1, '请上传法人身份证'),
+  licenseImage: string({errorMap: () => ({message: '请上传营业执照'})})
+    .min(1, '请上传营业执照'),
+});
+
+export function useFormState(isSuccess: Ref<boolean>) {
+  const {handleSubmit, handleReset} = useForm<FormState>({
+    initialValues: {
+      enterprise: '',
+      phone: '',
+      idCardImage: '',
+      licenseImage: '',
+    },
+    validationSchema: toTypedSchema(validate),
+  });
+
+  const onSubmit = handleSubmit(function(value) {
+    isSuccess.value = true;
+  });
+
+  return {onSubmit, handleReset};
+}

+ 100 - 0
src/pages/register/index.css

@@ -0,0 +1,100 @@
+main {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+  background-image: url('@assets/images/login/background.jpg');
+  background-repeat: no-repeat;
+  background-size: cover;
+}
+
+.container {
+  display: flex;
+  flex-direction: column;
+  width: 1400px;
+  height: 760px;
+  padding: 40px 50px;
+  overflow: hidden;
+  background: #fff;
+  border-radius: 20px;
+  box-shadow: 0 2px 57px 24px rgb(0 0 0 / 11%);
+}
+
+.logo {
+  display: flex;
+  align-items: center;
+
+  & img {
+    width: 140px;
+  }
+
+  & h1 {
+    margin-left: 22px;
+    font-family: PingFang SC;
+    font-size: 28px;
+    font-weight: 500;
+    line-height: 28px;
+    color: #010101;
+  }
+}
+
+.tab {
+  position: relative;
+  display: flex;
+  margin-top: 40px;
+
+  &::after {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1;
+    height: 1px;
+    content: '';
+    background-color: #f1f1f1;
+    border-radius: 1px;
+  }
+
+  & span {
+    position: relative;
+    flex: 1;
+    padding: 16px 0;
+    font-size: 22px;
+    color: #333;
+    text-align: center;
+  }
+}
+
+.tab-item-active {
+  color: var(--primary-color) !important;
+
+  &::after {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 2;
+    height: 4px;
+    content: '';
+    background-color: var(--primary-color);
+    border-top-left-radius: 2px;
+    border-bottom-left-radius: 2px;
+  }
+
+  &:last-child {
+    &::after {
+      border-radius: 2px 0;
+    }
+  }
+}
+
+.content {
+  flex: 1;
+  margin-top: 50px;
+  overflow: hidden;
+
+  & form {
+    height: 100%;
+  }
+}

+ 46 - 0
src/pages/register/index.vue

@@ -0,0 +1,46 @@
+<script setup lang='ts'>
+import logo from '@assets/images/logo.webp';
+import {useToggle} from '@vueuse/core';
+import {useI18n} from 'vue-i18n';
+import RegisterContent from './content/index.vue';
+import {useFormState} from './hooks';
+import Success from './success/index.vue';
+
+defineOptions({name: 'Register'});
+
+const [isSuccess] = useToggle(true);
+const {t} = useI18n();
+
+const {onSubmit, handleReset} = useFormState(isSuccess);
+
+</script>
+
+<template>
+  <main>
+    <div class="container">
+      <section class="logo">
+        <img :src="logo" />
+        <h1 v-t="'login.title'" />
+      </section>
+
+      <section class="tab">
+        <span class="tab-item-active">{{t('register.tabs[0]')}}</span>
+        <span :class="{'tab-item-active': isSuccess}">
+          {{t('register.tabs[1]')}}
+        </span>
+      </section>
+
+      <section class="content">
+        <form v-show="!isSuccess" @submit="onSubmit" @reset="handleReset">
+          <RegisterContent />
+        </form>
+
+        <Success v-show="isSuccess" />
+      </section>
+    </div>
+  </main>
+</template>
+
+<style scoped lang='css'>
+@import './index.css';
+</style>

+ 32 - 0
src/pages/register/success/index.css

@@ -0,0 +1,32 @@
+.success-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  padding-bottom: 130px;
+}
+
+.result {
+  margin-top: 32px;
+  font-size: 20px;
+  color: #333;
+}
+
+.subtitle {
+  margin-top: 36px;
+  font-size: 16px;
+  color: #333;
+}
+
+.email {
+  margin-top: 32px;
+  font-size: 14px;
+  color: #666;
+}
+
+.notify-btn {
+  width: 180px;
+  height: 42px;
+  margin-top: auto;
+}

+ 23 - 0
src/pages/register/success/index.vue

@@ -0,0 +1,23 @@
+<script setup lang='ts'>
+import successIcon from '@assets/images/register/success.webp';
+import {Button} from '@components';
+import {useI18n} from 'vue-i18n';
+
+defineOptions({name: 'RegisterSuccess'});
+
+const {t} = useI18n();
+</script>
+
+<template>
+  <div class="success-info">
+    <img :src="successIcon" />
+    <p v-t="'register.successTitle'" class="result" />
+    <p v-t="'register.successSubtitle'" class="subtitle" />
+    <p class="email">1234@163.com</p>
+    <Button class="notify-btn">{{t('register.notifyButtonText')}}</Button>
+  </div>
+</template>
+
+<style scoped lang='css'>
+@import './index.css';
+</style>

+ 9 - 1
src/routes/index.ts

@@ -1,11 +1,19 @@
 import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router';
-import {HOME_NAME, HOME_PATH, LOGIN_NAME, LOGIN_PATH} from './name';
+import {
+  HOME_NAME,
+  HOME_PATH,
+  LOGIN_NAME,
+  LOGIN_PATH,
+  REGISTER_NAME,
+  REGISTER_PATH,
+} from './name';
 import Home from '@pages/home/index.vue';
 import Login from '@pages/login/index.vue';
 
 const routes: RouteRecordRaw[] = [
   {path: LOGIN_PATH, component: Login, name: LOGIN_NAME},
   {path: HOME_PATH, component: Home, name: HOME_NAME},
+  {path: REGISTER_PATH, name: REGISTER_NAME, component: () => import('@pages/register/index.vue')},
 ];
 
 export const router = createRouter({

+ 4 - 0
src/routes/name.ts

@@ -5,3 +5,7 @@ export const HOME_NAME = Symbol('HOME_NAME');
 /** 登录页面 */
 export const LOGIN_PATH = '/login';
 export const LOGIN_NAME = Symbol('LOGIN_NAME');
+
+/** 注册界面 */
+export const REGISTER_PATH = '/register';
+export const REGISTER_NAME = Symbol('REGISTER_NAME');

+ 0 - 4
src/styles/variable.css

@@ -4,7 +4,3 @@
   --el-tag-bg-color: var(--primary-color);
 }
 
-/*
-html.dark {
-
-} */