更新:登录功能

This commit is contained in:
2025-11-04 12:37:04 +08:00
commit a21fb92916
897 changed files with 51500 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
@import "../common/abstracts/variable";
@import "../common/abstracts/mixin";
.wot-theme-dark {
@include b(input) {
background: $-dark-background2;
&::after {
background: $-dark-color-gray;
}
@include when(not-empty) {
&:not(.is-disabled) {
&::after {
background-color: $-dark-color;
}
}
}
@include e(inner) {
color: $-dark-color;
&::-webkit-input-placeholder {
color: $-dark-color3;
}
}
@include e(count) {
color: $-dark-color3;
background: transparent;
}
@include e(count-current) {
color: $-dark-color;
}
:deep(.wd-input__icon),
:deep(.wd-input__clear) {
color: $-dark-color;
background: transparent;
}
@include when(cell) {
background-color: $-dark-background2;
line-height: $-cell-line-height;
@include when(border) {
@include halfPixelBorder("top", $-input-cell-padding, $-dark-border-color);
}
}
@include when(disabled) {
.wd-input__inner {
color: $-dark-color-gray;
background: transparent;
}
}
@include e(label) {
color: $-dark-color;
}
}
}
@include b(input) {
position: relative;
-webkit-tap-highlight-color: transparent;
text-align: left;
background: $-input-bg;
&::after {
position: absolute;
content: "";
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: $-input-border-color;
transform: scaleY(0.5);
transition: background-color 0.2s ease-in-out;
}
@include when(not-empty) {
&:not(.is-disabled) {
&::after {
background-color: $-input-not-empty-border-color;
}
}
}
@include e(label) {
position: relative;
display: flex;
width: $-input-cell-label-width;
color: $-cell-title-color;
margin-right: $-cell-padding;
box-sizing: border-box;
font-size: $-input-fs;
flex-shrink: 0;
}
@include e(label-inner) {
display: inline-block;
font-size: $-input-fs;
line-height: $-cell-line-height;
}
@include e(required) {
font-size: $-cell-required-size;
color: $-cell-required-color;
margin-left: $-cell-required-margin;
@include m(left) {
margin-left: 0;
margin-right: $-cell-required-margin;
}
}
@include e(body) {
flex: 1;
}
@include e(value) {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
}
@include e(prefix) {
margin-right: $-input-icon-margin;
font-size: $-input-fs;
line-height: initial;
:deep(.wd-input__icon),
:deep(.wd-input__clear) {
margin-left: 0;
}
}
@include e(suffix) {
flex-shrink: 0;
line-height: initial;
}
@include e(error-message){
color: $-form-item-error-message-color;
font-size: $-form-item-error-message-font-size;
line-height: $-form-item-error-message-line-height;
text-align: left;
vertical-align: middle;
}
@include when(disabled) {
.wd-input__inner {
color: $-input-disabled-color;
background: transparent;
}
}
@include when(error) {
.wd-input__inner {
color: $-input-error-color;
background: transparent;
}
}
@include when(no-border) {
&::after {
display: none;
}
.wd-input__inner {
height: $-input-inner-height-no-border;
padding-top: 0;
padding-bottom: 0;
}
}
@include when(cell) {
display: flex;
align-items: flex-start;
padding: $-input-cell-padding $-input-padding;
background-color: $-input-cell-bg;
&.is-error::after {
background: $-input-cell-border-color;
}
:deep(.wd-input__icon),
:deep(.wd-input__clear) {
display: inline-flex;
align-items: center;
height: $-input-cell-height;
line-height: $-input-cell-height;
}
.wd-input__prefix {
display: inline-block;
margin-right: $-cell-icon-right;
}
.wd-input__inner {
height: $-input-cell-height;
}
&.wd-input::after {
display: none;
}
@include when(center) {
align-items: center;
}
@include when(border) {
@include halfPixelBorder("top", $-input-cell-padding);
}
}
@include when(large) {
padding: $-input-cell-padding-large;
.wd-input__prefix {
font-size: $-input-fs-large;
}
.wd-input__label-inner {
font-size: $-input-fs-large;
}
.wd-input__inner {
font-size: $-input-fs-large;
}
.wd-input__count {
font-size: $-input-count-fs-large;
}
:deep(.wd-input__icon),
:deep(.wd-input__clear) {
font-size: $-input-icon-size-large;
}
}
@include e(inner) {
flex: 1;
height: $-input-inner-height;
font-size: $-input-fs;
color: $-input-color;
outline: none;
border: none;
background: none;
padding: 0;
box-sizing: border-box;
&::-webkit-input-placeholder {
color: $-input-placeholder-color;
}
@include when(align-right) {
text-align: right;
}
}
@include e(readonly-mask) {
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: 100%;
height: 100%;
}
@include edeep(icon) {
margin-left: $-input-icon-margin;
font-size: $-input-icon-size;
color: $-input-icon-color;
vertical-align: middle;
background: $-input-bg;
}
@include edeep(clear) {
margin-left: $-input-icon-margin;
font-size: $-input-icon-size;
color: $-input-clear-color;
vertical-align: middle;
background: $-input-bg;
}
@include e(count) {
margin-left: 15px;
font-size: $-input-count-fs;
color: $-input-count-color;
vertical-align: middle;
background: $-input-bg;
}
@include e(count-current) {
color: $-input-count-current-color;
@include when(error) {
color: $-input-error-color;
}
}
.wd-input__count,
.wd-input__count-current {
display: inline-flex;
}
}

