Browse Source

feat: 登录ui完成

xyh 2 years ago
parent
commit
e1040dc164

+ 7 - 1
.stylelintrc.json

@@ -15,6 +15,12 @@
     "no-duplicate-selectors": true,
     "no-descending-specificity": null,
     "selector-class-pattern": "^([a-z][a-z0-9]*)((-|__)[a-z0-9]+)*$",
-    "value-no-vendor-prefix": [true, {"ignoreValues": ["box"]}]
+    "value-no-vendor-prefix": [true, {"ignoreValues": ["box"]}],
+    "selector-pseudo-class-no-unknown": [
+      true,
+      {
+        "ignorePseudoClasses": ["deep"]
+      }
+    ]
   }
 }

+ 15 - 8
package.json

@@ -82,22 +82,28 @@
     ]
   },
   "dependencies": {
-    "@tanstack/vue-query": "^4.29.5",
+    "@icon-park/vue-next": "^1.4.2",
+    "@tanstack/vue-query": "^4.29.7",
+    "@vee-validate/zod": "^4.9.3",
+    "@vueuse/core": "^10.1.2",
     "axios": "^1.4.0",
+    "element-plus": "^2.3.4",
     "pinia": "^2.0.36",
-    "veboundary": "1.2.1",
-    "vue": "^3.3.1",
-    "vue-router": "^4.2.0"
+    "veboundary": "1.2.2",
+    "vee-validate": "^4.9.3",
+    "vue": "^3.3.2",
+    "vue-router": "^4.2.0",
+    "zod": "^3.21.4"
   },
   "devDependencies": {
     "@commitlint/cli": "^17.6.3",
     "@commitlint/config-conventional": "^17.6.3",
     "@nabla/vite-plugin-eslint": "^1.5.0",
-    "@types/node": "20.1.3",
+    "@types/node": "20.1.4",
     "@types/rollup-plugin-visualizer": "^4.2.1",
     "@typescript-eslint/eslint-plugin": "^5.59.5",
     "@typescript-eslint/parser": "^5.59.5",
-    "@vitejs/plugin-vue": "^4.2.2",
+    "@vitejs/plugin-vue": "^4.2.3",
     "@vue/test-utils": "^2.3.2",
     "browserslist-to-esbuild": "^1.2.0",
     "commitizen": "^4.3.0",
@@ -124,10 +130,11 @@
     "stylelint-config-recess-order": "^4.0.0",
     "stylelint-config-standard": "^33.0.0",
     "typescript": "^5.0.4",
+    "unplugin-element-plus": "^0.7.1",
     "vite": "^4.3.5",
     "vitest": "^0.31.0",
-    "vue-eslint-parser": "^9.2.1",
-    "vue-tsc": "^1.6.4"
+    "vue-eslint-parser": "^9.3.0",
+    "vue-tsc": "^1.6.5"
   },
   "browserslist": {
     "production": [

File diff suppressed because it is too large
+ 559 - 244
pnpm-lock.yaml


BIN
src/assets/images/login/enterprise.webp


BIN
src/assets/images/login/icon.webp


BIN
src/assets/images/login/passowrd.webp


BIN
src/assets/images/login/user.webp


BIN
src/assets/images/logo.png


BIN
src/assets/images/logo.webp


+ 13 - 11
src/main.ts

@@ -1,22 +1,24 @@
+import 'element-plus/theme-chalk/dark/css-vars.css';
 import '@styles/global.css';
+import '@styles/variable.css';
+import '@icon-park/vue-next/styles/index.css';
 import {createApp} from 'vue';
 import App from './App.vue';
 import {router} from '@routes';
-import {
-  VueQueryPlugin,
-  QueryClient,
-  VueQueryPluginOptions,
-} from '@tanstack/vue-query';
+import {VueQueryPlugin, QueryClient} from '@tanstack/vue-query';
 import {createPinia} from 'pinia';
 
-const client = new QueryClient();
+const client = new QueryClient({
+  defaultOptions: {
+    queries: {
+      refetchOnWindowFocus: false,
+      retry: false,
+    },
+  },
+});
 
 const app = createApp(App);
 
-const vueQueryOptions: VueQueryPluginOptions = {
-  queryClient: client,
-};
-
-app.use(router).use(createPinia()).use(VueQueryPlugin, vueQueryOptions);
+app.use(router).use(createPinia()).use(VueQueryPlugin, {queryClient: client});
 
 app.mount('#app');

+ 0 - 54
src/pages/home/index.css

@@ -1,54 +0,0 @@
-main {
-  display: flex;
-  flex-direction: column;
-  min-height: 100vh;
-  background-color: #20232a;
-}
-
-.title {
-  margin-top: 80px;
-  color: white;
-  text-align: center;
-}
-
-.name {
-  margin-top: 20px;
-  font-size: 60px;
-  transition: color 500ms;
-}
-
-.icon {
-  display: block;
-  width: 15vw;
-  margin: 80px auto 0;
-}
-
-.btn-group {
-  display: flex;
-  align-items: center;
-  justify-content: space-evenly;
-  width: 200px;
-  margin: 30px auto 0;
-
-  & button {
-    width: 100px;
-    padding: 6px 0;
-    color: white;
-    cursor: pointer;
-    border: unset;
-    border-radius: 4px;
-
-    &:nth-child(1) {
-      margin-right: 14px;
-      background-color: #43a047;
-    }
-
-    &:nth-child(2) {
-      background-color: #2196f3;
-    }
-  }
-}
-
-.title-red {
-  color: red;
-}

+ 1 - 16
src/pages/home/index.vue

@@ -1,26 +1,11 @@
 <script setup lang="ts">
-import {defineOptions} from 'vue';
-import {useCountState, storeToRefs} from '@stores';
-
 defineOptions({
   name: 'HomeComponent',
 });
-
-const countState = useCountState();
-const {count} = storeToRefs(countState);
 </script>
 
 <template>
-  <main>
-    <img src="@assets/images/logo.png" class="icon" />
-    <h1 id="count" :class="['title', 'name', {'title-red': count > 5}]">
-      count is {{ count }}
-    </h1>
-    <div class="btn-group">
-      <button id="inc_btn" @click="countState.increment">inc</button>
-      <button id="dec_btn" @click="countState.reduce">dec</button>
-    </div>
-  </main>
+  <main></main>
 </template>
 
 <style scoped lang="css">

+ 39 - 0
src/pages/login/hooks.ts

@@ -0,0 +1,39 @@
+import {object, string} from 'zod';
+import {useForm} from 'vee-validate';
+import {toTypedSchema} from '@vee-validate/zod';
+
+export type FormState = {
+  name: string;
+  password: string;
+  company: string;
+};
+
+export function useFormState() {
+  const {handleSubmit} = useForm<FormState>({
+    initialValues: {
+      name: '',
+      password: '',
+      company: '',
+    },
+    validationSchema: toTypedSchema(
+      object({
+        name: string({errorMap: () => ({message: '请输入姓名'})}).min(
+          1,
+          '请输入用户名',
+        ),
+        password: string({errorMap: () => ({message: '请输入密码'})}).min(
+          1,
+          '请输入密码',
+        ),
+        company: string().optional(),
+      }),
+    ),
+    validateOnMount: false,
+  });
+
+  const onSubmit = handleSubmit(function (value) {
+    console.log(value);
+  });
+
+  return {onSubmit};
+}

+ 68 - 0
src/pages/login/index.css

@@ -0,0 +1,68 @@
+main {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100vw;
+  height: 100vh;
+}
+
+.container {
+  display: flex;
+  width: 1400px;
+  height: 700px;
+  overflow: hidden;
+  background: #fff;
+  border-radius: 20px;
+  box-shadow: 0 2px 57px 24px rgb(0 0 0 / 11%);
+}
+
+.display {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  width: 714px;
+  height: 100%;
+  padding: 42px 32px 100px;
+  background-color: #f3f5fa;
+}
+
+.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;
+  }
+}
+
+.display-icon {
+  width: 460px;
+  margin: 0 auto;
+}
+
+.form-wrapper {
+  display: flex;
+  flex: 1;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  padding: 0 98px;
+  overflow: hidden;
+}
+
+.login-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 421px;
+}

