一 效果图
二 组件封装步骤
1 组件目录结构如下
2 组件是需要一个密码输入框和一个pop 框,在input获取焦点的时候展示popover,popover 中的每一项都要根据输入内容实时匹配是否满足某一规则,因此里面的内容需要动态渲染,如下结构
<template>
<el-popover popper-class="gt-password-popper" v-bind="popoverOptions">
<template slot="reference">
<el-input
class="gt-password"
v-bind="$attrs"
v-on="$listeners"
:type="passwordType ? 'password' : 'text'"
>
<i
@click="triggerPassword"
slot="suffix"
:class="['trigger-password', passwordType ? '' : 'show']"
></i>
</el-input>
</template>
<CheckList
:pagek="pagek"
:values.sync="checkValuesObj"
:title="$t('Basic.Password_must_include')"
:options="checkList"
/>
</el-popover>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Computed } from "vue-property-decorator";
import CheckList from "./CheckList.vue";
import { ruleItem } from "./index.type";
const popDefault = {
placement: "right-start",
width: 228,
trigger: "focus"
};
@Component({
components: { CheckList }
})
export default class Password extends Vue {
pagek = new Date().getTime();
@Prop({
type: Array as PropType<ruleItem[]>,
required: true
})
checkList;
@Prop({
type: Array as PropType<ruleItem[]>,
required: false
})
popover;
private popoverOptions = {};
checkValuesObj: any = {};
passwordType: Boolean = true;
created() {
this.popoverOptions = { ...this.popover, ...popDefault };
this.update(this.checkList);
}
update = (validList: string[] = [], clear: Boolean = false) => {
// 每次update 前先清空 checkValuesObj
Object.keys(this.checkValuesObj).forEach(v => {
this.$set(this.checkValuesObj, v, "");
});
let tempObj = {};
this.$nextTick(() => {
this.pagek = new Date().getTime();
tempObj =
validList.length > 0 &&
validList.reduce(
(res = {}, key = "1") => Object.assign(res, { [key]: "success" }),
{}
);
if (tempObj) {
Object.keys(tempObj).forEach(v => {
this.$set(this.checkValuesObj, v, tempObj[v]);
});
}
if (clear) {
Object.keys(this.checkValuesObj).forEach(v => {
this.$set(this.checkValuesObj, v, "");
});
}
});
};
triggerPassword() {
this.passwordType = !this.passwordType;
}
}
</script>
<style lang="scss" scoped>
.trigger-password {
display: block;
width: 24px;
height: 100%;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADmElEQVRoQ+2Y14vXUBCFv7X3Dgp2sIEVy4MP/vUKPth7x4q99658MIFLTDbJTRZ3IQNB/CV37pkzZ2bu3QlmuE3McPyMAfzvDI4ZGDPQk4FRQj0J7L18zEBvCns6mI4Z2AC8AL61iW26BbAV2Ax8Bk4Dv5uCGDKA2cCqeBYC84F5AeB7MPoFeB3PrxK4Arw/XwWeN4H3/RABCNq0rwBmtdk0mL0NPI7vs8D3DWB1pHtZAvo98BL4GIyr4z+RCTOyHFgZ767Hu2zwuQEIZGdIRR8/gIfAU0CpdLEUvLq/CHzt4qCrhNYB24A5wE/gfsgg1bPvNgJmyFowA4IyM49inRhT8PpynX7uJNJqjKVtAGpb1teGx1fAjQrGlceuKODy5hbltQrZ+NsbYAewJhY9AW4N1YVkZk8UqQzpWLmUTfD748d3IasP8f9FwNsa8M8SRxIkURJmUFeSjFVmoykD6n0vsCTYvgQUoFKHBnkkmH8A3A2w5U1T2dyskYpNwT3nAp8A96yti8kCkDUZNYimAiuAyfz5GvBbAJ/C7gE+VWbtGIQYbAz6FMM/VhfAAuBgtD9boyzYbepM9hcDl6NYy9+l4JWfzcBW67StMzOwO6RrOz5XlYm6AA4BSwEZtbWVp2Z502OAk/hkxRkmBW/BWgtHQ9snJgnAV/rcF/ND6Z4pfz/VAZTBW7BK0gAk5fhUBdBVQoej0C9E9xBXFXh/t1Xa1dpIyDqwqK2Ds10k5EZdiliwfl/0+TrwZvxASMIhaLeqMglUOvpU/xLTqYgLp+U2aj3IXNmKKerUTVulARV9XvC+2xSgTtX0eGtP5j3JupcNpPZu0DQHBCo4u4GDarJB5rcpeLtNwbCgPF54mNNSqaVk2J22R/EOMsgK51VHCQdRykwKvq42/d5TqOBSk20nsOcnzax5VBn8QpMy5FneIMoF61HDHm6xqmWz7EXG85OnVg9uhdkm14esigOi94Sqo0olKW0kVF4oKAOxCCfTfF0W/F3G9aGsDFbzpibrre7ChfOcAFJgqWwMxsGnPPxXILY/97AZ+HimMjPlS5BkmKHO1ieAtFU6XQXV5UrpGu8HMp9tuQFU9fm+l/qsIHICqBtSWQD6LuoagH998EqppUOqL47s9V0DsBC9I3iOb/V3m2xkLRd2DUC3FmrjgGm5f+/PcgLovemQDsYAhmQzx9eYgRzWhlwzZmBINnN8jRnIYW3INX8BE5PmMWUaunsAAAAASUVORK5CYII=")
no-repeat center;
background-size: 100%;
cursor: pointer;
&.show {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC80lEQVRoQ+2XV4sVQRCFvzXnDIoYEExgVnzwwV/vgw8GjGBEzArmnJVvqYFhdubO9E4vy0oXXO6yt7u6zqlT1dVTLHCbWuDxUwDMdwZLBkoGRjJQJDSSwNHbSwZGUzjSQcnASAJHb8+ZgcXApvisBJYDyyLCH8B34CvwBngL/BodPWSZhQx6B7ABWDQwqL8B4nF8D9w2c9mYDGwGdgPram4/AK+AT8G4rGtmwoysBdy3vrbnI/Aw9iUDmQ0AAzkQUvHAn4BMvgCUyhBbCmyPzPm39hq4E8CH+JhekwpgG7AXWBIafgQ8BX7XTvS3ncG0taCpfQMUaF371o1AzGTl8z7wfCiCoQDUtqxvrbF1u4XxjcDBkEtbDErqVovuzer+AO0+AdwF/vQBGQJAZg5Hkcq0jpVL0wz+WPzzfbCtvjW1b1Yq7V/tKF4JkigJs1Pd7OtWfQBk5giwJti+FgXaDF6Qp4N5ZfUAsNPUzbP2ALtC5xc6grMpeKa18Rm4DnzrysQkAKuCUUF8AQy+y5GBqWOZv9ISfHW+5x2PTNh5BNpm1o4gjMHGoE9jmGFdAFYAJ6P92RplwW7TZbK/GrgxoB1uCUnaai9O8GkGDoV0BXG5jcAuAKdCtzIq8/Uu03bmWcCOcn5AGzSjZ8LnuZ4i1efRyJj1dKm5/r8FoIRORFHOp4SsA4s6WUJmaj6LWAKVjjF4d9h2k4q4klqzjVrMVW+vyzFnG/XOkHnnJwvdM6uZanAXagZnN/CimuuLzFFlXzSELBdZBWSuRwnZ9gZ2UtVeAo4qWUaJejaaw5yX0bOOYc5+r5a1lGHuXseo0tpx+0aJtk05xmkZlwzno2qc9qUm6516T7mJe+6X6Z8nPWgs9OoZ6drqeWmBmpnmI8hMOm4n22wy0DxkNk9Ktf0OeBJv5OTAqw05AFS+Uh71yqVvPBkEKieAQQfmXlQA5GY01V/JQCpjudeXDORmNNVfyUAqY7nXlwzkZjTVX8lAKmO51/8DYoDEMYv3QPQAAAAASUVORK5CYII=");
}
}
</style>
CheckList 组件内容如下
<template>
<div class="check-list" :key="pagek">
<div class="check-list__title">{{ title }}</div>
<div v-for="{ key, label } in options" :key="key">
<div class="check-list-item">
<span class="check-list-item__prefix">
<i
v-if="isEqual(key, 'success')"
class="iconfont el-icon-success"
></i>
<i
v-else-if="isEqual(key, 'error')"
class="iconfont el-icon-error"
></i>
<i
v-else-if="isEqual(key, 'warning')"
class="iconfont el-icon-warning"
></i>
<i v-else-if="isEqual(key, 'info')" class="el-icon-info"></i>
<i v-else class="iconfont el-icon-pending"></i>
</span>
<div class="check-list-label">{{ label }}</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Computed } from "vue-property-decorator";
interface option {
key: string | number | symbol;
label: string;
}
@Component({})
export default class CheckList extends Vue {
status = ["pending", "success", "error", "warning", "info"];
@Prop({
default: new Date().getTime()
})
pagek;
@Prop({
type: Object,
default: () => ({})
})
values;
@Prop({
type: Array as PropType<option[]>,
default: () => []
})
options;
@Prop({
type: String,
default: ""
})
title;
get defaultStatus() {
return this.options.reduce(
(res, { key }) => Object.assign(res, { [key]: status[0] }),
{}
);
}
get statusMap() {
return Object.assign({}, this.defaultStatus, this.values);
}
isEqual = (
key: string | number | symbol,
statusCode: string | number
): boolean => {
return this.statusMap[key] == statusCode;
};
}
</script>
<style lang="scss" scoped>
// 胶囊状态颜色
$--color--capsule--success: #40b828;
$--color--capsule--danger: #f0553a;
$--color--capsule--cancel: #878e99;
$--color--capsule--warning: #fc971c;
$--color--capsule--processing: #1a9bf6;
$--color--capsule--info: #878e99;
.el-icon-success {
color: $--color--capsule--success;
}
.el-icon-error {
color: $--color--capsule--danger;
}
.el-icon-warning {
color: $--color--capsule--warning;
}
.el-icon-info {
color: $--color--capsule--info;
}
.el-icon-pending {
&::before {
content: "";
display: inline-flex;
width: 12px;
height: 12px;
border: 2px solid rgba(#000, 0.15);
border-radius: 50%;
// vertical-align: baseline;
// margin-bottom: -2px;
}
}
.check-list {
font-size: 13px;
}
.check-list__title {
line-height: 20px;
color: $--color-text-lighter;
margin-bottom: 12px;
}
.check-list-item {
line-height: 20px;
margin-bottom: 8px;
}
.check-list-item__prefix {
float: left;
font-size: 16px;
}
.check-list-label {
margin-left: 16px + 12px;
}
</style>
由于我们项目使用了 ts ,因此会有一些必要的类型校验
index.type.ts 内容如下
export interface rule {
key: string;
label: string;
}
export interface ruleItem extends rule {
regExp?: RegExp;
validator?: (value: string) => boolean;
}
utils.ts 中为校验规则,以及校验结果
import { ruleItem } from "./index.type";
import i18nInstance from "@root/i18n";
export const usePassword = (options?: Array<ruleItem | string>) => {
const defaultRules: ruleItem[] = [
{
key: "length",
label: i18nInstance.t("Basic.At_least_8_characters"),
regExp: /^.{8,}$/
},
{
key: "lowercase",
label: i18nInstance.t("Basic.Use_lowercase_letters"),
regExp: /[a-z]+/
},
{
key: "capital",
label: i18nInstance.t("Basic.Use_capital_letters"),
regExp: /[A-Z]+/
},
{
key: "number",
label: i18nInstance.t("Basic.Use_numbers"),
regExp: /\d{1,}/
},
{
key: "special",
label: i18nInstance.t("Basic.Use_special_characters"),
regExp: /[!@#$%^&*? ]{1,}/
}
];
const ruleItems: ruleItem[] = options
? options.reduce((res, item) => {
if (typeof item === "string") {
const rule = defaultRules.find(i => item === i.key);
if (rule) {
res.push(rule);
} else {
throw new Error("invalid ruleKey");
}
} else {
res.push(item);
}
return res;
}, [] as ruleItem[])
: [...defaultRules];
// 返回默认的匹配规则map 和 校验结果mapList
return {
checkList: ruleItems.map(({ key, label }) => ({ key, label })),
validate: (value: string): { validList: string[]; valid: boolean } => {
// 过滤出符合正则的rule[], map返回key[]
const validList = ruleItems
.filter(({ regExp, validator }) => {
if (validator) {
return validator(value);
} else if (regExp) {
return regExp.test(value);
} else {
return false;
}
})
.map(({ key }) => {
return key;
});
return {
validList,
valid: validList.length === ruleItems.length
};
}
};
};
为了方便使用容易将逻辑与组件放入index.ts
import Password from "./src/index.vue";
import { usePassword } from "./src/utils";
export { usePassword };
export default Password;
三 使用组件![使用组件](https://i-blog.csdnimg.cn/blog_migrate/0ee0bc8f296c72dbff798a5ab1d40ff3.png)
import Password, { usePassword } from "@/components/page/Password/index"; //引入组件
passwordE = null; // password 组件元素相关校验逻辑
...
created() { //组件生命周期内使用组件
this.passwordE = usePassword();
this.checkList = this.passwordE.checkList;
}
...
// 密码规则自定义校验器
get rules(){
let _ = this;
return {
password: {
validator: (
rule: any,
value: string,
callback: (v?: Error) => void
) => {
const { validList = [], valid } = _.passwordE.validate(value);
(_.$refs.passwordRef as any).update(validList);
// 几条规则是否都以校验完成,否则会有无效密码提示
if (!valid) {
callback(new Error(_.$t("Basic.invalid_password") as string));
} else {
callback();
}
},
trigger: ["change", "blur"]
},
}
}