View File

@@ -0,0 +1,21 @@
@import "../common/abstracts/variable";
@import "../common/abstracts/mixin";
.wot-theme-dark {
@include b(input) {
@include e(placeholder) {
color: $-dark-color3;
}
}
}
@include b(input) {
@include e(placeholder) {
color: $-input-placeholder-color;
&.is-error {
color: $-input-error-color;
}
}
}

View File

@@ -0,0 +1,189 @@
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp } from '../common/props'
import type { FormItemRule } from '../wd-form/types'
export type InputClearTrigger = 'focus' | 'always'
export type InputType = 'text' | 'number' | 'digit' | 'idcard' | 'safe-password' | 'nickname' | 'tel'
export type InputConfirmType = 'send' | 'search' | 'next' | 'go' | 'done'
export type InputSize = 'large'
export type InputMode = 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'
export const inputProps = {
...baseProps,
customInputClass: makeStringProp(''),
customLabelClass: makeStringProp(''),
// 原生属性
/**
* 占位文本
*/
placeholder: String,
/**
* 原生属性,指定 placeholder 的样式目前仅支持color,font-size和font-weight
*/
placeholderStyle: String,
/**
* 原生属性,指定 placeholder 的样式类
*/
placeholderClass: makeStringProp(''),
/**
* 原生属性,指定光标与键盘的距离。取 input 距离底部的距离和cursor-spacing指定的距离的最小值作为光标与键盘的距离
*/
cursorSpacing: makeNumberProp(0),
/**
* 原生属性指定focus时的光标位置
*/
cursor: makeNumberProp(-1),
/**
* 原生属性光标起始位置自动聚集时有效需与selection-end搭配使用
*/
selectionStart: makeNumberProp(-1),
/**
* 原生属性光标结束位置自动聚集时有效需与selection-start搭配使用
*/
selectionEnd: makeNumberProp(-1),
/**
* 原生属性,键盘弹起时,是否自动上推页面
*/
adjustPosition: makeBooleanProp(true),
/**
* focus时点击页面的时候不收起键盘
*/
holdKeyboard: makeBooleanProp(false),
/**
* 设置键盘右下角按钮的文字仅在type='text'时生效可选值done / go / next / search / send
*/
confirmType: makeStringProp<InputConfirmType>('done'),
/**
* 点击键盘右下角按钮时是否保持键盘不收起
*/
confirmHold: makeBooleanProp(false),
/**
* 原生属性,获取焦点
*/
focus: makeBooleanProp(false),
/**
* 类型可选值text / number / digit / idcard / safe-password / nickname / tel
*/
type: makeStringProp<InputType>('text'),
/**
* 原生属性,最大长度
*/
maxlength: {
type: Number,
// #ifndef MP-ALIPAY
default: -1
// #endif
},
/**
* 原生属性,禁用
*/
disabled: makeBooleanProp(false),
/**
* 微信小程序原生属性,强制 input 处于同层状态,默认 focus 时 input 会切到非同层状态 (仅在 iOS 下生效)
*/
alwaysEmbed: makeBooleanProp(false),
// 原生属性结束
/**
* 输入框的值靠右展示
*/
alignRight: makeBooleanProp(false),
/**
* 绑定值
*/
modelValue: makeNumericProp(''),
/**
* 显示为密码框
*/
showPassword: makeBooleanProp(false),
/**
* 显示清空按钮
*/
clearable: makeBooleanProp(false),
/**
* 只读
*/
readonly: makeBooleanProp(false),
/**
* 前置图标icon组件中的图标类名
*/
prefixIcon: String,
/**
* 后置图标icon组件中的图标类名
*/
suffixIcon: String,
/**
* 显示字数限制,需要同时设置 maxlength
*/
showWordLimit: makeBooleanProp(false),
/**
* 设置左侧标题
*/
label: String,
/**
* 设置左侧标题宽度
*/
labelWidth: makeStringProp(''),
/**
* 设置输入框大小可选值large
*/
size: String as PropType<InputSize>,
/**
* 设置输入框错误状态,错误状态时为红色
*/
error: makeBooleanProp(false),
/**
* 当有label属性时设置标题和输入框垂直居中默认为顶部居中
*/
center: makeBooleanProp(false),
/**
* 非 cell 类型下是否隐藏下划线
*/
noBorder: makeBooleanProp(false),
/**
* 是否必填
*/
required: makeBooleanProp(false),
/**
* 表单域 model 字段名,在使用表单校验功能的情况下,该属性是必填的
*/
prop: String,
/**
* 表单验证规则结合wd-form组件使用
*/
rules: makeArrayProp<FormItemRule>(),
/**
* 显示清除图标的时机always 表示输入框不为空时展示focus 表示输入框聚焦且不为空时展示
* 类型: "focus" | "always"
* 默认值: "always"
*/
clearTrigger: makeStringProp<InputClearTrigger>('always'),
/**
* 是否在点击清除按钮时聚焦输入框
* 类型: boolean
* 默认值: true
*/
focusWhenClear: makeBooleanProp(true),
/**
* 是否忽略组件内对文本合成系统事件的处理。为 false 时将触发 compositionstart、compositionend、compositionupdate 事件,且在文本合成期间会触发 input 事件
* 类型: boolean
* 默认值: true
*/
ignoreCompositionEvent: makeBooleanProp(true),
/**
* 它提供了用户在编辑元素或其内容时可能输入的数据类型的提示。在符合条件的高版本webview里uni-app的web和app-vue平台中可使用本属性。
* 类型: InputMode
* 可选值: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | "password"
* 默认值: "text"
*/
inputmode: makeStringProp<InputMode>('text'),
/**
* 必填标记位置可选值before标签前、after标签后
*/
markerSide: makeStringProp<'before' | 'after'>('before')
}
export type InputProps = ExtractPropTypes<typeof inputProps>

