包括 element-plus 的安装、登录、svg icons 的处理(坑:svg 不显示的问题)

一、安装 element-plus

1
2
3
npm install @element-plus/icons
yarn add @element-plus/icons
pnpm install @element-plus/icons

安装成功,会出现一个 element-plus 安装成功和一个按钮

二、公开路由表配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// router/router.js
import { createRouter, createWebHistory } from "vue-router";

const publicRoutes = [
{
path: "/login",
component: () => import("@/views/login/index"),
},
];

const router = createRouter({
history: createWebHistory(),
routes: publicRoutes,
});

export default router;

三、登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div class="login-container">
<el-form
class="login-form"
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
>
<div class="title-container">
<h3 class="title">后台管理系统</h3>
</div>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon="user" />
</span>
<el-input
placeholder="username"
name="username"
type="text"
v-model="loginForm.username"
></el-input>
</el-form-item>
<el-form-item>
<span class="svg-container">
<svg-icon icon="password" />
</span>
<el-input
placeholder="password"
name="password"
:type="passwordType"
v-model="loginForm.password"
></el-input>
<span class="show-pwd">
<svg-icon
:icon="passwordType === 'password' ? 'eye' : 'eye-open'"
@click="onChangePwdType"
></svg-icon>
</span>
</el-form-item>
<el-button
type="primary"
style="width:100%;margin-bottom: 30px"
:loading="loading"
@click="handleLogin"
>login
</el-button>
</el-form>
</div>
</template>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script setup>
import { ref, computed } from 'vue'
import { validatePassword } from './rules'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'

// 数据源
const loginForm = ref({
username: 'super-admin',
password: '123456'
})
// 验证规则
const loginRules = ref({
username: [{
required: true,
trigger: 'blur',
message: computed(() => {
return 'error'
})
}
],
password: [
{
required: true,
trigger: 'blur',
validator: validatePassword()
}
]
})
// 处理密码
const passwordType = ref('password')
const onChangePwdType = () => {
if (passwordType.value === 'password') {
passwordType.value = 'text'
} else {
passwordType.value = 'password'
}
}
// 登录
const loading = ref(false)
const loginFormRef = ref(null)
const router = useRouter()
const store = useStore()
const handleLogin = () => {
loginFormRef.value.validate(valid => {
if (!valid) return
loading.value = false
store.dispatch('user/login', loginForm.value).then(() => {
loading.value = false
router.push('/')
}).catch(err => {
console.log(err)
loading.value = false
})
})
}

</script>

校验规则:

1
2
3
4
5
6
7
8
9
10
export const validatePassword = () => {
return (rule, value, callback) => {
if (value.length < 6) {
// callback(new Error(i18n.global.t('msg.login.passwordRule')))
callback(new Error("error"));
} else {
callback();
}
};
};

登录页样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<style lang="scss" scoped>
$bgColor: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;
$cursor: #fff;

.login-container {
min-height: 100%;
width: 100%;
background-color: $bgColor;
overflow: hidden;

.login-form {
position: relative;
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin: 0 auto;
overflow: hidden;

::v-deep .el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}

::v-deep .el-input {
display: inline-block;
height: 47px;
width: 85%;

input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
caret-color: $cursor;
}
}
}
}

.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
display: inline-block;
}

.title-container {
position: relative;

.title {
font-size: 26px;
color: $light_gray;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}

::v-deep .lang-select {
position: absolute;
top: 4px;
right: 0;
background-color: white;
font-size: 22px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
}
}

.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
</style>

全局样式:src/styles/index.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
html,
body {
height: 100%;
margin: 0;
padding: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft
YaHei, Arial, sans-serif;
}

#app {
height: 100%;
}

*,
*:before,
*:after {
box-sizing: inherit;
margin: 0;
padding: 0;
}

a:focus,
a:active {
outline: none;
}

a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}

div:focus {
outline: none;
}

.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}

导入全局样式

1
import "./styles/index.scss";

这里有个坑: 登录页输入框的 svg 小图标不显示

四、icon 图标处理方法:SvgIcon

结合 vue-element-admin 项目的借鉴,将 icon 图标分为 element-plus 图标和自定义引入图标

自定义 SVG 图标组件的能力:显示外部引入图标和项目内的 svg 图标

封装 SvgIcon 组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<div
v-if="isExternal"
:style="styleExternalIcon"
class="svg-external-icon svg-icon"
:class="className"
/>
<svg v-else class="svg-icon" :class="className" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// utils/validate.js
/**
* 判断是否为外部资源
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}


<script setup>
import { isExternal as external } from '@/utils/validate'
import { defineProps, computed } from 'vue'

const props = defineProps({
icon: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
})
// 判断是否为外部图标
const isExternal = computed(() => external(props.icon))
// 外部图标样式
const styleExternalIcon = computed(() => ({
mask: `url(${props.icon}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.icon}) no-repeat 50% 50%`
}))
// 项目内图标
const iconName = computed(() => `#icon-${props.icon}`)

</script>

样式

1
2
3
4
5
6
7
8
9
10
11
12
13
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}

.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}

上面只处理了外部 svg 图标的展示,还要处理 element-plus 的图标

// 创建 icons 文件夹 内部包含:内部 svg 图标、SvgIcon 全局注册

1
2
3
4
5
6
7
8
9
10
11
12
import SvgIcon from "@/components/SvgIcon";
// 通过 require.context() 函数来创建自己的 context
const svgRequire = require.context("./svg", false, /\.svg$/);
// 此时返回一个 require 的函数,可以接受一个 request 的参数,用于 require 的导入。
// 该函数提供了三个属性,可以通过 require.keys() 获取到所有的 svg 图标
// 遍历图标,把图标作为 request 传入到 require 导入函数中,完成本地 svg 图标的导入

svgRequire.keys().forEach((SvgIcon) => svgRequire(SvgIcon));

export default (app) => {
app.component("svg-icon", SvgIcon);
};

main.js 引入 svgIcon

1
2
3
// 导入 svgIcon
import installIcons from "@/icons";
installIcons(app);

此时,svg图标仍然不显示,还需要使用 svg-sprite-loader 处理 svg 图标

1
npm i --save-dev svg-sprite-loader@6.0.9

配置 vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir);
}

module.exports = {
chainWebpack(config) {
// 设置 svg-sprite-loader
config.module.rule("svg").exclude.add(resolve("src/icons")).end();
config.module
.rule("icons")
.test(/\.svg$/)
.include.add(resolve("src/icons"))
.end()
.use("svg-sprite-loader")
.loader("svg-sprite-loader")
.options({
symbolId: "icon-[name]",
})
.end();
},
};

至此,svg 图标可以正常显示。