This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, makeArrayProp, makeNumericProp, makeStringProp, nullableBooleanProp, truthProp } from '../_utils'
import type { FormItemLabelPosition, FormItemRule, FormItemStarPosition } from './types'
export const formitemProps = {
...commonProps,
/**
* @description 是否显示必填字段的标签旁边的红色星号
*/
required: nullableBooleanProp,
/**
* @description 表单域 `v-model` 字段,在使用表单校验功能的情况下,该属性是必填的
*/
prop: makeStringProp(''),
/**
* @description
*/
label: makeStringProp(''),
/**
* @description 定义校验规则
*/
rules: makeArrayProp<FormItemRule>([]),
/**
* @description 表单项 `label` 宽度,默认单位为 `px`
*/
labelWidth: makeNumericProp(''),
/**
* @description 表单项 `label` 对齐方式,可选值为 `center`、`right`
*/
labelAlign: makeStringProp<'left' | 'center' | 'right'>('left'),
/**
* @description 右侧插槽对齐方式,可选值为 `center`、`right`
*/
bodyAlign: makeStringProp<'left' | 'center' | 'right'>('left'),
/**
* @description 错误提示文案对齐方式,可选值为 `center`、`right`
*/
errorMessageAlign: makeStringProp<'left' | 'center' | 'right'>('left'),
/**
* @description 是否在校验不通过时标红输入框
*/
showErrorLine: truthProp,
/**
* @description 是否在校验不通过时在输入框下方展示错误提示
* @type {boolean}
* @default true
*/
showErrorMessage: truthProp,
/**
* @description 表单项 label 的位置,优先级高于 form 中的 `label-position`
*/
labelPosition: makeStringProp<FormItemLabelPosition | undefined>(undefined),
/**
* @description 必填表单项 label 的红色星标位置,优先级高于 form 中的 `star-position`
*/
starPosition: makeStringProp<FormItemStarPosition | undefined>(undefined),
}
export type FormItemProps = ExtractPropTypes<typeof formitemProps>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, useSlots } from 'vue'
import { PREFIX } from '../_constants'
import { useInject } from '../_hooks'
import { getMainClass, pxCheck } from '../_utils'
import NutCell from '../cell/cell.vue'
import { FORM_KEY } from '../form/form'
import type { FormItemProps } from './formitem'
import { formitemProps } from './formitem'
import type { FormItemRule } from './types'
const props = defineProps(formitemProps)
const slots = useSlots()
const Parent = useInject<{ formErrorTip: Required<any>, props: Required<FormItemProps> }>(FORM_KEY)
const isRequired = computed(() => {
if (props.required === false)
return false
const rules = Parent.parent?.props?.rules
let formRequired = false
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key as any]))
formRequired = rules[key as any].some((rule: FormItemRule) => rule.required)
}
return props.required || props.rules.some(rule => rule.required) || formRequired
})
const labelPositionClass = computed(() => {
const labelPosition = Parent.parent?.props.labelPosition
const position = props.labelPosition ? props.labelPosition : labelPosition
return `nut-form-item__${position}`
})
const starPositionClass = computed(() => {
const starPosition = Parent.parent?.props.starPosition
const position = props.starPosition ? props.starPosition : starPosition
return `nut-form-item__star-${position}`
})
const classes = computed(() => {
return getMainClass(props, componentName)
})
const labelStyle = computed(() => {
return {
width: pxCheck(props.labelWidth),
textAlign: props.labelAlign,
} as CSSProperties
})
const bodyStyle = computed(() => {
return {
textAlign: props.bodyAlign,
} as CSSProperties
})
const formErrorTip = Parent.parent?.formErrorTip || {}
const errorMessageStyle = computed(() => {
return {
textAlign: props.errorMessageAlign,
} as CSSProperties
})
const getSlots = (name: string) => slots[name]
</script>
<script lang="ts">
const componentName = `${PREFIX}-form-item`
export default defineComponent({
name: componentName,
inheritAttrs: false,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutCell
:custom-class="[{ error: formErrorTip[prop], line: showErrorLine }, classes, labelPositionClass]"
:custom-style="customStyle"
>
<view
v-if="label || getSlots('label')"
class="nut-cell__title nut-form-item__label"
:style="labelStyle"
:class="{ required: isRequired, [starPositionClass]: starPositionClass }"
>
<slot name="label">
{{ label }}
</slot>
</view>
<view class="nut-cell__value nut-form-item__body">
<view class="nut-form-item__body__slots" :style="bodyStyle">
<slot />
</view>
<view v-if="formErrorTip[prop] && showErrorMessage" class="nut-form-item__body__tips" :style="errorMessageStyle">
{{ formErrorTip[prop] }}
</view>
</view>
</NutCell>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1,129 @@
@import "../cell/index";
.nut-theme-dark {
.nut-form-item {
&__body {
&__slots {
:deep(.nut-input) {
color: $dark-color;
background: transparent;
}
}
}
}
}
.nut-form-item {
display: flex;
&::before {
position: absolute;
right: 16px;
bottom: 0;
left: 16px;
box-sizing: border-box;
pointer-events: none;
content: "";
transform: scaleX(0);
}
&.error {
&.line {
&::before {
border-bottom: 1px solid $form-item-error-line-color;
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1) 0ms;
transform: scaleX(1);
}
}
}
&__label {
display: inline-block !important;
flex: none !important;
width: $form-item-label-width;
margin-right: $form-item-label-margin-right;
font-size: $form-item-label-font-size;
font-weight: normal;
text-align: $form-item-label-text-align;
word-wrap: break-word;
&:deep(.nut-cell__title) {
min-width: auto;
}
&.required {
&::before {
margin-right: $form-item-required-margin-right;
color: $form-item-required-color;
content: '*';
}
&.nut-form-item__star-right {
&::before {
content: none;
}
&::after {
margin-left: $form-item-required-margin-right;
color: $form-item-required-color;
content: '*';
}
}
}
}
&__body {
display: flex !important;
flex: 1;
flex-direction: column;
&__slots {
text-align: $form-item-body-slots-text-align;
:deep(.nut-input) {
width: 100%;
font-size: $form-item-body-font-size;
color: $black;
text-align: $form-item-body-input-text-align;
text-decoration: none;
background: transparent;
border: 0;
outline: 0 none;
}
:deep(.nut-range-container) {
min-height: 24px;
}
:deep(.nut-textarea) {
padding: 0 !important;
.nut-textarea__textarea {
text-align: $form-item-body-input-text-align;
}
}
}
&__tips {
font-size: $form-item-tip-font-size;
color: $form-item-error-message-color;
text-align: $form-item-tip-text-align;
}
}
&__right {
--nut-form-item-label-text-align: right;
}
&__top {
flex-direction: column;
.nut-form-item__label {
box-sizing: border-box;
display: block;
width: 100%;
padding-right: 24px;
padding-bottom: 5px;
}
}
}

View File

@@ -0,0 +1,2 @@
export type * from './formitem'
export * from './types'

View File

@@ -0,0 +1,16 @@
export class FormItemRuleWithoutValidator {
regex?: RegExp
required?: boolean
message!: string;
[key: string]: any;
}
export interface FormItemRule extends FormItemRuleWithoutValidator {
validator?: (
value: any,
ruleCfg: FormItemRuleWithoutValidator
) => boolean | Promise<string> | Promise<boolean> | Promise<void> | Promise<unknown>
}
export type FormItemLabelPosition = 'left' | 'right' | 'top'
export type FormItemStarPosition = 'left' | 'right'