+ 35 - 0
src/pages/login/index.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import logo from '@assets/images/logo.webp';
+import displayImg from '@assets/images/login/icon.webp';
+import LoginInfo from './login-info/index.vue';
+import {useFormState} from './hooks';
+
+defineOptions({name: 'Login'});
+const {onSubmit} = useFormState();
+</script>
+
+<template>
+  <main>
+    <section class="container">
+      <div class="display">
+        <div class="logo">
+          <img :src="logo" />
+          <h1>供应商管理系统</h1>
+        </div>
+
+        <img :src="displayImg" class="display-icon" />
+      </div>
+      <div class="form-wrapper">
+        <form @submit="onSubmit">
+          <div class="login-info">
+            <LoginInfo />
+          </div>
+        </form>
+      </div>
+    </section>
+  </main>
+</template>
+
+<style scoped lang="css">
+@import './index.css';
+</style>

+ 11 - 0
src/pages/login/login-info/hooks.ts

@@ -0,0 +1,11 @@
+import {useField} from 'vee-validate';
+import {FormState} from '../hooks';
+
+export function useFieldItem(key: keyof FormState) {
+  const {value, errorMessage} = useField<string>(key, void 0, {
+    validateOnValueUpdate: true,
+    validateOnMount: false,
+  });
+
+  return {value, errorMessage};
+}