View File

@@ -0,0 +1,300 @@
<template>
<view :class="rootClass" :style="customStyle" @click="handleClick">
<view v-if="label || $slots.label" :class="labelClass" :style="labelStyle">
<text v-if="isRequired && markerSide === 'before'" class="wd-input__required wd-input__required--left">*</text>
<view v-if="prefixIcon || $slots.prefix" class="wd-input__prefix">
<wd-icon v-if="prefixIcon && !$slots.prefix" custom-class="wd-input__icon" :name="prefixIcon" @click="onClickPrefixIcon" />
<slot v-else name="prefix"></slot>
</view>
<view class="wd-input__label-inner">
<text v-if="label && !$slots.label">{{ label }}</text>
<slot v-else-if="$slots.label" name="label"></slot>
</view>
<text v-if="isRequired && markerSide === 'after'" class="wd-input__required">*</text>
</view>
<view class="wd-input__body">
<view class="wd-input__value">
<view v-if="(prefixIcon || $slots.prefix) && !label" class="wd-input__prefix">
<wd-icon v-if="prefixIcon && !$slots.prefix" custom-class="wd-input__icon" :name="prefixIcon" @click="onClickPrefixIcon" />
<slot v-else name="prefix"></slot>
</view>
<input
:class="[
'wd-input__inner',
prefixIcon ? 'wd-input__inner--prefix' : '',
showWordCount ? 'wd-input__inner--count' : '',
alignRight ? 'is-align-right' : '',
customInputClass
]"
:type="type"
:password="showPassword && !isPwdVisible"
v-model="inputValue"
:placeholder="placeholderValue"
:disabled="disabled || readonly"
:maxlength="maxlength"
:focus="focused"
:confirm-type="confirmType"
:confirm-hold="confirmHold"
:cursor="cursor"
:cursor-spacing="cursorSpacing"
:placeholder-style="placeholderStyle"
:selection-start="selectionStart"
:selection-end="selectionEnd"
:adjust-position="adjustPosition"
:hold-keyboard="holdKeyboard"
:always-embed="alwaysEmbed"
:placeholder-class="inputPlaceholderClass"
:ignoreCompositionEvent="ignoreCompositionEvent"
:inputmode="inputmode"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@confirm="handleConfirm"
@keyboardheightchange="handleKeyboardheightchange"
/>
<view v-if="props.readonly" class="wd-input__readonly-mask" />
<view v-if="showClear || showPassword || suffixIcon || showWordCount || $slots.suffix" class="wd-input__suffix">
<wd-icon v-if="showClear" custom-class="wd-input__clear" name="error-fill" @click="handleClear" />
<wd-icon v-if="showPassword" custom-class="wd-input__icon" :name="isPwdVisible ? 'view' : 'eye-close'" @click="togglePwdVisible" />
<view v-if="showWordCount" class="wd-input__count">
<text
:class="[
inputValue && String(inputValue).length > 0 ? 'wd-input__count-current' : '',
String(inputValue).length > maxlength! ? 'is-error' : ''
]"
>
{{ String(inputValue).length }}
</text>
/{{ maxlength }}
</view>
<wd-icon v-if="suffixIcon && !$slots.suffix" custom-class="wd-input__icon" :name="suffixIcon" @click="onClickSuffixIcon" />
<slot v-else name="suffix"></slot>
</view>
</view>
<view v-if="errorMessage" class="wd-input__error-message">{{ errorMessage }}</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-input',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, ref, watch, useSlots, type Slots } from 'vue'
import wdIcon from '../wd-icon/wd-icon.vue'
import { isDef, objToStyle, pause, isEqual } from '../common/util'
import { useCell } from '../composables/useCell'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
import { useParent } from '../composables/useParent'
import { useTranslate } from '../composables/useTranslate'
import { inputProps } from './types'
interface InputSlots extends Slots {
prefix?: () => any
suffix?: () => any
label?: () => any
}
const props = defineProps(inputProps)
const emit = defineEmits([
'update:modelValue',
'clear',
'blur',
'focus',
'input',
'keyboardheightchange',
'confirm',
'clicksuffixicon',
'clickprefixicon',
'click'
])
const slots = useSlots() as InputSlots
const { translate } = useTranslate('input')
const isPwdVisible = ref<boolean>(false)
const clearing = ref<boolean>(false) // 是否正在清空操作,避免重复触发失焦
const focused = ref<boolean>(false) // 控制聚焦
const focusing = ref<boolean>(false) // 当前是否激活状态
const inputValue = ref<string | number>(getInitValue()) // 输入框的值
const cell = useCell()
watch(
() => props.focus,
(newValue) => {
focused.value = newValue
},
{ immediate: true, deep: true }
)
watch(
() => props.modelValue,
(newValue) => {
inputValue.value = isDef(newValue) ? String(newValue) : ''
}
)
const { parent: form } = useParent(FORM_KEY)
const placeholderValue = computed(() => {
return isDef(props.placeholder) ? props.placeholder : translate('placeholder')
})
/**
* 展示清空按钮
*/
const showClear = computed(() => {
const { disabled, readonly, clearable, clearTrigger } = props
if (clearable && !readonly && !disabled && inputValue.value && (clearTrigger === 'always' || (props.clearTrigger === 'focus' && focusing.value))) {
return true
} else {
return false
}
})
/**
* 展示字数统计
*/
const showWordCount = computed(() => {
const { disabled, readonly, maxlength, showWordLimit } = props
return Boolean(!disabled && !readonly && isDef(maxlength) && maxlength > -1 && showWordLimit)
})
/**
* 表单错误提示信息
*/
const errorMessage = computed(() => {
if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
return form.errorMessages[props.prop]
} else {
return ''
}
})
// 是否展示必填
const isRequired = computed(() => {
let formRequired = false
if (form && form.props.rules) {
const rules = form.props.rules
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
formRequired = rules[key].some((rule: FormItemRule) => rule.required)
}
}
}
return props.required || props.rules.some((rule) => rule.required) || formRequired
})
const rootClass = computed(() => {
return `wd-input ${props.label || slots.label ? 'is-cell' : ''} ${props.center ? 'is-center' : ''} ${cell.border.value ? 'is-border' : ''} ${
props.size ? 'is-' + props.size : ''
} ${props.error ? 'is-error' : ''} ${props.disabled ? 'is-disabled' : ''} ${
inputValue.value && String(inputValue.value).length > 0 ? 'is-not-empty' : ''
} ${props.noBorder ? 'is-no-border' : ''} ${props.customClass}`
})
const labelClass = computed(() => {
return `wd-input__label ${props.customLabelClass}`
})
const inputPlaceholderClass = computed(() => {
return `wd-input__placeholder ${props.placeholderClass}`
})
const labelStyle = computed(() => {
return props.labelWidth
? objToStyle({
'min-width': props.labelWidth,
'max-width': props.labelWidth
})
: ''
})
// 状态初始化
function getInitValue() {
const formatted = formatValue(props.modelValue)
if (!isValueEqual(formatted, props.modelValue)) {
emit('update:modelValue', formatted)
}
return formatted
}
function formatValue(value: string | number) {
const { maxlength } = props
if (isDef(maxlength) && maxlength !== -1 && String(value).length > maxlength) {
return value.toString().slice(0, maxlength)
}
return value
}
function togglePwdVisible() {
isPwdVisible.value = !isPwdVisible.value
}
async function handleClear() {
focusing.value = false
inputValue.value = ''
if (props.focusWhenClear) {
clearing.value = true
focused.value = false
}
await pause()
if (props.focusWhenClear) {
focused.value = true
focusing.value = true
}
emit('update:modelValue', inputValue.value)
emit('clear')
}
async function handleBlur() {
// 等待150毫秒clear执行完毕
await pause(150)
if (clearing.value) {
clearing.value = false
return
}
focusing.value = false
emit('blur', {
value: inputValue.value
})
}
function handleFocus({ detail }: any) {
focusing.value = true
emit('focus', detail)
}
function handleInput({ detail }: any) {
emit('update:modelValue', inputValue.value)
emit('input', detail)
}
function handleKeyboardheightchange({ detail }: any) {
emit('keyboardheightchange', detail)
}
function handleConfirm({ detail }: any) {
emit('confirm', detail)
}
function onClickSuffixIcon() {
emit('clicksuffixicon')
}
function onClickPrefixIcon() {
emit('clickprefixicon')
}
function handleClick(event: MouseEvent) {
emit('click', event)
}
function isValueEqual(value1: number | string, value2: number | string) {
return isEqual(String(value1), String(value2))
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>
<style lang="scss">
@import './placeholder.scss';
</style>