UNPKG

@ithinkdt/naive

Version:

iThinkDT Naive UI

539 lines (498 loc) 24.6 kB
import { defineComponent, ref, h, watch, resolveComponent, mergeProps, nextTick, onMounted } from 'vue' import { NGrid, gridProps, NGi, NFlex, NButton, NIcon, NTooltip, NForm, NFormItem } from 'ithinkdt-ui' import { promiseTimeout } from '@vueuse/core' import { clone } from '@ithinkdt/common' import { $msg, useI18n } from '@ithinkdt/core' import { c, cB, cE, cM, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p } from '@ithinkdt/core/cssr' import { IHelp, IUp, IDown } from './assets' export const DtForm = defineComponent({ name: 'DtForm', props: { ...gridProps, collapsed: undefined, cols: { type: [Number, String], required: false, default: '12 768:18 1280:24 1536:30', }, yGap: { type: [Number, String], required: false, default: undefined }, items: { type: Array, required: true }, model: { type: Object, required: true }, preset: { type: String, required: false, default: undefined }, loading: { type: Boolean, required: false, default: false }, disabled: { type: Boolean, required: false, default: false }, showColon: { type: Boolean, required: false, default: false }, labelWidth: { type: [String, Number], required: false, default: undefined }, labelAlign: { type: String, required: false, default: undefined }, labelPlacement: { type: String, required: false, default: 'left' }, requireMarkPlacement: { type: String, required: false, default: 'left' }, showRequireMark: { type: Boolean, required: false, default: undefined }, showFeedback: { type: Boolean, required: false, default: undefined }, readonly: { type: Boolean, required: false, default: false }, onReset: { type: [Array, Function], default: undefined }, defaultCollapsed: { type: Boolean, default: true }, actionAlign: { type: String, default: 'right' }, showAction: { type: Boolean, default: undefined }, submitText: { type: String, default: undefined }, }, emits: ['submit'], setup(props, { emit, expose }) { const cls = `${p}-grid-form` createStyle(cls) const formRef = ref({}) const delegate = { validate: (...p) => { return Promise.all(props.items.map((it) => it && !it.hidden && it.submit?.())).then(() => formRef.value?.validate(...p), ) }, restoreValidation: () => formRef.value?.restoreValidation(), } expose(delegate) const collapsed = ref(true) onMounted(async () => { await nextTick() await promiseTimeout(10) watch( () => props.defaultCollapsed, (defaultCollapsed) => { collapsed.value = defaultCollapsed }, { immediate: true }, ) }) let modelBak = {} watch( () => props.model, (model, oModel) => { if (model && model !== oModel) modelBak = clone(model) }, { immediate: true }, ) const cacheRule = Symbol() const validing = ref(false) const onSubmit = (e) => { e?.preventDefault?.() validing.value = true delegate .validate() .then(() => { emit('submit', props.model, e) }) .catch((error) => { console.debug('form: invalid value on submit', error) if (props.preset === 'search') { // TODO 优化提示 $msg.warning(error?.message || error?.[0]?.[0]?.message || error) } }) .finally(() => { validing.value = false }) } const gridRef = ref() return () => { const { items, model, preset, readonly, loading, showColon, labelWidth, labelPlacement, labelAlign = labelPlacement === 'top' ? 'left' : 'right', requireMarkPlacement, showRequireMark, showFeedback, disabled, xGap, yGap, cols, ...gridProps } = props const spans = formRef.value?.$el ? gridRef.value?.responsiveCols ?? 0 : 0 const isSearch = preset === 'search' const _showFeedback = !readonly && (showFeedback ?? !isSearch) let sumspan = 0 const _gridProps = mergeProps(gridProps, { class: `${cls}__grid` }) return ( <NForm ref={formRef} class={{ [cls]: true, [`${cls}--readonly`]: readonly }} model={model} labelWidth={labelWidth ?? (preset === 'search' ? (showColon ? '6.2em' : '5.8em') : '7.2em')} labelAlign={labelAlign} labelPlacement={labelPlacement} requireMarkPlacement={requireMarkPlacement} showFeedback={_showFeedback} showRequireMark={readonly || showRequireMark === false ? false : undefined} disabled={disabled} onReset={(e) => { e?.preventDefault?.() formRef.value?.restoreValidation() let reset = props.onReset if (reset) { reset = Array.isArray(reset) ? reset : [reset] for (const it of reset) it() } else { for (const it of props.items) { if (it?.name && !it.name.startsWith('$')) { // eslint-disable-next-line unicorn/no-null model[it.name] = modelBak[it.name] ?? null } } Object.assign(model, modelBak) } if (props.preset === 'search') { onSubmit() } }} onSubmit={onSubmit} > <NGrid {..._gridProps} ref={gridRef} cols={cols} xGap={xGap} yGap={readonly ? 0 : yGap ?? (_showFeedback ? 0 : 20)} collapsed={preset === 'search' && collapsed.value} itemResponsive > { ((sumspan = 0), items.map((item, i) => { if (!item[cacheRule]) { item[cacheRule] = (item.rule ? [item.rule].flat() : []).map((it) => { const validator = (_, v) => { return Promise.resolve( (typeof it === 'function' ? it : it.validator)(v, props.model), ).then((res) => { if (res === false || (res && res !== true)) { throw res === false ? new Error(item.tip) : res } }) } return typeof it === 'function' ? { validator, trigger: 'blur' } : { ...it, validator } }) } let { type, name, readonly: readonly2, modelValue, trim: _0, props: cProps, slots, tip: _tip, parse, transform, rule: _1, hidden, label, labelPlacement: lp2 = labelPlacement, labelProps = {}, required, showColon: itemColon, first = true, view, component: _2, showFeedback, offset, span, rowSpan, suffix, ...it } = item const tip = _tip && typeof _tip !== 'function' ? () => _tip : _tip readonly2 ||= readonly if (hidden) return sumspan += span * (rowSpan || 1) const _labelProps = mergeProps(labelProps, { class: `${cls}__label` }) if (i === items.length - 1 && readonly2 && preset !== 'search') { span += sumspan % spans } return name && !name.startsWith('$') ? ( <NGi class={`${cls}__gi`} offset={offset} span={span} style={rowSpan ? { gridRowEnd: `span ${rowSpan}` } : undefined} suffix={suffix} key={name} > <NFormItem {...it} class={`${cls}__item`} path={name} required={required && !readonly2} first={first} rule={item[cacheRule]} labelPlacement={lp2} labelProps={_labelProps} showFeedback={showFeedback} > {{ label: () => ( <span style="display: inline-flex; align-items: flex-start; gap: 3px"> <span>{typeof label === 'function' ? label() : label}</span> {lp2 === 'left' && isSearch && tip ? ( <NTooltip> {{ default: tip, trigger: () => ( <NButton text style="font-size: 1.25em; opacity: 0.8" > <NIcon> <IHelp /> </NIcon> </NButton> ), }} </NTooltip> ) : undefined} {showColon && itemColon !== false ? ( <span style="place-self: center">:</span> ) : undefined} {lp2 === 'top' && !isSearch && tip ? ( <span class={[`${cls}__tip`, `${cls}__tip--top`]}> {tip()} </span> ) : undefined} </span> ), default: () => { let fVNode let v = model[name] if (readonly2 || !type || type === 'view') { fVNode = ( <span style="width: 100%"> {view ? view(v, { model }) : v} </span> ) } else { v = transform ? transform(v) : v const c = typeof type === 'string' ? resolveComponent(type) : type const _modelValue = modelValue ?? 'value' let _cProps = mergeProps( { name, }, cProps, { // eslint-disable-next-line unicorn/no-null [_modelValue]: v ?? null, ['onUpdate:' + _modelValue]: (v) => { model[name] = parse ? parse(v) : v }, }, ) fVNode = h(c, _cProps, slots) } return isSearch ? ( fVNode ) : ( <div style={`width: 100%; display: flex; flex-direction: column;`} > <div style="min-height: 32px; display: flex; align-items: center"> {fVNode} </div> {lp2 === 'left' && !readonly2 && tip ? ( <span class={`${cls}__tip`}>{tip()}</span> ) : undefined} </div> ) }, }} </NFormItem> </NGi> ) : ( <NGi class={`${cls}__gi`} offset={offset} span={span} style={rowSpan ? { gridRowEnd: `span ${rowSpan}` } : undefined} suffix={suffix} key={name || i} > {h(typeof type === 'string' ? resolveComponent(type) : type, cProps, slots)} </NGi> ) })) } {!readonly && preset && props.showAction !== false ? ( <NGi suffix={preset === 'search' && spans !== 0 && sumspan + 6 > spans} span={preset === 'search' ? 6 : 30} key="preset" > {{ default: () => { return ( <NFormItem label={isSearch || props.actionAlign === 'left' ? ' ' : undefined} showLabel={isSearch || props.labelPlacement !== 'top'} > <DtFormAction v-model:collapsed={collapsed.value} preset={preset} justify={ preset === 'form' ? props.actionAlign === 'left' ? 'start' : props.actionAlign === 'right' ? 'end' : 'center' : undefined } overflow={spans !== 0 && sumspan + 6 > spans} loading={loading || validing.value} disabled={disabled || validing.value} submitText={props.submitText} style="flex: 1 1 auto" /> </NFormItem> ) }, }} </NGi> ) : undefined} </NGrid> </NForm> ) } }, }) let style function createStyle(cls) { if (!style) { style = cB('grid-form', [ cM('readonly', [ cE('grid', { paddingBottom: '1px', }), cE('gi', { border: '1px solid var(--dt-border-color)', margin: '0 -1px -1px 0', }), cE('label', { backgroundColor: 'var(--dt-tab-color)', borderRight: '1px solid var(--dt-border-color)', height: '100%', }), cE( 'item', { height: '100%', gridTemplateAreas: `'label blank'`, gridTemplateRows: 'auto', }, [ c('.n-form-item-blank', { height: '100%', padding: '6px 14px', }), ], ), ]), cE( 'tip', { color: `var(--${p}-text-color3)`, padding: '4px 1px 0', fontSize: '13px', }, [ cM('top', { padding: '0px 0 0 12px', placeSelf: 'flex-end', }), ], ), ]) style.mount({ id: cls, anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME, }) } } const DtFormAction = defineComponent({ props: { collapsed: { type: Boolean, required: true, }, preset: { type: String, required: true, }, justify: { type: String, default: 'start', }, overflow: { type: Boolean, required: true, }, loading: { type: Boolean, required: true, }, disabled: { type: Boolean, default: false, }, submitText: { type: String, default: undefined, }, }, emits: { 'update:collapsed': () => true, reset: () => true, }, setup(props, { emit }) { const { t } = useI18n() const showCollapsed = ref(false) watch( [() => props.collapsed, () => props.overflow], ([collapsed, overflow]) => { if (collapsed) { showCollapsed.value = overflow } }, { immediate: true }, ) return () => { const { preset, justify, disabled, loading, collapsed } = props const isSearch = preset === 'search' return ( <NFlex justify={isSearch && showCollapsed.value ? 'end' : justify} align="center"> <NButton type="primary" disabled={disabled} loading={loading} attrType="submit"> {props.submitText || t(isSearch ? 'form.search' : 'form.save')} </NButton> <NButton attrType="reset" onClick={() => emit('reset')} disabled={disabled || loading}> {t('form.reset')} </NButton> {isSearch && showCollapsed.value ? ( <NButton text type="primary" iconPlacement="right" renderIcon={collapsed ? IDown : IUp} onClick={() => emit('update:collapsed', !collapsed)} > {collapsed ? t('form.expand') : t('form.collapse')} </NButton> ) : undefined} </NFlex> ) } }, })