+ 168 - 0
src/pages/login/login-info/index.css

@@ -0,0 +1,168 @@
+.login-title {
+  width: 100%;
+  height: 41px;
+  font-size: 40px;
+  font-weight: 500;
+  color: #010101;
+  text-align: center;
+}
+
+.tab {
+  position: relative;
+  display: flex;
+  width: 100%;
+  padding: 14px;
+  margin-top: 24px;
+
+  &::after {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    z-index: 1;
+    width: 100%;
+    height: 2px;
+    content: '';
+    background-color: #f1f1f1;
+    border-radius: 1px;
+  }
+
+  & span {
+    flex: 1;
+    font-size: 18px;
+    color: #666;
+    text-align: center;
+    cursor: pointer;
+    transition: all 200ms color;
+  }
+}
+
+.tab-item-active {
+  color: #010101 !important;
+}
+
+.tab-indicator {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+  width: 50%;
+  height: 4px;
+  background-color: var(--primary-color);
+  border-radius: 2px;
+  transition: transform 200ms linear;
+}
+
+.tab-item-active:nth-child(1) ~ .tab-indicator {
+  transform: translateX(0);
+}
+
+.tab-item-active:nth-child(2) ~ .tab-indicator {
+  transform: translateX(100%);
+}
+
+.info {
+  margin-top: 20px;
+
+  & :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;
+  }
+}
+
+.input-icon {
+  width: 18px;
+}
+
+.password-icon {
+  cursor: pointer;
+}
+
+.login-btn {
+  width: 400px;
+  height: 46px;
+  color: white;
+  background: #2e3092;
+  border: none;
+  border-radius: 25px;
+  box-shadow: 0 2px 28px 1px rgb(204 212 255 / 59%);
+}
+
+.operation {
+  display: flex;
+  width: 300px;
+  margin: 20px auto 32px;
+  font-size: 14px;
+  color: #666;
+
+  & span {
+    position: relative;
+    display: flex;
+    flex: 1;
+    align-items: center;
+    justify-content: center;
+    line-height: 14px;
+    cursor: pointer;
+
+    &:first-child {
+      &::before {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 20px;
+        height: 20px;
+        margin-right: 6px;
+        font-size: 14px;
+        content: '';
+        border: 2px solid #666;
+        border-radius: 20px;
+        transform: scale(0.6);
+      }
+
+      &::after {
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        width: 1px;
+        content: '';
+        background-color: #999;
+      }
+    }
+  }
+}
+
+.rember-password {
+  &::before {
+    color: white;
+    content: '\2714' !important;
+    background-color: var(--primary-color);
+    border-color: var(--primary-color);
+  }
+}
+
+.regist {
+  display: inline-block;
+  margin-top: 20px;
+  font-size: 18px;
+  color: var(--primary-color);
+  text-decoration: underline;
+}
+
+.error-tip {
+  height: 14px;
+  padding: 0 34px;
+  margin-top: 4px;
+  font-size: 14px;
+  line-height: 14px;
+  color: red;
+}
+
+.error-top-hidden {
+  visibility: hidden;
+}

