feat(VInputPassword): Add VInputPassword Component
This commit is contained in:
127
src/App.vue
127
src/App.vue
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted, markRaw } from 'vue'
|
||||
import { ElConfigProvider, ElIcon } from 'element-plus'
|
||||
import { VConfigGlobal } from '@/components/ConfigGlobal'
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
// import en from 'element-plus/lib/locale/lang/en'
|
||||
import { VFrom } from '@/components/Form'
|
||||
@@ -1016,73 +1017,93 @@ const schema = reactive<VFormSchema[]>([
|
||||
field: 'field63',
|
||||
component: 'TimeSelect',
|
||||
label: t('formDemo.default')
|
||||
},
|
||||
{
|
||||
field: 'field64',
|
||||
component: 'Divider',
|
||||
label: t('formDemo.inputPassword')
|
||||
},
|
||||
{
|
||||
field: 'field65',
|
||||
component: 'InputPassword',
|
||||
label: t('formDemo.default')
|
||||
},
|
||||
{
|
||||
field: 'field66',
|
||||
component: 'InputPassword',
|
||||
label: t('formDemo.passwordStrength'),
|
||||
componentProps: {
|
||||
strength: true
|
||||
}
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="zhCn">
|
||||
<VFrom :schema="schema">
|
||||
<template #field4-prefix>
|
||||
<ElIcon class="el-input__icon"><Calendar /></ElIcon>
|
||||
</template>
|
||||
<template #field4-suffix>
|
||||
<ElIcon class="el-input__icon"><Calendar /></ElIcon>
|
||||
</template>
|
||||
<VConfigGlobal>
|
||||
<ElConfigProvider :locale="zhCn">
|
||||
<VFrom :schema="schema">
|
||||
<template #field4-prefix>
|
||||
<ElIcon class="el-input__icon"><Calendar /></ElIcon>
|
||||
</template>
|
||||
<template #field4-suffix>
|
||||
<ElIcon class="el-input__icon"><Calendar /></ElIcon>
|
||||
</template>
|
||||
|
||||
<template #field5-prepend> Http:// </template>
|
||||
<template #field5-append> .com </template>
|
||||
<template #field5-prepend> Http:// </template>
|
||||
<template #field5-append> .com </template>
|
||||
|
||||
<template #field9-default="{ item }">
|
||||
<div class="value">{{ item.value }}</div>
|
||||
<span class="link">{{ item.link }}</span>
|
||||
</template>
|
||||
<template #field9-default="{ item }">
|
||||
<div class="value">{{ item.value }}</div>
|
||||
<span class="link">{{ item.link }}</span>
|
||||
</template>
|
||||
|
||||
<template #field15-option="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #field15-option="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #field17-option="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #field17-option="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #field20-default="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #field20-default="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #field22-default="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #field22-default="{ item }">
|
||||
<span style="float: left">{{ item.label }}</span>
|
||||
<span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #field25-default="{ node, data }">
|
||||
<span>{{ data.label }}</span>
|
||||
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||
</template>
|
||||
<template #field25-default="{ node, data }">
|
||||
<span>{{ data.label }}</span>
|
||||
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||
</template>
|
||||
|
||||
<template #field36-default="{ option }">
|
||||
<span>{{ option.value }} - {{ option.desc }}</span>
|
||||
</template>
|
||||
<template #field36-default="{ option }">
|
||||
<span>{{ option.value }} - {{ option.desc }}</span>
|
||||
</template>
|
||||
|
||||
<template #field55-default="cell">
|
||||
<div class="cell" :class="{ current: cell.isCurrent }">
|
||||
<span class="text">{{ cell.text }}</span>
|
||||
<span v-if="isHoliday(cell)" class="holiday"></span>
|
||||
</div>
|
||||
</template>
|
||||
</VFrom>
|
||||
</ElConfigProvider>
|
||||
<template #field55-default="cell">
|
||||
<div class="cell" :class="{ current: cell.isCurrent }">
|
||||
<span class="text">{{ cell.text }}</span>
|
||||
<span v-if="isHoliday(cell)" class="holiday"></span>
|
||||
</div>
|
||||
</template>
|
||||
</VFrom>
|
||||
</ElConfigProvider>
|
||||
</VConfigGlobal>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
3
src/components/ConfigGlobal/index.ts
Normal file
3
src/components/ConfigGlobal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import VConfigGlobal from './src/VConfigGlobal.vue'
|
||||
|
||||
export { VConfigGlobal }
|
||||
15
src/components/ConfigGlobal/src/VConfigGlobal.vue
Normal file
15
src/components/ConfigGlobal/src/VConfigGlobal.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="tsx">
|
||||
import { provide, defineComponent } from 'vue'
|
||||
import { vConfigGlobalProps } from './props'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VConfigGlobal',
|
||||
inheritAttrs: false,
|
||||
props: vConfigGlobalProps,
|
||||
setup(props, { slots }) {
|
||||
provide('configGlobal', props)
|
||||
|
||||
return () => slots.default?.()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
5
src/components/ConfigGlobal/src/props.ts
Normal file
5
src/components/ConfigGlobal/src/props.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
export const vConfigGlobalProps = {
|
||||
size: propTypes.oneOf(['default', 'medium', 'small', 'mini']).def('default')
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="tsx">
|
||||
import { PropType, defineComponent, ref, computed, unref, reactive, watch } from 'vue'
|
||||
import { PropType, defineComponent, ref, computed, unref, watch } from 'vue'
|
||||
import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus'
|
||||
import { componentMap } from './componentMap'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
setGridProp,
|
||||
setComponentProps,
|
||||
setItemComponentSlots,
|
||||
setModel
|
||||
initModel
|
||||
} from './helper'
|
||||
import { useRenderSelect } from './components/useRenderSelect'
|
||||
import { useRenderRadio } from './components/useRenderRadio'
|
||||
@@ -44,13 +44,22 @@ export default defineComponent({
|
||||
const getProps = computed(() => props)
|
||||
const { schema, isCol, isCustom, autoSetPlaceholder } = unref(getProps)
|
||||
// 表单数据
|
||||
const formModel = reactive<Recordable>({})
|
||||
const formModel = ref<Recordable>({})
|
||||
watch(
|
||||
() => formModel.value,
|
||||
(formModel: Recordable) => {
|
||||
console.log(formModel)
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
// 监听表单结构化数组,重新生成formModel
|
||||
watch(
|
||||
() => schema,
|
||||
(schema) => {
|
||||
setModel(schema, formModel)
|
||||
formModel.value = initModel(schema, unref(formModel))
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
@@ -108,7 +117,7 @@ export default defineComponent({
|
||||
const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
|
||||
return (
|
||||
<Com
|
||||
vModel={formModel[item.field]}
|
||||
vModel={formModel.value[item.field]}
|
||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||
{...setComponentProps(item)}
|
||||
{...(notRenderOptions.includes(item?.component as string) &&
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ElAutocomplete,
|
||||
ElDivider
|
||||
} from 'element-plus'
|
||||
import { VInputPassword } from '@/components/InputPassword'
|
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = {
|
||||
Radio: ElRadioGroup,
|
||||
@@ -38,7 +39,8 @@ const componentMap: Recordable<Component, ComponentName> = {
|
||||
Divider: ElDivider,
|
||||
TimeSelect: ElTimeSelect,
|
||||
SelectV2: ElSelectV2,
|
||||
RadioButton: ElRadioGroup
|
||||
RadioButton: ElRadioGroup,
|
||||
InputPassword: VInputPassword
|
||||
}
|
||||
|
||||
export { componentMap }
|
||||
|
||||
@@ -17,7 +17,7 @@ interface PlaceholderMoel {
|
||||
* @description 用于自动设置placeholder
|
||||
*/
|
||||
export function setTextPlaceholder(schema: VFormSchema): PlaceholderMoel {
|
||||
const textMap = ['Input', 'Autocomplete', 'InputNumber']
|
||||
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
|
||||
const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
|
||||
if (textMap.includes(schema?.component as string)) {
|
||||
return {
|
||||
@@ -114,17 +114,20 @@ export function setItemComponentSlots(
|
||||
*
|
||||
* @param schema Form表单结构化数组
|
||||
* @param formModel FormMoel
|
||||
* @returns FormMoel
|
||||
* @description 生成对应的formModel
|
||||
*/
|
||||
export function setModel(schema: VFormSchema[], formModel: Recordable) {
|
||||
export function initModel(schema: VFormSchema[], formModel: Recordable) {
|
||||
const model: Recordable = { ...formModel }
|
||||
schema.map((v) => {
|
||||
// 如果是hidden,就删除对应的值
|
||||
if (v.hidden) {
|
||||
delete formModel[v.field]
|
||||
delete model[v.field]
|
||||
} else {
|
||||
const hasField = Reflect.has(formModel, v.field)
|
||||
const hasField = Reflect.has(model, v.field)
|
||||
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
|
||||
formModel[v.field] = hasField ? formModel[v.field] : v.value !== void 0 ? v.value : ''
|
||||
model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
|
||||
}
|
||||
})
|
||||
return model
|
||||
}
|
||||
|
||||
3
src/components/InputPassword/index.ts
Normal file
3
src/components/InputPassword/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import VInputPassword from './src/VInputPassword.vue'
|
||||
|
||||
export { VInputPassword }
|
||||
137
src/components/InputPassword/src/VInputPassword.vue
Normal file
137
src/components/InputPassword/src/VInputPassword.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, unref, computed, watch } from 'vue'
|
||||
import { ElInput, ElIcon } from 'element-plus'
|
||||
import EyeInvisibleOutlinedE from '~icons/ant-design/eyeInvisibleOutlined'
|
||||
import EyeOutlined from '~icons/ant-design/eye-outlined'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
|
||||
const { configGlobal } = useConfigGlobal()
|
||||
import { zxcvbn } from '@zxcvbn-ts/core'
|
||||
import type { ZxcvbnResult } from '@zxcvbn-ts/core'
|
||||
|
||||
defineProps({
|
||||
// 是否显示密码强度
|
||||
strength: propTypes.bool.def(false),
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 生成class前缀
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = ref(getPrefixCls('inputpassword'))
|
||||
|
||||
// 设置input的type属性
|
||||
const textType = ref<'password' | 'text'>('password')
|
||||
function changeTextType() {
|
||||
textType.value = unref(textType) === 'text' ? 'password' : 'text'
|
||||
}
|
||||
|
||||
// 输入框的值
|
||||
const valueRef = ref('')
|
||||
// 监听
|
||||
watch(
|
||||
() => valueRef.value,
|
||||
(val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
)
|
||||
// 获取密码强度
|
||||
const getPasswordStrength = computed(() => {
|
||||
const value = unref(valueRef)
|
||||
const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
|
||||
return value ? zxcvbnRef.score : -1
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
|
||||
<ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
|
||||
<template #suffix>
|
||||
<ElIcon class="el-input__icon el-input__clear">
|
||||
<EyeInvisibleOutlinedE v-if="textType === 'password'" @click="changeTextType" />
|
||||
<EyeOutlined v-else @click="changeTextType" />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
<div
|
||||
v-if="strength"
|
||||
:class="`${prefixCls}__bar`"
|
||||
class="relative h-6px mt-10px mb-6px mr-auto ml-auto"
|
||||
>
|
||||
<div :class="`${prefixCls}__bar--fill`" :data-score="getPasswordStrength"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-inputpassword';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__bar {
|
||||
background-color: var(--el-text-color-disabled-base);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 20%;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-color: var(--el-color-white);
|
||||
border-style: solid;
|
||||
border-width: 0 5px 0 5px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 20%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 20%;
|
||||
}
|
||||
|
||||
&--fill {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: inherit;
|
||||
background-color: transparent;
|
||||
border-radius: inherit;
|
||||
transition: width 0.5s ease-in-out, background 0.25s;
|
||||
|
||||
&[data-score='0'] {
|
||||
width: 20%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='1'] {
|
||||
width: 40%;
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
&[data-score='2'] {
|
||||
width: 60%;
|
||||
background-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
&[data-score='3'] {
|
||||
width: 80%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&[data-score='4'] {
|
||||
width: 100%;
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--mini > &__bar {
|
||||
border-radius: var(--el-border-radius-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
src/hooks/web/useConfigGlobal.ts
Normal file
9
src/hooks/web/useConfigGlobal.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { inject } from 'vue'
|
||||
|
||||
export function useConfigGlobal() {
|
||||
const configGlobal = inject('configGlobal', {}) as VConfigGlobalTypes
|
||||
|
||||
return {
|
||||
configGlobal
|
||||
}
|
||||
}
|
||||
18
src/hooks/web/useDesign.ts
Normal file
18
src/hooks/web/useDesign.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import variables from '@/styles/variables.module.less'
|
||||
|
||||
export function useDesign() {
|
||||
const lessVariables = variables
|
||||
|
||||
/**
|
||||
* @param scope 类名
|
||||
* @returns 返回空间名-类名
|
||||
*/
|
||||
function getPrefixCls(scope: string) {
|
||||
return `${lessVariables.namespace}-${scope}`
|
||||
}
|
||||
|
||||
return {
|
||||
variables: lessVariables,
|
||||
getPrefixCls
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ export default {
|
||||
dateTimePicker: 'DateTimePicker',
|
||||
dateTimerange: 'Datetime Range',
|
||||
timePicker: 'Time Picker',
|
||||
timeSelect: 'Time Select'
|
||||
timeSelect: 'Time Select',
|
||||
inputPassword: 'input Password',
|
||||
passwordStrength: 'Password Strength'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export default {
|
||||
dateTimePicker: '日期时间选择器',
|
||||
dateTimerange: '日期时间范围',
|
||||
timePicker: '时间选择器',
|
||||
timeSelect: '时间选择'
|
||||
timeSelect: '时间选择',
|
||||
inputPassword: '密码输入框',
|
||||
passwordStrength: '密码强度'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// 命名空间
|
||||
@namespace: v;
|
||||
7
src/styles/variables.module.less
Normal file
7
src/styles/variables.module.less
Normal file
@@ -0,0 +1,7 @@
|
||||
// 命名空间
|
||||
@namespace: v;
|
||||
|
||||
// 导出变量
|
||||
:export {
|
||||
namespace: @namespace;
|
||||
}
|
||||
1
src/types/componentType.d.ts
vendored
1
src/types/componentType.d.ts
vendored
@@ -23,6 +23,7 @@ declare global {
|
||||
| 'Divider'
|
||||
| 'TimeSelect'
|
||||
| 'SelectV2'
|
||||
| 'InputPassword'
|
||||
|
||||
declare type ColProps = {
|
||||
span?: number
|
||||
|
||||
3
src/types/configGlobal.d.ts
vendored
Normal file
3
src/types/configGlobal.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare interface VConfigGlobalTypes {
|
||||
size?: ElememtPlusSzie
|
||||
}
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -6,7 +6,7 @@ declare type Nullable<T> = T | null
|
||||
|
||||
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>
|
||||
|
||||
declare type ElememtPlusSzie = 'medium' | 'small' | 'mini'
|
||||
declare type ElememtPlusSzie = 'default' | 'medium' | 'small' | 'mini'
|
||||
|
||||
declare type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user