+ 101 - 0
src/pages/login/login-info/index.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import {ref} from 'vue';
+import {ElInput, ElIcon, ElButton} from 'element-plus';
+import userIcon from '@assets/images/login/user.webp';
+import posswordIcon from '@assets/images/login/passowrd.webp';
+import enterpriseIcon from '@assets/images/login/enterprise.webp';
+import {PreviewOpen, PreviewCloseOne} from '@icon-park/vue-next';
+import {useToggle} from '@vueuse/core';
+import {RouterLink} from 'vue-router';
+import {useFieldItem} from './hooks';
+
+defineOptions({name: 'LoginInfo'});
+const active = ref(0);
+function onTabClick(value: number) {
+  active.value = value;
+}
+
+const [isPassword, togglePassword] = useToggle(true);
+const [remberPassword, toggleRemberPassword] = useToggle();
+const {value: nameValue, errorMessage: nameErrorMessage} = useFieldItem('name');
+const {value: passwordValue, errorMessage: passwordErrorMessage} =
+  useFieldItem('password');
+const {value: companyValue, errorMessage: companyErrorMessage} =
+  useFieldItem('company');
+</script>
+
+<template>
+  <h2 class="login-title">Login</h2>
+  <div class="tab">
+    <span :class="{'tab-item-active': active === 0}" @click="onTabClick(0)">
+      供应商管理
+    </span>
+    <span :class="{'tab-item-active': active === 1}" @click="onTabClick(1)">
+      企业管理
+    </span>
+
+    <i class="tab-indicator"></i>
+  </div>
+
+  <div class="info">
+    <ElInput v-model="nameValue" name="userName" placeholder="用户名">
+      <template #prefix>
+        <img :src="userIcon" class="input-icon" />
+      </template>
+    </ElInput>
+    <p :class="['error-tip', {'error-top-hidden': !nameErrorMessage}]">
+      {{ nameErrorMessage }}
+    </p>
+
+    <ElInput
+      v-model="passwordValue"
+      name="password"
+      placeholder="密码"
+      :type="isPassword ? 'password' : 'text'"
+    >
+      <template #prefix>
+        <img :src="posswordIcon" class="input-icon" />
+      </template>
+      <template #suffix>
+        <ElIcon
+          :size="20"
+          class="password-icon"
+          @click="togglePassword(!isPassword)"
+        >
+          <PreviewOpen v-if="!isPassword" theme="filled" fill="#B6B6B6" />
+          <PreviewCloseOne v-if="isPassword" theme="filled" fill="#B6B6B6" />
+        </ElIcon>
+      </template>
+    </ElInput>
+    <p :class="['error-tip', {'error-top-hidden': !passwordErrorMessage}]">
+      {{ passwordErrorMessage }}
+    </p>
+
+    <ElInput v-model="companyValue" name="userName" placeholder="企业名称">
+      <template #prefix>
+        <img :src="enterpriseIcon" class="input-icon" />
+      </template>
+    </ElInput>
+    <p :class="['error-tip', {'error-top-hidden': !companyErrorMessage}]">
+      {{ companyErrorMessage }}
+    </p>
+
+    <div class="operation">
+      <span
+        :class="{'rember-password': remberPassword}"
+        @click="toggleRemberPassword(!remberPassword)"
+      >
+        记住密码
+      </span>
+      <span>忘记密码</span>
+    </div>
+
+    <ElButton native-type="submit" class="login-btn">登录</ElButton>
+  </div>
+
+  <RouterLink to="/" class="regist">申请成为供应商</RouterLink>
+</template>
+
+<style scoped lang="css">
+@import './index.css';
+</style>

+ 3 - 1
src/routes/index.ts

@@ -1,8 +1,10 @@
 import {createRouter, createWebHistory, RouteRecordRaw} from 'vue-router';
-import {HOME_NAME, HOME_PATH} from './name';
+import {HOME_NAME, HOME_PATH, LOGIN_NAME, LOGIN_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},
 ];
 

+ 5 - 0
src/routes/name.ts

@@ -1,2 +1,7 @@
+/** 首页 */
 export const HOME_PATH = '/';
 export const HOME_NAME = Symbol('HOME_NAME');
+
+/** 登录页面 */
+export const LOGIN_PATH = '/login';
+export const LOGIN_NAME = Symbol('LOGIN_NAME');

+ 5 - 0
src/styles/global.css

@@ -8,6 +8,11 @@
   letter-spacing: 0.5px;
 }
 
+#app {
+  width: 100vw;
+  height: 100vh;
+}
+
 a {
   color: #222;
   text-decoration: none;

+ 10 - 0
src/styles/variable.css

@@ -0,0 +1,10 @@
+:root {
+  --primary-color: #1e3588;
+  --accent-color: #fcaf17;
+  --el-tag-bg-color: var(--primary-color);
+}
+
+/*
+html.dark {
+
+} */

+ 4 - 0
vite.config.ts

@@ -7,6 +7,9 @@ import postcssNest from 'postcss-nesting';
 import postcssPresetEnv from 'postcss-preset-env';
 import eslint from '@nabla/vite-plugin-eslint';
 import browserslistToEsbuild from 'browserslist-to-esbuild';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import ElementPlus from 'unplugin-element-plus/vite';
 
 export default defineConfig({
   define: {
@@ -34,6 +37,7 @@ export default defineConfig({
     },
   },
   plugins: [
+    ElementPlus({}),
     vue(),
     visualizer({
       filename: './visualizer/index.html',